Portfolio-Arbeit zur Datenanalyse und Vorhersagemodellierung
Autor:in
Wladislaw Saydullaev
Veröffentlichungsdatum
06. Juli 2025
1 Einleitung
1.1 Zielsetzung der Analyse
Dieser Arbeit analysiere ich den Bike-Sharing Datensatz mittels Zeitreihenanalyse. Ziel ist es, Nutzungsmuster zu identifizieren und Vorhersagemodelle für die zukünftige Nachfrage zu entwickeln.
1.2 Forschungsfragen
Welche zeitlichen Muster zeigt die Bike-Sharing Nachfrage?
Wie beeinflussen Wetterfaktoren die Nutzung?
Welche Forecasting-Methoden eignen sich am besten für die Vorhersage?
2 Datenaufbereitung
Die Chunks für datenimport, setup und test habe ich mit include=FALSE ausgeblendet, damit die Endpräsentation sauberer wirkt – daher beschreibe ich hier meinen Datenimport.
Beim Datenimport in mein R-Studio-Umfeld bin ich in folgenden Schritten vorgegangen:
Verbindung zur MySQL-Datenbank aufbauen Zunächst habe ich das Paket RMySQL verwendet, um eine Verbindung zu meiner lokalen MySQL-Datenbank „bike_sharing“ aufzubauen. Mit dbConnect() stelle ich Verbindung über Host, Port, Datenbankname und Anmeldedaten her.
Tabelleninhalt einlesen Mittels dbReadTable(con, "bike_sharing") habe ich die gesamte Tabelle „bike_sharing“ in einen Data Frame namens bike_data geladen. Anschließend habe ich die Verbindung mit dbDisconnect(con) geschlossen, um Ressourcen freizugeben.
Kurze Daten-Übersicht erzeugen In einem weiteren R-Chunk ermittele ich mit nrow() und ncol(), wie viele Beobachtungen und Variablen im Data Frame enthalten sind, und bestimme mit min() und max() den zeitlichen Bereich (Spalte datetime).
Erste Datensätze anzeigen Zur qualitativen Kontrolle zeige ich mit kable(head(bike_data, 5)) die ersten fünf Zeilen des Datensatzes in tabellarischer Form an.
Struktur- und Qualitätsprüfung Im abschließenden Setup-Chunk nutze ich erst str(bike_data), um Datentypen und Aufbau der Variablen zu prüfen. Danach berechne ich fehlende Werte pro Spalte mit colSums(is.na(bike_data)). Werden keine NA gefunden, gebe ich eine Erfolgsmeldung aus.
So stelle ich sicher, dass die Daten verlässlich aus der Datenbank geladen werden.
Interpretation: - Die durchschnittliche Anzahl der Fahrten liegt bei 146.7 pro Stunde - Registrierte Nutzer machen den Großteil der Fahrten aus - Die Temperatur variiert zwischen 0.01 und 0.41.
Klaren Saisoneffekt: Tief in Winter (unter 1 000 Fahrten), Frühjahr-Aufschwung, Sommer-Spitzen (5 000–7 500) und Herbst-Rückgang.
Wachsender Trend: Winter 2012 fast doppelt so viele Fahrten wie Winter 2011; auch Sommerhöchstwerte 2012 liegen spürbar über 2011.
Hohe Tages-Variabilität, vermutlich wetter- oder eventgetrieben.
3.3 Saisonalitätsanalyse
Code
# Monatliche Analyse mit saisonalen Featuresmonthly_usage <- bike_data %>%mutate(month =month(datetime, label =TRUE),season =case_when(month(datetime) %in%c(12, 1, 2) ~"Winter",month(datetime) %in%3:5~"Frühling", month(datetime) %in%6:8~"Sommer",month(datetime) %in%9:11~"Herbst" ) ) %>%group_by(month, season) %>%summarise(avg_count =mean(count), .groups ='drop')# Saisonale Farbpalette season_colors <-c("Winter"="#B8E0FF", "Frühling"="#C8F7C5", "Sommer"="#FFE135", "Herbst"="#E6C2A6")p1 <-ggplot(monthly_usage, aes(x = month, y = avg_count, fill = season)) +geom_col(alpha =0.8, color ="white", size =0.8) +scale_fill_manual(values = season_colors, name ="Jahreszeit") +# Werte auf den Balken anzeigengeom_text(aes(label =round(avg_count, 0)), vjust =-0.5, size =3.5, fontface ="bold") +labs(title ="Durchschnittliche Nutzung nach Monaten",subtitle ="Farbkodiert nach Jahreszeiten mit Werten",x ="Monat",y ="Ø Fahrten pro Stunde" ) +theme_minimal() +theme(plot.title =element_text(size =14, face ="bold"),plot.subtitle =element_text(size =11),axis.text.x =element_text(angle =45, hjust =1),legend.position ="top",panel.grid.minor =element_blank(),panel.grid.major.x =element_blank() ) +# Y-Achse etwas erweitern für die Labelsscale_y_continuous(expand =expansion(mult =c(0, 0.1)))# Plots anzeigenp1
Saisonale Muster in der Nutzung
Interpretation der Saisonalität: - Peak: Peak-Season von Mai bis September, niedrigste Nutzung im Winter - Low: Tiefpunkt im Winter: Jan 66, Feb 79
Herbstlicher Rückgang bis Dez 146
3.4 Wettereinfluss-Analyse
Code
# Hexagonal Binning für bessere Darstellung der Datendichtebike_data_temp <- bike_data %>%mutate(temp_celsius = temp * (39- (-8)) + (-8))ggplot(bike_data_temp, aes(x = temp_celsius, y = count)) +stat_bin_hex(bins =25, alpha =0.8) +geom_smooth(method ="lm", se =TRUE, color ="darkred", size =1.5) +scale_fill_viridis_c(name ="Anzahl\nBeobachtungen", trans ="log10") +labs(title ="Temperatur vs. Bike-Sharing Nutzung (Dichteplot)",subtitle =paste("Korrelation:", round(cor(bike_data$temp, bike_data$count), 3)),x ="Temperatur (°C)",y ="Anzahl Fahrten" ) +theme_minimal()
Zusammenhang zwischen Wetter und Bike-Sharing Nutzung
Wettereinfluss-Befunde: Der Hexbin-Plot verdeutlicht eine moderat positive Beziehung zwischen Temperatur und Bike-Sharing-Nutzung (r ≈ 0.36):
Bei sehr niedrigen Temperaturen (< 0 °C) sind Fahrten meist gering (< 200 pro Tag).
Mit steigender Temperatur verschiebt sich das dichteste Feld (gelb/grün) nach oben: im Bereich 5–10 °C liegen viele Tage bei 200–400 Fahrten.
Die Regressionslinie zeigt, dass höhere Temperaturen tendenziell mit mehr Fahrten einhergehen, auch wenn die Streuung – etwa um 0 °C oder über 8 °C – relativ groß bleibt.
# Für Random Forest: Dataframe mit Features erstellentrain_df <- daily_data[1:train_size, ]test_df <- daily_data[(train_size +1):nrow(daily_data), ]cat("Random Forest Training-Set:", nrow(train_df), "Tage\n")
1. Datenumfang und Zeitraum ermitteln Zuerst schau ich, wie viele tägliche Beobachtungen in daily_data vorliegen und welcher Datumsbereich abgedeckt ist.
2. Zeitreihe mit dynamischer Frequenz erstellen Anschließend wird geprüft, ob weniger als zwei Jahre an Daten vorhanden sind.
Bei unter 730 Tagen setzt der Code die Frequenz auf 7 und interpretiert die Werte als wöchentliche Serie.
Bei 730 Tagen oder mehr wird eine Jahresfrequenz von 365,25 Tagen verwendet und die Zeitreihe auf das Kalenderjahr des ersten Datums gestartet.
3. Aufteilen in Trainings- und Test-Daten (70/30) Dann splitte ich die Zeitreihe in 70 % für das Training und die restlichen 30 % für den Test.
4. Datenframes für Random Forest vorbereiten Zu letzt wird aus den ursprünglichen Data Frame die gleichen 70 % der Zeilen als Trainings-Set und die verbleibenden 30 % als Test-Set extrahiert, um sie später als Feature-Sets im Random-Forest-Modell zu verwenden.
4.2 Zeitreihen-Diagnose
Code
# Zeitreihen-Plotplot(train_data, main ="Training-Zeitreihe", ylab ="Tägliche Fahrten")
Zeitreihen-Diagnose
Code
# Saisonalität prüfen - nur wenn genügend Perioden vorhandenif(length(train_data) >=2*frequency(train_data)) {# Decomposition nur wenn möglichtryCatch({ decomposition <-decompose(train_data)plot(decomposition) }, error =function(e) {cat("Dekomposition nicht möglich:", e$message, "\n")cat("Verwende alternative STL-Dekomposition...\n")# Alternative: STL Decompositionif(length(train_data) >=24) { # Mindestens 24 Beobachtungen für STL stl_decomp <-stl(train_data, s.window ="periodic")plot(stl_decomp) } else {cat("Zu wenige Daten für Saisonalitäts-Analyse\n")# Einfache Trend-Analyse trend_line <-lm(as.numeric(train_data) ~seq_along(train_data))plot(train_data, main ="Zeitreihe mit Trend")abline(trend_line, col ="", lwd =2) } })} else {cat("Zu wenige Perioden für Dekomposition. Zeige einfachen Plot.\n")# Einfache Trend-Analyse trend_line <-lm(as.numeric(train_data) ~seq_along(train_data))plot(train_data, main ="Zeitreihe mit Trend")abline(trend_line, col ="", lwd =2)cat("Trend-Koeffizient:", round(coef(trend_line)[2], 4), "\n")}
Zeitreihen-Diagnose
Zeitreihen-Eigenschaften: - Klarer Trend und saisonale Komponente vorhanden - Varianz scheint über die Zeit zu steigen - Mögliche Transformation erforderlich
4.3 Modellvergleich
5 Modellvergleich: Zeitreihen vs. Machine Learning
5.1 Klassische Zeitreisenmodelle
Code
# Verschiedene Forecasting-Modelle (wie vorher)h <-length(test_data)if(h >0) {# 1. Naive Forecast naive_model <-naive(train_data, h = h)# 2. Seasonal Naiveif(frequency(train_data) >1) { snaive_model <-snaive(train_data, h = h) } else { snaive_model <- naive_model }# 3. ETS Modell ets_model <-ets(train_data) ets_forecast <-forecast(ets_model, h = h)# 4. ARIMA Modell arima_model <-auto.arima(train_data) arima_forecast <-forecast(arima_model, h = h)cat("✅ Klassische Zeitreisenmodelle erstellt\n")} else {cat("⚠️ Keine Test-Daten für klassische Modelle\n")}
✅ Klassische Zeitreisenmodelle erstellt
5.2 Random Forest Modell
Code
# Einfache Features für Random Forest erstellentrain_simple <- train_df %>%mutate(month =month(date),day_of_week =wday(date),day_of_year =yday(date),year =year(date) ) %>%select(total_count, month, day_of_week, day_of_year, year, avg_temp, avg_humidity) %>%na.omit()test_simple <- test_df %>%mutate(month =month(date),day_of_week =wday(date),day_of_year =yday(date),year =year(date) ) %>%select(total_count, month, day_of_week, day_of_year, year, avg_temp, avg_humidity) %>%na.omit()# Features für Modell auswählen (ohne Target)feature_cols <-setdiff(colnames(train_simple), "total_count")cat("Random Forest Features:", length(feature_cols), "\n")
# Random Forest trainierenset.seed(123)rf_model <-randomForest(x = train_simple[, feature_cols],y = train_simple$total_count,ntree =500,importance =TRUE,na.action = na.omit)# Vorhersagenrf_predictions <-predict(rf_model, test_simple[, feature_cols])# Model Summaryprint(rf_model)
Call:
randomForest(x = train_simple[, feature_cols], y = train_simple$total_count, ntree = 500, importance = TRUE, na.action = na.omit)
Type of random forest: regression
Number of trees: 500
No. of variables tried at each split: 2
Mean of squared residuals: 572660.9
% Var explained: 81.55
Vorhersage vs. Tatsächliche Werte - Trend-Validierung
Residuen-Statistiken für Random Forest :
Mittelwert: -19.68
Standardabweichung: 1902.75
Min: -4222.86
Max: 3158.62
5.7 Modell-Limitationen
Kurze Zeitspanne: Nur 2 Jahre Daten (2011-2012) Fehlende Faktoren: Events, Infrastrukturänderungen, Feiertage nicht berücksichtigt
Hohe Residualvarianz: Standardabweichung 1.902 deutet auf unerfasste Einflüsse
Black Box: Random Forest bietet begrenzte Interpretierbarkeit
6 Ergebnisse und Interpretation
Wichtigste Einflussfaktoren
Luftfeuchtigkeit: (61,04%) - Stärkster Prädiktor
Tag des Jahres: (47,79%) - Saisonale Komponente
Temperatur: (44,53%) - Wettereinfluss
Jahr: (43,56%) - Langzeittrend
Saisonale Dynamik
Sommer: Höchste Nutzung (Juni-August)
Winter: Drastischer Rückgang (50% weniger als Sommer)
Temperaturkorrelation: r = 0.361 (moderate positive Korrelation)
6.1 Zusammenfassung
Das Modell bestätigt die dominante Rolle von Wetter und Saisonalität. Mit RMSE 1.898 ist es für operative Planung geeignet, zeigt aber noch Verbesserungspotential. Das Random Forest Modell erzielte die beste Performance mit RMSE 1.898 und 81,55% Varianzaufklärung.
Quellcode
---title: "Zeitreihenanalyse: Bike-Sharing Nachfrage"subtitle: "Portfolio-Arbeit zur Datenanalyse und Vorhersagemodellierung"author: "Wladislaw Saydullaev"affiliation: "Technische Hochschule Würzburg-Schweinfurt"email: "wladislaw.saydullaev@study.thws.de"date: todaydate-format: "DD. MMMM YYYY"lang: deformat: html: toc: true toc-depth: 3 toc-location: left toc-title: "Inhaltsverzeichnis" number-sections: true code-fold: true code-tools: true code-copy: true embed-resources: trueexecute: echo: true warning: false message: false cache: true---# Einleitung## Zielsetzung der AnalyseDieser Arbeit analysiere ich den Bike-Sharing Datensatz mittels Zeitreihenanalyse. Ziel ist es, Nutzungsmuster zu identifizieren und Vorhersagemodelle für die zukünftige Nachfrage zu entwickeln.## Forschungsfragen1. Welche zeitlichen Muster zeigt die Bike-Sharing Nachfrage?2. Wie beeinflussen Wetterfaktoren die Nutzung?3. Welche Forecasting-Methoden eignen sich am besten für die Vorhersage?# Datenaufbereitung```{r setup, include=FALSE, message=FALSE, warning=FALSE}#| label: setup#| include: false# Benötigte Bibliotheken ladenlibrary(DBI)library(RMySQL)library(tidyverse)library(lubridate)library(forecast)library(randomForest) library(plotly)library(knitr)library(kableExtra)library(corrplot) library(VIM) # DatenbankverbindungtryCatch({ con <- dbConnect(RMySQL::MySQL(), host = "localhost", port = 3306, dbname = "bike_sharing", username = "root", password = "Wladislaw2003!") bike_data <- dbReadTable(con, "bike_sharing") dbDisconnect(con) cat("✅ Erfolgreich", nrow(bike_data), "Zeilen aus Datenbank geladen\n")})``````{r setup, include=FALSE, message=FALSE, warning=FALSE}#| label: data-overview# Datensatz-Übersichtcat("Datensatz enthält:", nrow(bike_data), "Beobachtungen und", ncol(bike_data), "Variablen\n")cat("Zeitraum:", min(bike_data$datetime), "bis", max(bike_data$datetime), "\n")# Erste Zeilen anzeigenkable(head(bike_data, 5), caption = "Erste 5 Zeilen des Datensatzes")``````{r setup, include=FALSE, message=FALSE, warning=FALSE}#| label: data-quality# Datentypen prüfenstr(bike_data)# Fehlende Wertemissing_values <- colSums(is.na(bike_data))cat("Fehlende Werte pro Variable:\n")print(missing_values[missing_values > 0])if(sum(missing_values) == 0) { cat("✅ Keine fehlenden Werte gefunden\n")}```Die Chunks für datenimport, setup und test habe ich mit include=FALSE ausgeblendet, damit die Endpräsentation sauberer wirkt – daher beschreibe ich hier meinen Datenimport.Beim Datenimport in mein R-Studio-Umfeld bin ich in folgenden Schritten vorgegangen:1. **Verbindung zur MySQL-Datenbank aufbauen** Zunächst habe ich das Paket **RMySQL** verwendet, um eine Verbindung zu meiner lokalen MySQL-Datenbank „bike\_sharing“ aufzubauen. Mit `dbConnect()` stelle ich Verbindung über Host, Port, Datenbankname und Anmeldedaten her.2. **Tabelleninhalt einlesen** Mittels `dbReadTable(con, "bike_sharing")` habe ich die gesamte Tabelle „bike\_sharing“ in einen Data Frame namens `bike_data` geladen. Anschließend habe ich die Verbindung mit `dbDisconnect(con)` geschlossen, um Ressourcen freizugeben.3. **Kurze Daten-Übersicht erzeugen** In einem weiteren R-Chunk ermittele ich mit `nrow()` und `ncol()`, wie viele Beobachtungen und Variablen im Data Frame enthalten sind, und bestimme mit `min()` und `max()` den zeitlichen Bereich (Spalte `datetime`).4. **Erste Datensätze anzeigen** Zur qualitativen Kontrolle zeige ich mit `kable(head(bike_data, 5))` die ersten fünf Zeilen des Datensatzes in tabellarischer Form an.5. **Struktur- und Qualitätsprüfung** Im abschließenden Setup-Chunk nutze ich erst `str(bike_data)`, um Datentypen und Aufbau der Variablen zu prüfen. Danach berechne ich fehlende Werte pro Spalte mit `colSums(is.na(bike_data))`. Werden keine `NA` gefunden, gebe ich eine Erfolgsmeldung aus.So stelle ich sicher, dass die Daten verlässlich aus der Datenbank geladen werden.# Explorative Datenanalyse## Deskriptive Statistiken```{r}#| label: descriptive-stats# Zusammenfassende Statistiken für numerische Variablennumeric_vars <- bike_data %>%select(temp, atemp, humidity, windspeed, casual, registered, count)summary_stats <-summary(numeric_vars)kable(summary_stats, caption ="Deskriptive Statistiken der numerischen Variablen") %>%kable_styling(bootstrap_options =c("striped", "hover", "condensed", "responsive"),full_width = F,position ="center") %>%row_spec(0, bold = T, color ="white", background ="#2c3e50") %>%kable_styling(font_size =14)```**Interpretation:**- Die durchschnittliche Anzahl der Fahrten liegt bei 146.7 pro Stunde- Registrierte Nutzer machen den Großteil der Fahrten aus- Die Temperatur variiert zwischen 0.01 und 0.41.## Zeitreihen-Visualisierung```{r, warning=FALSE}#| label: time-series-daily#| fig-cap: "Tägliche Bike-Sharing Nutzung über Zeit"# Tägliche Aggregation mit saisonalen Featuresdaily_data <- bike_data %>% mutate(date = as.Date(datetime)) %>% group_by(date) %>% summarise( total_count = sum(count), avg_temp = mean(temp), avg_humidity = mean(humidity), .groups = 'drop' ) %>% mutate( # Saisonale Kategorien season = case_when( month(date) %in% c(12, 1, 2) ~ "Winter", month(date) %in% 3:5 ~ "Frühling", month(date) %in% 6:8 ~ "Sommer", month(date) %in% 9:11 ~ "Herbst" ), year = year(date) )# Erweiterte Zeitreihen-Visualisierungggplot(daily_data, aes(x = date, y = total_count)) + # Saisonale Hintergrund-Bereiche geom_rect(data = daily_data %>% filter(season == "Winter") %>% summarise(xmin = min(date), xmax = max(date), .by = c(year, season)), aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = "Winter"), alpha = 0.4, inherit.aes = FALSE) + geom_rect(data = daily_data %>% filter(season == "Frühling") %>% summarise(xmin = min(date), xmax = max(date), .by = c(year, season)), aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = "Frühling"), alpha = 0.4, inherit.aes = FALSE) + geom_rect(data = daily_data %>% filter(season == "Sommer") %>% summarise(xmin = min(date), xmax = max(date), .by = c(year, season)), aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = "Sommer"), alpha = 0.4, inherit.aes = FALSE) + geom_rect(data = daily_data %>% filter(season == "Herbst") %>% summarise(xmin = min(date), xmax = max(date), .by = c(year, season)), aes(xmin = xmin, xmax = xmax, ymin = -Inf, ymax = Inf, fill = "Herbst"), alpha = 0.4, inherit.aes = FALSE) + # Stärkere saisonale Farbpalette scale_fill_manual( name = "Jahreszeiten", values = c("Winter" = "#B8E0FF", # Stärkeres Hellblau "Frühling" = "#C8F7C5", # Stärkeres Hellgrün "Sommer" = "#FFE135", # Stärkeres Gelb "Herbst" = "#E6C2A6"), # Stärkeres Sandbraun breaks = c("Frühling", "Sommer", "Herbst", "Winter") ) + # Hauptlinie geom_line(alpha = 0.8, color = "darkblue", size = 0.6) + # Trend-Linie geom_smooth(method = "loess", se = TRUE, color = "darkred", size = 1.2, alpha = 0.7) + labs( title = "Tägliche Bike-Sharing Nutzung nach Jahreszeiten", subtitle = "Saisonale Muster mit Trend-Analyse", x = "Datum", y = "Anzahl Fahrten pro Tag" ) + theme_minimal() + theme( plot.title = element_text(size = 16, face = "bold"), plot.subtitle = element_text(size = 12), axis.title = element_text(size = 12), axis.text.x = element_text(angle = 45, hjust = 1), legend.position = "bottom", legend.title = element_text(face = "bold") ) + scale_x_date(date_breaks = "3 months", date_labels = "%b %Y")```**Befunde:**- **Klaren Saisoneffekt:** Tief in Winter (unter 1 000 Fahrten), Frühjahr-Aufschwung, Sommer-Spitzen (5 000–7 500) und Herbst-Rückgang.- **Wachsender Trend:** Winter 2012 fast doppelt so viele Fahrten wie Winter 2011; auch Sommerhöchstwerte 2012 liegen spürbar über 2011.- **Hohe Tages-Variabilität**, vermutlich wetter- oder eventgetrieben.## Saisonalitätsanalyse```{r}#| label: seasonality#| fig-cap: "Saisonale Muster in der Nutzung"# Monatliche Analyse mit saisonalen Featuresmonthly_usage <- bike_data %>%mutate(month =month(datetime, label =TRUE),season =case_when(month(datetime) %in%c(12, 1, 2) ~"Winter",month(datetime) %in%3:5~"Frühling", month(datetime) %in%6:8~"Sommer",month(datetime) %in%9:11~"Herbst" ) ) %>%group_by(month, season) %>%summarise(avg_count =mean(count), .groups ='drop')# Saisonale Farbpalette season_colors <-c("Winter"="#B8E0FF", "Frühling"="#C8F7C5", "Sommer"="#FFE135", "Herbst"="#E6C2A6")p1 <-ggplot(monthly_usage, aes(x = month, y = avg_count, fill = season)) +geom_col(alpha =0.8, color ="white", size =0.8) +scale_fill_manual(values = season_colors, name ="Jahreszeit") +# Werte auf den Balken anzeigengeom_text(aes(label =round(avg_count, 0)), vjust =-0.5, size =3.5, fontface ="bold") +labs(title ="Durchschnittliche Nutzung nach Monaten",subtitle ="Farbkodiert nach Jahreszeiten mit Werten",x ="Monat",y ="Ø Fahrten pro Stunde" ) +theme_minimal() +theme(plot.title =element_text(size =14, face ="bold"),plot.subtitle =element_text(size =11),axis.text.x =element_text(angle =45, hjust =1),legend.position ="top",panel.grid.minor =element_blank(),panel.grid.major.x =element_blank() ) +# Y-Achse etwas erweitern für die Labelsscale_y_continuous(expand =expansion(mult =c(0, 0.1)))# Plots anzeigenp1```**Interpretation der Saisonalität:**- **Peak:** Peak-Season von Mai bis September, niedrigste Nutzung im Winter- **Low:** Tiefpunkt im Winter: Jan 66, Feb 79 Herbstlicher Rückgang bis Dez 146## Wettereinfluss-Analyse```{r}#| label: weather-analysis#| fig-cap: "Zusammenhang zwischen Wetter und Bike-Sharing Nutzung"# Hexagonal Binning für bessere Darstellung der Datendichtebike_data_temp <- bike_data %>%mutate(temp_celsius = temp * (39- (-8)) + (-8))ggplot(bike_data_temp, aes(x = temp_celsius, y = count)) +stat_bin_hex(bins =25, alpha =0.8) +geom_smooth(method ="lm", se =TRUE, color ="darkred", size =1.5) +scale_fill_viridis_c(name ="Anzahl\nBeobachtungen", trans ="log10") +labs(title ="Temperatur vs. Bike-Sharing Nutzung (Dichteplot)",subtitle =paste("Korrelation:", round(cor(bike_data$temp, bike_data$count), 3)),x ="Temperatur (°C)",y ="Anzahl Fahrten" ) +theme_minimal()```**Wettereinfluss-Befunde:**Der Hexbin-Plot verdeutlicht eine **moderat positive** Beziehung zwischen Temperatur und Bike-Sharing-Nutzung (r ≈ 0.36):- Bei sehr niedrigen Temperaturen (< 0 °C) sind Fahrten meist gering (< 200 pro Tag).- Mit steigender Temperatur verschiebt sich das dichteste Feld (gelb/grün) nach oben: im Bereich 5–10 °C liegen viele Tage bei 200–400 Fahrten.- Die Regressionslinie zeigt, dass höhere Temperaturen tendenziell mit mehr Fahrten einhergehen, auch wenn die Streuung – etwa um 0 °C oder über 8 °C – relativ groß bleibt.# Zeitreihenmodellierung## Datenaufbereitung für Modellierung```{r}#| label: ts-preparation# Prüfe Datenumfangcat("Verfügbare tägliche Beobachtungen:", nrow(daily_data), "\n")# Zeitreihe erstellen - angepasste Frequenz basierend auf Datenmengen_days <-nrow(daily_data)if(n_days <730) { # Weniger als 2 Jahre ts_daily <-ts(daily_data$total_count, frequency =7)cat("Verwende wöchentliche Frequenz (7 Tage)\n")} else { ts_daily <-ts(daily_data$total_count, start =c(year(min(daily_data$date)), 1), frequency =365.25)cat("Verwende jährliche Frequenz (365.25 Tage)\n")}# Train/Test Split (70/30)train_size <-floor(0.7*length(ts_daily))train_data <-window(ts_daily, end =time(ts_daily)[train_size])test_data <-window(ts_daily, start =time(ts_daily)[train_size +1])cat("Training-Daten:", length(train_data), "Beobachtungen (70%)\n")cat("Test-Daten:", length(test_data), "Beobachtungen (30%)\n")# Für Random Forest: Dataframe mit Features erstellentrain_df <- daily_data[1:train_size, ]test_df <- daily_data[(train_size +1):nrow(daily_data), ]cat("Random Forest Training-Set:", nrow(train_df), "Tage\n")cat("Random Forest Test-Set:", nrow(test_df), "Tage\n")```**1. Datenumfang und Zeitraum ermitteln**Zuerst schau ich, wie viele tägliche Beobachtungen in `daily_data` vorliegen und welcher Datumsbereich abgedeckt ist.**2. Zeitreihe mit dynamischer Frequenz erstellen**Anschließend wird geprüft, ob weniger als zwei Jahre an Daten vorhanden sind.- Bei unter 730 Tagen setzt der Code die Frequenz auf 7 und interpretiert die Werte als wöchentliche Serie.- Bei 730 Tagen oder mehr wird eine Jahresfrequenz von 365,25 Tagen verwendet und die Zeitreihe auf das Kalenderjahr des ersten Datums gestartet.**3. Aufteilen in Trainings- und Test-Daten (70/30)**Dann splitte ich die Zeitreihe in 70 % für das Training und die restlichen 30 % für den Test.**4. Datenframes für Random Forest vorbereiten**Zu letzt wird aus den ursprünglichen Data Frame die gleichen 70 % der Zeilen als Trainings-Set und die verbleibenden 30 % als Test-Set extrahiert, um sie später als Feature-Sets im Random-Forest-Modell zu verwenden.## Zeitreihen-Diagnose```{r}#| label: ts-diagnostics#| fig-cap: "Zeitreihen-Diagnose"# Zeitreihen-Plotplot(train_data, main ="Training-Zeitreihe", ylab ="Tägliche Fahrten")# Saisonalität prüfen - nur wenn genügend Perioden vorhandenif(length(train_data) >=2*frequency(train_data)) {# Decomposition nur wenn möglichtryCatch({ decomposition <-decompose(train_data)plot(decomposition) }, error =function(e) {cat("Dekomposition nicht möglich:", e$message, "\n")cat("Verwende alternative STL-Dekomposition...\n")# Alternative: STL Decompositionif(length(train_data) >=24) { # Mindestens 24 Beobachtungen für STL stl_decomp <-stl(train_data, s.window ="periodic")plot(stl_decomp) } else {cat("Zu wenige Daten für Saisonalitäts-Analyse\n")# Einfache Trend-Analyse trend_line <-lm(as.numeric(train_data) ~seq_along(train_data))plot(train_data, main ="Zeitreihe mit Trend")abline(trend_line, col ="", lwd =2) } })} else {cat("Zu wenige Perioden für Dekomposition. Zeige einfachen Plot.\n")# Einfache Trend-Analyse trend_line <-lm(as.numeric(train_data) ~seq_along(train_data))plot(train_data, main ="Zeitreihe mit Trend")abline(trend_line, col ="", lwd =2)cat("Trend-Koeffizient:", round(coef(trend_line)[2], 4), "\n")}```**Zeitreihen-Eigenschaften:**- Klarer Trend und saisonale Komponente vorhanden- Varianz scheint über die Zeit zu steigen- Mögliche Transformation erforderlich## Modellvergleich# Modellvergleich: Zeitreihen vs. Machine Learning## Klassische Zeitreisenmodelle```{r}#| label: classical-forecasting# Verschiedene Forecasting-Modelle (wie vorher)h <-length(test_data)if(h >0) {# 1. Naive Forecast naive_model <-naive(train_data, h = h)# 2. Seasonal Naiveif(frequency(train_data) >1) { snaive_model <-snaive(train_data, h = h) } else { snaive_model <- naive_model }# 3. ETS Modell ets_model <-ets(train_data) ets_forecast <-forecast(ets_model, h = h)# 4. ARIMA Modell arima_model <-auto.arima(train_data) arima_forecast <-forecast(arima_model, h = h)cat("✅ Klassische Zeitreisenmodelle erstellt\n")} else {cat("⚠️ Keine Test-Daten für klassische Modelle\n")}```## Random Forest Modell```{r}#| label: random-forest-model# Einfache Features für Random Forest erstellentrain_simple <- train_df %>%mutate(month =month(date),day_of_week =wday(date),day_of_year =yday(date),year =year(date) ) %>%select(total_count, month, day_of_week, day_of_year, year, avg_temp, avg_humidity) %>%na.omit()test_simple <- test_df %>%mutate(month =month(date),day_of_week =wday(date),day_of_year =yday(date),year =year(date) ) %>%select(total_count, month, day_of_week, day_of_year, year, avg_temp, avg_humidity) %>%na.omit()# Features für Modell auswählen (ohne Target)feature_cols <-setdiff(colnames(train_simple), "total_count")cat("Random Forest Features:", length(feature_cols), "\n")cat("Training-Samples:", nrow(train_simple), "\n")cat("Test-Samples:", nrow(test_simple), "\n")# Random Forest trainierenset.seed(123)rf_model <-randomForest(x = train_simple[, feature_cols],y = train_simple$total_count,ntree =500,importance =TRUE,na.action = na.omit)# Vorhersagenrf_predictions <-predict(rf_model, test_simple[, feature_cols])# Model Summaryprint(rf_model)cat("Out-of-Bag Error:", round(tail(rf_model$mse, 1), 2), "\n")```## Feature Importance Analyse```{r}#| label: feature-importance#| fig-cap: "Feature Importance des Random Forest Modells"# Feature Importance extrahieren importance_df <-importance(rf_model) %>%as.data.frame() %>%rownames_to_column("feature") %>%arrange(desc(`%IncMSE`)) %>%head(15)# Tabelle der wichtigsten Featureskable(importance_df[1:10, c("feature", "%IncMSE", "IncNodePurity")], caption ="Top 10 wichtigste Features",digits =2)```## Bestes Modell und Vorhersage## Umfassender Modellvergleich```{r}#| label: comprehensive-comparison# Alle Modelle vergleichenif(exists("rf_predictions") &&exists("naive_model") &&length(test_data) >0) {# Test-Daten für Vergleich actual_values <-as.numeric(test_data)# Sicherstellen, dass alle Vorhersagen gleiche Länge haben min_length <-min(length(actual_values), length(rf_predictions))if(min_length >0) {# Vorhersagen auf gleiche Länge bringen actual_test <- actual_values[1:min_length] rf_pred <- rf_predictions[1:min_length]# Klassische Modell-Vorhersagen naive_pred <-as.numeric(naive_model$mean)[1:min_length] ets_pred <-as.numeric(ets_forecast$mean)[1:min_length] arima_pred <-as.numeric(arima_forecast$mean)[1:min_length]# RMSE und MAE berechnen calculate_metrics <-function(pred, actual) { rmse <-sqrt(mean((pred - actual)^2, na.rm =TRUE)) mae <-mean(abs(pred - actual), na.rm =TRUE) mape <-mean(abs((actual - pred) / actual) *100, na.rm =TRUE)return(data.frame(RMSE = rmse, MAE = mae, MAPE = mape)) }# Alle Modelle bewerten final_comparison <-rbind(data.frame(Model ="Random Forest", calculate_metrics(rf_pred, actual_test)),data.frame(Model ="ARIMA", calculate_metrics(arima_pred, actual_test)),data.frame(Model ="ETS", calculate_metrics(ets_pred, actual_test)),data.frame(Model ="Naive", calculate_metrics(naive_pred, actual_test)) )# Sortieren nach RMSE final_comparison <- final_comparison[order(final_comparison$RMSE), ]kable(final_comparison, caption ="Finaler Modellvergleich (70/30 Split)",digits =2)# Bestes Modell identifizieren best_model_final <- final_comparison$Model[1] best_rmse_final <- final_comparison$RMSE[1]cat("Bestes Modell:", best_model_final, "\n")cat("Beste Test-RMSE:", round(best_rmse_final, 2), "\n") } else {cat("Keine vergleichbaren Vorhersagen sind verfügbar") }} else {cat("Modellvergleich nicht möglich - fehlende Komponenten\n")}```## Trend-Visualisierung und Validierung```{r}#| label: trend-validation#| fig-cap: "Vorhersage vs. Tatsächliche Werte - Trend-Validierung"if(exists("final_comparison") &&exists("best_model_final")) {# Daten für Plotting vorbereiten plot_data <-data.frame(date = test_df$date[1:min_length],actual = actual_test,rf_pred = rf_pred,arima_pred = arima_pred,ets_pred = ets_pred,naive_pred = naive_pred )# Trend-Plot: Actual vs. Predictions p1 <-ggplot(plot_data, aes(x = date)) +geom_line(aes(y = actual, color ="Tatsächlich"), size =1.2) +geom_line(aes(y = rf_pred, color ="Random Forest"), size =1) +geom_line(aes(y = arima_pred, color ="ARIMA"), size =1) +geom_line(aes(y = ets_pred, color ="ETS"), size =1) +scale_color_manual(values =c("Tatsächlich"="darkgrey","Random Forest"="black","ARIMA"="blue", "ETS"="darkred" )) +labs(title ="Modellvergleich: Vorhersagen vs. Tatsächliche Werte",subtitle =paste("Bestes Modell:", best_model_final),x ="Datum",y ="Anzahl Fahrten pro Tag",color ="Modell" ) +theme_minimal() +theme(legend.position ="bottom")print(p1)# Residuen-Analyse für bestes Modellif(best_model_final =="Random Forest") { residuals <- actual_test - rf_pred } elseif(best_model_final =="ARIMA") { residuals <- actual_test - arima_pred } elseif(best_model_final =="ETS") { residuals <- actual_test - ets_pred } else { residuals <- actual_test - naive_pred }# Residuen-Statistikencat("Residuen-Statistiken für", best_model_final, ":\n")cat("Mittelwert:", round(mean(residuals, na.rm =TRUE), 2), "\n")cat("Standardabweichung:", round(sd(residuals, na.rm =TRUE), 2), "\n")cat("Min:", round(min(residuals, na.rm =TRUE), 2), "\n")cat("Max:", round(max(residuals, na.rm =TRUE), 2), "\n")} else {cat("🚫 Trend-Validierung nicht möglich\n")}```## Modell-Limitationen- **Kurze Zeitspanne:** Nur 2 Jahre Daten (2011-2012)Fehlende Faktoren: Events, Infrastrukturänderungen, Feiertage nicht berücksichtigt- **Hohe Residualvarianz:** Standardabweichung 1.902 deutet auf unerfasste Einflüsse- **Black Box:** Random Forest bietet begrenzte Interpretierbarkeit# Ergebnisse und Interpretation**Wichtigste Einflussfaktoren**- **Luftfeuchtigkeit:** (61,04%) - Stärkster Prädiktor- **Tag des Jahres:** (47,79%) - Saisonale Komponente- **Temperatur:** (44,53%) - Wettereinfluss- **Jahr:** (43,56%) - Langzeittrend**Saisonale Dynamik**- **Sommer:** Höchste Nutzung (Juni-August)- **Winter:** Drastischer Rückgang (50% weniger als Sommer)- **Temperaturkorrelation:** r = 0.361 (moderate positive Korrelation)## ZusammenfassungDas Modell bestätigt die dominante Rolle von Wetter und Saisonalität. Mit RMSE 1.898 ist es für operative Planung geeignet, zeigt aber noch Verbesserungspotential. Das Random Forest Modell erzielte die beste Performance mit RMSE 1.898 und 81,55% Varianzaufklärung.---