Bellabeat is a high-tech manufacturer of health-focused products for women. The CCO and cofounder of Bellabeat, Urska Srsen, believes the company is well positioned to gain additional market share through thoughtful analysis of wearable smart device data. The marketing strategy will use this analysis as guidance.
As a junior data analyst with just 6 months at the company, I have been tasked with analyzing smart device data and developing high-level recommendations to inform marketing strategy. The scope has been limited to a single product line.
I have found a dataset from Fitbit (a wearable watch) that has features similar to Bellabeat’s Time. The dataset contains no demographic information and has a very limited number of unique participants. We assume the sample population is representative, though additional data might prove this assumption inaccurate. The Fitbit dataset is publicly available as Fitbit Fitness Tracker Data.
What follows is a reproducible examination of the data contained within the Fitbit dataset, and how we addressed oversights in cleaning and processing the data when they arose. We highlight identified limitations in the analyzed data, and any problems that should be considered before a go-forward marketing strategy is deployed.
Business Task or Problem
Analyze competitor data to identify and evaluate user trends, opportunity areas, and insights to inform Bellabeat’s go-forward marketing strategy as it relates to our wearable watch, Time. Because Time connects directly to the Bellabeat app, the analysis performed can also inform system changes.
The stakeholders have been identified as the ownership team and the marketing analytics team. The dataset being evaluated is public, so there are no security concerns. If there were an actual gap-to-target, formalized problem-solving would have to occur.
Key Questions:
Kaggle has made the Fitbit Fitness Tracker Data publicly available. I performed analysis on the April–May dataset and focused on daily activity, daily sleep, hourly steps, hourly intensities, calories, and weight. The number of participants who logged weight data was statistically out-of-phase with the other unique-user data, so weight log trends were not included in our analysis of usage trends.
Evaluating the data by day of the week and, where available and relevant, hour by hour, allowed the most granular assessment possible given our notably limited skillset.
A note on the data files: the raw CSVs in the
files folder were originally saved with mismatched names
(each file’s contents did not match its filename, and the weight log was
missing). This was corrected with a content-based rename utility
(fix_fitbit_filenames.R) before running this analysis. See
the README for detail.
Let’s get started!
# The tidyverse bundles readr, dplyr, ggplot2, tidyr, lubridate, etc.
# 'hms' handles time-of-day values; 'here' anchors file paths to the
# project root so this knits the same from scripts/ or interactively.
library(tidyverse)
library(lubridate)
library(hms)
library(here)
activity <- read_csv(here("files", "dailyActivity_merged.csv"))
sleep <- read_csv(here("files", "sleepDay_merged.csv"))
hourly_intensities <- read_csv(here("files", "hourlyIntensities_merged.csv"))
steps <- read_csv(here("files", "hourlySteps_merged.csv"))
weight <- read_csv(here("files", "weightLogInfo_merged.csv"))
head(activity)
## # A tibble: 6 × 15
## Id ActivityDate TotalSteps TotalDistance TrackerDistance
## <dbl> <chr> <dbl> <dbl> <dbl>
## 1 1503960366 4/12/2016 13162 8.5 8.5
## 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
## # ℹ 10 more variables: LoggedActivitiesDistance <dbl>,
## # VeryActiveDistance <dbl>, ModeratelyActiveDistance <dbl>,
## # LightActiveDistance <dbl>, SedentaryActiveDistance <dbl>,
## # VeryActiveMinutes <dbl>, FairlyActiveMinutes <dbl>,
## # LightlyActiveMinutes <dbl>, SedentaryMinutes <dbl>, Calories <dbl>
The datasets have a few easily discernible issues: the date and datetime fields are not in a useful data type for further analysis (the dates/times are character strings), column names vary from dataframe to dataframe, and weekday information cannot be derived without further manipulation.
For activity we need to change ActivityDate
from character to date, then add a column identifying the weekday the
date is associated with.
As identified in the preliminary look (head),
the column ActivityDate is a character string.
There are surely more elegant approaches than the iterative one below,
but the intent is that each transformation is easy to follow.
activity <- activity %>% mutate(ActivityDate = mdy(ActivityDate))
# label = TRUE returns the day name (not a number); abbr = FALSE spells it out.
activity <- activity %>%
mutate(day_of_week = wday(ActivityDate, label = TRUE, abbr = FALSE))
# ActiveHours = all active minutes / 60; SedentaryHours = sedentary minutes / 60.
activity <- activity %>%
mutate(ActiveHours = (VeryActiveMinutes + FairlyActiveMinutes + LightlyActiveMinutes) / 60,
SedentaryHours = SedentaryMinutes / 60)
Lastly, rename the date column. This will assist us later in analysis.
activity <- activity %>% rename(Date = ActivityDate)
Now let’s turn our attention to the sleep data. We will clean it in a similar, iterative way.
We first split SleepDay (which holds date and
time in a single character column) into two columns we can manipulate as
needed.
sleep <- sleep %>% separate(SleepDay, into = c("Date", "Time"), sep = " ")
sleep <- sleep %>% mutate(Date = mdy(Date))
Add a column for day of week.
sleep <- sleep %>%
mutate(day_of_week = wday(Date, label = TRUE, abbr = FALSE))
For an apples-to-apples comparison later, convert sleep minutes to hours, total time in bed to hours, and define the difference between being in bed and being asleep as the winddown time.
sleep <- sleep %>% mutate(SleepHours = (TotalMinutesAsleep / 60),
InBedHours = (TotalTimeInBed / 60),
WindDown = (InBedHours - SleepHours))
# Step 1: separate date and time from the original column.
hourly_intensities <- hourly_intensities %>%
separate(ActivityHour, into = c("ActivityDate", "ActivityTime"), sep = " ", extra = "merge")
# Change the data type of ActivityDate.
hourly_intensities <- hourly_intensities %>%
mutate(ActivityDate = mdy(ActivityDate))
Add a weekday column.
hourly_intensities <- hourly_intensities %>%
mutate(day_of_week = wday(ActivityDate, label = TRUE, abbr = FALSE))
Split out the time so AM and PM are identifiable. Here I use a 24-hour approach.
hourly_intensities <- hourly_intensities %>%
mutate(ActivityTime = as_hms(parse_date_time(ActivityTime, "I:M:S p")))
hourly_intensities <- hourly_intensities %>% rename(Date = ActivityDate)
# Confirm the date data type has been applied:
str(hourly_intensities$Date)
## Date[1:22099], format: "2016-04-12" "2016-04-12" "2016-04-12" "2016-04-12" "2016-04-12" ...
Having been through the cleaning process, let’s cut to the quick with hourly steps.
steps <- steps %>%
separate(ActivityHour, into = c("ActivityDate", "ActivityTime"), sep = " ", extra = "merge") %>%
mutate(ActivityDate = mdy(ActivityDate)) %>%
mutate(ActivityTime = as_hms(parse_date_time(ActivityTime, "I:M:S p"))) %>%
mutate(day_of_week = wday(ActivityDate, label = TRUE, abbr = FALSE)) %>%
rename(Date = ActivityDate)
The weight log data is limited, as seen below. We will use
the unique user/day data in our data viz in the Analyze section. To do
so, we transform the Date column from character to date and
add the day-of-week column.
weight <- weight %>% separate(Date, into = c("Date", "Time"), sep = " ") %>%
mutate(Date = mdy(Date)) %>%
mutate(day_of_week = wday(Date, label = TRUE, abbr = FALSE))
n_distinct(weight$Id)
## [1] 11
Now let’s put our efforts to use, compiling summaries and looking at how the data interacts.
# Average steps and calories by day of week (na.rm removes missing values).
activity_weekly_averages <- activity %>%
group_by(day_of_week) %>%
summarise(
avg_steps = mean(TotalSteps, na.rm = TRUE),
avg_calories = mean(Calories, na.rm = TRUE))
What can we learn about the sleep habits of Fitbit users that can be applied to our strategic marketing initiatives?
# Summary of sleep hours per weekday.
sleep_averages <- sleep %>%
group_by(day_of_week) %>%
summarise(avg_sleep = mean(SleepHours, na.rm = TRUE),
avg_time_in_bed = mean(InBedHours, na.rm = TRUE),
avg_winddown = mean(WindDown, na.rm = TRUE))
Late addition: a winddown percent, to see if there is significant variance.
sleep_averages <- sleep_averages %>%
mutate(winddown_percent = (avg_winddown / avg_time_in_bed) * 100)
To enable a stacked bar graph, transform the sleep data into long format. This lets us visualize the processed data better when we build our graphs.
# Long format is ideal when you want a single column ("metric") to group by.
sleep_averages_long <- sleep_averages %>%
pivot_longer(
cols = c(avg_sleep, avg_winddown),
names_to = "metric",
values_to = "hours"
)
Average the hourly intensity data so we can visualize it.
intensities_averages <- hourly_intensities %>%
group_by(day_of_week, ActivityTime) %>%
summarise(avg_total_intensity = mean(TotalIntensity, na.rm = TRUE))
Average daily steps.
step_averages <- steps %>%
group_by(day_of_week, ActivityTime) %>%
summarise(avg_step_total = mean(StepTotal, na.rm = TRUE))
Our next steps involve full joins to evaluate relationships
between datasets. NOTE: it is important to pass a vector to the
by argument (the c(...)). Otherwise you get
many duplicate rows.
merged_activitySleep <- full_join(activity, sleep, by = c("Id", "Date"))
merged_data_all <- activity %>%
full_join(sleep, by = c("Id", "Date")) %>%
full_join(steps, by = c("Id", "Date")) %>%
full_join(hourly_intensities, by = c("Id", "Date"))
Several users log sleep at a different rate than their activity is recorded. This warrants investigation. It appears the applications requiring the user to do more than wear the watch are used less than the applications that automatically catalog data.
First, let’s look at distinct users per day.
activity_distinct_users_per_day <- activity %>%
group_by(Date) %>%
summarise(activity_distinct_users = n_distinct(Id))
sleep_distinct_users_per_day <- sleep %>%
group_by(Date) %>%
summarise(sleep_distinct_users_per_day = n_distinct(Id))
intensities_distinct_users_per_day <- hourly_intensities %>%
group_by(Date) %>%
summarise(intensities_distinct_users_per_day = n_distinct(Id))
steps_distinct_users_per_day <- steps %>%
group_by(Date) %>%
summarise(steps_distinct_users_per_day = n_distinct(Id))
weight_distinct_users_per_day <- weight %>%
group_by(Date) %>%
summarise(weight_distinct_users_per_day = n_distinct(Id))
activity_distinct_users_per_day <- activity_distinct_users_per_day %>%
rename(distinct_users = names(.)[2]) %>%
mutate(source = "activity")
sleep_distinct_users_per_day <- sleep_distinct_users_per_day %>%
rename(distinct_users = names(.)[2]) %>%
mutate(source = "sleep")
intensities_distinct_users_per_day <- intensities_distinct_users_per_day %>%
rename(distinct_users = names(.)[2]) %>%
mutate(source = "hourly_intensities")
steps_distinct_users_per_day <- steps_distinct_users_per_day %>%
rename(distinct_users = names(.)[2]) %>%
mutate(source = "steps")
weight_distinct_users_per_day <- weight_distinct_users_per_day %>%
rename(distinct_users = names(.)[2]) %>%
mutate(source = "weight")
Create a summary of distinct users per application per day.
combined_distinct_user_data <- bind_rows(
activity_distinct_users_per_day,
sleep_distinct_users_per_day,
intensities_distinct_users_per_day,
steps_distinct_users_per_day,
weight_distinct_users_per_day)
Create a dataframe with usage rates by application. Distinct Ids by application are 33 for activity, steps, and hourly intensities; 24 for sleep; and 8 for weight. The different totals are built into the code.
combined_distinct_data_with_percent <- combined_distinct_user_data %>%
mutate(
percent_of_users = case_when(
source %in% c("activity", "hourly_intensities", "steps") ~ (distinct_users / 33) * 100,
source == "sleep" ~ (distinct_users / 24) * 100,
source == "weight" ~ (distinct_users / 8) * 100
)
) %>%
mutate(
percent_of_users = round(percent_of_users, 0)
)
We need an average use rate to complete our visual.
average_use_rate <- combined_distinct_data_with_percent %>%
group_by(source) %>%
summarise(
average_percent = mean(percent_of_users, na.rm = TRUE)
)