library(tidyverse)
library(readr)
library(ggplot2)
library(tibble)
library(lubridate)
library(janitor)
library(scales)
library(knitr)
library(dplyr)
Sys.setlocale("LC_TIME", "C") # englische Wochentagsnamen unabhaengig vom Systemgebietsschema
## [1] "C"
Zwei Monate Fitbit-Trackerdaten von 35 Nutzenden zeigen, dass der Wellnessmarkt die falsche Gewohnheit verkauft. Der staerkste Zusammenhang in den Daten besteht nicht zwischen Schritten und Gesundheit, sondern zwischen Stillsitzen und schlechtem Schlaf.
tibble(
Kennzahl = c("Ausgewertete Nutzende", "Tage mit erfasster Aktivitaet", "Naechte mit Schlafdaten",
"Mediane Tragequote", "Durchschnittliche Sitzzeit pro Tag",
"Naechte unter 7 Stunden Schlaf", "Tage mit 10.000 Schritten",
"Korrelation: Sitzzeit und Schlaf"),
Wert = 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), " Stunden"),
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 = "Wichtigste Kennzahlen")
| Kennzahl | Wert |
|---|---|
| Ausgewertete Nutzende | 35 |
| Tage mit erfasster Aktivitaet | 1,235 |
| Naechte mit Schlafdaten | 882 |
| Mediane Tragequote | 63% |
| Durchschnittliche Sitzzeit pro Tag | 15.9 Stunden |
| Naechte unter 7 Stunden Schlaf | 49.1% |
| Tage mit 10.000 Schritten | 34.0% |
| Korrelation: Sitzzeit und Schlaf | -0.48 |
Die drei wichtigsten Empfehlungen: das feste Ziel von 10.000 Schritten durch ein anpassungsfaehiges Ziel ersetzen; die Bellabeat-App ueber den Zusammenhang “weniger sitzen, besser schlafen” vermarkten; und Benachrichtigungen auf die beiden Stunden legen, in denen sich die Nutzenden ohnehin bewegen.
Ergebnis 1: Eine klare Zusammenfassung der Geschaeftsaufgabe
Bellabeat ist ein Hightech-Hersteller von gesundheitsorientierten Produkten fuer Frauen, gegruendet 2013 von Urška Sršen und Sando Mur. Die Produktlinie umfasst die Bellabeat-App, den Leaf-Tracker, die Time-Uhr, die Spring-Wasserflasche und eine Abonnement-Mitgliedschaft.
Urška Sršen ist ueberzeugt, dass die Auswertung von Fitnessdaten intelligenter Geraete neues Wachstum erschliessen kann. Sie hat das Marketing-Analyseteam gebeten, Nutzungsdaten von intelligenten Geraeten anderer Hersteller zu analysieren, ein Bellabeat-Produkt auszuwaehlen und Empfehlungen auf hoher Ebene fuer die Marketingstrategie zu erarbeiten.
Ermitteln, wie Verbraucherinnen und Verbraucher intelligente Geraete anderer Hersteller tatsaechlich nutzen, und bestimmen, welche der daraus erkennbaren Verhaltensluecken Bellabeat durch die Vermarktung der Bellabeat-App schliessen kann.
| Beteiligte Person | Rolle | Was sie daraus braucht |
|---|---|---|
| Urška Sršen | Mitgruenderin, Chief Creative Officer | Wachstumschancen mit belastbarer Grundlage |
| Sando Mur | Mitgruender, Geschaeftsleitung | Analytisch fundierte Schlussfolgerungen |
| Marketing-Analyseteam | Kolleginnen und Kollegen | Eine reproduzierbare Basis fuer Kampagnenentscheidungen |
Die Bellabeat-App. Sie verbindet alle Geraete der Produktlinie, ist die Oberflaeche, ueber die Verhaltensimpulse ausgeliefert werden, und traegt das Abonnement. Erkenntnisse ueber Verhalten lassen sich dort am schnellsten und guenstigsten umsetzen.
Ergebnis 2: Eine Beschreibung aller verwendeten Datenquellen
Datensatz auf Kaggle herunterladen
| Punkt | Details |
|---|---|
| Datensatz | FitBit Fitness Tracker Data |
| Bereitgestellt von | Mobius, ueber Kaggle |
| Lizenz | CC0 gemeinfrei, keine Nutzungsbeschraenkung |
| Erhebung | Umfrage ueber Amazon Mechanical Turk, 12. Maerz bis 12. Mai 2016 |
| Einwilligung | Die Teilnehmenden haben der Uebermittlung ihrer Trackerdaten ausdruecklich zugestimmt |
| Datenschutz | Keine Namen, Adressen oder demografischen Angaben; Nutzende nur ueber eine numerische ID identifiziert |
| Speicherung | Lokal in datierten Unterordnern abgelegt; die Originaldateien wurden nicht veraendert |
Da die Daten unter CC0 stehen und keine personenbezogenen Kennungen
enthalten, bestehen fuer diese Analyse keine lizenz-, datenschutz- oder
sicherheitsrechtlichen Huerden. Die gesamte Bereinigung erfolgte an
Kopien; die Quelldateien im .csv-Format wurden nie
ueberschrieben.
Der Datensatz wird als zwei Exportordner fuer aufeinanderfolgende Monate ausgeliefert:
mturkfitbit_export_3.12.16-4.11.16, 12. Maerz bis 11.
April 2016mturkfitbit_export_4.12.16-5.12.16, 12. April bis 12.
Mai 2016Beide enthalten dieselben Messwerte auf vier Detailebenen: Tag,
Stunde, Minute und Sekunde, jeweils im langen (Narrow) und
im breiten Format.
Die meisten veroeffentlichten Analysen dieses Datensatzes
nutzen nur den zweiten Ordner. Beide Ordner haben ein
identisches Schema in dailyActivity_merged.csv, deshalb
kombiniert diese Analyse sie:
tibble(
Umfang = c("Uebliche Analyse mit einem Ordner", "Diese Analyse (beide Ordner)"),
Nutzende = c(33, n_users),
`Abgedeckte Tage` = c(31, as.numeric(max(daily$date) - min(daily$date)) + 1),
`Naechte mit Schlafdaten` = c(410, nrow(sleep))
) |> kable(caption = "Die Kombination beider Exportordner verdoppelt die Datengrundlage annaehernd")
| Umfang | Nutzende | Abgedeckte Tage | Naechte mit Schlafdaten |
|---|---|---|---|
| Uebliche Analyse mit einem Ordner | 33 | 31 | 410 |
| Diese Analyse (beide Ordner) | 35 | 62 | 882 |
Von den 18 verfuegbaren Dateien tragen sechs die Analyse.
dailySteps_merged.csv,
dailyCalories_merged.csv und
dailyIntensities_merged.csv wurden ausgeschlossen, weil sie
Spalten doppeln, die bereits in dailyActivity_merged.csv
enthalten sind. Das wurde geprueft, nicht angenommen:
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("Verglichene Nutzertage:", nrow(check), "\n")
## Verglichene Nutzertage: 578
cat("Schrittzahlen in jeder Zeile identisch:", all(check$StepTotal == check$TotalSteps), "\n")
## Schrittzahlen in jeder Zeile identisch: TRUE
| Verwendete Datei | Zweck |
|---|---|
dailyActivity_merged |
Schritte, Distanz, Intensitaetsminuten und Kalorien pro Tag |
hourlySteps_merged |
Aktivitaetsmuster im Tagesverlauf |
sleepDay_merged |
Naechtliche Schlafsummen, Zeitraum April |
minuteSleep_merged |
Zu Naechten aggregiert, erweitert die Schlafabdeckung auf Maerz |
weightLogInfo_merged |
Nutzung von Funktionen und Verhalten bei manueller Eingabe |
heartrate_seconds_merged.csv wurde ausgeschlossen: 86 MB
fuer nur 14 von 35 Nutzenden, eine zu kleine Teilstichprobe fuer eine
belastbare Aussage. Die minutengenauen Aktivitaetsdateien wurden als
redundant zu den stuendlichen und taeglichen Aggregaten
ausgeschlossen.
tibble(
Kriterium = c("Reliable (verlaesslich)", "Original (urspruenglich)",
"Comprehensive (vollstaendig)", "Current (aktuell)",
"Cited (belegt)"),
Bewertung = c("Schwach", "Schwach", "Schwach", "Mangelhaft", "Gut"),
Details = c(
paste0(n_users, " selbst ausgewaehlte Teilnehmende; keine unabhaengige Pruefung"),
"Weitergabe durch Dritte, nicht von Fitbit oder Bellabeat erhoben",
"Weder Alter noch Geschlecht, Groesse, Ort oder gesundheitliche Ausgangslage erfasst",
"2016 erhoben; Hardware und Nutzererwartungen haben sich seitdem veraendert",
"Klar belegt und offen unter CC0 lizenziert"
)
) |> kable(caption = "ROCCC-Bewertung der FitBit Fitness Tracker Data")
| Kriterium | Bewertung | Details |
|---|---|---|
| Reliable (verlaesslich) | Schwach | 35 selbst ausgewaehlte Teilnehmende; keine unabhaengige Pruefung |
| Original (urspruenglich) | Schwach | Weitergabe durch Dritte, nicht von Fitbit oder Bellabeat erhoben |
| Comprehensive (vollstaendig) | Schwach | Weder Alter noch Geschlecht, Groesse, Ort oder gesundheitliche Ausgangslage erfasst |
| Current (aktuell) | Mangelhaft | 2016 erhoben; Hardware und Nutzererwartungen haben sich seitdem veraendert |
| Cited (belegt) | Gut | Klar belegt und offen unter CC0 lizenziert |
Dieser Datensatz erfuellt ROCCC nicht gut, und das wird vor den Ergebnissen festgehalten, nicht danach. Die Stichprobe ist klein und selbst ausgewaehlt, und sie enthaelt kein Feld zum Geschlecht. Das ist eine erhebliche Einschraenkung bei einer Analyse fuer ein Unternehmen, das ausschliesslich an Frauen verkauft. Jedes Ergebnis unten ist ein Richtungssignal, das an Bellabeats eigenen Daten zu pruefen ist, keine Grundlage fuer Budgetentscheidungen.
Sršen hat angeregt, einen zusaetzlichen Datensatz einzubeziehen, um diese Grenzen zu adressieren. Am wertvollsten waeren Bellabeats eigene App-Telemetriedaten, in denen Geschlecht, Alter und Produktlinie bekannt sind.
Ergebnis 3: Dokumentation jeder Bereinigung und Umformung der Daten
R mit dem tidyverse, aus drei Gruenden: Der Datensatz sprengt auf Minutenebene den komfortablen Arbeitsbereich von Excel (ueber 1,3 Millionen Zeilen); jeder Bereinigungsschritt ist als Code festgehalten und damit reproduzierbar und pruefbar; und dieselbe Umgebung erledigt Bereinigung, Statistik und Visualisierung, ohne zwischen Werkzeugen exportieren zu muessen. Dieses Dokument ist in R Markdown geschrieben, jede genannte Zahl entsteht daher beim Rendern und wird nicht von Hand eingetragen.
tibble(
Pruefung = c("Doppelte Zeilen in der Tagesaktivitaet",
"Doppelte Zeilen im Schlaf (April-Tageswerte)",
"Fehlende Werte in der Tagesaktivitaet",
"Tage mit null Schritten",
"Gewichtseintraege ohne Koerperfettwert"),
Ergebnis = 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 = "Integritaetspruefungen vor jeder Analyse")
| Pruefung | Ergebnis |
|---|---|
| Doppelte Zeilen in der Tagesaktivitaet | 0 |
| Doppelte Zeilen im Schlaf (April-Tageswerte) | 3 |
| Fehlende Werte in der Tagesaktivitaet | 22867493 |
| Tage mit null Schritten | 138 |
| Gewichtseintraege ohne Koerperfettwert | 94 |
period
haelt fest, aus welchem Zeitraum jede Zeile stammt.snake_case mit janitor::clean_names().read_csv() importiert "4/12/2016" als
Zeichenkette; lubridate::mdy() und mdy_hms()
wandeln diese in Datumsobjekte um, damit sie sortiert, gefiltert und
nach Wochentag gruppiert werden koennen.sleepDay_merged.csv enthaelt drei exakt identische Zeilen,
ein dokumentierter Mangel dieses Datensatzes. Die Entfernung von
Duplikaten ueber id und date wurde auf jede
Tabelle angewendet, als Absicherung an der Ordnergrenze.138 Zeilen weisen null Schritte auf, zusammen mit nahezu einem vollen Tag an Sitzminuten. Sie stehen fuer ein Geraet am Ladekabel, nicht fuer eine Person, die 24 Stunden bewegungslos war:
daily |>
group_by(`Art des Tages` = ifelse(is_wear_day, "Geraet getragen", "Null Schritte erfasst")) |>
summarise(Tage = n(),
`Mittlere Sitzminuten` = round(mean(sedentary_minutes)),
`Mittlere Kalorien` = round(mean(calories)), .groups = "drop") |>
kable(caption = "Tage mit null Schritten liegen im Mittel nahe bei 1.440 Sitzminuten, was bestaetigt, dass das Geraet nicht getragen wurde")
| Art des Tages | Tage | Mittlere Sitzminuten | Mittlere Kalorien |
|---|---|---|---|
| Geraet getragen | 1235 | 952 | 2336 |
| Null Schritte erfasst | 138 | 1367 | 1621 |
Diese Zeilen wurden aus allen Aktivitaetsmittelwerten ausgeschlossen, aber fuer die Analyse der Geraetenutzung behalten, wo sie der direkte Beleg dafuer sind, wie oft der Tracker getragen wird. Sie in die Mittelwerte einzubeziehen wuerde die Aktivitaet um rund 10 Prozent zu niedrig ausweisen.
sleepDay_merged.csv existiert nur im April-Ordner. Diese
Datei allein zu nutzen wuerde bedeuten, Aktivitaet aus beiden Monaten
heranzuziehen, Schlaf aber nur aus einem.
minuteSleep_merged.csv deckt den Maerz auf Minutenebene ab.
Dort ist value codiert als 1 = schlafend, 2 = unruhig, 3 =
wach, und jede Zeile steht fuer eine Minute im Bett. Die Zeilen pro
Nacht zu zaehlen ergibt die Zeit im Bett; value == 1 zu
zaehlen ergibt die Schlafminuten. Das sind genau die beiden Groessen,
die auch die April-Datei ausweist.
Erklaerte Annahme: Schlaf ueberschreitet Mitternacht, eine Gruppierung nach Kalenderdatum wuerde eine einzelne Nacht auf zwei Datensaetze aufteilen. Sechs Stunden abzuziehen, bevor das Datum gebildet wird, ordnet die Minuten vor Tagesanbruch der vorherigen Nacht zu. Datensaetze unter 60 Minuten wurden als Fragmente verworfen, nicht als Naechte gewertet. Diese Grenze bei 6 Uhr morgens ist eine Ermessensentscheidung. Den Maerz vollstaendig auszuschliessen reduziert die Datengrundlage von 882 auf 410 Naechte, ohne die Richtung oder Bedeutung eines Ergebnisses zu veraendern.
tibble(
Tabelle = c("Tagesaktivitaet (Tage mit Tragen)", "Schlaf", "Stuendliche Schritte", "Gewichtsprotokolle"),
Zeilen = c(nrow(worn), nrow(sleep), nrow(hourly), nrow(weight)),
Nutzende = c(n_distinct(worn$id), n_distinct(sleep$id),
n_distinct(hourly$id), n_distinct(weight$id)),
`Zeitraum` = c(
paste(format(min(worn$date), "%d.%m."), "bis", format(max(worn$date), "%d.%m.%Y")),
paste(format(min(sleep$date), "%d.%m."), "bis", format(max(sleep$date), "%d.%m.%Y")),
paste(format(min(as_date(hourly$ts)), "%d.%m."), "bis", format(max(as_date(hourly$ts)), "%d.%m.%Y")),
paste(format(min(weight$date), "%d.%m."), "bis", format(max(weight$date), "%d.%m.%Y"))
)
) |> kable(caption = "Bereinigte Tabellen, bereit fuer die Analyse")
| Tabelle | Zeilen | Nutzende | Zeitraum |
|---|---|---|---|
| Tagesaktivitaet (Tage mit Tragen) | 1235 | 35 | 12.03. bis 12.05.2016 |
| Schlaf | 882 | 25 | 11.03. bis 12.05.2016 |
| Stuendliche Schritte | 46008 | 35 | 12.03. bis 12.05.2016 |
| Gewichtsprotokolle | 98 | 13 | 30.03. bis 12.05.2016 |
Ergebnis 4: Eine Zusammenfassung der Analyse
Die Analyse verlief in vier Durchgaengen. Deskriptive Statistik legte die Ausgangswerte fuer Aktivitaet, Schlaf und Kalorien im Vergleich zu veroeffentlichten Gesundheitsrichtwerten fest. Segmentierung auf Nutzerebene gruppierte die Teilnehmenden nach durchschnittlichen Tagesschritten und nach Tragequote, wobei immer zuerst pro Person aggregiert wurde, bevor Personen verglichen wurden, damit Teilnehmende mit mehr erfassten Tagen die Mittelwerte nicht dominieren. Zeitliche Aggregation untersuchte die Aktivitaet nach Tagesstunde und Wochentag. Korrelation und Regression pruefte die Zusammenhaenge zwischen Schritten, Kalorien, Sitzzeit und Schlafdauer.
Die zentrale Ueberraschung: Die Groesse, die am
staerksten mit Schlaf zusammenhaengt, ist nicht die Schrittzahl, sondern
die Sitzzeit, und der Zusammenhang ist rund dreimal
staerker. Das verschiebt die Marketingaussage von “mehr Sport” hin zu
“weniger sitzen”. Das ist eine deutlich niedrigere Huerde fuer die
Nutzenden und eine Aussage, die derzeit kein grosser Wettbewerber
trifft.
sa <- worn |> inner_join(sleep, by = c("id", "date"))
tibble(
Zusammenhang = c("Tagesschritte und verbrannte Kalorien",
"Sitzminuten und verbrannte Kalorien",
"Tagesschritte und Schlafstunden",
"Sitzminuten und Schlafstunden"),
`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 = "Geprüfte Korrelationen. Nur Zusammenhang, die Wirkungsrichtung ist nicht geprueft.")
| Zusammenhang | Pearson r | n |
|---|---|---|
| Tagesschritte und verbrannte Kalorien | 0.563 | 1235 |
| Sitzminuten und verbrannte Kalorien | 0.033 | 1235 |
| Tagesschritte und Schlafstunden | -0.150 | 603 |
| Sitzminuten und Schlafstunden | -0.483 | 603 |
Ergebnis 5: Unterstuetzende Visualisierungen und wichtigste Ergebnisse
period_len <- daily |> group_by(period) |>
summarise(days = as.numeric(max(date) - min(date)) + 1, .groups = "drop")
user_window <- daily |> distinct(id, period) |>
left_join(period_len, by = "period") |>
group_by(id) |> summarise(window_days = sum(days), .groups = "drop")
usage <- worn |> count(id, name = "wear_days") |>
left_join(user_window, by = "id") |>
mutate(wear_rate = wear_days / window_days,
segment = case_when(wear_rate >= 0.80 ~ "Hohe Nutzung (80 bis 100%)",
wear_rate >= 0.50 ~ "Mittlere Nutzung (50 bis 79%)",
TRUE ~ "Geringe Nutzung (unter 50%)") |>
factor(levels = c("Hohe Nutzung (80 bis 100%)", "Mittlere Nutzung (50 bis 79%)",
"Geringe Nutzung (unter 50%)")))
usage_sum <- usage |> count(segment, .drop = FALSE) |> mutate(pct = n / sum(n))
ggplot(usage_sum, aes(reorder(segment, n), n, fill = segment)) +
geom_col(width = .62) +
geom_text(aes(label = paste0(n, " Nutzende (", percent(pct, 1), ")")),
hjust = -0.08, size = 3.8, colour = SLATE) +
coord_flip() + scale_y_continuous(expand = expansion(c(0, .3))) +
scale_fill_manual(values = c(TEAL, MINT, CORAL), guide = "none") +
labs(title = paste0("Die mediane Person trug den Tracker an nur ",
percent(median(usage$wear_rate), 1), " der Tage"),
subtitle = "Tage mit erfassten Schritten, als Anteil am jeweiligen Exportzeitraum",
x = NULL, y = "Anzahl der Nutzenden") + theme_bb
Die mediane Person erfasste an 63% der verfuegbaren Tage Schritte.
Keine einzige teilnehmende Person trug das Geraet an
80% oder mehr der Tage, und 13 trugen es an weniger als der
Haelfte.
Warum das wichtig ist: Ein Tracker, der ein Drittel seines Lebens in einer Schublade verbringt, kann weder zu Schlaf noch zu Stress oder Zyklus verlaessliche Erkenntnisse liefern. Die Nutzungshaeufigkeit ist die bindende Grenze fuer jede weitere Funktion.
user_avg <- worn |> group_by(id) |>
summarise(avg_steps = mean(total_steps), .groups = "drop") |>
mutate(level = case_when(avg_steps < 5000 ~ "Sitzend (unter 5k)",
avg_steps < 7500 ~ "Wenig aktiv (5 bis 7,5k)",
avg_steps < 10000 ~ "Maessig aktiv (7,5 bis 10k)",
TRUE ~ "Aktiv (ueber 10k)") |>
factor(levels = c("Sitzend (unter 5k)", "Wenig aktiv (5 bis 7,5k)",
"Maessig aktiv (7,5 bis 10k)", "Aktiv (ueber 10k)")))
lvl <- user_avg |> count(level, .drop = FALSE) |> mutate(pct = n / sum(n))
ggplot(lvl, aes(level, n, fill = level)) +
geom_col(width = .62) +
geom_text(aes(label = paste0(n, "\n", percent(pct, 1))),
vjust = -0.25, size = 3.6, colour = SLATE) +
scale_y_continuous(expand = expansion(c(0, .25))) +
scale_fill_manual(values = c(CORAL, SAND, MINT, TEAL), guide = "none") +
labs(title = paste0(sum(lvl$n[1:2]), " von ", sum(lvl$n),
" Nutzenden liegen im Mittel unter 7.500 Schritten pro Tag"),
subtitle = "Nutzende gruppiert nach ihrer eigenen mittleren Tagesschrittzahl",
x = NULL, y = "Anzahl der Nutzenden") + theme_bb
Die mittlere Tagesschrittzahl lag bei 8,057, der Median bei 7,623. Nur 34.0% der Tage erreichten 10.000 Schritte.
worn |> group_by(weekday) |>
summarise(avg_steps = mean(total_steps), .groups = "drop") |>
ggplot(aes(weekday, avg_steps, fill = avg_steps)) +
geom_col(width = .68) +
geom_hline(yintercept = 10000, linetype = "dashed", colour = CORAL, linewidth = .7) +
annotate("text", x = 1, y = 10400, label = "Ziel: 10.000 Schritte",
colour = CORAL, size = 3.2, hjust = 0) +
scale_x_discrete(labels = WOCHENTAGE) +
scale_fill_gradient(low = MINT, high = TEAL, guide = "none") +
scale_y_continuous(labels = comma, expand = expansion(c(0, .12))) +
labs(title = "Kein Wochentag erreicht im Mittel das Ziel von 10.000 Schritten",
subtitle = "Der Sonntag ist der am wenigsten aktive Tag",
x = NULL, y = "Mittlere Schrittzahl") +
theme_bb
Warum das wichtig ist: Fuer die Haelfte der Nutzenden ist das voreingestellte Ziel unerreichbar. Ein Ziel, das taeglich verfehlt wird, wirkt nicht mehr motivierend, sondern erinnert an das Scheitern.
mins <- worn |>
summarise(Sitzend = mean(sedentary_minutes), Leicht = mean(lightly_active_minutes),
Maessig = mean(fairly_active_minutes), Intensiv = mean(very_active_minutes)) |>
pivot_longer(everything(), names_to = "intensity", values_to = "minutes") |>
mutate(pct = minutes / sum(minutes),
intensity = factor(intensity, levels = c("Sitzend", "Leicht", "Maessig", "Intensiv")))
ggplot(mins, aes("", minutes, fill = intensity)) +
geom_col(width = .55) +
geom_text(aes(label = ifelse(pct > .03, paste0(intensity, "\n", round(minutes), " Min"), "")),
position = position_stack(vjust = .5), size = 3.6,
colour = "white", fontface = "bold") +
coord_flip() + scale_fill_manual(values = c(SLATE, MINT, SAND, CORAL)) +
labs(title = paste0(percent(mins$pct[mins$intensity == "Sitzend"], 1),
" der erfassten Zeit ist Sitzzeit"),
subtitle = "Mittlere Minuten pro getragenem Tag nach Intensitaetsstufe",
x = NULL, y = "Minuten", fill = NULL) +
theme_bb + theme(axis.text.y = element_blank())
Die Nutzenden kamen im Mittel auf 15.9 Sitzstunden pro erfasstem Tag gegenueber 21.7 intensiv aktiven Minuten. An 55.8% der Tage lag die Summe aus maessig und intensiv aktiver Zeit unter 30 Minuten.
ct <- cor.test(worn$total_steps, worn$calories)
m <- lm(calories ~ total_steps, data = worn)
ggplot(worn, aes(total_steps, calories)) +
geom_point(alpha = .28, colour = TEAL, size = 1.5) +
geom_smooth(method = "lm", se = TRUE, colour = CORAL, fill = SAND) +
scale_x_continuous(labels = comma) + scale_y_continuous(labels = comma) +
labs(title = "Mehr Schritte bedeuten mehr verbrannte Kalorien, aber der Ertrag ist gering",
subtitle = paste0("r = ", round(ct$estimate, 2), ", etwa ",
round(coef(m)[2] * 1000), " Kalorien je zusaetzliche 1.000 Schritte"),
x = "Schritte pro Tag", y = "Verbrannte Kalorien") + theme_bb
Warum das wichtig ist: Der ansprechbare Hebel sind die 15.9 Sitzstunden, nicht die 22 aktiven Minuten. Sitzphasen zu unterbrechen ist fuer die Nutzenden eine deutlich niedrigere Huerde, als zusaetzliche Trainingseinheiten einzuplanen.
by_hour <- hourly |> group_by(hour) |>
summarise(avg_steps = mean(step_total), .groups = "drop")
ggplot(by_hour |> mutate(peak = hour %in% c(12, 13, 17, 18, 19)),
aes(factor(hour), avg_steps, fill = peak)) +
geom_col(width = .78) +
scale_fill_manual(values = c(`FALSE` = GREY, `TRUE` = TEAL), guide = "none") +
labs(title = "Die Aktivitaet erreicht mittags und erneut von 17 bis 19 Uhr ihre Spitzen",
subtitle = "Mittlere Schrittzahl je Tagesstunde, alle Nutzenden zusammengefasst",
x = "Stunde des Tages", y = "Mittlere Schrittzahl") + theme_bb
by_hour |> arrange(desc(avg_steps)) |> head(5) |>
transmute(Stunde = paste0(hour, ":00"), `Mittlere Schrittzahl` = round(avg_steps)) |>
kable(caption = "Die fuenf aktivsten Stunden des Tages")
| Stunde | Mittlere Schrittzahl |
|---|---|
| 19:00 | 555 |
| 18:00 | 550 |
| 12:00 | 534 |
| 14:00 | 506 |
| 17:00 | 500 |
Warum das wichtig ist: Der Zeitpunkt von Benachrichtigungen ist bei den meisten Apps Raterei. In diesen Zeitfenstern sind die Nutzenden ohnehin in Bewegung und am ehesten fuer einen Impuls empfaenglich.
ggplot(sleep, aes(hours_asleep)) +
geom_histogram(binwidth = .5, fill = TEAL, colour = "white") +
geom_vline(xintercept = 7, linetype = "dashed", colour = CORAL, linewidth = .8) +
annotate("text", x = 7.12, y = Inf, vjust = 2, hjust = 0, colour = CORAL, size = 3.4,
label = "7 Stunden = empfohlenes Minimum") +
labs(title = paste0(percent(mean(sleep$hours_asleep < 7), 1),
" der Naechte bleiben unter 7 Stunden Schlaf"),
subtitle = paste0(nrow(sleep), " Naechte von ", n_distinct(sleep$id), " Nutzenden"),
x = "Stunden Schlaf", y = "Naechte") + theme_bb
Der mittlere Schlaf lag bei 6.68 Stunden, mit durchschnittlich 35.2 Minuten, die wach im Bett verbracht wurden.
ggplot(sa, aes(sedentary_minutes, hours_asleep)) +
geom_point(alpha = .3, colour = TEAL, size = 1.5) +
geom_smooth(method = "lm", se = TRUE, colour = CORAL, fill = SAND) +
geom_hline(yintercept = 7, linetype = "dotted", colour = SLATE) +
labs(title = "Je inaktiver der Tag, desto weniger Schlaf in der Nacht",
subtitle = paste0("r = ", round(cor(sa$sedentary_minutes, sa$hours_asleep), 2),
" ueber ", nrow(sa), " zugeordnete Nutzertage"),
x = "Inaktive Minuten", y = "Stunden Schlaf") + theme_bb
Warum das wichtig ist: Das ist der Zusammenhang im Datensatz, aus dem sich am meisten ableiten laesst. Die Aussage lautet nicht “mehr gehen, besser schlafen”, sondern “weniger sitzen, besser schlafen”, eine konkrete, belegbare und unterscheidbare Botschaft.
adopt <- tibble(
feature = c("Aktivitaet (passiv)", "Schlaf (nachts tragen)", "Gewicht (manuelle Eingabe)"),
users = c(n_distinct(daily$id), n_distinct(sleep$id), n_distinct(weight$id))
) |> mutate(pct = users / n_users)
ggplot(adopt, aes(reorder(feature, pct), pct, fill = feature)) +
geom_col(width = .6) +
geom_text(aes(label = paste0(users, " Nutzende (", percent(pct, 1), ")")),
hjust = -0.08, size = 3.8, colour = SLATE) +
coord_flip() +
scale_y_continuous(labels = percent, limits = c(0, 1.3), expand = c(0, 0)) +
scale_fill_manual(values = c(TEAL, SAND, CORAL), guide = "none") +
labs(title = "Die Erfassung bricht ein, sobald manuelle Eingabe noetig ist",
subtitle = "Anteil der Nutzenden, die den jeweiligen Datentyp mindestens einmal erfasst haben",
x = NULL, y = "Anteil der Nutzenden") + theme_bb
64.3% der Gewichtseintraege wurden von Hand eingetippt, mit einem Median von 2 Eintraegen je erfassender Person ueber den gesamten Zeitraum.
Warum das wichtig ist: Jede zusaetzliche Huerde kostet rund ein Drittel der Nutzenden. Funktionen, die manuelle Eingaben verlangen, werden unabhaengig von der Gestaltungsqualitaet nicht angenommen.
Ergebnis 6: Die wichtigsten inhaltlichen Empfehlungen auf hoher Ebene
1. Welche Trends gibt es bei der Nutzung intelligenter
Geraete? Die Geraete werden unregelmaessig getragen, im Median
an 63% der Tage, wobei keine Person 80% ueberschreitet. Die
Aktivitaet bleibt deutlich hinter gesundheitspolitischen Zielen zurueck:
Nur 34.0% der Tage erreichen 10.000 Schritte, und 15.9 Stunden des
durchschnittlich erfassten Tages sind Sitzzeit. Die Aktivitaet
konzentriert sich auf die Mittagszeit und den fruehen Abend. Der Schlaf
ist kurz, 49.1% der Naechte bleiben unter sieben Stunden, und die
Nutzung von Funktionen bricht ein, sobald manuelle Eingaben noetig
werden.
2. Wie lassen sich diese Trends auf Bellabeat-Kundinnen uebertragen? Bellabeat-Kundinnen nutzen dieselben Geraetekategorien zu denselben Zwecken, also gelten dieselben Verhaltensgrenzen. Zwei uebertragen sich am unmittelbarsten. Erstens begrenzt die Tragequote alles: Die Funktionen von Bellabeat zu Stress, Schlaf und Zyklus setzen konsequentes Tragen voraus, weshalb die Schmuckform des Leaf eine echte Einschraenkung adressiert und nicht bloss eine aeusserliche. Zweitens passt der Zusammenhang von Sitzzeit und Schlaf genau zu Bellabeats bestehender Positionierung rund um ganzheitliches Wohlbefinden statt sportlicher Leistung.
3. Wie koennen diese Trends die Marketingstrategie von Bellabeat beeinflussen? Sie verschieben die Botschaft von Leistung zu Machbarkeit. Statt mit Fitbit und Apple ueber Schrittzahlen und Trainingsaufzeichnung zu konkurrieren, wo die Daten zeigen, dass die meisten Nutzenden scheitern, kann Bellabeat die niedrigere, erreichbarere und besser belegte Aussage besetzen: weniger sitzen, um besser zu schlafen.
Nur 34.0% der Tage erreichen 10.000 Schritte, und 18 von 35 Nutzenden liegen im Mittel unter 7.500. Das Anfangsziel jeder Person aus ihrem eigenen Ausgangswert der ersten Woche ableiten und schrittweise anheben.
Werbeaussage: “Ein Ziel, das dich dort abholt, wo du stehst.”
Inaktive Zeit sagt kurzen Schlaf rund dreimal staerker voraus als die Schrittzahl (r = -0.48 gegenueber r = -0.15). Die zentrale App-Funktion um diesen Zusammenhang herum aufbauen und stuendliche Bewegungserinnerungen mit dem Schlafwert verbinden.
Werbeaussage: “Heute weniger sitzen, heute Nacht besser schlafen.”
Die Aktivitaet erreicht ihre Spitzen von 12 bis 14 Uhr und von 17 bis 19 Uhr. Bewegungsimpulse in diese Fenster legen, den Tagesplan in die ruhige Zeit um 7 Uhr und einen Impuls zum Herunterfahren gegen 22 Uhr. Das ist von den drei Empfehlungen die guenstigste in der Umsetzung und die am schnellsten messbare.
Werbeaussage: “Impulse, wenn du ohnehin in Bewegung bist.”
4. Die Bauform des Leaf zum Argument fuer die Bindung machen. Die Tragequote ist die Obergrenze fuer jede weitere Funktion. Den Leaf als “den Tracker, den du nicht ablegst” vermarkten und Tag fuer Tag des Tragens belohnen statt erreichter Schritte.
5. Manuelles Erfassen abschaffen. Die Gewichtserfassung erreichte 37% der Nutzenden. Es ist davon auszugehen, dass jede Funktion mit Handeingabe scheitert. Vorrang haben die automatische Trinkmengen-Synchronisation von Spring und die Anbindung intelligenter Waagen.
6. Mitgliedschaftsinhalte auf die Wochenmitte legen. Schlaf und Aktivitaet sinken beide zur Wochenmitte. Coaching-Inhalte fuer den Sonntagabend und den Dienstagmorgen einplanen.
tibble(
Einschraenkung = c("Stichprobengroesse", "Angaben zum Geschlecht", "Alter der Daten",
"Rekrutierung", "Kausalitaet", "Aggregation der Schlafdaten"),
Details = c(
paste0(n_users, " Teilnehmende, zu wenige fuer eine Verallgemeinerung auf einen Verbrauchermarkt"),
"Nicht erfasst, obwohl Bellabeat ausschliesslich an Frauen verkauft",
paste0("Erhoben von ", format(min(daily$date), "%m/%Y"), " bis ",
format(max(daily$date), "%m/%Y"), ", also neun Jahre alt"),
"Selbst ausgewaehlte MTurk-Freiwillige, vermutlich nicht deckungsgleich mit Bellabeats Kundinnen",
"Der Zusammenhang von Sitzzeit und Schlaf ist eine Assoziation; die Richtung ist ungeprueft",
"Die Maerz-Naechte beruhen auf der Annahme einer Grenze um 6 Uhr morgens"
)
) |> kable(caption = "Grenzen dieser Ergebnisse")
| Einschraenkung | Details |
|---|---|
| Stichprobengroesse | 35 Teilnehmende, zu wenige fuer eine Verallgemeinerung auf einen Verbrauchermarkt |
| Angaben zum Geschlecht | Nicht erfasst, obwohl Bellabeat ausschliesslich an Frauen verkauft |
| Alter der Daten | Erhoben von 03/2016 bis 05/2016, also neun Jahre alt |
| Rekrutierung | Selbst ausgewaehlte MTurk-Freiwillige, vermutlich nicht deckungsgleich mit Bellabeats Kundinnen |
| Kausalitaet | Der Zusammenhang von Sitzzeit und Schlaf ist eine Assoziation; die Richtung ist ungeprueft |
| Aggregation der Schlafdaten | Die Maerz-Naechte beruhen auf der Annahme einer Grenze um 6 Uhr morgens |
Diese Ergebnisse sollten geprueft werden, bevor Budget gebunden wird. Empfohlene naechste Schritte, nach Kosten geordnet:
Dieses Dokument ist eine maschinelle Uebersetzung der englischen Originalfassung, angefertigt mit einem KI-Sprachmodell. Der Originalbericht wurde auf Englisch verfasst; die Analyse, der Code und alle Zahlen stammen aus dieser Originalfassung.
Uebersetzt wurden ausschliesslich der Fliesstext, die Ueberschriften, die Tabellenbeschriftungen und die Diagrammtexte. Funktionsnamen, Variablennamen, Spaltennamen und Dateinamen bleiben auf Englisch, weil sie Bestandteil des Codes und des Datensatzes sind.
Bei Abweichungen zwischen dieser Fassung und dem englischen Original ist das englische Original massgeblich. Diese Uebersetzung wurde nicht von einer muttersprachlichen Fachperson geprueft.
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 bit64_4.8.2
## [21] splines_4.6.1 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 farver_2.1.2 nlme_3.1-169 htmltools_0.5.9
## [45] rmarkdown_2.31 labeling_0.4.3 compiler_4.6.1 S7_0.2.2