Aufgabe: hands-on EDA-Fallstudie E-Scooter

Autor:in
Zugehörigkeit

Markus Geuss

Fernfachhochschule Schweiz

WichtigHinweise zur Bearbeitung dieser Aufgabe

Zum Lösen der nachfolgenden Aufgaben generieren Sie ein neues R Script (oder Quarto Dokument, *.qmd) in RStudio und kopieren Sie die Code-Chunks hinein und vervollständigen Sie diese. Nach Bearbeitung der Aufgabe(n) können Sie ihre Lösungen mit der Musterlösung vergleichen.

Ausgangssituation

Sie arbeiten als Data Analyst/in bei einem E-Scooter-Anbieter in der Schweiz. Ziel ist eine erste EDA, um (i) Nachfrage-Peaks über den Tag zu identifizieren, (ii) typische vs. auffällige Fahrten zu erkennen und (iii) zu prüfen, ob Abbrüche mit der Akkuwarnung zusammenhängen. Auf Basis Ihrer Ergebnisse sollen konkrete betriebliche Maßnahmen (z.B. Rebalancing-/Charging-Zeiten, Umgang mit Ausreißern, Priorisierung von Akku-Checks) abgeleitet werden.

Beschreibung der Daten

In der Woche vom 10.–17. Mai 2025 wurden Fahrten mit E-Scootern eines Anbieters als Testdatensatz in einer Schweizer Stadt (anonymisiert) protokolliert. Der Datensatz enthält 2’000 Fahrten.

Codebook: Variablenbeschreibung

Datensatz: scooter.csv
Beobachtungseinheit: eine Zeile = eine Scooter-Fahrt (trip_id)

Variable Typ (wie geliefert) Bedeutung Einheit / Wertebereich
trip_id Zahl eindeutige Fahrten-ID 1001…
start_time Text Startzeitpunkt der Fahrt ISO-Zeitstempel als String
scooter_type Text Scooter-Modell "Model A", "Model B"
trip_distance_m Zahl gefahrene Distanz Meter
trip_duration_s Zahl Fahrtdauer Sekunden
user_rating Text Rating durch Nutzer/in "1""5" und "NA"
battery_low logisch Akkuwarnung bei Start TRUE/FALSE
trip_aborted logisch Fahrt abgebrochen? TRUE/FALSE

Sie werden in dieser Aufgabe Datums- und Uhrzeitangaben im Rahmen des Data Wranglings anpassen müssen. Die state-of-the-art Library dazu ist lubridate, die eine grosse Hilfe darstellt. Wenn es in der Aufgabe so weit ist, verwenden Sie die Informationen und Hinweise zu lubridate aus der nachfolgenden Box.

TippSpickzettel: lubridate (Datum & Zeit in R)
Warum lubridate?

Mit lubridate können Sie Zeitstempel (Strings) zuverlässig in Datum/Zeit umwandeln und bequem Bestandteile wie Tag, Wochentag oder Stunde extrahieren.

1) Zeitstempel parsen (String → Datum/Zeit)

Wenn start_time als Text vorliegt, müssen Sie ihn zuerst in ein datetime umwandeln:

library(lubridate)

start_time_dt <- ymd_hms(start_time, tz = "Europe/Zurich")
2) Bestandteile extrahieren
hour(start_time_dt)      # 0–23
minute(start_time_dt)    # 0–59
second(start_time_dt)    # 0–59
day(start_time_dt)       # 1–31
month(start_time_dt)     # 1–12 (oder month(..., label=TRUE))
year(start_time_dt)
wday(start_time_dt)      # Wochentag als Zahl (1–7)
wday(start_time_dt, label = TRUE, abbr = TRUE)
3) Datum/Zeit abrunden (für Aggregationen)

Das ist hilfreich, wenn Sie z.B. nach Stunde oder Tag zählen möchten:

floor_date(start_time_dt, unit = "hour")  # auf volle Stunde
floor_date(start_time_dt, unit = "day")   # auf Mitternacht (Tagesstart)
4) Zeitdifferenzen & Dauer

Wenn Sie Zeiten vergleichen oder Differenzen bilden möchten:

difftime(t2, t1, units = "mins")
as.duration(trip_duration_s)
5) Nützliche Tipps (typisch in mutate())
mutate(
  start_time = ymd_hms(start_time, tz = "Europe/Zurich"),
  start_hour = hour(start_time),
  weekday = wday(start_time, label = TRUE, abbr = TRUE)
)

Aufgabe 1: Import & erste Orientierung

Laden Sie die Rohdatendatei scooter.csv aus Moodle herunter: Link

1.1 Import des Datensatzes

Importieren Sie scooter.csv in RStudio. Wie viele verschiedene Scooter-Modelle (scooter_type) wurden verwendet?

library(tidyverse)

scooter <- read_csv("___/scooter.csv", show_col_types = FALSE)

scooter %>%
  count(scooter_type)

1.2 Variablentypen und Auffälligkeiten in den Daten

Welche Variablen sind numerisch, welche kategorial? Nennen Sie mindestens 2 Variablen pro Typ und begründen Sie kurz. Gibt es Auffälligkeiten?

glimpse(scooter)
summary(scooter)

Aufgabe 2: Data Wrangling

2.1 Variablentransformation

Erstellen Sie eine neue Spalte trip_duration_min, die die Fahrtdauer in Minuten. Stören die NAs die Umrechnung (Datentransformation). Begründen Sie kurz?

library(lubridate)

scooter <- scooter %>%
  mutate(trip_duration_min = ___ / ___)
summary(scooter)

2.2 Entfernung Fahrten < 1 Min.

Häufig kommt es zu Fehlern z.B. bei der Initialisierung des Fahrzeugs und der Vorgang wird abgebrochen. Wir wollen alle Fahrten mit trip_duration_min < 1 Min. entfernen. Warum ist die naheliegende Codelösung: scooter %>% filter(trip_duration_min >= 1) problematisch? Analysieren Sie das summary(scooter), was fällt auf? Wie kann man es korrekt machen?

scooter %>%
  filter(___ >= ___) %>%
  summary()
scooter %>%
  mutate(trip_duration_min = ___ / ___) %>%
    filter(is.na(trip_duration_min) | trip_duration_min >= 1) %>%
  summary()
scooter <- scooter %>%
  mutate(trip_duration_min = ___ / ___) %>%
filter(is.na(trip_duration_min) | trip_duration_min >= 1)
summary(scooter)

2.3 Fehlende Werte

In der Variablen trip_distance_m gibt es Werte, die fehlen. Wie hoch ist ihr Anteil an allen Beobachtungen? Wenn sie unter 10% sind, entfernen Sie sie.

# Anzahl und Anteil fehlender Distanzwerte
summary(scooter$trip_distance_m)
na_dist <- sum(is.na(scooter$trip_distance_m))
n_total <- nrow(scooter)

na_dist
na_dist / n_total
scooter <- scooter %>%
  filter(!is.na(___))
summary(scooter$trip_distance_m)
nrow(scooter)

2.4 Erzeugung neuer Variablen

Unser Management interessiert sich vor allem dafür, zu welchen Uhrzeiten und an welchen Wochentagen die Nutzer ihren E-Scooter verwenden. Generieren Sie die beiden neuen Variablen start_hour und weekday.

scooter <- scooter %>%
  mutate(
    start_hour = hour(start_time),
    weekday    = wday(start_time, label = TRUE, abbr = TRUE)
  )
summary(scooter)

Aufgabe 3: Datenvisualisierung, Kennwerte (Metriken)

3.1 Wie verteilt sich die durchschnittliche Fahrtdauer auf die Wochentage?

scooter %>%
  group_by(___) %>%
  summarise(
    mean_duration = mean(trip_duration_min, na.rm = TRUE),
    n = n()
  ) %>%
  arrange(weekday)
scooter %>%
  count(start_hour) %>%
  ggplot(aes(x = start_hour, y = n)) +
  geom_col() +
  labs(x = "Startstunde (0–23)", y = "Anzahl Fahrten")

3.2 Welche Fahrtdauern und -distanzen sind „typisch“ und welche sind Ausreißer?

Erstellen Sie Boxplots der Fahrtdauer (trip_duration_min)/ Dauer (trip_distance_m) nach Tageszeit-Klasse und bestimmen Sie die Kennwerte (IQR, Mean, Median).
Diskutieren Sie kurz: Was bedeutet das für die Akkulaufzeit und die erwartete Anzahl Fahrten pro Scooter (bei gleicher Verfügbarkeit)?

library(lubridate)

scooter <- scooter %>%
  mutate(
    start_hour = hour(start_time),
    time_block = case_when(
      start_hour %in% 0:5   ~ "Nacht (0–5)",
      start_hour %in% 6:9   ~ "Morgen (6–9)",
      start_hour %in% 10:15 ~ "Tag (10–15)",
      start_hour %in% 16:19 ~ "Abend (16–19)",
      TRUE                  ~ "Spät (20–23)"
    ),
    time_block = factor(time_block,
                        levels = c("Nacht (0–5)","Morgen (6–9)","Tag (10–15)","Abend (16–19)","Spät (20–23)"))
  )

ggplot(scooter, aes(x = time_block, y = trip_duration_min)) +
  geom_boxplot() +
  labs(x = "Tageszeit-Klasse", y = "Fahrtdauer (Minuten)")
scooter %>%
  group_by(___) %>%
  summarise(
    n = n(),
    mean_duration = mean(trip_duration_min, na.rm = TRUE),
    median_duration = median(trip_duration_min, na.rm = TRUE),
    iqr_duration = IQR(trip_duration_min, na.rm = TRUE)
  ) %>%
  arrange(time_block)
scooter <- scooter %>%
  mutate(
    start_hour = hour(start_time),
    time_block = case_when(
      start_hour %in% 0:5   ~ "Nacht (0–5)",
      start_hour %in% 6:9   ~ "Morgen (6–9)",
      start_hour %in% 10:15 ~ "Tag (10–15)",
      start_hour %in% 16:19 ~ "Abend (16–19)",
      TRUE                  ~ "Spät (20–23)"
    ),
    time_block = factor(time_block,
                        levels = c("Nacht (0–5)","Morgen (6–9)","Tag (10–15)","Abend (16–19)","Spät (20–23)"))
  )

ggplot(scooter, aes(x = time_block, y = trip_distance_m)) +
  geom_boxplot() +
  labs(x = "Tageszeit-Klasse", y = "Distanz (Meter)")
scooter %>%
  group_by(___) %>%
  summarise(
  n = n(),
  mean_distance = mean(trip_distance_m, na.rm = TRUE),
  median_distance = median(trip_distance_m, na.rm = TRUE),
  q1_distance = quantile(trip_distance_m, probs = 0.25, na.rm = TRUE),
  q3_distance = quantile(trip_distance_m, probs = 0.75, na.rm = TRUE),
  iqr_distance = IQR(trip_distance_m, na.rm = TRUE)
)

3.3 Abbruchquote wegen battery_low

Einer Kollegin ist noch etwas aufgefallen. Wie viele Fahrten “verlieren” wir, weil der Batteriestand zu niedrig ist, d.h. ein Kunde oder eine Kundin befürchtet aufgrund des Batteriestandes nicht bis ans Ziel zu gelangen?

Analysieren Sie hierfür, ob die Abbruchrate bei niedrigem Batteriestand höher ist und visualisieren Sie den Zusammenhang mit einem Balkendiagramm.

scooter %>%
  group_by(battery_low) %>%
  summarise(
  n = n(),
  abort_rate = mean(trip_aborted, na.rm = TRUE)
  )
scooter %>%
   group_by(battery_low) %>%
   summarise(abort_rate = mean(trip_aborted, na.rm = TRUE)) %>%
   ggplot(aes(x = battery_low, y = abort_rate)) +
   geom_col() +
   labs(x = "Akkustand-Warnung (battery_low)", y = "Abbruchrate")

3.4 Verlorene Fahrten

Ihr Business-Analyst sagt, die Abbruchraten sagen zwar etwas über das Risiko aus, dass ein Kunde oder eine Kundin die Fahrt erst gar nicht antritt, aber wo verlieren wir das meiste Geld während des Tages? Plotten Sie, um dies zu beantworten, ein Balkendiagramm der Anzahl “verlorener” Fahrten pro Tageszeitklasse.

scooter %>%
  group_by(___) %>%
  summarise(lost_trips = sum(trip_aborted, na.rm = TRUE)) %>%
  ggplot(aes(x = time_block, y = lost_trips)) +
  geom_col() +
  labs(x = "Tageszeit-Klasse", y = "Verlorene Fahrten (Abbrüche)")

Aufgabe 4: Nächste mögliche Schritte

Sie haben schon einige Einblicke in die Eigenschaften der Daten erhalten. Im zyklischen Ablauf der EDA wäre nun eine neue Frage/Hypothese an die Daten zu formulieren. Welchen interessanten Fragen könnte man als nächstes nachgehen?