Hintergrund
Bei vilisto prognostizieren wir die Batterielebensdauer für 100k+ smarte Thermostate. Die Algorithmen schätzen verbleibende Kapazität (mAh) und Laufzeit (Tage) pro Gerät, und Gebäudemanager planen mit diesen Vorhersagen ihre Wartung.
Die Algorithmen liegen in einer internen Python-Bibliothek, und neue Versionen mit geänderter Vorhersagelogik erscheinen regelmäßig. Bevor eine neue Version auf der Flotte läuft, muss gezeigt sein, dass sie besser vorhersagt als die aktuelle. Ein A/B-Test ist unpraktikabel, weil eine Batterie erst nach Monaten leer ist.
Ich habe dafür zwei Tools gebaut: eine Evaluierungspipeline, die Algorithmusversionen auf kuratierten Felddaten vergleicht, und einen operativen Service, der die gewählte Version auf der gesamten Flotte ausführt.
Datensatzerstellung aus Felddaten
Labordaten aus Teststand-Messungen unter kontrollierten Bedingungen gibt es, aber Felddaten sind unregelmäßiger. Thermostate hängen in Gebäuden mit unterschiedlichen Heizmustern, Firmware-Versionen ändern sich im laufenden Betrieb, und Geräte gehen tagelang offline.
Die Evaluierungspipeline ist eine Dagster-Anwendung mit monatlich partitionierten Assets. Für jeden Monat:
- ruft sie Ventilbewegungen und Sensorlogs aus der Analytics-Datenbank ab,
- holt sie Kapazitäts-Testdaten über die Geräte-API,
- erfasst sie die Versionshistorie jedes Geräts (Hardware und Firmware zum Monatsanfang, Software-Änderungen während des Monats),
- speichert sie das Ergebnis als Parquet in MinIO unter einem deterministischen Pfad pro Monat.
Die Algorithmen verhalten sich je nach Hardware-Revision und Firmware unterschiedlich. Mit der Versionshistorie lässt sich jede Vorhersage gegen das Firmware-Verhalten evaluieren, das ihre Eingangsdaten erzeugt hat.
Manuelles Review der Samples
Nicht jedes Gerät liefert brauchbare Evaluierungsdaten. Ein Thermostat, das zwei Wochen mitten im Monat offline war, ist kein fairer Test für eine Laufzeitvorhersage. Die Grenzfälle sind zu vielfältig, um "brauchbar" im Code zu definieren.
Nach dem Aufbau der Datensätze öffnet ein Reviewer eine Streamlit-UI, die pro Sample Plots zeigt (Spannungskurven, Ventilbewegungsmuster, Konnektivitätslücken), und markiert jedes Sample als angenommen oder abgelehnt. Die Entscheidungen werden als CSV-Manifest in MinIO neben dem Datensatz gespeichert, und der Evaluierungsschritt läuft nur auf angenommenen Samples. Dasselbe Manifest ergibt immer dieselbe Evaluierung. So bleibt die Pipeline reproduzierbar, während Domain-Experten Urteile treffen können, die sich schwer als Regeln ausdrücken lassen.
Isolierte Environments pro Algorithmusversion
Die Evaluierung vergleicht zwei oder mehr Bibliotheksversionen auf demselben Datensatz. Sie können nicht im selben Python-Prozess laufen, weil es verschiedene Versionen desselben Pakets sind, die inkompatible Dependencies haben können.
Die Pipeline erstellt für jedes Versionslabel ein temporäres Virtual Environment:
# Für jedes Versionslabel (ein Release, "latest" oder ein Git-Ref)
# 1. Temporäres Venv erstellen
# 2. pip install der Bibliothek in dieser Version aus dem privaten PyPI-Index
# 3. Evaluierung in einem Subprocess ausführen
# 4. Ergebnisse sammeln und Venv entfernen
Versionslabels können Release-Versionen, latest oder Git-Refs sein. Dagster
partitioniert die Evaluierung nach Monat und nach Bibliotheksversion, sodass
jede Kombination nachverfolgt wird.
Dieser Aufbau braucht keine Container-Builds und keine CI-Pipelines pro Version. Jedes Venv existiert nur für die Dauer eines Evaluierungslaufs.
Versionsvergleich in MLflow
Jede Evaluierung loggt in MLflow:
- Vorhersagen und Ground Truth pro Sample (Kapazität in mAh, Laufzeit in Tagen)
- Fehlerverteilungen (Mittelwert, Median, Perzentile)
- Vergleichsplots über Versionen hinweg
Mit diesen Runs lässt sich eine Frage wie die, ob eine neue Version den medianen Laufzeitfehler auf dem Datensatz eines bestimmten Monats verringert hat, in MLflow beantworten, ohne ein neues Analyseskript zu schreiben.
Die Dagster-Assets zeichnen Metadaten auf (Sample-Anzahl, Fehleranzahl, Ausführungsdauer), die in der Dagster-UI angezeigt werden. Ein fehlgeschlagener oder unvollständiger Evaluierungslauf ist dort sichtbar.
Rollout auf die Flotte
Die Evaluierungspipeline verarbeitet kuratierte Samples von einigen hundert Geräten. Nachdem eine Version gewählt ist, muss sie auf allen 100k+ Geräten laufen.
Das übernimmt eine separate FastAPI-Anwendung. Sie startet Hintergrund-Worker-Prozesse, jeder in einem eigenen Venv mit der Ziel-Bibliotheksversion:
- Worker verarbeiten Geräte in Batches von 250
- jeder Batch schreibt einen Fortschritts-Checkpoint auf die Festplatte (JSON- und CSV-Snapshots)
- Ergebnisse werden nach Abschluss des Laufs als Parquet nach MinIO hochgeladen
- eine Admin-UI zeigt den Fortschritt: verarbeitete Geräte, Fehleranzahl, verstrichene Zeit
Ein vollständiger Fleet-Run dauert Stunden. Stürzt ein Worker mittendrin ab, kann der Lauf beim letzten Checkpoint fortgesetzt werden.
Der Service hat drei Berechnungsmodi:
- Q10/Q90-Schätzungen für Kapazität und Laufzeit pro Gerät
- gebuckete Verteilungs-Snapshots (30-Tage-Laufzeitbereiche, 10%-Kapazitätsbereiche) für Gesundheitsansichten auf Flottenebene
- ein Konfigurationsmodus, der Batterieparameter über die API für die gesamte Flotte zurücksetzt
Wie die beiden Tools zusammenhängen
Felddaten
-> kuratierte Datensätze
-> Versions-Evaluierung
-> Vergleich in MLflow
-> menschliche Entscheidung
-> Fleet-Rollout der gewählten Version
-> neue Felddaten
Evaluierungsdatensätze und Fleet-Ergebnisse liegen beide als versioniertes Parquet in MinIO.
Was ich ändern würde
- Dataset-Versionierung: MinIO-Pfade kodieren Jahr, Monat und Version, aber es gibt kein formales Lineage-Tracking. Wenn sich die Extraktionslogik ändert, lassen sich alte Datensätze nicht mehr mit neuen vergleichen, und nichts weist darauf hin. DVC oder eine Metadaten-Tabelle würden das lösen.
- Worker-Orchestrierung: Das FastAPI-Subprocess-Pattern funktioniert, ist aber fragil. Dagster könnte auch die Fleet-Runs ausführen und Retries und Checkpointing bereitstellen.
- Gating: Eine Person liest die MLflow-Ergebnisse und entscheidet, ob eine Version übernommen wird. Ein automatisches Gate, zum Beispiel "übernehmen, wenn der Median-Fehler um mehr als 5% gesunken ist", würde diesen manuellen Schritt ersetzen.
Stack
| Schicht | Tool |
|---|---|
| Evaluierungs-Orchestrierung | Dagster (monatliche und multi-partitionierte Assets) |
| Dataset-Speicher | MinIO (Parquet) |
| Experiment-Tracking | MLflow |
| Review-UI | Streamlit |
| Fleet-Berechnung | FastAPI + Subprocess-Worker |
| Algorithmus-Bibliothek | Internes Python-Paket (privater PyPI) |
| Datenverarbeitung | Polars, PyArrow |
| Datenbanken | PostgreSQL |