Introduction

In this case study scenario, I will identify smart device trends, identify how smart device users are using their smart devices to help Bellabeat’s marketing team unlock new growth opportunities. Bellabeat was founded in 2013 and has grown rapidly and quickly positioned itself as a tech-driven wellness company for women. Bellabeat is a successful small company, but they have the potential to become a larger player in the global smart device market. Bellabeat’s Chief Creative Officer believes that analyzing competitor smart device fitness data could help unlock new growth opportunities for the company.

Data Source

The data used for this analysis comes from public data available on Kaggle. The dataset was generated by respondents to a distributed survey via Amazon Mechanical Turk between the period of 4.12.16 – 5.11.16.  This Kaggle data set contains personal fitness tracker from thirty (30) Fitbit users who 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. Eleven (11) files were exported from Kaggle, https://www.kaggle.com/datasets/arashnic/fitbit .

Data Validation and Integrity

The data is open to the public via public domain through the Kaggle website and made available by Mobius which makes this data secondary data.  The data in each file is structured in a long format.  The data will be validated by checking that the data matches the data type for a field, check that the data falls within an acceptable range of values defined for the field, check the data constraints such as field attributes, and finally ensure that the data is consistent in the context of other related data.

Data Limitations

There are notable limitations with the data to be analyzed:

  • Sample size is limited since the Fitbit datasets provided only collected data from 30 participants

  • The dataset does not contain any demographic information such as gender, age, or location which indicates a possibility of sampling bias

  • The data was collected in 2016, so it is outdated

To deal with insufficient data, the Fitbit data will be used to identify trends that meet the business objectives of this project. Data from the U.S. Department of Health and Human Services 2018 Physical Activity Guidelines for Americans, 2nd edition and the National Heart, Lung and Blood Institute to aid with the analysis.

Prepare in RStudio

For this analysis, I am using RStudio because it works best with large datasets.  RStudio provides a robust environment that allows for statistical analysis and visualizations. Various programs in R allow for organization, modification, and the cleaning of data frames to create insightful data visualizations for the end user.

Load in Libraries and Import Data

#Load in libraries
library(tidyverse)
## ── 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.1     ✔ 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
library(parsedate)
## 
## Attaching package: 'parsedate'
## 
## The following object is masked from 'package:readr':
## 
##     parse_date
library(skimr)
library(janitor)
## 
## Attaching package: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(lubridate)
# Import the data sets
daily_activity <- read.csv("C:/Users/tefloyd/OneDrive - Texas Tech University/Google Data Analyst Certificate/Capstone Project/Fitabase Data 4.12.16-5.12.16/dailyActivity_merged.csv")
heartrate <- read.csv("C:/Users/tefloyd/OneDrive - Texas Tech University/Google Data Analyst Certificate/Capstone Project/Fitabase Data 4.12.16-5.12.16/heartrate_seconds_merged.csv")
daily_sleep <- read.csv("C:/Users/tefloyd/OneDrive - Texas Tech University/Google Data Analyst Certificate/Capstone Project/Fitabase Data 4.12.16-5.12.16/sleepDay_merged.csv")
weight <- read.csv("C:/Users/tefloyd/OneDrive - Texas Tech University/Google Data Analyst Certificate/Capstone Project/Fitabase Data 4.12.16-5.12.16/weightLogInfo_merged.csv")
hourly_steps <- read.csv("C:/Users/tefloyd/OneDrive - Texas Tech University/Google Data Analyst Certificate/Capstone Project/Fitabase Data 4.12.16-5.12.16/hourlySteps_merged.csv")

Data Validation Processes

Sample Size

#Check the sample size in each data set by the Id column
n_distinct(daily_activity$Id)
## [1] 33
n_distinct(daily_sleep$Id)
## [1] 24
n_distinct(heartrate$Id)
## [1] 14
n_distinct(weight$Id)
## [1] 8
n_distinct(hourly_steps$Id)
## [1] 33

The initial data summary indicated that data was collected from 30 Fitbit users; however, upon review most of the data sets contained data from 33 users with the exception of the weight and heart rate data sets which only contained 8 and 14 respectively. Since the weight and heart rate data sets did not contain all Id’s, they will be dropped from the analysis.

Duplicates

#Check for duplicates in each data set
sum(duplicated(daily_activity))
## [1] 0
sum(duplicated(daily_sleep))
## [1] 3
sum(duplicated(hourly_steps))
## [1] 0

The results indicate that the there are duplicates in the daily_sleep data set which require removal.

#Remove duplicates
daily_sleep <- daily_sleep %>%
  distinct()

#Recheck data set to ensure duplicates were removed.
sum(duplicated(sleep))
## [1] 0

Investigate Data

#Step 1 - Review top six rows of data set Get a comprehensive overview of the imported data sets.
head(daily_activity)
##           Id ActivityDate TotalSteps TotalDistance TrackerDistance
## 1 1503960366    4/12/2016      13162          8.50            8.50
## 2 1503960366    4/13/2016      10735          6.97            6.97
## 3 1503960366    4/14/2016      10460          6.74            6.74
## 4 1503960366    4/15/2016       9762          6.28            6.28
## 5 1503960366    4/16/2016      12669          8.16            8.16
## 6 1503960366    4/17/2016       9705          6.48            6.48
##   LoggedActivitiesDistance VeryActiveDistance ModeratelyActiveDistance
## 1                        0               1.88                     0.55
## 2                        0               1.57                     0.69
## 3                        0               2.44                     0.40
## 4                        0               2.14                     1.26
## 5                        0               2.71                     0.41
## 6                        0               3.19                     0.78
##   LightActiveDistance SedentaryActiveDistance VeryActiveMinutes
## 1                6.06                       0                25
## 2                4.71                       0                21
## 3                3.91                       0                30
## 4                2.83                       0                29
## 5                5.04                       0                36
## 6                2.51                       0                38
##   FairlyActiveMinutes LightlyActiveMinutes SedentaryMinutes Calories
## 1                  13                  328              728     1985
## 2                  19                  217              776     1797
## 3                  11                  181             1218     1776
## 4                  34                  209              726     1745
## 5                  10                  221              773     1863
## 6                  20                  164              539     1728
#Step 2 - Get structure of the data frame
str(daily_activity)
## 'data.frame':    940 obs. of  15 variables:
##  $ Id                      : num  1.5e+09 1.5e+09 1.5e+09 1.5e+09 1.5e+09 ...
##  $ ActivityDate            : chr  "4/12/2016" "4/13/2016" "4/14/2016" "4/15/2016" ...
##  $ TotalSteps              : int  13162 10735 10460 9762 12669 9705 13019 15506 10544 9819 ...
##  $ TotalDistance           : num  8.5 6.97 6.74 6.28 8.16 ...
##  $ TrackerDistance         : num  8.5 6.97 6.74 6.28 8.16 ...
##  $ LoggedActivitiesDistance: num  0 0 0 0 0 0 0 0 0 0 ...
##  $ VeryActiveDistance      : num  1.88 1.57 2.44 2.14 2.71 ...
##  $ ModeratelyActiveDistance: num  0.55 0.69 0.4 1.26 0.41 ...
##  $ LightActiveDistance     : num  6.06 4.71 3.91 2.83 5.04 ...
##  $ SedentaryActiveDistance : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ VeryActiveMinutes       : int  25 21 30 29 36 38 42 50 28 19 ...
##  $ FairlyActiveMinutes     : int  13 19 11 34 10 20 16 31 12 8 ...
##  $ LightlyActiveMinutes    : int  328 217 181 209 221 164 233 264 205 211 ...
##  $ SedentaryMinutes        : int  728 776 1218 726 773 539 1149 775 818 838 ...
##  $ Calories                : int  1985 1797 1776 1745 1863 1728 1921 2035 1786 1775 ...
#Step 3 - Review columns to check for missing or null values.
colSums(is.na(daily_activity))
##                       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
#Step 1 - Review top six rows of data set Get a comprehensive overview of the imported data sets.
head(daily_sleep)
##           Id              SleepDay TotalSleepRecords TotalMinutesAsleep
## 1 1503960366 4/12/2016 12:00:00 AM                 1                327
## 2 1503960366 4/13/2016 12:00:00 AM                 2                384
## 3 1503960366 4/15/2016 12:00:00 AM                 1                412
## 4 1503960366 4/16/2016 12:00:00 AM                 2                340
## 5 1503960366 4/17/2016 12:00:00 AM                 1                700
## 6 1503960366 4/19/2016 12:00:00 AM                 1                304
##   TotalTimeInBed
## 1            346
## 2            407
## 3            442
## 4            367
## 5            712
## 6            320
#Step 2 - Get structure of the data frame
str(daily_sleep)
## 'data.frame':    410 obs. of  5 variables:
##  $ Id                : num  1.5e+09 1.5e+09 1.5e+09 1.5e+09 1.5e+09 ...
##  $ SleepDay          : chr  "4/12/2016 12:00:00 AM" "4/13/2016 12:00:00 AM" "4/15/2016 12:00:00 AM" "4/16/2016 12:00:00 AM" ...
##  $ TotalSleepRecords : int  1 2 1 2 1 1 1 1 1 1 ...
##  $ TotalMinutesAsleep: int  327 384 412 340 700 304 360 325 361 430 ...
##  $ TotalTimeInBed    : int  346 407 442 367 712 320 377 364 384 449 ...
#Step 3 - Review columns to check for missing or null values.
colSums(is.na(daily_sleep))
##                 Id           SleepDay  TotalSleepRecords TotalMinutesAsleep 
##                  0                  0                  0                  0 
##     TotalTimeInBed 
##                  0
#Step 1 - Review top six rows of data set Get a comprehensive overview of the imported data sets.
head(hourly_steps)
##           Id          ActivityHour StepTotal
## 1 1503960366 4/12/2016 12:00:00 AM       373
## 2 1503960366  4/12/2016 1:00:00 AM       160
## 3 1503960366  4/12/2016 2:00:00 AM       151
## 4 1503960366  4/12/2016 3:00:00 AM         0
## 5 1503960366  4/12/2016 4:00:00 AM         0
## 6 1503960366  4/12/2016 5:00:00 AM         0
#Step 2 - Get structure of the data frame
str(hourly_steps)
## 'data.frame':    22099 obs. of  3 variables:
##  $ Id          : num  1.5e+09 1.5e+09 1.5e+09 1.5e+09 1.5e+09 ...
##  $ ActivityHour: chr  "4/12/2016 12:00:00 AM" "4/12/2016 1:00:00 AM" "4/12/2016 2:00:00 AM" "4/12/2016 3:00:00 AM" ...
##  $ StepTotal   : int  373 160 151 0 0 0 0 0 250 1864 ...
#Step 3 - Review columns to check for missing or null values.
colSums(is.na(hourly_steps))
##           Id ActivityHour    StepTotal 
##            0            0            0

Formatting

Format date columns. The dates in the data sets are set as chr format. I will change this to a date format and separate the fields that are date/time.

#Format date columns to change from character to a date format.

#daily_activity
daily_activity <- daily_activity %>% 
  rename(date = activitydate) %>% 
  mutate(date = as_date(date, format = "%m/%d/%Y"))

#daily_sleep
daily_sleep <- daily_sleep %>% 
  separate(sleepday, into = c("date", "time"), sep= "^\\S*\\K") %>% 
  mutate(date = as_date(date, format = "%m/%d/%Y"))

#hourly_steps
hourly_steps <- hourly_steps %>% 
  separate(activityhour, into = c("date", "time"), sep= "^\\S*\\K") %>% 
  mutate(date = as_date(date, format = "%m/%d/%Y"))
#Add a column to hourly_steps to convert the time column to a 24 hour format
hourly_steps$mil_time <-  format(parse_date(hourly_steps$time), "%H:%M")
#Format the Id columns from numerical to a character format.
daily_activity$id <- as.character(daily_activity$id)
daily_sleep$id <- as.character(daily_sleep$id)
hourly_steps$id <- as.character(hourly_steps$id)

Check for Outliers

#Check for outliers
summary(daily_activity$totalsteps)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##       0    3790    7406    7638   10727   36019
#Plot total steps to visualize outliers
ggplot(daily_activity, aes(x = totalsteps)) +
  geom_boxplot()

#Use the empirical rule to identify three standard deviations from the mean
steps_empirical <- quantile(daily_activity$totalsteps, .997, na.rm = TRUE)

#According to the results, 99.7% of the observations are at 24,382 or below.  Any values above this amount are considered to be outliers and will be removed.
daily_activity <- subset(daily_activity, totalsteps <= 24382)

Merge Data

#Merge Data Sets
activity_sleep <- merge(daily_activity,daily_sleep, by = c("id", "date"))

Statistical Insights

#Get statistical data from activity_sleep
activity_sleep %>%  
  select(totalsteps, calories, veryactiveminutes, fairlyactiveminutes, lightlyactiveminutes, sedentaryminutes, totaltimeinbed, totalminutesasleep) %>%
  summary()
##    totalsteps       calories    veryactiveminutes fairlyactiveminutes
##  Min.   :   17   Min.   : 257   Min.   :  0.00    Min.   :  0.00     
##  1st Qu.: 5189   1st Qu.:1841   1st Qu.:  0.00    1st Qu.:  0.00     
##  Median : 8913   Median :2207   Median :  9.00    Median : 11.00     
##  Mean   : 8515   Mean   :2389   Mean   : 25.05    Mean   : 17.92     
##  3rd Qu.:11370   3rd Qu.:2920   3rd Qu.: 38.00    3rd Qu.: 26.75     
##  Max.   :22770   Max.   :4900   Max.   :210.00    Max.   :143.00     
##  lightlyactiveminutes sedentaryminutes totaltimeinbed  totalminutesasleep
##  Min.   :  2.0        Min.   :   0.0   Min.   : 61.0   Min.   : 58.0     
##  1st Qu.:158.0        1st Qu.: 631.2   1st Qu.:403.8   1st Qu.:361.0     
##  Median :208.0        Median : 717.0   Median :463.0   Median :432.5     
##  Mean   :216.5        Mean   : 712.1   Mean   :458.5   Mean   :419.2     
##  3rd Qu.:263.0        3rd Qu.: 782.8   3rd Qu.:526.0   3rd Qu.:490.0     
##  Max.   :518.0        Max.   :1265.0   Max.   :961.0   Max.   :796.0
#Calculate Active Minutes per Week
daily_activity$week <-  format(week(daily_activity$date))
#Get statistical data from hourly_steps
hourly_steps %>% 
  select(steptotal, date, mil_time) %>%
  summary()
##    steptotal            date              mil_time        
##  Min.   :    0.0   Min.   :2016-04-12   Length:22099      
##  1st Qu.:    0.0   1st Qu.:2016-04-19   Class :character  
##  Median :   40.0   Median :2016-04-26   Mode  :character  
##  Mean   :  320.2   Mean   :2016-04-26                     
##  3rd Qu.:  357.0   3rd Qu.:2016-05-03                     
##  Max.   :10554.0   Max.   :2016-05-12

Visualize the Data

Daily Activity Analysis

#Plot the relationship between daily total steps taken and calories expended
ggplot(data = daily_activity, mapping = aes(x = totalsteps, y = calories, color = calories)) +
  geom_point() + geom_smooth(method = lm) + 
      labs(title = "Total Steps vs Calories Expended",
       x = "Total Steps",
       y = "Calories") 
## `geom_smooth()` using formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

The plot indicates a positive correlation between steps and calories expended. The higher the step count, the more calories that are burned per day.

Weekly Activity

#Create a visualization to analyze the moderately active minutes per week
ggplot(data = daily_activity, 
       aes(x = fairlyactiveminutes, y = week, color = calories)) +
  geom_jitter() + 
  geom_smooth(method = lm) + 
    labs(title = "Moderately Active Minutes by Week",
       x = "Moderately Active Minutes",
       y = "Week") +
  geom_vline(xintercept = 150, linetype = 'dashed', color = 'red') +
  geom_vline(xintercept = 300, linetype = 'dashed', color = 'red')
## `geom_smooth()` using formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

#Create a visualization to analyze the very active minutes per week
ggplot(data = daily_activity, 
       aes(x = veryactiveminutes , y = week, color = calories)) +
  geom_jitter() + 
  geom_smooth(method = lm) + 
    labs(title = "Very Active Minutes by Week",
       x = "Very Active Minutes",
       y = "Week") +
  geom_vline(xintercept = 75, linetype = 'dashed', color = 'red') +
  geom_vline(xintercept = 150, linetype = 'dashed', color = 'red')
## `geom_smooth()` using formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
##   the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
##   variable into a factor?

The recommended weekly physical activity for adults is between 75 minutes to 150 minutes of vigorous-intensity physical activity, 150 minutes to 300 minutes of moderate or an equivalent combination of moderate and vigorous intensity physical activity per week. The data collected was analyzed to determine how many of the participants weekly activity fell within those recommended activity parameters. The plot shows that only around 7% of the minutes recorded were within the recommended weekly vigorous-intensity physical activity guidelines while a whopping 100% were not getting in the recommended weekly moderate-intensity physical guidelines. Average vigorous-intensity minutes was 20.8 and average moderate-intensity minutes was 13.5.

Steps per Hour

#Create a visualization to show average steps per hour
avg_steps <- hourly_steps %>%
  group_by(mil_time) %>%
  drop_na() %>%
  summarise(mean_hourly_steps = mean(steptotal))
ggplot(data = avg_steps, aes(x = mil_time, y = mean_hourly_steps)) + geom_histogram(stat = "identity", fill='blue', breaks = 6) +
  theme(axis.text.x = element_text(angle = 90)) +
  labs(title="Average Daily Steps per Hour", x = "Hour", y = "Average Daily Steps")
## Warning in geom_histogram(stat = "identity", fill = "blue", breaks = 6):
## Ignoring unknown parameters: `binwidth`, `bins`, `pad`, and `breaks`

The histogram above shows average steps taken per hour. According to the data, the participants were most active during the hours of 6 pm to 7 pm. This is likely due to after work/school activities where they were able to get out to exercise. During the period of 8 am to 5pm, the least activity occurred at 3 pm.

Sleep

#Analyze Time Asleep By Sedentary Activity
ggplot(data = activity_sleep, aes(x = totalminutesasleep, y = sedentaryminutes)) + geom_point() + geom_smooth() + geom_jitter() + labs(title = "Minutes Asleep vs Total Sedentary Minutes", x = "Total Minutes Asleep", y = "Sedentary Minutes") +
  geom_vline(xintercept = 420, linetype = 'dashed', color = 'red') +
  geom_vline(xintercept = 540, linetype = 'dashed', color = 'red')
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

## `geom_smooth()` using method = 'loess' and formula 'y ~ x'

According to data published by the National Institute of Health on March 24, 2022, experts recommend that adults sleep between 7 and 9 hours per night. The data was analyzed to see the relationship between sleep and activity. The curve indicates a negative correlation between sleep and activity. Out of the 410 sleep observations:

  • 46% of the participants logged less than 7 hours of sleep

  • 44% of the participants did get the recommended amount of sleep

  • 10% got over the recommended amount of sleep

The average amount of sleep was 419 minutes which indicates that almost half of the participants didn’t get the minimum amount of sleep. The data indicates that the more sedentary a person is, the less sleep they get.

Recommendations

After analyzing the Fitbit data my recommendations for Bellabeat are as follows:

Thank you for taking the time to review my analysis.