Claude Chat ‐ Verbesserung der Lösung - roger-infanger-weibel/cas-hzw-ai-innovation GitHub Wiki
Zuerst die Dokumentation, dann deine Fragen:
Dokumentation ist committet und gepusht (d30246a → origin/main). Jetzt zu deinen zwei Fragen.
1. Wie die Prognosen weiter besser werden
Die Rangfolge unten ist nach Wirkung pro Aufwand sortiert — die ersten drei würde ich zuerst machen.
| # | Massnahme | Wirkung | Aufwand |
|---|---|---|---|
| 1 | Bias-Korrektur pro Parkhaus | mittel–hoch | ½ Tag |
| 2 | Feiertage + Schulferien als Feature | mittel–hoch | 1 Tag |
| 3 | „Wird es voll?" statt nur Punktprognose | hoch (für Nutzer) | 1–2 Tage |
| 4 | Echte Events statt der erfundenen | hoch | mehrere Tage, Datenbeschaffung |
| 5 | Die 15 nicht zugeordneten Häuser mappen | mittel (lokal hoch) | 1 Tag |
| 6 | Trainingsfenster verlängern | klein–mittel | 10 Minuten |
| 7 | Datenqualitäts-Filter beim Training | klein–mittel | ½ Tag |
1.1 Bias-Korrektur pro Parkhaus (der billigste echte Gewinn)
Du sammelst in ai_predictions bereits Prognose und Ist-Wert, und in ai_accuracy_daily steht der Bias pro Parkhaus und Horizont. Damit lässt sich das globale Modell nachträglich pro Haus geraderücken:
korrigiert = prognose − mittlerer_bias(parkhaus, horizont, letzte 14 Tage)
Ein globales Modell über 110 Häuser hat systematische Abweichungen bei einzelnen Häusern — genau die zeigt dein Genauigkeits-Dashboard schon an. Die Korrektur greift in predict.py direkt vor dem Schreiben, kostet kein Training und ist über den Skill-Score sofort messbar (nur bei mindestens ~50 Auswertungen anwenden, sonst rauscht es).
1.2 Feiertage und Schulferien
In features.py:24 gibt es Kalender-Features (Stunde, Wochentag, Monat, Wochenende), aber keine Feiertage und keine Ferien. Für Parkhäuser ist das ein grosser blinder Fleck: Der 1. August verhält sich wie ein Sonntag, die Herbstferien senken den Pendlerverkehr wochenlang. Drei neue Spalten reichen:
is_holiday(kantonal — Basel, Bern, Luzern, St. Gallen, Zürich haben unterschiedliche Feiertage)is_school_holiday(kantonaler Ferienkalender)days_to_holiday(Brückentage, Vorabende)
Die Daten sind statisch und lassen sich als kleine Tabelle oder JSON-Datei ablegen — kein externer Dienst nötig.
1.3 „Wird es voll?" — die Frage, die Nutzer wirklich haben
Heute sagt das Modell „noch 34 Plätze frei". Die Nutzerfrage lautet aber „finde ich noch einen Platz?". Zwei Ergänzungen, beide mit denselben Features:
a) Quantile statt Mittelwert. LightGBM mit objective='quantile', alpha=0.2 gibt eine pessimistische Untergrenze: „mit 80 % Wahrscheinlichkeit mindestens 12 Plätze frei". Das ist ehrlicher als ein Punktwert und bestraft Überoptimismus, der den Nutzer vergeblich hinfahren lässt.
b) Ein Klassifikator „voll" (free < 5). Kostet ein zusätzliches Modell pro Horizont, liefert aber eine Wahrscheinlichkeit statt einer Zahl — und ist genau dort informativ, wo die MAE-Metrik unbrauchbar wird (bei free ≈ 0).
Beides erklärt auch, warum der MAE bei vollen Häusern schlecht aussieht: Das Modell ist genau dann unsicher, wenn es am meisten zählt.
1.4 Events — die grösste bekannte Schwäche
Aus dem README, und es ist die ehrlichste Lücke im System: die Events in local_events sind erzeugt, nicht real — wiederkehrende Fr–So-Termine für Luzern und Zürich, für Basel, Bern und St. Gallen gar keine. Die Features event_active, event_soon, event_bonus tragen damit heute fast nichts bei. Echte Veranstaltungsdaten (Ticketportale, Stadt-Veranstaltungskalender, Hallen-Websites) wären der grösste Sprung — aber auch der einzige Punkt auf dieser Liste, der Datenbeschaffung statt Code bedeutet.
Direkt daran hängt Punkt 5: 15 von 110 Häusern haben keinen Stammdaten-Match und bekommen deshalb grundsätzlich keine Event-Features, egal wie gut die Eventdaten werden.
1.5 Kleinigkeiten mit sofortiger Wirkung
- Trainingsfenster:
TRAIN_DAYSsteht auf 120 Tagen. Der neue Server hat genug RAM — mit 365 Tagen sieht das Modell einen kompletten Jahreszyklus (Sommerferien, Advent, Winterwetter). Einfach umstellen und den Skill-Score vergleichen. - Datenqualität: Scanner-Ausfälle erzeugen wiederholte identische Werte. Beim Training Zeilen ausschliessen, bei denen
totalsprunghaft wechselt oder der Messwert älter als 15 Minuten ist. - Wetter: aktuell nur Temperatur und Niederschlag. Wind und Schnee sind für die Verkehrsmittelwahl relevanter als die Temperatur allein.
Wichtig für alle Punkte: Du hast mit Skill-Score und Holdout-Vergleich bereits die Messinfrastruktur. Jede Änderung einzeln trainieren, den Skill-Score vorher/nachher vergleichen — nicht drei Dinge gleichzeitig ändern.
2. Wie der Chat-Assistent intelligenter wird
Der heutige Stand: acht Regex-Intents in intents.py:4, Entitäten per Keyword und difflib, Antworten aus Templates. Das ist schnell, gratis und vorhersehbar — aber es scheitert an allem, was nicht ins Muster passt.
Stufe 0: erst messen, dann bauen
ai_chat_log speichert bereits den erkannten Intent. Bevor du irgendetwas baust:
SELECT intent, COUNT(*) FROM ai_chat_log GROUP BY intent ORDER BY 2 DESC;
Ist der Fallback-Anteil klein, brauchst du keinen LLM — dann fehlen nur Synonyme. Ist er gross, siehst du in den Log-Texten genau, welche Fragen scheitern. Das ist eine Stunde Arbeit und entscheidet, ob sich der Rest lohnt.
Stufe 1: ohne LLM deutlich besser (1–2 Tage, keine laufenden Kosten)
- Gesprächskontext: Heute wird pro Frage neu geraten. Merkt sich die Session die letzte Stadt und das letzte Parkhaus, funktionieren Anschlussfragen wie „und morgen?", „und in Basel?", „warum?".
- Begründungen statt Zahlen: „In 2 Stunden noch ca. 40 Plätze — es regnet und im KKL beginnt um 19:30 eine Veranstaltung." Die Daten dafür liegen in den Handlern schon vor.
- Unsicherheit nennen: „±25 Plätze" — die historische Genauigkeit pro Haus steht in
ai_accuracy_daily. - Mehrere Intents pro Satz: „Wie ist das Wetter und wo parke ich am besten?" trifft heute nur den ersten Treffer der geordneten Regelliste.
Stufe 2: Claude API als Aufsatz (der eigentliche Sprung)
Die Architektur ist dafür vorbereitet — ChatEngine in engine.py ist die Fassade, die Handler liefern strukturierte Daten. Es gibt zwei Ausbaustufen:
Variante A — LLM nur fürs Verstehen. Claude klassifiziert Intent und extrahiert Entitäten (Stadt, Parkhaus, Zeitpunkt) als JSON, die Antworttexte bleiben deine Templates. Halluzinationen sind praktisch ausgeschlossen, weil das LLM keine Zahlen produziert. Günstig, weil Ausgabe = wenige Tokens.
Variante B — LLM mit Tools (empfohlen). Deine Handler werden zu Tools, Claude entscheidet selbst, welche es aufruft, und formuliert die Antwort. Damit funktionieren offene Fragen, die du nie als Regel schreiben würdest: „Ich muss am Freitag um 19 Uhr ins KKL, wo stelle ich das Auto hin und wie früh muss ich los?" oder „Ist die Prognose für das Bahnhofparking überhaupt zuverlässig?"
Skizze für Python (der Tool-Runner der offiziellen anthropic-Bibliothek übernimmt die Schleife):
from anthropic import Anthropic, beta_tool
client = Anthropic() # ANTHROPIC_API_KEY aus der .env
@beta_tool
def freie_plaetze(stadt: str, parkhaus: str = "") -> str:
"""Aktuelle Belegung. Args: stadt: Stadtname. parkhaus: optional."""
return json.dumps(handlers.current(stadt, parkhaus))
@beta_tool
def prognose(stadt: str, parkhaus: str, stunden_voraus: int) -> str:
"""Belegungsprognose für 1, 2, 4 oder 8 Stunden voraus."""
return json.dumps(handlers.forecast(stadt, parkhaus, stunden_voraus))
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=2000,
thinking={"type": "adaptive"},
system="Du bist ein Parkhaus-Assistent für Schweizer Städte. "
"Antworte kurz und auf Deutsch. Nenne nur Zahlen aus den Tools, "
"erfinde nie welche. Sag es, wenn Daten fehlen.",
tools=[freie_plaetze, prognose, bestes_parkhaus, wetter, events, genauigkeit],
messages=verlauf,
)
antwort = runner.until_done()
Kosten: Claude Opus 5 kostet $5 pro Million Eingabe-Tokens und $25 pro Million Ausgabe-Tokens. Eine typische Frage mit Tool-Ergebnissen liegt bei rund 3'000 Eingabe- und 300 Ausgabe-Tokens — also etwa 2 Rappen pro Frage. Mit Prompt-Caching für System-Prompt und Tool-Schemas sinkt der Eingabeteil auf etwa ein Zehntel. Wenn dir das für einen öffentlichen Chat zu viel ist, gibt es günstigere Modelle (Sonnet 5 zu $3/$15, Haiku 4.5 zu $1/$5) — das ist aber deine Entscheidung, nicht meine; qualitativ empfehle ich Opus 5.
Wichtig für den Betrieb: LLMEngine als zweite Implementierung neben der regelbasierten anlegen, per .env-Schalter umstellbar, und bei fehlendem API-Key oder API-Fehler automatisch auf die Regeln zurückfallen. Dann bleibt der Chat auch funktionsfähig, wenn die API nicht erreichbar ist oder das Budget aufgebraucht ist. Ein Zähler pro Session verhindert Kostenüberraschungen.
Meine Empfehlung, wenn du nur eine Sache machst: Bei der Prognose die Bias-Korrektur pro Parkhaus (½ Tag, sofort messbar). Beim Chat zuerst eine Stunde ai_chat_log auswerten — das sagt dir, ob sich der LLM überhaupt lohnt.
Plan: 4 Prognose-Verbesserungen umsetzen
Context
Die ML-Prognose hat vier bekannte Schwächen: (1) systematische Abweichungen pro Parkhaus, (2) fehlende Feiertags-/Ferien-Features (Code existiert in kalender.py, wird aber nie aufgerufen), (3) nur Punktprognosen statt Voll-Wahrscheinlichkeit, (4) erfundene statt echte Events. Der User will alle vier umsetzen, mit Feiertagen/Ferien in der Datenbank statt in Dateien.
1. Bias-Korrektur pro Parkhaus
Was: Nach der ML-Prognose den mittleren Bias der letzten 14 Tage abziehen.
Dateien:
core/data_access.py— neue Funktionbias_lookup(env, model_type="ml")dieai_accuracy_dailyabfragt (Spaltebias_freeexistiert schon), gefiltert auf letzte 14 Tage, min. 50 Auswertungenforecast/predict.py— nach Zeile ~124 (wofreeberechnet wird):free = free - biasfalls Bias-Daten vorhanden
Logik:
# In predict.py, vor dem Append der insert_rows:
bias = bias_lookup.get((row["city"], row["pls_id"], h), 0)
free = max(0, min(total, free - round(bias)))
2. Feiertage/Schulferien als DB-Features
Was: Die bestehende kalender.py nutzt hardcodierte Daten und JSON-Dateien. Der User will alles in der DB. Zwei neue Tabellen + kalender.py anpassen, build_horizon_frame um den Aufruf ergänzen.
Neue DB-Tabellen (in schema.sql):
ai_feiertage—(datum DATE, kanton VARCHAR(4), name VARCHAR(100), PRIMARY KEY (datum, kanton))ai_schulferien—(kanton VARCHAR(4), jahr SMALLINT, von DATE, bis DATE, bezeichnung VARCHAR(64), PRIMARY KEY (kanton, jahr, von))
Dateien:
schema.sql— zwei CREATE TABLE Statementscore/kalender.py—feiertage()undschulferien()lesen aus DB (Fallback auf bisherige Berechnung falls DB leer). Neue Funktionsync_kalender_to_db(env)die die berechneten Werte in die DB schreibt (idempotent, einmalig aufrufbar).forecast/features.py— inbuild_horizon_frame()nach den Kalender-Features (Zeile ~163) den Aufrufkalender.kalender_spalten(df["city"], df["target_slot"])einfügen und die 3 Spalten zuweisen
Wichtig: kalender_spalten() existiert bereits und liefert genau die 3 Spalten is_holiday, is_bridge_day, is_school_holiday. Es fehlt nur der Aufruf in build_horizon_frame.
3. "Wird es voll?" — Quantil-Modell + Voll-Klassifikator
Was: Zusätzlich zum Punktmodell (a) ein Quantil-Modell (α=0.2, pessimistisch) und (b) einen Binär-Klassifikator "voll" (free < 5).
Dateien:
forecast/ml_model.py— neue KlasseQuantileModel(LGBMRegressor mitobjective='quantile', alpha=0.2) undFullClassifier(LGBMClassifier mitobjective='binary', target =free < 5)forecast/train.py— nach dem normalen ML-Training: Quantil-Modell und Klassifikator trainieren, als eigenemodel_type('ml_q20','ml_full') inai_model_runsspeichernforecast/predict.py— Quantil- und Klassifikator-Prognosen schreiben. Braucht neue Spalten inai_predictionsschema.sql—ai_predictionserweitern:predicted_free_q20 INT NULL,predicted_full_prob DECIMAL(5,4) NULLapi/Routen — die neuen Werte im API-Response mitgeben
4. Echte Events statt erfundene
Was: Die local_events-Tabelle enthält heute erfundene Fr–So-Termine. Ersetzen durch echte Eventdaten. Das ist primär Datenbeschaffung — der Code-Teil ist:
Dateien:
- Neues Script
scanner/fetch_events.py— Scraper/API-Client für echte Veranstaltungskalender (Stadt-Websites, Ticketportale). Schreibt inlocal_eventsim bestehenden Schema. core/data_access.py—events_range()funktioniert bereits korrekt mit echten Daten, keine Änderung nötig- Bestehende
event_parkhaus-Zuordnungen müssen für echte Events neu gemacht werden (manuell oder regelbasiert über Venue → nächstes Parkhaus)
Datenquellen: Offizielle Stadt-Veranstaltungskalender scrapen:
- Basel: basel.ch/veranstaltungen
- Bern: bern.ch/veranstaltungen
- Luzern: luzern.ch/veranstaltungen
- St.Gallen: stadt.sg.ch/veranstaltungen
- Zürich: zuerich.com/veranstaltungen
Zuerst die Seiten untersuchen (JSON-API? iCal? HTML?), dann pro Stadt einen Scraper schreiben. Relevante Felder: Titel, Venue/Ort, Start/Ende, Kategorie. Venue → Parkhaus-Zuordnung über Geodaten oder manuelle Tabelle.
Reihenfolge der Umsetzung
- Punkt 2 zuerst — Kalender-Features aktivieren (einfachster Fix:
kalender_spalten-Aufruf inbuild_horizon_frame+ DB-Tabellen) - Punkt 1 — Bias-Korrektur (unabhängig, sofort messbar)
- Punkt 3 — Quantil + Klassifikator (braucht Schema-Änderung + neues Training)
- Punkt 4 — Events (Datenbeschaffung, längste Laufzeit)
Verifizierung
- Nach Punkt 1+2: Neu trainieren (
python -m forecast.train --env test), Skill-Score vorher/nachher vergleichen - Nach Punkt 3: Prüfen dass
ai_predictionsdie neuen Spalten befüllt - Nach Punkt 4:
SELECT * FROM local_events WHERE ...zeigt echte Events - Alle Punkte einzeln testen, nicht gleichzeitig ändern