apartment-predictor-chat / documentation.md
dubattim's picture
Add finalized documentation.md
9448c02 verified

A newer version of the Gradio SDK is available: 6.16.0

Upgrade

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):

  1. rooms
  2. area_m2
  3. pop (Einwohnerzahl der Gemeinde)
  4. pop_dens (Einwohnerdichte)
  5. frg_pct (Ausländer-Anteil in %)
  6. emp (Anzahl Beschäftigte)
  7. tax_income (durchschnittliches steuerbares Einkommen)
  8. distance_to_zurich (Haversine-Distanz zur Zürich HB in km — pro Gemeinde gemittelt)
  9. 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:

  1. Aus dem extrahierten town wird der Eintrag in bfs_municipality_data_enriched.csv gesucht (case-insensitive, mit Substring-Fallback).
  2. Aus der Zeile werden pop, pop_dens, frg_pct, emp, tax_income und distance_to_zurich gelesen.
  3. area_per_room wird aus area_m2 / rooms berechnet.
  4. Die 9 Features werden in der gepickelten Reihenfolge (model_features.pkl) zu einem (1, 9)-Numpy-Array zusammengesetzt.
  5. 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 … ```).
  • null fü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:

  1. Leere Antwort → ValueError.
  2. Code-Fences werden entfernt (defensive Sicherheit, falls das Modell sie trotzdem produziert).
  3. json.loads — ungültiges JSON → ValueError mit der Roh-Antwort als Hinweis.
  4. Fehlende Schlüssel → ValueError.

Zusätzlich validiert extract_preferences:

  • rooms/area_m2 dürfen nicht null sein.
  • town muss von match_town zu einer kanonischen bfs_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

  1. Nutzer gibt eine deutsche Beschreibung in das Textfeld ein und klickt auf „Schätzen".
  2. extract_preferences ruft das LLM mit dem Extraktions-Prompt auf und validiert das JSON.
  3. match_town löst den extrahierten Ortsnamen auf den kanonischen bfs_name aus dem Gemeinde-CSV auf (case-insensitive, Substring-Fallback).
  4. predict_apartment_price baut den 9-Feature-Vektor aus den extrahierten Werten + dem Gemeinde-Lookup und ruft model.predict auf.
  5. generate_explanation ruft das LLM mit dem Erklärungs-Prompt auf, das die Präferenzen und die Vorhersage als JSON-Payload bekommt.
  6. 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 als object. Der frühere Conditional-Check dtype == object traf nicht zu.
  • Fix: Cleaning unbedingt als astype(str).str.replace("'", "", regex=False).astype(float) durchführen, ohne Conditional. Wird sowohl in train_model.py als auch in app.py so 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_response strippt führende ``` und das optionale json-Label, bevor json.loads aufgerufen 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_name vorkommt — 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.py
  • requirements.txt
  • apartment_price_model.pkl
  • model_features.pkl
  • bfs_municipality_data_enriched.csv
  • README.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: 3.5-Zimmer Winterthur

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: 4.5-Zimmer Dietlikon

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.