library(tidyverse)
library(readr)
library(ggplot2)
library(tibble)
library(lubridate)
library(janitor)
library(scales)
library(knitr)
library(dplyr)
Sys.setlocale("LC_TIME", "C") # ensuring English weekday names regardless of system locale
## [1] "C"
Two months of Fitbit tracker data from 35 users shows the wellness market has been selling the wrong habit. The strongest relationship in the data is not between steps and health it is between Sitting still and sleeping badly.
tibble(
Metric = c("Users Analysed", "Days of tracked activity", "Nights of sleep",
"Median device wear rate", "Average sedentary time per day",
"Nights under 7 hours of sleep", "Days reaching 10,000 steps",
"Correlation: sedentary time vs sleep"),
Value = c(
as.character(n_users),
comma(nrow(worn)),
comma(nrow(sleep)),
percent(median((worn |> count(id, name = "d") |>
left_join(daily |> distinct(id, period) |>
left_join(daily |> group_by(period) |>
summarise(w = as.numeric(max(date) - min(date)) + 1, .groups = "drop"),
by = "period") |> group_by(id) |>
summarise(w = sum(w), .groups = "drop"), by = "id") |>
mutate(r = d / w))$r), 1),
paste0(round(mean(worn$sedentary_minutes) / 60, 1), " hours"),
percent(mean(sleep$hours_asleep < 7), 0.1),
percent(mean(worn$total_steps >= 10000), 0.1),
round(cor((worn |> inner_join(sleep, by = c("id", "date")))$sedentary_minutes,
(worn |> inner_join(sleep, by = c("id", "date")))$hours_asleep), 2)
)
) |> kable(caption = "Headline figures")
| Metric | Value |
|---|---|
| Users Analysed | 35 |
| Days of tracked activity | 1,235 |
| Nights of sleep | 882 |
| Median device wear rate | 63% |
| Average sedentary time per day | 15.9 hours |
| Nights under 7 hours of sleep | 49.1% |
| Days reaching 10,000 steps | 34.0% |
| Correlation: sedentary time vs sleep | -0.48 |
Top three recommendations: replacing the fixed 10,000-step goal with an adaptive one; Market the Bellabeat app on the sit less sleep better link; And time notifications to the two hours when users are already moving.
Deliverable 1: A clear Summary of the business task
Bellabeat is a high tech manufacturer of health focused products for women, founded in 2013 by Urška Sršen and Sando Mur. The product line comprises the Bellabeat app, the Leaf tracker, the Time watch, the Spring water bottle and a subscription membership.
Urška Sršen believes analysing smart device fitness data could unlock new growth. She has asked the marketing analytics team to analyse usage data from non Bellabeat smart devices, select one Bellabeat product, and produce high level recommendations for marketing strategy.
Identify how consumers actually use non-Bellabeat smart devices, and determine which of the resulting behavioural gaps Bellabeat is positioned to close through its marketing of the Bellabeat app.
| Stakeholder | Role | What they need from this |
|---|---|---|
| Urška Sršen | Cofounder, Chief Creative Officer | Growth opportunities grounded in evidence |
| Sando Mur | Cofounder, executive team | Analytically sound conclusions |
| Marketing analytics team | Colleagues | A reproducible basis for campaign decisions |
The Bellabeat App. It connects every device in the range, is the surface where behavioural nudges are delivered, and drives the membership subscription so insights about behaviour can be acted on there fastest and most cheaply.
Deliverable 2: A Description of all Data sources used
Download the dataset on Kaggle ↗
| Item | Detail |
|---|---|
| Dataset | FitBit Fitness Tracker Data |
| Made available by | Mobius, via Kaggle |
| Licence | CC0 public domain, no restriction on use |
| Collection | Amazon Mechanical Turk survey, 12 March to 12 May 2016 |
| Consent | Participants explicitly consented to submit personal tracker data |
| Privacy | No names, addresses or demographics; users identified by numeric ID only |
| Storage | Downloaded and stored locally in dated subfolders; originals unmodified |
Because the data is CC0 and carries no personal identifiers, there
are no licensing, Privacy or security barriers to this analysis. All
cleaning was done on copies; the source .csv files were
never overwritten.
The dataset ships as two Export folders covering consecutive months:
mturkfitbit_export_3.12.16-4.11.16 - 12 Mar to 11 Apr
2016mturkfitbit_export_4.12.16-5.12.16 - 12 Apr to 12 May
2016Both contain the same Measurements at four Level of details : daily,
hourly, minute and second in long (Narrow) and wide
formats.
Most published Analyses of this Dataset use only the second
folder. Both folders share an identical
dailyActivity_merged.csv schema, so this analysis combines
them:
tibble(
Scope = c("Typical single folder analysis", "This analysis (both folders)"),
Users = c(33, n_users),
`Days covered` = c(31, as.numeric(max(daily$date) - min(daily$date)) + 1),
`Sleep nights` = c(410, nrow(sleep))
) |> kable(caption = "Combining both export folders roughly doubles the evidence base")
| Scope | Users | Days covered | Sleep nights |
|---|---|---|---|
| Typical single folder analysis | 33 | 31 | 410 |
| This analysis (both folders) | 35 | 62 | 882 |
Of the 18 files available, six carry the analysis.
dailySteps_merged.csv,
dailyCalories_merged.csv and
dailyIntensities_merged.csv were excluded as duplicates of
columns already present in dailyActivity_merged.csv
verified, not assumed:
steps_only <- read_csv(file.path(P2, "dailySteps_merged.csv"), show_col_types = FALSE)
activity <- read_csv(file.path(P2, "dailyActivity_merged.csv"), show_col_types = FALSE)
check <- steps_only |> rename(day = ActivityDay) |>
inner_join(activity |> rename(day = ActivityDate), by = c("Id", "day"))
cat("User days compared:", nrow(check), "\n")
## User days compared: 578
cat("Step counts identical in every row:", all(check$StepTotal == check$TotalSteps), "\n")
## Step counts identical in every row: TRUE
| File used | Purpose |
|---|---|
dailyActivity_merged |
Steps, distance, intensity minutes and calories per day |
hourlySteps_merged |
Time of day activity patterns |
sleepDay_merged |
Nightly sleep totals April period |
minuteSleep_merged |
Aggregated to nights to extend sleep coverage into March |
weightLogInfo_merged |
Feature adoption and manual entry Behaviour |
heartrate_seconds_merged.csv was Excluded: 86 MB
covering only 14 of 35 users, too small a subsample to support a
finding. Minute level activity files were excluded as redundant to the
hourly and daily aggregates.
tibble(
Criterion = c("Reliable", "Original", "Comprehensive", "Current", "Cited"),
Assessment = c("Weak", "Weak", "Weak", "Poor", "Good"),
Detail = c(
paste0(n_users, " self selected participants; no independent verification"),
"Third party redistribution, not collected by Fitbit or Bellabeat",
"No age, gender, height, location or health baseline recorded",
"Collected in 2016; wearable hardware and user expectations have since changed",
"Clearly sourced and openly licensed under CC0"
)
) |> kable(caption = "ROCCC assessment of the FitBit Fitness Tracker Data")
| Criterion | Assessment | Detail |
|---|---|---|
| Reliable | Weak | 35 self selected participants; no independent verification |
| Original | Weak | Third party redistribution, not collected by Fitbit or Bellabeat |
| Comprehensive | Weak | No age, gender, height, location or health baseline recorded |
| Current | Poor | Collected in 2016; wearable hardware and user expectations have since changed |
| Cited | Good | Clearly sourced and openly licensed under CC0 |
This data does not ROCCC well, and that is stated before the findings rather than after. The sample is small and self-selected, and it contains no gender field a material limitation when Analysing on behalf of a company selling exclusively to women. Every finding below is a directional signal to be validated against Bellabeat’s own data, not a conclusion to commit budget to.
Sršen suggested considering an additional dataset to address these limits. The most valuable addition would be Bellabeat’s own app telemetry, where gender, age and product line are known.
Deliverable 3: Documentation of any cleaning or manipulation of data
R with the tidyverse, for three reasons: the dataset
exceeds Excel’s comfortable working range at minute level
over 1.3 million rows; every cleaning step is recorded as
code and is therefore reproducible and auditable; and the same
environment handles cleaning, statistics and visualisation without
exporting between tools. This document is written in R Markdown, so
every figure quoted is generated at knit time rather than typed in by
hand.
tibble(
Check = c("Duplicate rows in daily activity",
"Duplicate rows in sleep (April roll up)",
"Missing values in daily activity",
"Days recording zero steps",
"Weight entries with missing body fat value"),
Result = c(
sum(duplicated(daily_raw)),
sum(duplicated(sleep_apr_raw)),
sum(is.na(daily_raw)),
sum(!daily$is_wear_day),
sum(is.na(weight$fat))
)
) |> kable(caption = "Integrity checks Run before any analysis")
| Check | Result |
|---|---|
| Duplicate rows in daily activity | 0 |
| Duplicate rows in sleep (April roll up) | 3 |
| Missing values in daily activity | 22867493 |
| Days recording zero steps | 138 |
| Weight entries with missing body fat value | 94 |
period column preserves the source window for each
row.snake_case using janitor::clean_names().read_csv()
imports "4/12/2016" as a character string;
lubridate::mdy() and mdy_hms() convert these
to date objects so they can be sorted, filtered and grouped by
weekday.sleepDay_merged.csv contains three exact duplicate rows, a
documented flaw in this dataset. removing duplication on id
Plus date was applied to every table as a safeguard at the
folder boundary.138 rows record zero steps alongside close to a full day of sedentary minutes. These represent a device left on a charger, not a participant who remained motionless for 24 hours:
daily |>
group_by(`Day type` = ifelse(is_wear_day, "Device worn", "Zero steps recorded")) |>
summarise(Days = n(),
`Mean sedentary minutes` = round(mean(sedentary_minutes)),
`Mean calories` = round(mean(calories)), .groups = "drop") |>
kable(caption = "Zero step days average close to 1,440 sedentary minutes, confirming non wear")
| Day type | Days | Mean sedentary minutes | Mean calories |
|---|---|---|---|
| Device worn | 1235 | 952 | 2336 |
| Zero steps recorded | 138 | 1367 | 1621 |
These rows were excluded from all activity averages but kept for the device engagement analysis, where they are the direct evidence of how often the tracker is worn. Including them in averages would understate activity by approximately 10%.
sleepDay_merged.csv exists only in the April folder.
Using it alone would draw activity from both months but sleep from one.
minuteSleep_merged.csv covers March at minute level, where
value is coded 1 = asleep, 2 = restless, 3 = awake, and
every row represents one minute in bed. Counting rows per night yields
time in bed; counting value == 1 yields minutes asleep the
same two measures the April file reports.
Assumption declared: sleep crosses midnight, so grouping by calendar date would split a single night into two records. Subtracting six hours before taking the date assigns before dawn minutes to the previous night. Records under 60 Minutes were dropped as fragments rather than nights. This 6 a.m cutoff is a Judgement call; excluding March entirely reduces the evidence base from 882 to 410 nights without changing the direction or meaning of any finding.
tibble(
Table = c("Daily activity (worn days)", "Sleep", "Hourly steps", "Weight logs"),
Rows = c(nrow(worn), nrow(sleep), nrow(hourly), nrow(weight)),
Users = c(n_distinct(worn$id), n_distinct(sleep$id),
n_distinct(hourly$id), n_distinct(weight$id)),
`Date range` = c(
paste(format(min(worn$date), "%d %b"), "-", format(max(worn$date), "%d %b %Y")),
paste(format(min(sleep$date), "%d %b"), "-", format(max(sleep$date), "%d %b %Y")),
paste(format(min(as_date(hourly$ts)), "%d %b"), "-", format(max(as_date(hourly$ts)), "%d %b %Y")),
paste(format(min(weight$date), "%d %b"), "-", format(max(weight$date), "%d %b %Y"))
)
) |> kable(caption = "Cleaned tables ready for Analysis")
| Table | Rows | Users | Date range |
|---|---|---|---|
| Daily activity (worn days) | 1235 | 35 | 12 Mar - 12 May 2016 |
| Sleep | 882 | 25 | 11 Mar - 12 May 2016 |
| Hourly steps | 46008 | 35 | 12 Mar - 12 May 2016 |
| Weight logs | 98 | 13 | 30 Mar - 12 May 2016 |
Deliverable 4: An Analysis Summary
The Analysis Proceeded in four passes. Descriptive statistics established baseline activity, sleep and calorie levels against published health benchmarks. User level segmentation grouped participants by average daily steps and by device wear rate always aggregating per user before comparing users, so that participants with more logged days do not dominate the averages. Temporal aggregation examined activity by hour of day and day of week. Correlation and regression tested the relationships between steps, calories, sedentary time and sleep duration.
The central surprise: the variable most strongly
associated with sleep is not step count but sedentary
time, and the relationship is roughly three times stronger.
This Reframes the marketing proposition from motivating exercise to
reducing sitting a substantially lower bar for the user and a claim no
major competitor is currently making.
sa <- worn |> inner_join(sleep, by = c("id", "date"))
tibble(
Relationship = c("Daily steps vs calories burned",
"Sedentary minutes vs calories burned",
"Daily steps vs hours asleep",
"Sedentary minutes vs hours asleep"),
`Pearson r` = c(
round(cor(worn$total_steps, worn$calories), 3),
round(cor(worn$sedentary_minutes, worn$calories), 3),
round(cor(sa$total_steps, sa$hours_asleep), 3),
round(cor(sa$sedentary_minutes, sa$hours_asleep), 3)
),
n = c(nrow(worn), nrow(worn), nrow(sa), nrow(sa))
) |> kable(caption = "Correlations tested. Association only causal direction is untested.")
| Relationship | Pearson r | n |
|---|---|---|
| Daily steps vs calories burned | 0.563 | 1235 |
| Sedentary minutes vs calories burned | 0.033 | 1235 |
| Daily steps vs hours asleep | -0.150 | 603 |
| Sedentary minutes vs hours asleep | -0.483 | 603 |
Deliverable 6: Top high level content Recommendations
1. What are some trends in smart device usage?
Devices are worn inconsistently a mMedian of 63% of days, with no user
exceeding 80%. Activity falls well short of public health
targets: only 34.0% of days reach 10,000 steps, and 15.9 hours of the
Average tracked day are sedentary. Activity concentrates at lunchtime
and early evening. Sleep is short 49.1% of nights fall under seven hours
and feature adoption drops sharply whenever manual input is
required.
2. How could these trends apply to Bellabeat Customers? Bellabeat customers use the same categories of device for the same purposes, so the same behavioural ceilings apply. Two carry across most directly. First, wear rate caps everything: Bellabeat’s stress, sleep and the cycle features depend on consistent wear, so the Leaf’s Jewellery form factor Addresses a real constraint rather than a cosmetic one. Second, the restful sleep link maps precisely onto Bellabeat’s existing positioning around holistic wellness rather than athletic performance.
3. How could these trends help influence Bellabeat marketing strategy? They shift the message from performance to realism. Rather than competing with Fitbit and Apple on step counts and workout tracking where the data shows most users are failing Bellabeat can own the lower, more achievable and better evidenced proposition of sitting less to sleep better.
Only 34.0% of days reach 10,000 steps and 18 of 35 users average under 7,500. Set each user’s initial target from their own first week baseline and raise it incrementally.
Marketing line: “A goal that meets you where you are.”
Inactive time predicts short sleep roughly three times more strongly than step count does (r = -0.48 against r = -0.15). Build the flagship app feature around this relationship, pairing hourly move reminders with the sleep score.
Marketing line: “Sit less today, sleep better tonight.”
Activity peaks at 12 to 2 p.m. and 5 to 7 p.m. Deliver movement prompts in those windows, the daily plan in the 7 a.m. lull, and a wind down prompt around 10 p.m This is the cheapest of the three to implement and the fastest to measure.
Marketing line: “Nudges when you’re already moving.”
4. Make the Leaf’s form factor the retention pitch. Wear rate is the ceiling on every other feature. Market the Leaf as “the tracker you don’t take off”, and reward day after day worn rather than steps achieved.
5. Eliminate manual logging. Weight logging reached 37% of users. Assume any hand entry feature will fail; Prioritise Spring’s automatic hydration sync and smart scale integration.
6. Position membership content around midweek. Sleep and Activity both dip midweek. Schedule coaching content for Sunday evening and Tuesday morning.
tibble(
Limitation = c("Sample size", "Gender data", "Data age", "Recruitment",
"Causality", "Sleep aggregation"),
Detail = c(
paste0(n_users, " participants too few to generalise to a consumer market"),
"Not recorded, yet Bellabeat sells exclusively to women",
paste0("Collected ", format(min(daily$date), "%b %Y"), " to ",
format(max(daily$date), "%b %Y"), " nine years old"),
"Self selected MTurk volunteers, unlikely to match Bellabeat's customer base",
"The sedentary sleep link is an association; direction is untested",
"March nights rely on a 6 a.m. cutoff assumption (see section 3.5)"
)
) |> kable(caption = "Constraints on these findings")
| Limitation | Detail |
|---|---|
| Sample size | 35 participants too few to generalise to a consumer market |
| Gender data | Not recorded, yet Bellabeat sells exclusively to women |
| Data age | Collected Mar 2016 to May 2016 nine years old |
| Recruitment | Self selected MTurk volunteers, unlikely to match Bellabeat’s customer base |
| Causality | The sedentary sleep link is an association; direction is untested |
| Sleep aggregation | March nights rely on a 6 a.m. cutoff assumption (see section 3.5) |
These findings should be validated before budget is committed. Recommended next steps, in order of cost:
sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=German_Germany.utf8 LC_CTYPE=German_Germany.utf8
## [3] LC_MONETARY=German_Germany.utf8 LC_NUMERIC=C
## [5] LC_TIME=C
##
## time zone: Europe/Berlin
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.51 scales_1.4.0 janitor_2.2.1 lubridate_1.9.5
## [5] forcats_1.0.1 stringr_1.6.0 dplyr_1.2.1 purrr_1.2.2
## [9] readr_2.2.0 tidyr_1.3.2 tibble_3.3.1 ggplot2_4.0.3
## [13] tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 lattice_0.22-9 stringi_1.8.9
## [5] hms_1.1.4 digest_0.6.39 magrittr_2.0.5 evaluate_1.0.5
## [9] grid_4.6.1 timechange_0.4.0 RColorBrewer_1.1-3 fastmap_1.2.0
## [13] Matrix_1.7-5 jsonlite_2.0.0 mgcv_1.9-4 jquerylib_0.1.4
## [17] cli_3.6.6 rlang_1.3.0 crayon_1.5.3 splines_4.6.1
## [21] bit64_4.8.2 withr_3.0.3 cachem_1.1.0 yaml_2.3.12
## [25] otel_0.2.0 tools_4.6.1 parallel_4.6.1 tzdb_0.5.0
## [29] vctrs_0.7.3 R6_2.6.1 lifecycle_1.0.5 snakecase_0.11.1
## [33] bit_4.6.0 vroom_1.7.1 pkgconfig_2.0.3 pillar_1.11.1
## [37] bslib_0.12.0 gtable_0.3.6 glue_1.8.1 xfun_0.60
## [41] tidyselect_1.2.1 rstudioapi_0.19.0 farver_2.1.2 nlme_3.1-169
## [45] htmltools_0.5.9 rmarkdown_2.31 labeling_0.4.3 compiler_4.6.1
## [49] S7_0.2.2