Eine Übungsaufgabe aus einem ML-Kurs wurde zu einem kleinen Data-Engineering-Projekt: echte Kölner Zähldaten auftreiben, mit DWD-Wetterdaten und Kalenderinformationen verknüpfen, und daraus ein lineares Regressionsmodell bauen — samt aller Umwege, Sackgassen und der ein oder anderen Überraschung unterwegs.
Der Auslöser war simpel: ein Online-Kurs zu maschinellem Lernen, und die Frage, ob man die Übungen nicht mit echten, selbst besorgten Daten statt mit sklearn- Beispieldatensätzen durchspielen könnte. Mit Bezug zu Köln, der eigenen Stadt — und mit einer Zeitkomponente, die sich lohnt zu modellieren: Wochentage, Jahreszeiten, Wetter.
Die Fahrrad-Dauerzählstelle im Stadtwald war der naheliegende Kandidat: automatisch erfasste, mehrjährige Messreihen, mit einem öffentlichen Dashboard. Nur: ein sauberer CSV-Download war nirgends zu finden.
Das Köln-Dashboard der Eco-Counter-Zählstellen ist eine Next.js-Anwendung ohne öffentliche Daten-API. Der Weg zu den echten Tageswerten führte über die Browser-Entwicklertools: die React-Server-Components-Antwort (_rsc-Parameter) enthält die kompletten Rohdaten als eingebettetes JSON-Fragment — man muss sie nur aus dem Antworttext herausschälen, statt über eine dokumentierte Schnittstelle abzufragen.
Deutlich kooperativer: der Deutsche Wetterdienst stellt Tageswerte (Temperatur, Niederschlag, Sonnenscheindauer, Wind) offen als ZIP-Download bereit. Die einzige Komplikation: zwischen dem rollierenden recent-Archiv (letzte 500 Tage) und dem jährlich aktualisierten historical-Archiv klaffte zeitweise eine Lücke von mehreren Wochen — erst nach einem DWD-internen Update war der Zeitraum durchgängig verfügbar.
Gesetzliche Feiertage und Schulferien NRW wurden für 2020–2025 als feste Datumslisten hinterlegt (keine stabile, kostenlose API für historische Mehrjahresdaten gefunden). Als dritter Kalender-Faktor kamen später die Heimspieltage des 1. FC Köln dazu — eine Hypothese aus der Fehleranalyse, dazu gleich mehr. Karneval und der 11.11. brauchen dagegen gar keine Liste: Weiberfastnacht bis Veilchendienstag lässt sich über die Gauß'sche Osterformel für jedes beliebige Jahr direkt aus dem Datum berechnen, der Elfte im Elften sowieso.

Lineare Regression nimmt jede Eingabezahl wörtlich als lineares Verhältnis — das passt nicht automatisch zu Kalenderdaten. Drei Entscheidungen waren hier zentral:
Ein roher Wochentags-Code (0–6) würde dem Modell suggerieren, Sonntag sei "sechsmal so viel Wochentag" wie Montag — Unsinn, Wochentage sind nicht ordinal. Stattdessen bekommt jeder Tag eine eigene 0/1-Spalte, mit einer Referenzkategorie als Vergleichspunkt.
Der Tag im Jahr (1–365) hat am Jahresende einen künstlichen Sprung: der 31. Dezember und der 1. Januar liegen kalendarisch nebeneinander, numerisch aber 364 Tage auseinander. Eine Sinus/Cosinus-Kodierung bildet das Jahr stattdessen als Kreis ab.
# Tag im Jahr als Punkt auf dem Einheitskreis, nicht als rohe Zahl doy = df["datum"].dt.dayofyear df["doy_sin"] = np.sin(2 * np.pi * doy / 365) df["doy_cos"] = np.cos(2 * np.pi * doy / 365)
Aus der Fehleranalyse (dazu im nächsten Abschnitt mehr) kamen zusätzliche Features: ein binäres hat_geregnet statt der reinen Millimeterzahl, eine Kälte-Schwelle (kaelte_unter_m0), und eine Interaktion aus Ferien-Flag und Wochenend-Flag.
Der ehrlichste Teil eines Datenprojekts ist selten die finale Pipeline — sondern das, was auf dem Weg dahin kaputtging und warum. Eine Auswahl der Momente, die das Modell am meisten vorangebracht haben:
tn_min bei festgehaltenem tx_max, steigt die Vorhersage — physikalisch Unsinn. Grund: diese Kombination (warmer Tag, sehr kalte Nacht) kam im Trainingsdatensatz praktisch nie vor, das Modell extrapolierte. Konsequenz: tm_mittel komplett entfernt, nur tx_max und tn_min bleiben — VIF sinkt auf 36 bzw. 17, die Koeffizienten werden wieder einzeln interpretierbar.hat_geregnet im Modell (ja/nein). Ergänzt um niederschlag_mmals eigenes, kontinuierliches Feature: jetzt gibt es sowohl einen festen Abschlag für "es regnet überhaupt" als auch einen weiteren, proportionalen Rückgang pro Millimeter — beide Koeffizienten bleiben trotz der Kombination klar signifikant (VIF je unter 3).ist_karneval (Weiberfastnacht bis Veilchendienstag, über die Osterformel berechnet) und ist_elfter_elfter kamen als zwei weitere Kalender-Features dazu — beide hoch signifikant und mit kräftigem negativem Effekt: an diesen Tagen fährt Köln offenbar lieber kostümiert als mit dem Rad zur Arbeit.
Das finale Modell — multiple lineare Regression mit 19 bereinigten Features — erreicht auf nie gesehenen Testdaten ein R² von 0,83 bei einem mittleren absoluten Fehler von rund 357 Radfahrer:innen pro Tag.

Der stärkste positive Einzeleffekt im ganzen Modell ist kein Wetter-Feature — es ist ein Heimspiel des 1. FC Köln. An Spieltagen liegt der geschätzte Radverkehr über 750 Fahrten höher als an einem sonst identischen Tag. Das RheinEnergieSTADION liegt nur wenige Minuten vom Stadtwald entfernt — ein plausibler, aber im Vorfeld nicht bedachter Zusammenhang, der erst durch die Ausreißeranalyse der Residuen auffiel.
Das zweitstärkste Muster ist weit weniger überraschend, aber ebenso klar: Samstag und Sonntag senken den erwarteten Radverkehr um 870 bis knapp 1.000 Fahrten gegenüber einem normalen Werktag — ein Hinweis darauf, dass die Zählstelle stark pendlergeprägt ist. Schulferien wirken in dieselbe Richtung und fast genauso stark wirkt mittlerweile auch Karneval (−773 Fahrten) — nur der 11.11. fällt mit −470 etwas moderater aus. Regen drückt den Radverkehr ebenfalls, während Sonnenscheindauer und die Maximaltemperatur die stärksten positiven, wettergetriebenen Effekte sind.


Die reinen Koeffizienten aus sklearn sagen nichts darüber, wie sicher sich das Modell bei den geschätzten Werten eigentlich ist. Für die statistische Absicherung wurde dasselbe Modell zusätzlich mit statsmodels.OLS gerechnet — das liefert Standardfehler, t-Werte, p-Werte und Konfidenzintervalle pro Koeffizient.
| Feature | Koeffizient | Std.-Fehler | t | p-Wert | 95%-Konfidenzintervall |
|---|---|---|---|---|---|
| const | 1989,00 | 62,11 | 32,02 | < 0,001 | [1867,2 ; 2110,8] |
| tx_max | 55,29 | 4,76 | 11,61 | < 0,001 | [45,9 ; 64,6] |
| tn_min | 0,83 | 5,29 | 0,16 | 0,876 | [−9,5 ; 11,2] |
| niederschlag_mm | −22,15 | 2,57 | −8,62 | < 0,001 | [−27,2 ; −17,1] |
| sonnenschein_h | 81,67 | 4,72 | 17,31 | < 0,001 | [72,4 ; 90,9] |
| wind_mittel_ms | −88,68 | 9,89 | −8,97 | < 0,001 | [−108,1 ; −69,3] |
| fc_koeln_heimspiel | 754,19 | 134,82 | 5,59 | < 0,001 | [489,8 ; 1018,6] |
| ist_feiertag | −439,27 | 63,64 | −6,90 | < 0,001 | [−564,1 ; −314,4] |
| ist_ferien | −813,11 | 31,66 | −25,68 | < 0,001 | [−875,2 ; −751,0] |
| ist_karneval | −772,78 | 92,05 | −8,40 | < 0,001 | [−953,3 ; −592,2] |
| ist_elfter_elfter | −469,97 | 190,76 | −2,46 | 0,014 | [−844,1 ; −95,8] |
| doy_sin | −74,90 | 19,77 | −3,79 | < 0,001 | [−113,7 ; −36,1] |
| doy_cos | −184,88 | 31,07 | −5,95 | < 0,001 | [−245,8 ; −123,9] |
| wochentag_Freitag | −345,45 | 33,63 | −10,27 | < 0,001 | [−411,4 ; −279,5] |
| wochentag_Samstag | −994,58 | 36,90 | −26,95 | < 0,001 | [−1066,9 ; −922,2] |
| wochentag_Sonntag | −870,81 | 36,09 | −24,13 | < 0,001 | [−941,6 ; −800,0] |
| hat_geregnet | −228,11 | 30,04 | −7,60 | < 0,001 | [−287,0 ; −169,2] |
| ist_ferien_wochenende | 439,52 | 58,90 | 7,46 | < 0,001 | [324,0 ; 555,0] |
| kaelte_unter_m0 | −61,93 | 11,09 | −5,58 | < 0,001 | [−83,7 ; −40,2] |
| heimspiel_temp | 18,31 | 7,98 | 2,29 | 0,022 | [2,6 ; 34,0] |
Rot markierte Zeile: p > 0,05, statistisch nicht verlässlich von 0 unterscheidbar. Einzig tn_min betrifft das — nachdem tm_mittel aus dem Modell entfernt wurde, bleiben nur noch tx_max und tn_min als Temperaturmaße übrig, die sich immer noch spürbar ähneln: sobald tx_maxden Haupteffekt trägt, bleibt für tn_min kaum noch unabhängige Information übrig. Mit der Ergänzung von niederschlag_mm als eigenem, kontinuierlichem Feature neben dem binären hat_geregnet ist heimspiel_temp (Interaktion aus Heimspiel und Temperatur) jetzt übrigens signifikant (p=0,022) — ein Hinweis, dass an Heimspieltagen wärmeres Wetter den Effekt zusätzlich leicht verstärkt.
Faustregel: VIF > 10 gilt als auffällig, VIF > 100 als stark.
| Feature | VIF |
|---|---|
| tx_max | 35,8 |
| tn_min | 17,1 |
| wind_mittel_ms | 7,9 |
| sonnenschein_h | 7,5 |
| fc_koeln_heimspiel | 5,4 |
| heimspiel_temp | 5,2 |
| doy_cos | 2,9 |
| hat_geregnet | 2,9 |
| ist_ferien | 1,9 |
| ist_ferien_wochenende | 1,7 |
| niederschlag_mm | 1,7 |
| wochentag_Samstag | 1,6 |
| kaelte_unter_m0 | 1,5 |
| doy_sin | 1,5 |
| wochentag_Sonntag | 1,5 |
| wochentag_Freitag | 1,2 |
| ist_karneval | 1,1 |
| ist_feiertag | 1,0 |
| ist_elfter_elfter | 1,0 |
Deutlich besser als in der ersten Modellversion (dort standen tx_max, tn_min und tm_mittel gleichzeitig im Modell, mit VIF-Werten bis 445). Der Auslöser für die Bereinigung war übrigens nicht die VIF-Tabelle selbst, sondern der Live-Rechner weiter unten: beim Durchspielen fiel auf, dass ein Senken von tn_min bei festgehaltenem tx_max die Vorhersage erhöhte statt senkte — ein Kunstgriff, der in den echten Daten praktisch nie vorkam und den das Modell entsprechend falsch extrapolierte. Nach dem Entfernen von tm_mittel bleiben tx_max und tn_min zwar immer noch moderat korreliert (VIF 36 bzw. 17), aber deutlich im vertretbaren Bereich.
Der Residuenplot zeigt keine systematische Trichterform — ein gutes Zeichen für die Grundannahmen des Modells. Trotzdem bleibt lineare Regression strukturell limitiert: sie kann keine Sättigungseffekte abbilden (irgendwann bremst auch Hitze den Radverkehr statt ihn weiter zu steigern — dieser Bereich kam 2020–2025 in Köln allerdings kaum vor, ein quadratischer Temperaturterm blieb entsprechend statistisch bedeutungslos), und sie extrapoliert jenseits des beobachteten Wertebereichs ohne jede eingebaute Vorsicht.
Für eine Vorhersage-Funktion heißt das konkret: Eingaben weit außerhalb des beobachteten Trainingsbereichs sollten eigentlich gegen eben diesen Bereich geprüft werden, bevor man dem Ergebnis traut — aktuell übernimmt das noch niemand automatisch, das ist ein offener Punkt für die nächste Ausbaustufe. Kalender-Flags wie Feiertage oder Schulferien lassen sich dagegen für jedes beliebige Datum von Hand übergeben; nur die automatische Erkennung aus den hinterlegten Listen deckt bislang nur 2020–2025 ab. Karneval und der 11.11. sind davon ausgenommen — die werden über die Osterformel direkt aus dem Datum berechnet und funktionieren deshalb für jedes beliebige Jahr, auch außerhalb des Trainingszeitraums.
| tx_max, tn_min | Tagesmaximum und -minimum der Temperatur (das Tagesmittel wurde wegen zu hoher Kollinearität mit den beiden anderen Maßen entfernt) |
| sonnenschein_h | Sonnenscheindauer in Stunden |
| wind_mittel_ms | mittlere Windgeschwindigkeit |
| niederschlag_mm, hat_geregnet | Niederschlagsmenge in mm plus zusätzliches binäres Regen-Flag — modelliert einen festen „Es regnet“-Abschlag plus einen weiteren, proportionalen Effekt pro Millimeter |
| kaelte_unter_m0 | Frost-Schwelle, wie weit unter 0°C |
| doy_sin, doy_cos | Position im Jahreszyklus (zyklisch kodiert) |
| wochentag_Freitag/Samstag/Sonntag | One-Hot, Referenz: Mo–Do zusammengefasst |
| ist_feiertag, ist_ferien | gesetzliche Feiertage und NRW-Schulferien |
| ist_karneval | Weiberfastnacht bis Veilchendienstag, über die Osterformel aus dem Datum berechnet |
| ist_elfter_elfter | 11.11., Kölner Sessionsauftakt |
| ist_ferien_wochenende | Interaktion: Ferien, die auf ein Wochenende fallen |
| fc_koeln_heimspiel, heimspiel_temp | 1. FC Köln Heimspieltag, plus Interaktion mit Temperatur |
Keine Serveranfrage, kein Backend — das lineare Modell ist nichts weiter als 19 Koeffizienten plus ein Achsenabschnitt. Der Rechner unten wertet dieselbe Formel aus, die statsmodels beim Training gelernt hat, direkt in deinem Browser. Karneval und der 11.11. werden dabei automatisch aus dem gewählten Datum berechnet, nicht manuell angehakt. Die Startwerte für Temperatur, Niederschlag, Sonnenschein und Wind kommen aus einer aktuellen Open-Meteo-Vorhersage für Köln, die beim Laden der Seite einmal serverseitig abgerufen wird — die Berechnung selbst läuft aber weiterhin komplett im Browser, und jedes Feld lässt sich frei überschreiben.
Live-Wetterdaten: Open-Meteo (Vorhersage für Köln, stündlich aktualisiert).
Das Modell wurde nur mit Daten bis 2025 trainiert. Seit Jahresbeginn 2026 holt ein täglicher Job die tatsächlich gezählten Radfahrer:innen sowie die DWD-Wetterdaten und rechnet damit die Vorhersage für jeden Tag nach — hier die Gegenüberstellung mit nie gesehenen Daten.
| Datum | Gezählt | Vorhersage | Abweichung |
|---|---|---|---|
| 15.10.2026 | 3.503 | 3.595 | −92 |
| 14.10.2026 | 3.592 | 3.469 | +123 |
| 13.10.2026 | 2.677 | 2.364 | +313 |
| 12.10.2026 | 3.383 | 3.426 | −43 |
| 11.10.2026 | 3.411 | 3.107 | +304 |
| 10.10.2026 | 1.573 | 1.901 | −328 |
| 09.10.2026 | 1.464 | 1.617 | −153 |