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.
I will analyze Fitbit data to identify trends in competitor smart device usage and analyze the data to see if the trends identified could apply to Bellabeat customers. The trends identified during the analysis will identify opportunities for growth and provide a marketing plan that will target new areas for expansion.
Questions that guide this analysis include:
What are some trends in smart device usage?
How could these trends apply to Bellabeat customers?
How could these trends help influence Bellabeat marketing strategy?
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 .
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.
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.
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
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")
#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.
#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
#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
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
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 Sets
activity_sleep <- merge(daily_activity,daily_sleep, by = c("id", "date"))
#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
#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.
#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.
#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.
#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.
After analyzing the Fitbit data my recommendations for Bellabeat are as follows:
The majority of the participants did not meet the minimum activity guidelines per week. This is an alarming trend that indicates most activity is sedentary or light. One way to target customers is to implement activity alarms that alert users of their activity progress to meet at least 30 minutes of moderate activity five (5) days a week or 15 minutes of vigorous activity five (5) days a week. This could encourage the users to create goals for daily activity that will improve their overall health.
The average number of steps participants completed each day was 8,515. Bellabeat can create hourly reminders for it’s users to get up and move throughout the day in order to increase their daily step counts and activity.
49% of the Fitbit users did not get at least 7 hours of sleep which is the minimum requirement set by the National Heart, Lung and Blood Institute. An increase in daily activity could help improve sleep duration and quality. The more sedentary a person is throughout the day, the less sleep they get at night. In order to improve sleep at night, Bellabeat can create monthly activity programs for users to compete against other Bellabeat users or set goals that encourage more activity throughout the day to improve sleep.