Technical Documentation

Modellarchitektur, Feature Engineering, Evaluation und API — für Data Scientists, ML Engineers und alle, die es genau wissen wollen. 📘 Interactive API Docs (Swagger)

1. System Overview

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.

Datenpunkte
~1.6 Mio.
pls_fetch_current
Parkhäuser
110
5 Städte
Features
40
pro Observation
Trainingsfenster
120 Tage
rollierend

2. Model Architecture

2.1 Regression (Punktprognose)
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.
2.2 Quantile Regression (pessimistisch)
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
2.3 Binary Classifier (Voll-Wahrscheinlichkeit)
Algorithmus LightGBM (LGBMClassifier), objective='binary'
Target 1 wenn free < 5, sonst 0
Output predicted_full_prob ∈ [0, 1] — P(Parkhaus praktisch voll)
2.4 Baseline (Referenzmodell)

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.

2.5 Bias-Korrektur (Post-Processing)

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.

3. Feature Engineering

40 Features pro Observation, gruppiert nach Kategorie:

Temporal (zyklisch kodiert)

hour quarter weekday is_weekend month sin_hour cos_hour sin_weekday cos_weekday sin_month cos_month

Kalender

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.

Lag Features & Statistiken

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.

Wetter

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.

Events

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, …).

Interaktions-Features

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.

Metadaten

log_total city pls_key

log_total = log(Kapazität) — normalisiert den Grösseneffekt. city und pls_key sind kategorial kodiert.

4. Training Pipeline

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

5. Evaluation & Metriken

5.1 Aktuelle Performance (letzte 7 Tage, Prod)
Horizont MAEfree MAEocc (pp) Biasfree Baseline MAE (pp) Skill Score

Metriken erklärt:

5.2 Evaluation Pipeline
  1. Alle 15 Min: gereifte Prognosen dem nächsten Messwert zuordnen (±20 Min Toleranz)
  2. Fehler berechnen: error = predicted_free − actual_free
  3. Tagesweise aggregieren in ai_accuracy_daily: MAE, Bias, Anzahl je (day, city, pls_id, model_type, horizon_h)
  4. Stadtweite und globale Aggregate separat (mit pls_id = '')

6. Data Pipeline

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
Rasterung & Preprocessing

7. Parkhaus Identity Mapping

Die Messwert-IDs (pls_fetch_current.id) und Stammdaten-IDs (parkhaeuser.id) verwenden unterschiedliche Namenskonventionen:

StadtMethodeBeispiel
Basel, ZürichExakter Matchbaselparkhaussteinen
Luzern, St. GallenNamens-ContainmentSP03luzernparkhausbahnhof
BernWortmengen-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.

8. REST API

Vollständige, interaktive Dokumentation: 📘 Swagger UI öffnen

Alle Endpunkte akzeptieren ?env=prod|test.

EndpointMethodeBeschreibung
/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/summaryGET MAE, Bias, Skill-Score je Stadt und Horizont
/api/accuracy/parkhaus/{stadt}/{id}GET Detaillierte Genauigkeit eines Hauses
/api/accuracy/timeseriesGET MAE-Verlauf über die letzten N Tage
/api/chatPOST Chat-Assistent mit semantischer Intent-Erkennung (Sentence-Transformer, Regex-Fallback)
/api/healthGET Status, aktive Modelle, Scheduler-Zeiten, DB-Info
/api/versionGET Version (Zeitstempel der jüngsten Quelldatei)
/api/citiesGET Verfügbare Städte

9. Datenbank-Schema

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.

Scanner-Tabellen (read-only)
TabelleInhalt~Zeilen
pls_fetch_currentAlle Belegungsmesswerte (15-Min-Takt)1.6 Mio
weather_forecastsStündliches Wetter je Stadt27 000
local_eventsVeranstaltungen mit Kategorie und Bonus393
event_parkhausEvent → Parkhaus Zuordnung (n:m)720
parkhaeuserStammdaten inkl. parking_group85
citiesStadt-Konfiguration mit Koordinaten5
KI-Tabellen (read-write)
TabelleInhalt
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

10. Stack & Deployment

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

11. Change History

Änderungsprotokoll aus der Zusammenarbeit mit Claude Code — Anfrage und Umsetzung.

12. August 2026
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).
11. August 2026
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.
9. August 2026
Fehler
R²-Wert wird in TechDoc nicht angezeigt (zeigt «—»)
Api.env (Property) → Api.env() (Funktion). Fetch-URL war mit Funktionsbody statt «prod» zusammengebaut.
7. August 2026
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.
6. August 2026
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.
5. August 2026
Verbesserung
UI: Prod als Default, Suchfeld, gemerkte Einstellungen
Umschalter nur mit ?admin sichtbar. Suchfeld, localStorage-Persistenz, Footer mit Version.
4. August 2026
Erweiterung
Umzug auf neuen Server (87.106.21.252)
FastAPI-ML auf eigenen Server migriert. Holdout-Vergleich gegen vorheriges Modell.
31. Juli 2026
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.
30. Juli 2026
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.
29. Juli 2026
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.
28. Juli 2026
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.
12. Juli 2026
Erweiterung
Initiales Projekt: Parkhaus-Daten-Crawler
5 Stadt-Collectoren, Scheduler alle 15 Min, MariaDB, .env-Konfiguration.