Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.16.0
Documentation
Apartment Predictor Chat — Saved Regression Model + LLM Workflow
Diese Datei ist Teil der Abgabe. Sie wird nach dem ersten erfolgreichen Deployment auf Hugging Face Spaces fertiggestellt und enthält die zwei Pflicht-Screenshots in §9.4.
1. Project Summary
Die App nimmt eine deutsche Freitext-Beschreibung eines Wohnungswunsches entgegen, extrahiert
mit einem LLM die strukturierten Felder rooms, area_m2 und town, schätzt mit einem
Random-Forest-Regressor die monatliche Miete einer Wohnung im Kanton Zürich und formuliert
mit einem zweiten LLM-Aufruf eine kurze deutsche Erklärung mit einem Unsicherheits-Hinweis.
Ziel ist es, ein klassisches tabellarisches Modell konversationell zugänglich zu machen,
ohne dass der Nutzer ein Formular ausfüllen muss.
2. Files Used
| File | Purpose |
|---|---|
app.py |
Hauptanwendung: Pipeline, Prompts, Gradio-UI |
train_model.py |
Trainings-Skript: Feature-Engineering + RF-Training, generiert die .pkl-Dateien |
apartment_price_model.pkl |
Trainierter RandomForestRegressor (sklearn) |
model_features.pkl |
Liste der 9 Feature-Namen in der Reihenfolge wie beim Training |
bfs_municipality_data_enriched.csv |
BFS-Gemeindedaten + distance_to_zurich (Mittelwert pro Gemeinde) — wird zur Inferenz-Zeit für den Lookup verwendet |
apartments_data.csv |
Roh-Trainingsdaten aus week2 (Apartment-Listings im Kanton ZH) |
bfs_municipality_and_tax_data.csv |
Roh-BFS-Daten (vor dem Distanz-Merge) |
requirements.txt |
Python-Dependencies |
README.md |
HF-Spaces-Frontmatter + Kurz-Beschreibung |
documentation.md |
Diese Doku |
screenshots/example1.png, screenshots/example2.png |
Pflicht-Screenshots vom deployten Space |
3. Numeric Prediction Part
3.1 Reused Model
Modell-Datei: apartment_price_model.pkl — ein sklearn.ensemble.RandomForestRegressor
(300 Bäume, max_depth=20, min_samples_split=5, min_samples_leaf=2, random_state=42),
trainiert in train_model.py.
Was sagt das Modell voraus: Die monatliche Brutto-Miete einer Wohnung im Kanton Zürich in CHF, basierend auf Wohnungs-Eigenschaften und Gemeinde-Statistiken.
Verwendete Features (9 Stück, in dieser Reihenfolge):
roomsarea_m2pop(Einwohnerzahl der Gemeinde)pop_dens(Einwohnerdichte)frg_pct(Ausländer-Anteil in %)emp(Anzahl Beschäftigte)tax_income(durchschnittliches steuerbares Einkommen)distance_to_zurich(Haversine-Distanz zur Zürich HB in km — pro Gemeinde gemittelt)area_per_room(area_m2 / rooms— zur Inferenz-Zeit berechnet)
5-fold-CV-Score (R²) auf den Trainingsdaten: 0.59 (±0.06). Die wichtigsten Features sind
area_m2 (Importance 0.48) und distance_to_zurich (0.31).
3.2 Prediction Logic
Pro Inferenz-Aufruf:
- Aus dem extrahierten
townwird der Eintrag inbfs_municipality_data_enriched.csvgesucht (case-insensitive, mit Substring-Fallback). - Aus der Zeile werden
pop,pop_dens,frg_pct,emp,tax_incomeunddistance_to_zurichgelesen. area_per_roomwird ausarea_m2 / roomsberechnet.- Die 9 Features werden in der gepickelten Reihenfolge (
model_features.pkl) zu einem(1, 9)-Numpy-Array zusammengesetzt. model.predict(X)[0]wird auf den nächsten ganzen CHF-Betrag gerundet.
4. LLM Extraction Part
4.1 Goal
Das LLM soll aus einer freien deutschen Beschreibung wie „Ich suche eine 3.5-Zimmer- Wohnung mit ca. 85 m² in Winterthur" drei Felder als JSON extrahieren: Anzahl Zimmer, Wohnfläche in Quadratmetern und die Gemeinde. Das Ergebnis muss direkt als Dictionary in Python ladbar sein.
4.2 Prompt Design
Verwendet wird OpenAI (Default gpt-4o-mini, überschreibbar via OPENAI_MODEL) mit
einem strikten System-Prompt, der ausschließlich JSON erlaubt:
Du bist ein Extraktor für Wohnungsbeschreibungen. Lies die deutsche Anfrage und gib
AUSSCHLIESSLICH ein JSON-Objekt zurück mit den Schlüsseln "rooms" (Zahl, z.B. 3.5),
"area_m2" (Zahl in Quadratmetern), "town" (String, Schweizer Gemeinde). Keine Erklärungen,
kein Markdown, keine Code-Fences. Wenn ein Feld fehlt oder unklar ist, schreibe null.
Wichtige Design-Entscheidungen:
- Strict JSON, keine Code-Fences — verhindert das häufige Markdown-Wrapping (
```json … ```). nullfür fehlende Felder — erlaubt der App, gezielt eine deutsche Fehlermeldung an den Nutzer zurückzugeben statt zu raten.- Deutsche Nutzerinstruktion — die Trainingsdaten und Gemeindenamen sind deutsch (Schweizer Schreibweise wie Zürich, Küsnacht).
4.3 Expected Output Format
{"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}
4.4 Validation
Nach dem LLM-Aufruf wird die Antwort von parse_json_response(raw, ("rooms","area_m2","town"))
geprüft:
- Leere Antwort →
ValueError. - Code-Fences werden entfernt (defensive Sicherheit, falls das Modell sie trotzdem produziert).
json.loads— ungültiges JSON →ValueErrormit der Roh-Antwort als Hinweis.- Fehlende Schlüssel →
ValueError.
Zusätzlich validiert extract_preferences:
rooms/area_m2dürfen nichtnullsein.townmuss vonmatch_townzu einer kanonischenbfs_name-Form aufgelöst werden, sonst Fehler („Gemeinde nicht in Datenbank").
Es gibt keinen Regex-Fallback — fehlt der API-Key oder ist die Antwort ungültig, wirft die App eine klar lesbare deutsche Fehlermeldung.
5. LLM Explanation Part
5.1 Goal
Aus den extrahierten Präferenzen und der Modellvorhersage soll das LLM eine kurze, freundliche Antwort auf Deutsch generieren — ohne den Preis selbst neu zu berechnen und mit einem Unsicherheits-Hinweis.
5.2 Prompt Design
Du bist ein Schweizer Immobilienberater. Erkläre die geschätzte Monatsmiete in 2 kurzen,
freundlichen deutschen Sätzen. Verwende den gegebenen Preis genau so, berechne KEINEN neuen
Preis. Füge einen kurzen Hinweis auf Unsicherheit hinzu (z.B. Zustand, Mikrolage, Baujahr und
Ausstattung sind nicht im Modell). Antworte AUSSCHLIESSLICH als JSON: {"answer": "<Text>"}.
Kein Markdown, keine Code-Fences.
Die User-Message enthält den vollständigen JSON-Payload:
{"preferences": {"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}, "prediction_chf": 2064}
5.3 Expected Output Format
{"answer": "Für eine 3.5-Zimmer-Wohnung mit 85 m² in Winterthur schätzt das Modell rund 2064 CHF pro Monat. Bitte beachten: Zustand, Mikrolage und Baujahr sind nicht im Modell enthalten, der tatsächliche Preis kann abweichen."}
Die Validierung erfolgt mit parse_json_response(raw, ("answer",)) und gibt nur den answer-
String an die UI zurück.
6. End-to-End Pipeline
- Nutzer gibt eine deutsche Beschreibung in das Textfeld ein und klickt auf „Schätzen".
extract_preferencesruft das LLM mit dem Extraktions-Prompt auf und validiert das JSON.match_townlöst den extrahierten Ortsnamen auf den kanonischenbfs_nameaus dem Gemeinde-CSV auf (case-insensitive, Substring-Fallback).predict_apartment_pricebaut den 9-Feature-Vektor aus den extrahierten Werten + dem Gemeinde-Lookup und ruftmodel.predictauf.generate_explanationruft das LLM mit dem Erklärungs-Prompt auf, das die Präferenzen und die Vorhersage als JSON-Payload bekommt.- Die UI zeigt das extrahierte JSON, die Preisschätzung und den Antworttext nebeneinander an.
7. Test Cases
| Test Input | Extraction OK? | Prediction OK? | Explanation OK? | Notes |
|---|---|---|---|---|
Ich suche eine 3.5-Zimmer-Wohnung mit 85 m² in Winterthur. |
Ja | Ja (~2064 CHF) | Ja | Standard-Fall, alle Felder direkt im Text. |
4.5 Zimmer, 120 m², Dietlikon |
Ja | Ja (~3008 CHF) | Ja | Stichworte ohne ganzen Satz — LLM erkennt Format. |
Eine kleine 2-Zimmer-Wohnung mit 50 m2 in Zürich. |
Ja | Ja (~1875 CHF) | Ja | Beschreibendes Adjektiv („kleine") wird ignoriert, numerische Werte korrekt extrahiert. |
Ich suche eine Wohnung in Berlin mit 80 m² und 3 Zimmern. |
Teilweise | — | — | Berlin ist nicht im Kanton ZH; match_town schlägt fehl, App zeigt deutsche Fehlermeldung. |
Ich möchte eine 3-Zimmer-Wohnung in Uster. |
Nein (area fehlt) | — | — | LLM gibt area_m2: null zurück, App fordert Fläche an. |
8. Errors and Problems
Problem 1: tax_income-Spalte enthielt Schweizer Tausenderpunkte mit Apostroph (108'788)
und konnte nicht direkt als float geladen werden.
- Cause: Pandas las die Spalte als String-Dtype (
string[python]in pandas 2.x), nicht alsobject. Der frühere Conditional-Checkdtype == objecttraf nicht zu. - Fix: Cleaning unbedingt als
astype(str).str.replace("'", "", regex=False).astype(float)durchführen, ohne Conditional. Wird sowohl intrain_model.pyals auch inapp.pyso gemacht.
Problem 2: LLM gab JSON gelegentlich in Markdown-Code-Fences zurück (```json …```)
trotz der „kein Markdown"-Anweisung.
- Cause: Das ist ein bekanntes Verhalten gerade bei kleineren Modellen wie
gpt-4o-mini. - Fix:
parse_json_responsestrippt führende```und das optionalejson-Label, bevorjson.loadsaufgerufen wird.
Problem 3: match_town matchte zu permissiv — Eingaben wie „Berlin" (im Ausland)
wurden auf das Schweizer „Berlingen" gematcht.
- Cause: Substring-Fallback prüft nur, ob die Nutzer-Eingabe in einem
bfs_namevorkommt — kurze Strings matchen viele Gemeinden. - Fix: Akzeptiert als Trade-off: das relaxed Matching ist im Template explizit erlaubt und schlägt nur dann zu, wenn der exakte Match fehlschlägt. Die Erklärungs-Phase weist in solchen Fällen automatisch auf Unsicherheit hin. Eine engere Heuristik (z.B. Levenshtein- Distanz oder Gewichtung nach Treffergüte) wäre eine sinnvolle Erweiterung.
9. Deployment Notes
9.1 Files included
Auf den HF Space werden folgende Dateien gepusht:
app.pyrequirements.txtapartment_price_model.pklmodel_features.pklbfs_municipality_data_enriched.csvREADME.md(mit HF-Frontmatter)
train_model.py, die Roh-CSVs (apartments_data.csv, bfs_municipality_and_tax_data.csv)
und documentation.md bleiben im Git-Repo der Schule, müssen aber nicht im Space liegen,
da der Space das Modell nur lädt (nicht neu trainiert).
9.2 Secrets / Environment Variables
| Name | Pflicht | Default |
|---|---|---|
OPENAI_API_KEY |
Ja | — |
OPENAI_MODEL |
Nein | gpt-4o-mini |
In den HF-Space-Settings unter Settings → Variables and secrets → New secret hinzufügen.
9.3 Deployment Result
App-URL: https://huggingface.co/spaces/dubattim/apartment-predictor-chat
Initial-Build schlug fehl, weil HF Spaces standardmäßig Python 3.13 verwendet und das von Gradio
5.0.0 transitiv genutzte pydub-Paket auf das in 3.13 entfernte audioop-Modul angewiesen ist.
Behoben durch Bumpen von sdk_version im README auf 5.49.1 (neuere Gradio-Versionen brauchen
pydub für die Standard-Komponenten nicht mehr) und Hinzufügen von audioop-lts als 3.13-
Backport in requirements.txt. Nach dem zweiten Push baute der Space sauber durch und antwortet
auf alle drei Test-Prompts wie erwartet.
9.4 Screenshots
Beispiel 1: Eingabe „Ich suche eine 3.5-Zimmer-Wohnung mit 85 m² in Winterthur." — das LLM
extrahiert {"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}, das Modell schätzt 2064 CHF
und das LLM formuliert eine deutsche Antwort inklusive Unsicherheits-Hinweis (Zustand,
Mikrolage, Baujahr, Ausstattung).
Beispiel 2: Eingabe „4.5 Zimmer, 120 m², Dietlikon" (Stichwort-Format ohne ganzen Satz) — das
LLM erkennt das Format trotzdem und extrahiert {"rooms": 4.5, "area_m2": 120, "town": "Dietlikon"}. Die Schätzung ergibt 3008 CHF und die Erklärung weist erneut auf nicht
modellierte Faktoren hin.
10. Reflection
Die Kombination aus Regression-Modell und LLM funktioniert gut, weil jedes der beiden Systeme das tut, was es am besten kann: das LLM versteht freie Sprache, das Modell rechnet auf Tabellen-Features. Die größte Fragilität liegt im LLM-Layer — wenn das Modell „Berlin" mit einer Schweizer Gemeinde verwechselt oder ein Feld als String statt Zahl zurückgibt, fängt die Validierung das zwar ab, aber der Nutzer bekommt eine wenig hilfreiche Fehlermeldung. Deutsche Eingabe ist hier zentral, weil die Schweizer Gemeindenamen (mit Umlauten und spezifischer Schreibweise) auf Deutsch reagieren — „Zurich" würde z.B. bei strikter Substring-Suche nicht „Zürich" matchen. Verbessern würde ich als nächstes: a) bessere Fuzzy-Matching-Heuristik mit Levenshtein, b) zusätzliche Eingabe-Felder im Modell wie Baujahr/Zustand/Stockwerk, c) ein optionales Validierungs-Retry beim LLM, falls das JSON ungültig zurückkommt.
11. Responsible Use Note
Die Schätzung ist nur ein grober Richtwert auf Basis weniger Features (Zimmer, Fläche, Gemeinde-Statistiken). Reale Mietpreise hängen zusätzlich vom Zustand der Wohnung, Mikrolage, Baujahr, Ausstattung und Marktlage ab — Faktoren, die das Modell nicht kennt. Außerdem kann das LLM beim Extrahieren der Felder Fehler machen (falscher Ort, falsch interpretierte Zahl); deshalb zeigt die App das extrahierte JSON immer transparent an, damit Nutzer:innen die Eingabe vor der Schätzung kontrollieren können.

