zum Blättern · Enter öffnet · Esc schließt

Kontakt aufnehmen.

Projekte

Cyclistic in R: Kunden vs. Abonnenten (Capstone 2023).

Demo15. Juli 2026

Bereich
Mobility
Umsetzung
März 2023 (Capstone)
Kompetenz
Data Engineering
Stack
R · tidyverse · Jupyter · Git

Ergebnis

  • 3.603.082 Fahrten des Jahres 2018 zu einem Analyseframe zusammengeführt
  • Klare Verhaltensmuster: Abonnenten pendeln unter der Woche, Gelegenheitskunden am Wochenende
  • Heatmaps über Chicago: Bahnhof vs. Ufer je nach Nutzertyp, Wochentag und Tageszeit
  • Offenes Notebook auf GitHub; Grafiken und Walkthrough hier dokumentiert

Dieser Fall ist kein Kundenprojekt und keine aktuelle Produktion. Es ist der Google Data Analytics Capstone (Case Study 1, «Cyclistic»), den ich im März 2023 in R abgeschlossen habe - veröffentlicht hier im Juli 2026 als Rückblick, weil im Portfolio sonst keine R-Arbeit stand und weil der Fall gut zeigt, was man mit öffentlicher Mobility-Daten und dem tidyverse herausholt.

Die Business-Frage lautete damals: Worin unterscheiden sich Abonnenten («Subscriber») und Gelegenheitskunden («Customer»)? Und: Wie könnte der fiktive Anbieter Cyclistic mehr Jahresabos verkaufen? Die Datenbasis ist der öffentliche Divvy-Tripdatensatz (Chicago); ich habe das Jahr 2018 gewählt, weil dort neben Stationsnamen noch Geschlecht und Geburtsjahr der Abonnenten lagen - Felder, die in neueren Exporten aus Datenschutzgründen fehlen.

Quellcode: KyryloY/DA-cyclistic (Jupyter-Notebook mit IRkernel). Rohdaten: Divvy trip data. Die Analyse folgt der Capstone-Vorlage von Kevin Hartman; Zahlen und Grafiken stammen aus meinem eigenen Lauf vom März 2023.

Die Ausgangslage

Cyclistic (in der Zertifikatsaufgabe der Deckname für ein Bike-Share-Unternehmen nach Divvy-Vorbild) will den Anteil der Jahresabos steigern. Marketing braucht dafür keine Vermutung, sondern Unterschiede im Nutzungsverhalten: wann fahren welche Gruppen, wie lange, wo starten sie?

Für die Capstone-Aufgabe war der Stack bewusst «klassische Data Analytics»: R, Import, Bereinigung, Aggregation, ggplot2, kurze Empfehlungen an Stakeholder. Kein ML-Produkt, keine Pipeline im heutigen Sinn - ein Analyse-Walkthrough, der die Frage beantwortet und die Arbeit nachvollziehbar macht.

Daten und Vorbereitung

Vier Quartals-CSV für 2018:

d01 <- read.csv("Divvy_Trips_2018_Q1.csv")
d02 <- read.csv("Divvy_Trips_2018_Q2.csv")
d03 <- read.csv("Divvy_Trips_2018_Q3.csv")
d04 <- read.csv("Divvy_Trips_2018_Q4.csv")

Die Spaltennamen waren nicht einheitlich. Q1 kam mit langen, punktierten Headers (X01...Rental.Details.Rental.ID), Q2 und Q4 mit kurzen Namen (trip_id, usertype, birthyear). Bevor rbind greifen konnte, musste jedes Quartal auf denselben Kanon umbenannt werden:

d02 <- d02 %>%
  rename(
    rental_id = trip_id,
    ended_at = end_time,
    started_at = start_time,
    bike_id = bikeid,
    duration_sec = tripduration,
    start_station_id = from_station_id,
    start_station_name = from_station_name,
    end_station_id = to_station_id,
    end_station_name = to_station_name,
    user_type = usertype,
    user_gender = gender,
    user_bd = birthyear
  )

Danach:

df <- rbind(d01, d02, d03, d04)

3.603.082 Zeilen in einem Frame. Für die Karten fehlten in den 2018er Dateien Koordinaten. Ich habe sie aus einem neueren Monatsfile (202105-divvy-tripdata.csv) über den Stationsnamen zugezogen:

d05 <- read.csv("202105-divvy-tripdata.csv")
df$start_lat <- d05$start_lat[match(df$start_station_name, d05$start_station_name)]
df$start_lng <- d05$start_lng[match(df$start_station_name, d05$start_station_name)]

Das funktioniert nur, solange Stationsnamen stabil bleiben - im Rückblick einer der schwächsten Schritte (dazu unten mehr). Aus Start- und Endkoordinaten kam eine Luftlinie in Metern:

df <- df %>%
  rowwise() %>%
  mutate(distance = round(
    geosphere::distGeo(c(start_lng, start_lat), c(end_lng, end_lat))
  ))

Dauer in Minuten und eine bedingte «Geschwindigkeit» (Luftlinie, nicht Routenlänge):

df$duration <- as.integer(
  difftime(as_datetime(df$ended_at), as_datetime(df$started_at), units = "mins")
)
df$speed <- abs(round((df$distance * 0.06) / as.numeric(df$duration)))

Zusätzlich: Monat, Wochentag, Stunde; Altersgruppen nur für Abonnenten mit Geburtsjahr; Faktor-Spalten für ggplot. Packages damals: tidyverse, lubridate, janitor, geosphere, ggmap.

Analyse: Kunden vs. Abonnenten

Drei Schnitte - Jahr, Woche, Tag - mit derselben Frage: wie oft fährt welche Gruppe?

Säulendiagramm: Fahrten pro Monat für Customer und Subscriber
Fahrten pro Monat. Im Sommer steigen beide Kurven - bei Gelegenheitskunden um ein Vielfaches stärker als bei Abonnenten.

Im warmen Halbjahr fahren beide mehr. Bei Customers war der Sprung extrem (in der damaligen Auswertung grob fünfzigfach zwischen Tief- und Hochsaison). Abonnenten bleiben das ganze Jahr sichtbarer - typisch für Pendler, die nicht nur bei Sonnenschein fahren.

Säulendiagramm: Fahrten nach Wochentag für Customer und Subscriber
Wochentag: Abonnenten unter der Woche, Gelegenheitskunden am Wochenende.

Unter der Woche und am Wochenende trennen sich die Gruppen klar. Abonnenten dominieren Mo-Fr; Samstag und Sonntag gehören den Gelegenheitskunden. Das ist das stärkste, einfach kommunizierbare Muster des Capstones.

Säulendiagramm: Fahrten nach Stunde für Customer und Subscriber
Tagesverlauf: Abonnenten mit Morgen- und Abendspitze, Customers flacher und später.

Der Code für den Monats-Schnitt (gekürzt, Farben und Labels wie im Notebook):

df %>%
  group_by(user_type, month) %>%
  summarise(
    total_rides = n(),
    average_duration_mins = mean(duration)
  ) %>%
  ggplot(aes(
    x = factor(month, levels = rev(levels(month))),
    y = total_rides,
    fill = user_type
  )) +
  geom_col(position = "dodge") +
  coord_flip() +
  labs(
    x = "Month",
    y = "Number of Rides",
    title = "Rides per Month",
    fill = "User type"
  )

Geschlecht und Alter

Nur bei Abonnenten lagen Geschlecht und Geburtsjahr vor. Vier Altersbänder: unter 25, 25-50, 51-75, über 75. Dieselben drei Zeitschnitte noch einmal.

Fahrten nach Monat, Alter und Geschlecht bei Abonnenten
Abonnenten nach Monat, Alter und Geschlecht: Männer etwa dreimal so häufig wie Frauen, in jeder Saison.
Fahrten nach Wochentag, Alter und Geschlecht bei Abonnenten
Unter der Woche fahren Männer deutlich mehr; bei Frauen ist die Woche flacher.
Fahrten nach Stunde, Alter und Geschlecht bei Abonnenten
Tagesgang nach Geschlecht und Alter: Abendspitze (etwa 16-18 Uhr) besonders bei Männern in der Gruppe 25-50.

Damals habe ich das als «Muster» berichtet. Heute würde ich denselben Block vorsichtiger rahmen: die Felder sind weg aus den aktuellen Public Feeds, die Erhebung war unvollständig (leere Werte), und Geschlechterdifferenzen sind ohne Kontext leicht überinterpretiert. Für den Capstone 2023 waren sie Teil der Aufgabenstellung - für ein Kundenprojekt 2026 wären sie eher eine ethische und datenschutzliche Prüfung wert als ein Marketing-Hebel.

Distanz, Dauer, Tempo

Die mittlere Luftlinie Start-Ziel lag bei etwa zwei Kilometern und hing kaum von Geschlecht, Alter oder Wochentag ab:

Mittlere Distanz nach Wochentag und Altersgruppe
Mittlere Luftlinie nach Wochentag und Alter - wenig Variation um ~2 km.

Die mittlere Dauer bei Stammkunden (besonders 25-50) lag bei etwa 13 Minuten:

Mittlere Fahrtdauer nach Wochentag und Altersgruppe
Mittlere Dauer in Minuten: kurze Alltagsfahrten, nicht Touren.

Beim Filtern auf extrem lange Ausleihen tauchte eine eigene Gruppe auf - Fahrten über einen Tag, oft eher «Vergessen zurückgeben» als Reise:

Histogramm sehr langer Ausleihen über 24 Stunden
Ausleihen über 24 Stunden: Ausreißer, die Mittelwerte verzerren können.

Die «Geschwindigkeit» aus Luftlinie und Dauer war von Anfang an mit Vorsicht zu genießen - und die Altersgruppen lagen dicht beieinander (~10-11 km/h):

Verteilung der bedingten Geschwindigkeit nach Altersgruppe
Bedingte Geschwindigkeit nach Alter - Differenzen klein, Aussagekraft begrenzt.
Verteilung der bedingten Geschwindigkeit nach Geschlecht
Männer und Frauen: Differenz nur etwa 1 km/h in diesem Proxy.

Karten

Geodaten waren der Grund, warum ich genau diesen Datensatz gewählt habe. Über ggmap und Google-Maps-Kacheln entstanden Dichtekarten der Startpunkte.

Heatmap Chicago: Startpunkte von Abonnenten und Gelegenheitskunden
Abonnenten vs. Customers: unterschiedliche Schwerpunkte in der Stadt.

Kern des Overlay-Codes:

p <- ggmap(get_googlemap(
  center = c(lon = -87.61536, lat = 41.88473),
  zoom = 14, scale = 4,
  maptype = "terrain", color = "color"
))

p +
  stat_density2d(
    aes(x = start_lng, y = start_lat, fill = ..level..),
    alpha = 0.65, bins = 30, data = df,
    geom = "polygon"
  ) +
  facet_wrap(~ user_type)

Wochentag und Tageszeit verschieben die Hotspots:

Heatmap Dienstag vs. Samstag
Dienstag vs. Samstag: unter der Woche Bahnhof und Geschäftsviertel, am Wochenende stärker am Ufer.
Heatmap zu vier Tageszeiten: Morgen, Mittag, Abend, Nacht
Vier Tageszeiten: morgens Stationen, mittags Ufer/Pier, abends wieder Stationen und Business, Uferaktivität bis spät.

Typische Pendler-Routen einzelner Abonnenten konnte ich nicht rekonstruieren - dafür fehlen stabile Nutzer-IDs in dem Export.

Schlussfolgerungen 2023

Abonnenten und Gelegenheitskunden haben leicht erkennbare, getrennte Muster - zeitlich und räumlich. Abonnenten wirken wie Pendler (Werktage, Rush Hour, Bahnhof/Business). Customers wirken wie Freizeitnutzer (Wochenende, Saison, Ufer).

Die damalige Empfehlung an Marketing: nicht jeden Gelegenheitskunden zum Abo überreden wollen, sondern atypische Customers suchen, deren Fahrten schon wie Abonnenten aussehen (Werktage, kurze Fahrten, Stationsachsen) - und denen gezielt ein Jahresabo anbieten. Als Ausblick stand im Notebook bereits «Machine Learning zur Früherkennung potenzieller Abonnenten», sofern Kontakte vorliegen.

Das war für ein Zertifikats-Capstone eine saubere Story. Für ein reales Unternehmen wäre der nächste Schritt gewesen: Experiment (A/B auf dem Angebot), Messung der Conversion, und harte Trennung zwischen deskriptiver Analyse und kausaler Behauptung.

Was ich 2026 anders machen würde

Dieser Abschnitt ist der eigentliche Grund, den Fall jetzt zu publizieren. Der Capstone von 2023 bleibt, was er war. Die Revision sagt, was ich heute an derselben Aufgabe ändern würde - nicht, um das Notebook umzuschreiben, sondern um den Abstand von drei Jahren ehrlich zu machen.

Reproducibility zuerst

Das Repo enthielt lange nur ein Notebook mit Leerzeichen im Dateinamen und ohne README. Outputs waren nicht gespeichert; Grafiken lagen auf einer alten WordPress-Installation. Heute: klares Layout (data/, R/, reports/), renv.lock, ein Download-Skript für die Quartals-CSV, Quarto oder reine .R-Skripte statt eines 70-Zellen-Notebooks, und alle Figures im Repo.

Keine fragilen Joins über Stationsnamen

Koordinaten aus 2021 an Fahrten von 2018 über den Namen zu hängen, bricht bei Umbenennungen und Doppelstationen. Heute: offizielles Stationsverzeichnis oder ein Export, der Koordinaten schon im Tripfile hat; Match über stabile IDs; Abdeckung und Fehlerrate des Joins als eigene Kennzahl.

OSM statt Google-Maps-API

ggmap + API-Key war 2023 bequem und für ein Zertifikat ok. Für ein öffentliches Portfolio und für Wiederholbarkeit ist das die falsche Abhängigkeit (Key, Nutzungsbedingungen, Kacheln). Heute: sf + OpenStreetMap-Basiskarte oder vorgerenderte, selbst gehostete Kacheln - und die Heatmaps als WebP im gleichen Repo wie der Text.

Proxy-Metriken benennen, nicht verkaufen

Luftlinie ist nicht die gefahrene Strecke; «speed» aus Luftlinie und Dauer ist ein Proxy mit systematischem Bias (Umwege, Pausen, Einbahnstraßen). Heute: Metrik im Text als crow_flight_m und proxy_kmh führen, Ausreißer-Policy dokumentieren (Negativdauer, >24 h), und keine Alters-«Geschwindigkeits»-Story bauen, die die Messgrenzen überschreitet.

Weniger Demografie, mehr Verhalten

Geschlecht und Alter wirkten im Capstone wie zusätzliche Dimensionen. Neuere Public Feeds lassen sie weg - aus gutem Grund. Heute: Fokus auf verhaltensbasierte Segmente (Werktag/Wochenende, Dauer, Station-Cluster, Wiederkehrrate über Pseudonyme, falls vorhanden). Demografie nur, wenn sie rechtlich und fachlich nötig ist und die Erhebung vollständig ist.

Von Charts zu Entscheidungen

2023 endete die Arbeit bei Empfehlungen im Markdown. Heute: eine Seite «Entscheidung»: Hypothese, Metrik, benötigte Daten, nächstes Experiment. Parallel ein kleines Dashboard (auch in R mit Shiny oder als statisches Quarto) mit denselben Schnitten - nicht weil Stakeholder ggplot brauchen, sondern weil dieselbe Frage nächsten Monat wieder kommt.

Stack-Bewusstsein

R bleibt für diese Art von Analyse stark (tidyverse, ggplot2, Spatial). Was fehlte, war nicht «lieber Python», sondern Engineering drumherum: Tests für die Bereinigung, CI, die das Quartals-Schema prüft, und eine klare Grenze zwischen Lernprojekt und Liefergegenstand. Genau diese Grenze markiert dieser Fall: ein ehrlicher Capstone aus 2023, mit dem Blick von 2026 darauf, was daraus eine wiederholbare Analyse gemacht hätte.

← Alle Projekte