Bellabeat Case Study Using R.R

Eric Wheeler 2024-03-11

Background:

Bellabeat is a high-tech manufacturer of health-focused products for women. Their Bellabeat app provides users with health data related to their activity, sleep, stress, menstrual cycle, and mindfulness habits. The Chief Creative Officer of Bellabeat believes that analyzing smart device fitness data could help unlock new growth opportunities for the company.

Goal:

Analyze smart device data to gain insight into how consumers are using their smart devices. The insights will then be used to help guide marketing strategy for the company.

Guiding Questions:

  1. What are some trends in smart device usage?

  2. How could these trends apply to Bellabeat customers?

  3. How could these trends help influence Bellabeat marketing strategy?

1. Data Sourcing

FitBit Fitness Tracker Data (CC0: Public Domain, dataset made available through Mobius): This Kaggle data set contains personal fitness tracker from thirty Fitbit users. Thirty eligible Fitbit users consented to the submission of personal tracker data, including minute-level output for physical activity, heart rate, and sleep monitoring. It includes information about daily activity, steps, and heart rate that can be used to explore users’ habits.

I have identified a number of limitations to be aware of for this dataset:

The Bellabeat dataset contains 2 folders that have a total of 29 CSV files.

The file names in the two folders are identical, which could lead to confusion when trying to work with them. After selecting the files that would be useful for this analysis, I updated the file names to include the date range for the data.

I chose the files that contained:

Each of these areas had corresponding files in each folder, with the exception of sleep data. I will use data manipulation to convert the Minute Sleep data to Sleep Day data.

1.1 Data Importation

Load the necessary packages and import the CSV files.

library(tidyverse) # this collection contains packages used in the analysis i.e. dplyr, ggplot2, tidyr, lubridate
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.0     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
daily_activity_1  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/3.12.16-4.11.16_dailyActivity_merged.csv")
hourly_steps_1  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/3.12.16-4.11.16_hourlySteps_merged.csv")
minute_sleep_1  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/3.12.16-4.11.16_minuteSleep_merged.csv")
daily_activity_2  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/4.12.16-5.12.16_dailyActivity_merged.csv")
hourly_steps_2  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/4.12.16-5.12.16_hourlySteps_merged.csv")
day_sleep_2  <- read.csv("C:/Users/Eric/OneDrive/Desktop/Bellabeat Folder/4.12.16-5.12.16_sleepDay_merged.csv")

1.2 Validate Participants

Let’s validate the number of participants in each dataframe.

n_distinct(daily_activity_1$Id)
## [1] 35
n_distinct(daily_activity_2$Id)
## [1] 33
n_distinct(minute_sleep_1$Id)
## [1] 23
n_distinct(day_sleep_2$Id) 
## [1] 24
n_distinct(hourly_steps_1$Id)
## [1] 34
n_distinct(hourly_steps_2$Id)
## [1] 33

There is fluctuating participation for each dataset as well as more participants than stated in the brief. Also, it appears that there were much fewer participants using their smart devices to track their sleep.

2. Data Cleaning

2.1 Align the Sleep Data

I’ve already identified that one dataset on sleep contains minute measurements while another contains daily measurements for sleep minutes. First I’ll look at both sleep files to see how I can approach aligning the data. Then I’ll make the necessary adjustments to both data frames to prepare them to be combined later on.

glimpse(minute_sleep_1)
## Rows: 198,559
## Columns: 4
## $ Id    <dbl> 1503960366, 1503960366, 1503960366, 1503960366, 1503960366, 1503…
## $ date  <chr> "3/13/2016 02:39:30", "3/13/2016 02:40:30", "3/13/2016 02:41:30"…
## $ value <int> 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ logId <dbl> 11114919637, 11114919637, 11114919637, 11114919637, 11114919637,…
glimpse(day_sleep_2)
## Rows: 413
## Columns: 5
## $ Id                 <dbl> 1503960366, 1503960366, 1503960366, 1503960366, 150…
## $ SleepDay           <chr> "4/12/2016", "4/13/2016", "4/15/2016", "4/16/2016",…
## $ TotalSleepRecords  <int> 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ TotalMinutesAsleep <int> 327, 384, 412, 340, 700, 304, 360, 325, 361, 430, 2…
## $ TotalTimeInBed     <int> 346, 407, 442, 367, 712, 320, 377, 364, 384, 449, 3…
# Make adjustments to minute_sleep_1
minute_sleep_1 <- minute_sleep_1[-4] %>% # Remove the logId column
  mutate(date = as.Date(date, format = "%m/%d/%Y %H:%M:%S")) %>% # Change the date data type to timestamp
  rename(Date = date) # Rename the date column to align with day_sleep_2

# Create a new aggregated day_sleep_1 dataframe 
day_sleep_1 <- minute_sleep_1 %>%
  group_by(Id, Date) %>% # group the data by the Id and Date columns
  summarise(TotalMinutesAsleep = sum(value)) # Get the sum of the minute sleep values
## `summarise()` has grouped output by 'Id'. You can override using the `.groups`
## argument.
# Adjust day_sleep_2 to match newly created day_sleep_1
day_sleep_2 <- day_sleep_2[-c(3,5)] %>% # Drop unneccessary columns
  mutate(SleepDay = as.Date(SleepDay, format = "%m/%d/%Y")) %>% # Change the SleepDay data type to timestamp
  rename(Date = SleepDay) # rename the SleepDay column

2.2 Check for Null Values

colSums(is.na(daily_activity_1)) 
##                       Id             ActivityDate               TotalSteps 
##                        0                        0                        0 
##            TotalDistance          TrackerDistance LoggedActivitiesDistance 
##                        0                        0                        0 
##       VeryActiveDistance ModeratelyActiveDistance      LightActiveDistance 
##                        0                        0                        0 
##  SedentaryActiveDistance        VeryActiveMinutes      FairlyActiveMinutes 
##                        0                        0                        0 
##     LightlyActiveMinutes         SedentaryMinutes                 Calories 
##                        0                        0                        0
colSums(is.na(daily_activity_2)) 
##                       Id             ActivityDate               TotalSteps 
##                        0                        0                        0 
##            TotalDistance          TrackerDistance LoggedActivitiesDistance 
##                        0                        0                        0 
##       VeryActiveDistance ModeratelyActiveDistance      LightActiveDistance 
##                        0                        0                        0 
##  SedentaryActiveDistance        VeryActiveMinutes      FairlyActiveMinutes 
##                        0                        0                        0 
##     LightlyActiveMinutes         SedentaryMinutes                 Calories 
##                        0                        0                        0
colSums(is.na(day_sleep_1))
##                 Id               Date TotalMinutesAsleep 
##                  0                  0                  0
colSums(is.na(day_sleep_2))
##                 Id               Date TotalMinutesAsleep 
##                  0                  0                  0
colSums(is.na(hourly_steps_1)) 
##           Id ActivityHour    StepTotal 
##            0            0            0
colSums(is.na(hourly_steps_2)) 
##           Id ActivityHour    StepTotal 
##            0            0            0

There are no null values in any of the data frames.

2.3 Union the tables

We can union the tables to combine the 3/12/16-4/11/16 data with the 4/12/16-5/12/16 data. Once this is done we continue cleaning by adjusting data types and removing duplicates

# Combine the sleep data 
day_sleep_df <- rbind(day_sleep_1, day_sleep_2)

# combine the daily activity data
daily_activity_df <- rbind(daily_activity_1, daily_activity_2)

# drop distance related columns in the daily acivity data
daily_activity_df <- daily_activity_df[-c(4:10)] 

# make adjustments to the daily activity data
daily_activity_df <- daily_activity_df %>%
  mutate(ActivityDate = as.Date(ActivityDate, format = "%m/%d/%Y")) %>% # Change ActivityDate data type to Date
  rename(Date = ActivityDate) # rename the ActivityDate column

# combine the hourly steps data
hourly_steps_df <- rbind(hourly_steps_1,hourly_steps_2)

# make adjustments to the steps data
hourly_steps_df$ActivityHour <- strptime(hourly_steps_df$ActivityHour, format="%m/%d/%Y %H:%M:%S") # change the ActivityHour data type to timestamp

2.4 Remove Duplicates

Now remove any duplicate rows that might exist in the data.

daily_activity_df <- daily_activity_df %>% distinct()
day_sleep_df <- day_sleep_df %>% distinct()
hourly_steps_df <- hourly_steps_df %>% distinct()

3. Feature Engineering

In order to uncover some insights from the data it will be helpful to create some features from the existing dimensions. In terms of date/time columns, we can extract the hour of the day, day of the week, and week of the year from the data.

3.1 Extract Date/Time Features

# get the WeekOfYear and DayOfWeek for the daily activity data  
daily_activity_df$WeekOfYear <- as.integer(week(daily_activity_df$Date)) # create a WeekOfYear column

daily_activity_df$DayOfWeek <- wday(daily_activity_df$Date, label=TRUE, abbr=FALSE) # create a DayOfWeek column

# get the DayOfWeek for the sleep data
day_sleep_df$DayOfWeek <- wday(day_sleep_df$Date, label=TRUE, abbr=FALSE)

# get the HourOfDay for hourly steps data
hourly_steps_df$HourOfDay <- hour(hourly_steps_df$ActivityHour)

3.2 Create Sleep Categories

It would also be helpful to create some benchmarks that are fairly universal regardless of age, sex, or race. We can create bins for steps per day and sleep per day.

The National Health Institute recommends that adults sleep between 7 and 9 hours a night and that adults who sleep less than 7 hours a night may have more health issues.

We can create sleep categories that group users based on this information.

# Define conditions and create SleepCategory column
day_sleep_df <- day_sleep_df %>%
  mutate(SleepCategory = case_when(
    TotalMinutesAsleep >= 420 & TotalMinutesAsleep <= 540 ~ "7 to 9 hours",
    TotalMinutesAsleep < 420 ~ "less than 7 hours",
    TotalMinutesAsleep > 540 ~ "more than 9 hours",
    TRUE ~ NA_character_  # for any other cases
  ))

# adjust the categories into a specified order
sleep_categories <- c("less than 7 hours", "7 to 9 hours", "more than 9 hours")

# set the categorical variables
day_sleep_df$SleepCategory <- factor(day_sleep_df$SleepCategory, levels = sleep_categories)

3.3 Create Activity Level Benchmarks

The National Institute of Health has also set the following benchmarks for activity based on daily step counts:

We can create these categories as well.

# Define conditions and create ActivityLevel column
daily_activity_df <- daily_activity_df %>%
  mutate(ActivityLevel = case_when(
    is.na(TotalSteps) ~ "Unknown",
    TotalSteps < 5000 ~ "Sedentary",
    TotalSteps < 7500 ~ "Low Active",
    TotalSteps < 10000 ~ "Somewhat Active",
    TotalSteps < 12500 ~ "Active",
    TRUE ~ "Very Active"
  ))

# adjust the categories into a specified order
activity_levels <- c("Sedentary", "Low Active", "Somewhat Active", "Active", "Very Active")

# set the categorical variables
daily_activity_df$ActivityLevel <- factor(daily_activity_df$ActivityLevel, levels = activity_levels)

4. Exploratory Data Analysis

Get the summary statistics for our dataframes to understand the data better.

summary(daily_activity_df)
##        Id                 Date              TotalSteps    VeryActiveMinutes
##  Min.   :1.504e+09   Min.   :2016-03-12   Min.   :    0   Min.   :  0.00   
##  1st Qu.:2.320e+09   1st Qu.:2016-04-09   1st Qu.: 3146   1st Qu.:  0.00   
##  Median :4.445e+09   Median :2016-04-19   Median : 6999   Median :  2.00   
##  Mean   :4.781e+09   Mean   :2016-04-19   Mean   : 7281   Mean   : 19.68   
##  3rd Qu.:6.962e+09   3rd Qu.:2016-04-30   3rd Qu.:10544   3rd Qu.: 30.00   
##  Max.   :8.878e+09   Max.   :2016-05-12   Max.   :36019   Max.   :210.00   
##                                                                            
##  FairlyActiveMinutes LightlyActiveMinutes SedentaryMinutes    Calories   
##  Min.   :  0.0       Min.   :  0.0        Min.   :   0.0   Min.   :   0  
##  1st Qu.:  0.0       1st Qu.:111.0        1st Qu.: 729.0   1st Qu.:1799  
##  Median :  6.0       Median :195.0        Median :1057.0   Median :2114  
##  Mean   : 13.4       Mean   :185.4        Mean   : 992.5   Mean   :2266  
##  3rd Qu.: 18.0       3rd Qu.:262.0        3rd Qu.:1244.0   3rd Qu.:2770  
##  Max.   :660.0       Max.   :720.0        Max.   :1440.0   Max.   :4900  
##                                                                          
##    WeekOfYear        DayOfWeek           ActivityLevel
##  Min.   :11.00   Sunday   :193   Sedentary      :500  
##  1st Qu.:15.00   Monday   :188   Low Active     :254  
##  Median :16.00   Tuesday  :225   Somewhat Active:213  
##  Mean   :16.15   Wednesday:198   Active         :231  
##  3rd Qu.:18.00   Thursday :195   Very Active    :199  
##  Max.   :19.00   Friday   :199                        
##                  Saturday :199
summary(day_sleep_df)
##        Id                 Date            TotalMinutesAsleep     DayOfWeek  
##  Min.   :1.504e+09   Min.   :2016-03-11   Min.   :   6.0     Sunday   :129  
##  1st Qu.:3.977e+09   1st Qu.:2016-03-27   1st Qu.: 379.0     Monday   :119  
##  Median :4.703e+09   Median :2016-04-10   Median : 454.0     Tuesday  :131  
##  Mean   :4.910e+09   Mean   :2016-04-10   Mean   : 441.8     Wednesday:125  
##  3rd Qu.:6.776e+09   3rd Qu.:2016-04-26   3rd Qu.: 526.0     Thursday :123  
##  Max.   :8.792e+09   Max.   :2016-05-12   Max.   :1201.0     Friday   :121  
##                                                              Saturday :129  
##            SleepCategory
##  less than 7 hours:323  
##  7 to 9 hours     :369  
##  more than 9 hours:185  
##                         
##                         
##                         
## 
summary(hourly_steps_df)
##        Id             ActivityHour                      StepTotal      
##  Min.   :1.504e+09   Min.   :2016-03-12 00:00:00.00   Min.   :    0.0  
##  1st Qu.:2.320e+09   1st Qu.:2016-03-26 13:00:00.00   1st Qu.:    0.0  
##  Median :4.559e+09   Median :2016-04-10 11:00:00.00   Median :   21.0  
##  Mean   :4.870e+09   Mean   :2016-04-10 17:33:39.75   Mean   :  302.9  
##  3rd Qu.:6.962e+09   3rd Qu.:2016-04-25 16:00:00.00   3rd Qu.:  323.0  
##  Max.   :8.878e+09   Max.   :2016-05-12 15:00:00.00   Max.   :10565.0  
##                      NA's   :33                                        
##    HourOfDay    
##  Min.   : 0.00  
##  1st Qu.: 5.00  
##  Median :11.00  
##  Mean   :11.45  
##  3rd Qu.:17.00  
##  Max.   :23.00  
## 

Summary Statistics Insights:

4.1 Total Steps by Hour of the Day

I’ll start by looking at the hourly breakdown of steps for these participants

ggplot(data=hourly_steps_df, mapping = aes(x=HourOfDay, y=StepTotal, fill=HourOfDay)) +
  geom_bar(stat="sum") + 
  theme_minimal() + 
  theme(legend.position = "none") +
  labs(title = "Total Steps by Hour of the Day")

Hourly Steps Insights:

4.2 Breakdown of Activity Levels

# calculate the percentage of each ActivityLevel 
activity_percentage <- daily_activity_df %>%   
  count(ActivityLevel) %>%   
  mutate(percentage = n / sum(n) * 100)  

# Create the pie chart 
ggplot(activity_percentage, aes(x = "", y = n, fill = ActivityLevel)) + 
  geom_bar(stat = "identity") +   
  coord_polar("y", start = 0) +   
  geom_text(aes(label = paste0(round(percentage), "%")), position = position_stack(vjust = 0.5)) +   
  labs(title = "Activity Level Distribution") +   
  theme_void()

Activity Level Insights:

4.3 Activity Level by Day of the Week

# calculate the percentage of each ActivityLevel by DayOfWeek
dow_activity <- daily_activity_df %>%
  group_by(DayOfWeek, ActivityLevel) %>%
  summarise(count = n()) %>%
  group_by(DayOfWeek) %>%
  mutate(percentage = count / sum(count) * 100)
## `summarise()` has grouped output by 'DayOfWeek'. You can override using the
## `.groups` argument.
# create a bar chart 
ggplot(dow_activity, aes(x=DayOfWeek, y=count, fill=ActivityLevel)) +
  geom_bar(stat = "identity") +
  geom_text(aes(label = paste0(round(percentage), "%")),
            position = position_stack(vjust = 0.5), size = 3) +
  labs(title="Activity Level by Day of Week", x = "Day of Week", y = "Count", fill = "Activity Level") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  theme_minimal()

Day of Week Activity Level Insights:

4.4 Breakdown of Sleep Categories

# calculate percentage breakdown of Sleep Quality
sleep_percentage <- day_sleep_df %>%   group_by(SleepCategory) %>%   summarize(count = n()) %>%   mutate(percentage = count / sum(count) * 100)  

# Create pie chart 
ggplot(sleep_percentage, aes(x = "", y = percentage, fill = SleepCategory)) +   
  geom_bar(stat = "identity") +   
  coord_polar("y", start = 0) + 
  theme_void() +   theme(legend.position = "bottom") +   
  geom_text(aes(label = paste0(round(percentage), "%")), position = position_stack(vjust = 0.5)) +   
  labs(title = "Sleep Quality Breakdown", fill = "Sleep Quality") +
  theme_void()

Sleep insights

4.5 Sleep by Day of the Week

# calculate counts of SleepCategory for each DayOfWeek
sleep_counts <- day_sleep_df %>%
  group_by(DayOfWeek, SleepCategory) %>%
  summarise(count = n()) %>%
  group_by(DayOfWeek) %>%
  mutate(percentage = count / sum(count) * 100)
## `summarise()` has grouped output by 'DayOfWeek'. You can override using the
## `.groups` argument.
# create the bar chart 
ggplot(sleep_counts, aes(x = DayOfWeek, y = count, fill = SleepCategory)) +
  geom_bar(stat = "identity") +
  geom_text(aes(label = paste0(round(percentage), "%")),
            position = position_stack(vjust = 0.5), size = 3) +
  labs(title="Sleep Quality by Day of Week", x = "Day of Week", y = "Count", fill = "Sleep Category") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  theme_minimal()

Day of the Week Sleep Insights:

4.6 Join the Activity and Sleep Dataframes to Explore Potential Correlations

Remove days with double log entries in the daily_activity_df before joining

# find rows that contain the same Id and Date i.e. double entries
duplicate_rows <- daily_activity_df %>%
  filter(duplicated(daily_activity_df[, c("Id", "Date")]) | duplicated(daily_activity_df[, c("Id", "Date")], fromLast = TRUE))

# Remove double entries from daily_activity_df
daily_activity_df <- anti_join(daily_activity_df, duplicate_rows, by = c("Id", "Date"))

Join the Daily Activity and Day Sleep tables to see rows that have both sleep and activity data

daily_df <- inner_join(day_sleep_df, daily_activity_df, by = c("Id", "Date", "DayOfWeek"))

4.7 Does the Amount of Sleep Correlate with Sedentary Minutes?

# create a scatter plot for TotalMinutesAsleep and SedentaryMinutes with facets around SleepCategory
ggplot(data=daily_df, mapping = aes(x=TotalMinutesAsleep, y=SedentaryMinutes)) +
  geom_point() +
  geom_smooth() +
  theme_minimal() +
  facet_wrap(~SleepCategory) +
  labs(title = "Correlation Between Minutes Asleep and Minutes Spent Sedentary")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Sleep and Sedentary Minutes Insights:

4.8 Does Sleep Have a Correlation With Activity Level?

# calculate counts of instances for each combination of SleepCategory and ActivityLevel
count_data <- daily_df %>% 
  group_by(SleepCategory, ActivityLevel) %>%
  summarise(count = n()) %>%
  mutate(percentage = count / sum(count) * 100)
## `summarise()` has grouped output by 'SleepCategory'. You can override using the
## `.groups` argument.
# Create the bar chart
ggplot(count_data, aes(x = SleepCategory, y = count, fill = ActivityLevel)) +
  geom_bar(stat = "identity", position = "stack") +
  geom_text(aes(label = paste0(round(percentage), "%")), position = position_stack(vjust = 0.5), size = 3) +
              labs(title="Breakdown of Activity Level by Sleep Category", x = "Sleep Category", y = "Count", fill = "Activity Level") +
              theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  theme_minimal()

Activity Level and Sleep Category Insights:

5. Takeaways: