Modellarchitektur, Feature Engineering, Evaluation und API — für Data Scientists, ML Engineers und alle, die es genau wissen wollen. 📘 Interactive API Docs (Swagger)
Multi-Horizon Occupancy Forecasting für 110 Parkhäuser in 5 Schweizer
Städten. Vier unabhängige Modelle (h ∈ {1, 2, 4, 8})
prognostizieren alle 15 Minuten die Belegungsquote
occ = (total − free) / total ∈ [0, 1].
Zusätzlich ein Quantil-Modell (α = 0.2) und ein Binärklassifikator
(P(free < 5)) pro Horizont.
| Algorithmus | LightGBM (LGBMRegressor), Fallback: scikit-learn
HistGradientBoostingRegressor |
| Objective | regression_l1 (MAE) — robust gegen Ausreisser |
| Hyperparameter | num_leaves=63, n_estimators=600, learning_rate=0.05,
subsample=0.8, colsample_bytree=0.8, min_child_samples=50 |
| Target | occ ∈ [0, 1] (Belegungsquote), geclippt auf [0, 1] post-prediction |
| Scope | Ein globales Modell pro Horizont über alle Städte und
Parkhäuser — mit city und pls_key als kategoriale Features.
Einzelne Häuser liefern nur ~96 Samples/Tag; gemeinsames Training
ermöglicht Cross-Learning. |
| Objective | quantile, α = 0.2 |
| Interpretation | «Mit 80 % Wahrscheinlichkeit sind mindestens X Plätze frei.» Konservative Schätzung für risikoaverse Nutzer. |
| Output | predicted_free_q20 in ai_predictions |
| Algorithmus | LightGBM (LGBMClassifier), objective='binary' |
| Target | 1 wenn free < 5, sonst 0 |
| Output | predicted_full_prob ∈ [0, 1] — P(Parkhaus praktisch voll) |
Saisonaler Mittelwert je (parkhaus, wochentag, stunde, viertelstunde)
über 8 Wochen, mit Rückfallkette auf Stundenmittel → Stadtmittel → Hausmittel.
Additive Zuschläge für aktive Events (+Bonus) und Regen > 0.5 mm (+2 pp).
Dient ausschliesslich als Benchmark — kein Ensemble-Bestandteil.
Pro (city, pls_id, horizon_h) wird der gewichtete mittlere Bias der
letzten 14 Tage berechnet:
w(d) = exp(−0.14 × d) Halbwertszeit ≈ 5 Tage
bias = Σ(bias_free × n × w(d)) / Σ(n × w(d)) letzte 14 Tage, min. 50 Evaluationen
free_korrigiert = free − round(bias)
Exponentiell gewichtet: jüngere Tage haben mehr Einfluss, damit der Bias schnell auf Veränderungen reagiert (z.B. Baustellen, geänderte Kapazitäten). Wird nur auf ML-Prognosen angewandt, nicht auf Baseline.
40 Features pro Observation, gruppiert nach Kategorie:
hour quarter weekday is_weekend month sin_hour cos_hour sin_weekday cos_weekday sin_month cos_month
is_holiday is_bridge_day is_school_holiday
Kantonale Feiertage (BS, BE, LU, SG, ZH) inkl. Osterdatum-basierte bewegliche
Feiertage. Brückentage zwischen Feiertag und Wochenende. Schulferien pro Kanton
und Jahr. Gespeichert in ai_feiertage und ai_schulferien.
occ_now occ_1h_ago occ_2h_ago occ_3d_ago occ_24h_ago occ_7d_ago delta_occ_1h trend_2h occ_mean_3h occ_mean_24h occ_std_24h prior_occ
prior_occ = saisonaler Erwartungswert aus dem Trainingsteil
(kein Leakage ins Holdout). delta_occ_1h = Momentum der letzten Stunde.
Fehlende Lags bleiben NaN — LightGBM handhabt das nativ.
temperature precipitation is_raining temp_bin temp_sq
Quelle: Open-Meteo API,
stündlich pro Stadt. is_raining = binäres Flag bei > 0.1 mm.
Zum Zielzeitpunkt interpoliert.
event_active event_soon event_bonus event_category
Echte Veranstaltungsdaten von Venue-Websites (Hallenstadion, Tonhalle,
Stadtcasino Basel, Luzerner Theater, OLMA). event_bonus = kategorieabhängiger
Zuschlag (Konzert 0.40, Sport 0.45, Messe 0.50 etc.).
event_soon = Event beginnt innerhalb der nächsten 3 Stunden.
event_category = kategorische Variable (Konzert, Sport, Messe, …).
weekend_event rain_hour
weekend_event = is_weekend × event_active — Events am Wochenende haben stärkeren Effekt.
rain_hour = is_raining × hour — Regen wirkt je nach Tageszeit unterschiedlich.
log_total = log(Kapazität) — normalisiert den Grösseneffekt.
city und pls_key sind kategorial kodiert.
| Schedule | Täglich 03:30 Uhr (Europe/Zurich), APScheduler in-process |
| Trainingsfenster | 120 Tage rollierend (konfigurierbar via AI_TRAIN_DAYS) |
| Holdout | Letzte 14 Tage — zeitbasierter Split, kein Random-Split (verhindert Temporal Leakage) |
| Trainingsgrösse | ~780 000 Zeilen pro Horizont |
| Activation Gate | Neues Modell wird nur aktiviert wenn MAE_neu ≤ 1.10 × MAE_alt
(gemessen auf demselben Holdout). Schützt vor Qualitätseinbrüchen nach Datenausfällen. |
| Pro Horizont | 3 Modelle: Regression → Quantile (α=0.2) → Classifier (binary) |
| Artefakte | models_store/*.joblib, letzte 7 behalten, aktive geschützt |
| Speicher | ~735 MB bei 60-Tage-Fenster, ~1368 MB bei 120 Tagen |
| Horizont | MAEfree | MAEocc (pp) | Biasfree | Baseline MAE (pp) | Skill Score | R² |
|---|---|---|---|---|---|---|
Metriken erklärt:
1 − MAE(ML) / MAE(Baseline).
0.72 = 72 % weniger Fehler als der saisonale Durchschnitt.
Negativ = Modell schlechter als Baseline.1 − SSres / SStot.
Anteil der erklärten Varianz. 0.85 = das Modell erklärt 85 % der
Belegungsschwankungen. Berechnet auf dem Holdout (letzte 14 Tage)
und beim Training gespeichert.free → 0 explodiert der
relative Fehler.error = predicted_free − actual_freeai_accuracy_daily:
MAE, Bias, Anzahl je (day, city, pls_id, model_type, horizon_h)pls_id = '')Scanner (alle 15 Min)
│ Stadt-APIs → pls_fetch_current (~1.6 Mio Zeilen)
│ Open-Meteo → weather_forecasts (2× täglich)
│ Venues → local_events + event_parkhaus (2× täglich, automatisch)
│
FastAPI-ML (Port 80)
├── :10/:25/:40/:55 predict.py → ai_predictions
│ └── Bias-Korrektur aus ai_accuracy_daily
├── :13/:28/:43/:58 evaluate.py → ai_accuracy_daily
├── 03:15 identity.py → ai_parkhaus_map
└── 03:30 train.py → ai_model_runs + models_store/
└── Regression + Quantile + Classifier × 4 Horizonte
NaN
(fallen aus dem Training)occ = (total − free) / total — ermöglicht ein
globales Modell über Häuser mit 50 bis 1100 Plätzentotal
Die Messwert-IDs (pls_fetch_current.id) und Stammdaten-IDs
(parkhaeuser.id) verwenden unterschiedliche Namenskonventionen:
| Stadt | Methode | Beispiel |
|---|---|---|
| Basel, Zürich | Exakter Match | baselparkhaussteinen |
| Luzern, St. Gallen | Namens-Containment | SP03 → luzernparkhausbahnhof |
| Bern | Wortmengen-Vergleich | «Bahnhof Parking» → bernparkhausbahnhof |
Zentral gelöst in core/identity.py, Ergebnis in ai_parkhaus_map.
Häuser ohne Treffer werden trotzdem prognostiziert — nur ohne Event-Features.
Vollständige, interaktive Dokumentation: 📘 Swagger UI öffnen
Alle Endpunkte akzeptieren ?env=prod|test.
| Endpoint | Methode | Beschreibung |
|---|---|---|
/api/forecast/current/{stadt} | GET | Aktuelle Werte + Prognosen aller Häuser einer Stadt.
Enthält free_q20 und full_prob. |
/api/forecast/parkhaus/{stadt}/{id} | GET | Ist-Verlauf + Prognosen + Wetter + Events für ein Haus (Chart-Daten) |
/api/forecast/best/{stadt} | GET | Top-5-Empfehlung für einen Zeitpunkt (?at=), inkl. MAE-Konfidenz |
/api/accuracy/summary | GET | MAE, Bias, Skill-Score je Stadt und Horizont |
/api/accuracy/parkhaus/{stadt}/{id} | GET | Detaillierte Genauigkeit eines Hauses |
/api/accuracy/timeseries | GET | MAE-Verlauf über die letzten N Tage |
/api/chat | POST | Chat-Assistent mit semantischer Intent-Erkennung (Sentence-Transformer, Regex-Fallback) |
/api/health | GET | Status, aktive Modelle, Scheduler-Zeiten, DB-Info |
/api/version | GET | Version (Zeitstempel der jüngsten Quelldatei) |
/api/cities | GET | Verfügbare Städte |
MariaDB, zwei parallele Instanzen: ph_fetch_prod und
ph_fetch_test. Die KI-App liest bestehende Tabellen nur und
schreibt ausschliesslich in ai_-präfixierte Tabellen.
| Tabelle | Inhalt | ~Zeilen |
|---|---|---|
pls_fetch_current | Alle Belegungsmesswerte (15-Min-Takt) | 1.6 Mio |
weather_forecasts | Stündliches Wetter je Stadt | 27 000 |
local_events | Veranstaltungen mit Kategorie und Bonus | 393 |
event_parkhaus | Event → Parkhaus Zuordnung (n:m) | 720 |
parkhaeuser | Stammdaten inkl. parking_group | 85 |
cities | Stadt-Konfiguration mit Koordinaten | 5 |
| Tabelle | Inhalt |
|---|---|
ai_predictions |
Jede Prognose: predicted_free, predicted_occ, predicted_free_q20,
predicted_full_prob, Zielzeit, Horizont, Modelltyp |
ai_accuracy_daily |
Tagesaggregierte Fehler: MAE, Bias, n je (Tag, Stadt, Parkhaus, Modell, Horizont) |
ai_model_runs |
Trainingsprotokoll: Zeitpunkt, Datenmenge, CV-MAE, Artefakt-Pfad, is_active |
ai_parkhaus_map |
Identity-Mapping zwischen Messwert- und Stammdaten-IDs |
ai_feiertage |
Kantonale Feiertage: (datum, kanton, name) |
ai_schulferien |
Schulferien pro Kanton und Jahr: (kanton, jahr, von, bis) |
ai_chat_log |
Chat-Verlauf mit erkannter Absicht |
| Backend | Python 3.9+, FastAPI, uvicorn (1 Worker) |
| ML | LightGBM (Fallback: scikit-learn), pandas, numpy |
| Datenbank | MariaDB, pymysql |
| Scheduler | APScheduler (in-process), Zeitzone Europe/Zurich |
| Frontend | HTML, Bootstrap 5.3, Chart.js 4.4 — kein Build-Schritt |
| Server | 87.106.21.252, Port 80 |
Änderungsprotokoll aus der Zusammenarbeit mit Claude Code — Anfrage und Umsetzung.
| Verbesserung | Scanner Code Quality von C+ auf B verbessern (Top-5 Refactoring)
1) constants.py: SWISS_TZ, USER_AGENT zentral (statt 8x dupliziert). 2) Basel/Zürich: _is_stale, normalize_parkendd, collect, _collect_fallback in Basisklasse verschoben (~130 Zeilen eliminiert). 3) scheduler.py: 3 identische run_*-Funktionen durch eine generische _run_script ersetzt. 4) get_event_and_weather_data.py: pymysql durch db_utils.get_connection ersetzt, Weather-Duplikat in _fetch_weather zusammengefasst. 5) Dead Code: store_historical_events (100 Zeilen), unused imports, redundante Bedingungen entfernt. |
| Fehler | Chatbot erkennt Datumsangaben wie «14.8.» oder «14. August» nicht
entities.py: Datums-Parser um explizite Formate ergänzt (TT.MM., TT.MM.JJJJ, TT. Monatsname). |
| Fehler | Chatbot antwortet mit Parkhaus-Empfehlung statt Events, wenn nach «events am 14.8.» gefragt
intents.py: Keyword-Overrides eingeführt — enthält der Text «event», «veranstaltung» o.ä., wird immer Intent «events» gewählt, bevor der Semantic Classifier falsch zuordnen kann. Zusätzlich Events-Beispielsätze mit Datumsangaben in semantic.py ergänzt. |
| Erweiterung | Genauigkeitsseite: Radar-Charts statt Balken, 3 Tabs, 4 Spalten pro Horizont
Balkendiagramm durch 4 Radar-Charts (KI vs. Basis pro Stadt) ersetzt. Seite in 3 Tabs: Übersicht, Parkhaus, Trainingsläufe. |
| Erweiterung | Events nach Heute/Morgen gruppiert anzeigen mit Datum
forecast.js: Events nach Heute und Morgen getrennt mit Datumsanzeige. |
| Fehler | fetch_events.py schlägt fehl mit «unrecognized arguments: --days 60»
scheduler.py: --days 60 entfernt, fetch_events.py akzeptiert nur --venue. |
| Fehler | copy-github.sh klont den falschen Branch
--branch main explizit in git clone eingefügt. |
| Verbesserung | Fake/Dummy-Events aus der Datenbank löschen
170 Fake-Events + 476 Zuordnungen pro DB gelöscht (prod + test). |
| Fehler | «Prognose ist veraltet» erscheint nach jedem Server-Neustart
predict.run() wird jetzt sofort beim App-Start ausgeführt (main.py lifespan), damit frische Prognosen vor dem ersten Request vorliegen. |
| Verbesserung | Parkhäuser ohne Bewegungen in 48 h automatisch ausblenden
services.py: Parkhäuser mit min(free)=max(free) über 48 h werden aus der Prognosetabelle gefiltert (defekter Sensor / inaktiv). |
| Erweiterung | Chatbot intelligenter machen mit lokalem ML-Modell (keine externen API-Kosten)
Sentence-Transformer (paraphrase-multilingual-MiniLM-L12-v2, ~120 MB) integriert. 8 Intents, Cosine-Similarity, Regex-Fallback. |
| Verbesserung | Alle Dokumentationen mit neuem Chatbot und Event-Automatisierung aktualisieren
doku.html, techdoc.html, chat.html, alle README.md: «Regelbasierter Assistent» → «semantische Sprachverarbeitung», Events «manuell» → «automatisch». |
| Verbesserung | Events sollen automatisch gescrapt werden, nicht manuell erzeugt
scheduler.py: fetch_events.py als Job um 06:30/18:30. store_historical_events() entfernt. |
| Fehler | R²-Wert wird in TechDoc nicht angezeigt (zeigt «—»)
Api.env (Property) → Api.env() (Funktion). Fetch-URL war mit Funktionsbody statt «prod» zusammengebaut. |
| Erweiterung | R²-Metrik (Bestimmtheitsmass) einführen
R² in train.py berechnet (Holdout), in ai_model_runs gespeichert, über API ausgeliefert, in TechDoc angezeigt. |
| Erweiterung | Auto-Migration für neue DB-Spalten beim App-Start
main.py: _auto_migrate() prüft information_schema und fügt fehlende Spalten per ALTER TABLE hinzu. |
| Erweiterung | TechDoc Kapitel 5 (Live-Daten) ist leer
JS repariert: data.per_city → data.entries. Performance-Tabelle gefüllt. |
| Verbesserung | Features auf 40 erweitern mit Interaktionen
9 Interaktions-Features, exponentielle Bias-Gewichtung, Subsampling für Normalstunden. |
| Erweiterung | Neue TechDoc-Seite für Data Scientists
techdoc.html mit 5 Kapiteln: Modellarchitektur, Features, Metriken, API, Live-Daten. |
| Erweiterung | 4 Prognose-Verbesserungen (Bias, Kalender, Quantil, Events)
Bias-Korrektur (14-Tage-Mittel), Kalender aus DB, Quantil-Modell (α=0.2), Voll-Klassifikator, Event-Scraper für 6 Venues. |
| Fehler | None-Werte bei MAE in _insert_run für Klassifikator
None-Check für mae_free/mae_occ vor INSERT. |
| Verbesserung | UI: Prod als Default, Suchfeld, gemerkte Einstellungen
Umschalter nur mit ?admin sichtbar. Suchfeld, localStorage-Persistenz, Footer mit Version. |
| Erweiterung | Umzug auf neuen Server (87.106.21.252)
FastAPI-ML auf eigenen Server migriert. Holdout-Vergleich gegen vorheriges Modell. |
| Fehler | Chat tot über plain HTTP, «jetzt»-Fragen ohne Daten
HTTP-Fallback, Chat-Handler für «jetzt» liefert aktuelle Messwerte. |
| Verbesserung | Training vom Server auslagern (zu wenig RAM)
Training auf PC, Modelle per SCP übertragen. MODELL-REFRESH.md erstellt. |
| Erweiterung | KI-Prognose-App (FastAPI-ML) erstellen
Komplette App: LightGBM für 4 Horizonte, Prognoseseite, Genauigkeits-Dashboard, Chat, Dokumentation. |
| Fehler | Training hängt, Server-Speicher explodiert (641 MB RAM)
Speicherlimits via systemd-run, Training mit --days begrenzt. |
| Erweiterung | systemd-Units, Autostart, Start-Scripts
4 Service-Dateien, install-systemd.sh, start-all.sh, crontab @reboot. |
| Erweiterung | Wetter, 24h-Prognose, Prod/Test-Umschaltung im Flask-Dashboard
Wetteranzeige, Event-Sektion, ?env= Parameter, Fallback-APIs für Basel/Zürich. |
| Verbesserung | Von JSON-Dateien auf DB-Tabellen umstellen
Städte/Parkhäuser aus DB statt JSON. Statische Dateien entfernt. |
| Erweiterung | Wetter-/Event-Daten sammeln, Projektstruktur aufräumen
Open-Meteo API, Event-Generierung. flask/ und scanner/ Ordnerstruktur. |
| Verbesserung | Crawler robuster machen
Retry mit Backoff, UPSERT, Snapshot-Fallback, St. Gallen API-Mapping korrigiert. |
| Erweiterung | Simulationsmodus für den Crawler
--simulation Flag, MockCursor für Tests. |
| Erweiterung | Initiales Projekt: Parkhaus-Daten-Crawler
5 Stadt-Collectoren, Scheduler alle 15 Min, MariaDB, .env-Konfiguration. |