This project analyzes public Fitbit fitness-tracker data to identify
patterns in activity and sleep behavior and translate those patterns
into high-level marketing implications for Bellabeat. The analysis uses
two daily-level datasets: dailyActivity_merged.csv and
sleepDay_merged.csv.
The portfolio version of the analysis follows the case-study framework of Ask, Prepare, Process, Analyze, Share, and Act, while expanding the original notebook with explicit data-quality checks, reproducible transformations, relationship analysis, and interpretation.
Bellabeat is a wellness technology company whose products connect users with information about activity, sleep, stress, hydration, and other aspects of daily wellness. The business task is to use non-Bellabeat smart-device data to understand user behavior and identify insights that can inform Bellabeat’s marketing strategy.
The analysis addresses the original case-study questions:
The analysis focuses on the Bellabeat app as the product context because the app can connect multiple wellness behaviors and translate tracker data into personalized guidance.
The source case study identifies the FitBit Fitness Tracker Data, made available through Mobius, as the principal public dataset. The full source package contains multiple files at daily, hourly, and minute-level resolutions.
For this project, two files were selected:
dailyActivity_merged.csv: daily activity measures
including steps, distance, activity intensity, sedentary minutes, and
calories.sleepDay_merged.csv: daily sleep measures including
sleep records, minutes asleep, and time in bed.The two files are complementary rather than redundant: the activity table provides a broad daily activity profile, while the sleep table adds a second dimension of wellness behavior.
The dataset contains observations from a small group of Fitbit users over approximately one month. The activity file contains 33 distinct users, while the sleep file contains 24 distinct users. The dataset therefore does not represent a broad population sample, and the absence of demographic variables means that findings cannot be assumed to describe Bellabeat’s female customer base specifically.
The data are also from 2016 and therefore should be interpreted as behavioral evidence from an older smart-device ecosystem rather than as a current estimate of consumer behavior.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.4
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.4.4 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.0
## ✔ 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(here)
## here() starts at /Users/mac/Downloads
library(skimr)
library(janitor)
##
## Attaching package: 'janitor'
##
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
library(readr)
library(lubridate)
daily_activity <- read_csv("dailyActivity_merged.csv")
## Rows: 940 Columns: 15
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): ActivityDate
## dbl (14): Id, TotalSteps, TotalDistance, TrackerDistance, LoggedActivitiesDi...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
sleep_day <- read_csv("sleepDay_merged.csv")
## Rows: 413 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): SleepDay
## dbl (4): Id, TotalSleepRecords, TotalMinutesAsleep, TotalTimeInBed
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(daily_activity)
glimpse(daily_activity)
## Rows: 940
## Columns: 15
## $ Id <dbl> 1503960366, 1503960366, 1503960366, 150396036…
## $ ActivityDate <chr> "4/12/2016", "4/13/2016", "4/14/2016", "4/15/…
## $ TotalSteps <dbl> 13162, 10735, 10460, 9762, 12669, 9705, 13019…
## $ TotalDistance <dbl> 8.50, 6.97, 6.74, 6.28, 8.16, 6.48, 8.59, 9.8…
## $ TrackerDistance <dbl> 8.50, 6.97, 6.74, 6.28, 8.16, 6.48, 8.59, 9.8…
## $ LoggedActivitiesDistance <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ VeryActiveDistance <dbl> 1.88, 1.57, 2.44, 2.14, 2.71, 3.19, 3.25, 3.5…
## $ ModeratelyActiveDistance <dbl> 0.55, 0.69, 0.40, 1.26, 0.41, 0.78, 0.64, 1.3…
## $ LightActiveDistance <dbl> 6.06, 4.71, 3.91, 2.83, 5.04, 2.51, 4.71, 5.0…
## $ SedentaryActiveDistance <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ VeryActiveMinutes <dbl> 25, 21, 30, 29, 36, 38, 42, 50, 28, 19, 66, 4…
## $ FairlyActiveMinutes <dbl> 13, 19, 11, 34, 10, 20, 16, 31, 12, 8, 27, 21…
## $ LightlyActiveMinutes <dbl> 328, 217, 181, 209, 221, 164, 233, 264, 205, …
## $ SedentaryMinutes <dbl> 728, 776, 1218, 726, 773, 539, 1149, 775, 818…
## $ Calories <dbl> 1985, 1797, 1776, 1745, 1863, 1728, 1921, 203…
colnames(daily_activity)
## [1] "Id" "ActivityDate"
## [3] "TotalSteps" "TotalDistance"
## [5] "TrackerDistance" "LoggedActivitiesDistance"
## [7] "VeryActiveDistance" "ModeratelyActiveDistance"
## [9] "LightActiveDistance" "SedentaryActiveDistance"
## [11] "VeryActiveMinutes" "FairlyActiveMinutes"
## [13] "LightlyActiveMinutes" "SedentaryMinutes"
## [15] "Calories"
head(sleep_day)
glimpse(sleep_day)
## Rows: 413
## Columns: 5
## $ Id <dbl> 1503960366, 1503960366, 1503960366, 1503960366, 150…
## $ SleepDay <chr> "4/12/2016 12:00:00 AM", "4/13/2016 12:00:00 AM", "…
## $ TotalSleepRecords <dbl> 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ TotalMinutesAsleep <dbl> 327, 384, 412, 340, 700, 304, 360, 325, 361, 430, 2…
## $ TotalTimeInBed <dbl> 346, 407, 442, 367, 712, 320, 377, 364, 384, 449, 3…
colnames(sleep_day)
## [1] "Id" "SleepDay" "TotalSleepRecords"
## [4] "TotalMinutesAsleep" "TotalTimeInBed"
n_distinct(daily_activity$Id)
## [1] 33
n_distinct(sleep_day$Id)
## [1] 24
nrow(daily_activity)
## [1] 940
nrow(sleep_day)
## [1] 413
The activity table contains 940 daily observations from 33 users. The sleep table contains 413 records from 24 users.
A key improvement over the original notebook is that cleaning operations are assigned back to the data frames so that the cleaned objects are actually used in subsequent analysis.
daily_activity <- daily_activity %>%
clean_names() %>%
mutate(activity_date = mdy(activity_date))
sleep_day <- sleep_day %>%
clean_names() %>%
mutate(sleep_day = mdy_hms(sleep_day))
sum(is.na(daily_activity))
## [1] 0
sum(is.na(sleep_day))
## [1] 0
sum(duplicated(daily_activity))
## [1] 0
sum(duplicated(sleep_day))
## [1] 3
sleep_day %>%
count(id, sleep_day) %>%
filter(n > 1)
The supplied files contain no missing values. The activity table has no duplicate rows. The sleep table contains three exact duplicate records; these are removed before merging.
sleep_day <- sleep_day %>%
distinct()
sleep_day <- sleep_day %>%
distinct(id, sleep_day, .keep_all = TRUE)
The activity dataset contains some zero-step days and days with 1,440 sedentary minutes. These observations are retained rather than automatically deleted because a tracker may legitimately record a day with no detected steps. They should, however, be considered when interpreting averages.
mean(daily_activity$total_steps == 0)
## [1] 0.08191489
mean(daily_activity$sedentary_minutes == 1440)
## [1] 0.08404255
mean(daily_activity$calories == 0)
## [1] 0.004255319
daily_activity %>%
select(total_steps,
total_distance,
very_active_minutes,
fairly_active_minutes,
lightly_active_minutes,
sedentary_minutes,
calories) %>%
summary()
## total_steps total_distance very_active_minutes fairly_active_minutes
## Min. : 0 Min. : 0.000 Min. : 0.00 Min. : 0.00
## 1st Qu.: 3790 1st Qu.: 2.620 1st Qu.: 0.00 1st Qu.: 0.00
## Median : 7406 Median : 5.245 Median : 4.00 Median : 6.00
## Mean : 7638 Mean : 5.490 Mean : 21.16 Mean : 13.56
## 3rd Qu.:10727 3rd Qu.: 7.713 3rd Qu.: 32.00 3rd Qu.: 19.00
## Max. :36019 Max. :28.030 Max. :210.00 Max. :143.00
## lightly_active_minutes sedentary_minutes calories
## Min. : 0.0 Min. : 0.0 Min. : 0
## 1st Qu.:127.0 1st Qu.: 729.8 1st Qu.:1828
## Median :199.0 Median :1057.5 Median :2134
## Mean :192.8 Mean : 991.2 Mean :2304
## 3rd Qu.:264.0 3rd Qu.:1229.5 3rd Qu.:2793
## Max. :518.0 Max. :1440.0 Max. :4900
Across 940 daily activity observations, the mean number of steps is approximately 7,638 per day. Mean sedentary time is approximately 991 minutes per day, while the mean is approximately 21 minutes of very active time, 14 minutes of fairly active time, and 193 minutes of lightly active time.
The activity profile is therefore characterized by a substantial amount of sedentary time relative to recorded active time.
sleep_day %>%
select(total_sleep_records,
total_minutes_asleep,
total_time_in_bed) %>%
summary()
## total_sleep_records total_minutes_asleep total_time_in_bed
## Min. :1.00 Min. : 58.0 Min. : 61.0
## 1st Qu.:1.00 1st Qu.:361.0 1st Qu.:403.8
## Median :1.00 Median :432.5 Median :463.0
## Mean :1.12 Mean :419.2 Mean :458.5
## 3rd Qu.:1.00 3rd Qu.:490.0 3rd Qu.:526.0
## Max. :3.00 Max. :796.0 Max. :961.0
The mean recorded sleep duration is approximately 419 minutes, or 6.99 hours, while mean time in bed is approximately 459 minutes, or 7.64 hours.
The difference between time in bed and minutes asleep represents time spent in bed but not recorded as asleep. The mean difference is approximately 39 minutes.
ggplot(daily_activity,
aes(x = total_steps, y = sedentary_minutes)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Daily Steps and Sedentary Time",
x = "Total steps per day",
y = "Sedentary minutes per day"
)
## `geom_smooth()` using formula = 'y ~ x'
The relationship is negative: days with more steps tend to have fewer sedentary minutes. Pearson’s correlation is approximately r = -0.33, indicating a modest negative association. The relationship is not strong enough to imply that increasing steps alone determines sedentary behavior, but it supports the idea that activity and sedentary behavior are related dimensions of daily behavior.
This is useful for segmentation because Bellabeat could distinguish between users who are already active and users whose main opportunity is reducing prolonged sedentary time.
ggplot(daily_activity,
aes(x = very_active_minutes, y = calories)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Very Active Time and Calories Burned",
x = "Very active minutes per day",
y = "Calories burned per day"
)
## `geom_smooth()` using formula = 'y ~ x'
Very active minutes have a moderate positive relationship with calories burned (r ≈ 0.62). This indicates that days with more vigorous activity tend to coincide with higher recorded energy expenditure. This is a stronger relationship than the steps-versus-sedentary association and provides a clear example of how activity intensity can be used as a meaningful behavioral signal.
ggplot(sleep_day,
aes(x = total_minutes_asleep, y = total_time_in_bed)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Sleep Duration and Time in Bed",
x = "Minutes asleep",
y = "Minutes in bed"
)
## `geom_smooth()` using formula = 'y ~ x'
Sleep duration and time in bed are very strongly associated (r ≈ 0.93). This is expected because time asleep is a major component of time in bed, but the difference between the two variables is still informative: users can spend considerable time in bed without that entire period being recorded as sleep.
Before combining the datasets, dates are standardized and the tables
are joined by user ID and date. This is important
because joining only on Id would match each user’s sleep
observations to every activity day for that user and would artificially
duplicate observations.
combined_data <- daily_activity %>%
inner_join(
sleep_day,
by = c("id" = "id", "activity_date" = "sleep_day")
)
ggplot(combined_data,
aes(x = total_minutes_asleep, y = total_steps)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Sleep Duration and Daily Steps",
x = "Minutes asleep",
y = "Total steps"
)
## `geom_smooth()` using formula = 'y ~ x'
The matched daily observations show a weak negative association between sleep duration and steps (r ≈ -0.19). This association is small in practical terms and should not be interpreted causally. At the individual-user level, the relationship is even less conclusive, so the evidence does not support a strong claim that people who sleep longer necessarily walk less or more.
The more defensible business insight is that sleep and activity are related wellness dimensions that can be monitored together, rather than treated as independent behaviors.
daily_activity %>%
mutate(day_of_week = weekdays(activity_date)) %>%
group_by(day_of_week) %>%
summarise(
average_steps = mean(total_steps),
observations = n()
)
Average recorded steps are highest on Saturday and Tuesday in this sample and lowest on Sunday. The weekday pattern suggests that activity is not uniform across the week.
Because the differences are observational and the sample is small, these results should be used as a hypothesis for marketing experimentation rather than as a universal behavioral rule.
Finding 1 — Sedentary behavior is a meaningful component of
daily activity.
The average day contains approximately 991 sedentary minutes, and higher
sedentary time is associated with fewer steps. This suggests an
opportunity for interventions focused not only on exercise but also on
breaking up sedentary periods.
Finding 2 — Vigorous activity is strongly connected with
recorded calorie expenditure.
Very active minutes show a moderate positive association with calories
burned. Activity intensity can therefore provide a useful behavioral
signal for personalized feedback.
Finding 3 — Sleep duration is generally below eight hours in
the observed records.
The mean sleep duration is approximately 6.99 hours. Only about 28% of
recorded sleep observations reach eight hours or more, while about 24%
are below six hours. These descriptive results indicate substantial
variation in sleep behavior.
Finding 4 — Sleep and activity should be considered
together.
The matched data show only a weak negative relationship between sleep
duration and daily steps. Rather than supporting a simple “more sleep
means fewer steps” conclusion, the data suggest that sleep and physical
activity should be treated as complementary dimensions of wellness.
Finding 5 — Weekly behavior varies.
Average steps differ by day of week, with Saturday showing the highest
average in this sample and Sunday the lowest. This creates a potential
basis for time-sensitive engagement strategies.
The data suggest that users generate multiple behavioral signals—activity, sedentary time, and sleep—that can be interpreted together. For Bellabeat, this supports positioning the app not merely as a tracker, but as a tool that helps users understand relationships among everyday wellness behaviors.
A practical segmentation framework could distinguish:
The available data do not contain demographic information, so these should be treated as behavioral segments, not demographic segments.
Marketing content could emphasize small, measurable behavior changes:
The observed day-of-week differences suggest an opportunity to test whether engagement messages perform differently across the week. For example, Bellabeat could experiment with weekend movement prompts or end-of-week wellness summaries.
These recommendations are hypotheses derived from the observed sample and should be validated through current Bellabeat product data and controlled marketing experiments.
Several limitations constrain the conclusions:
For future analysis, current Bellabeat data, demographic information, longer observation windows, and more complete sleep/activity coverage would materially strengthen the evidence.
This analysis demonstrates an end-to-end R workflow for transforming public smart-device data into business insights. The evidence indicates that sedentary behavior, activity intensity, sleep duration, and weekly activity patterns are useful dimensions for understanding wellness behavior.
The strongest practical opportunity is not to optimize one metric in isolation, but to use multiple behavioral signals to deliver personalized and context-aware guidance. For Bellabeat, this supports a marketing strategy centered on personalized wellness coaching, behavioral segmentation, and timely engagement.
The findings should be regarded as hypothesis-generating rather than causal or population-representative. Their primary value is demonstrating how a reproducible analytical workflow can move from raw device data to interpretable business questions, evidence-based insights, and testable marketing recommendations.
The analysis is implemented in R using tidyverse,
janitor, lubridate, skimr,
here, and readr.
The portfolio version explicitly records the data-cleaning decisions, validates duplicate and missing records, standardizes dates before joining, and joins activity and sleep observations at the user-date level.
The project follows the original Google Data Analytics case-study structure:
Ask → Prepare → Process → Analyze → Share → Act
The original case study asks analysts to identify the business task, describe data sources, document cleaning, summarize analysis, communicate findings visually, and provide high-level recommendations.