File size: 10,636 Bytes
c9319a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51f94af
 
 
 
 
c9319a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51f94af
c9319a8
 
 
51f94af
c9319a8
51f94af
c9319a8
 
 
51f94af
c9319a8
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# Documentation
## Abgabe 3: Conversational Agent – Apartment Price Prediction

---

## 1. Project Summary

Die App ermöglicht es Nutzern, ihren Wohnungswunsch auf Deutsch zu beschreiben und erhält eine Mietpreisschätzung für den Kanton Zürich. Ein LLM (Claude von Anthropic) extrahiert strukturierte Daten (Zimmer, Fläche, Ort) aus dem Freitext. Diese werden mit Gemeindedaten aus dem BFS-Datensatz ergänzt und an ein trainiertes Random-Forest-Regressionsmodell übergeben. Das Modell liefert die Preisschätzung; ein zweiter LLM-Schritt erklärt das Ergebnis in natürlicher Sprache auf Deutsch.

---

## 2. Files Used

| Datei | Zweck |
|-------|-------|
| `app.py` | Hauptanwendung: Pipeline, LLM-Calls, Gradio-Interface |
| `random_forest_regression.pkl` | Gespeichertes Regressionsmodell (aus GitHub-Repo) |
| `bfs_municipality_and_tax_data.csv` | Gemeindedaten Kanton Zürich (Bevölkerung, Steuern, etc.) |
| `requirements.txt` | Python-Abhängigkeiten |
| `documentation.md` | Diese Dokumentation |
| `README.md` | Hugging Face Space Konfiguration und Beschreibung |

---

## 3. Numeric Prediction Part

### 3.1 Reused Model

**Verwendetes Modell:** `random_forest_regression.pkl`

**Was das Modell vorhersagt:**  
Die monatliche Bruttomiete (CHF) einer Wohnung im Kanton Zürich, basierend auf Wohnungsmerkmalen und Gemeindedaten.

**Input Features (exakt 7, in dieser Reihenfolge):**

1. `rooms` – Anzahl Zimmer
2. `area_m2` – Wohnfläche in m²
3. `pop` – Bevölkerung der Gemeinde
4. `pop_dens` – Bevölkerungsdichte (Einwohner/km²)
5. `frg_pct` – Ausländeranteil in Prozent
6. `emp` – Anzahl Beschäftigte in der Gemeinde
7. `tax_income` – Steuerbares Einkommen (Medianeinkommen der Gemeinde)

### 3.2 Prediction Logic

Der Ortsname aus der LLM-Extraktion wird mittels `match_town()` einem kanonischen BFS-Gemeindenamen zugeordnet (zunächst exakter Vergleich in Kleinbuchstaben, dann Teilstring-Matching). Die entsprechende Zeile aus `bfs_municipality_and_tax_data.csv` liefert die Gemeindedaten. Diese werden zusammen mit Zimmer und Fläche als DataFrame an `model.predict()` übergeben.

---

## 4. LLM Extraction Part

### 4.1 Goal

Das LLM soll aus einem deutschen Freitext drei strukturierte Werte extrahieren: die Anzahl Zimmer, die Wohnfläche in m² und den Gemeindenamen. Diese drei Werte werden benötigt, um die Modellvorhersage zu erstellen.

### 4.2 Prompt Design

Es wurde ein System-Prompt mit einer Developer-Instruction verwendet. Der Prompt:
- Definiert die Rolle explizit als «Datenextraktionssystem»
- Fordert strikte JSON-Ausgabe ohne Markdown und ohne Erklärung
- Nennt alle drei Pflichtfelder mit Typ und Beispiel
- Gibt an, `null` für fehlende Werte zu verwenden

```
Du bist ein Datenextraktionssystem für Schweizer Wohnungsanfragen.
Extrahiere aus dem Benutzertext genau drei Werte und gib sie als JSON zurück:
- "rooms": Anzahl Zimmer als Dezimalzahl (z.B. 3.5)
- "area_m2": Wohnfläche in m² als Zahl
- "town": Name der Gemeinde oder Stadt als String
Gib NUR gültiges JSON zurück, kein Markdown, keine Erklärung.
Beispiel: {"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}
Falls ein Wert fehlt oder unklar ist, setze ihn auf null.
```

### 4.3 Expected Output Format

```json
{"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}
```

### 4.4 Validation

Die Funktion `parse_json_response()` prüft:
1. Ob die Antwort nicht leer ist
2. Ob allfällige Markdown-Code-Blöcke (` ```json `) entfernt werden
3. Ob der String valides JSON ist (`json.loads`)
4. Ob alle drei Pflichtfelder vorhanden sind

Zusätzlich prüft `run_pipeline()`, ob alle drei Felder den Wert `null` haben und gibt eine benutzerfreundliche Fehlermeldung aus.

---

## 5. LLM Explanation Part

### 5.1 Goal

Nach der Preisberechnung soll das LLM eine kurze, verständliche Antwort auf Deutsch generieren. Das Modell erklärt den vorhergesagten Preis in Zusammenhang mit den Wohnungsmerkmalen – ohne einen eigenen neuen Preis zu berechnen.

### 5.2 Prompt Design

Der System-Prompt:
- Definiert die Rolle als «freundlicher Schweizer Wohnungsberater»
- Verlangt 2–3 Sätze auf Deutsch
- Fordert Erwähnung von Zimmer, Fläche, Ort und geschätztem Preis
- Verlangt einen Unsicherheitshinweis (Zustand, Mikrolage)
- Untersagt eine eigene Preisberechnung
- Fordert striktes JSON mit dem Schlüssel `"answer"`

### 5.3 Expected Output Format

```json
{"answer": "Für eine 3.5-Zimmer-Wohnung mit 85 m² in Winterthur schätzt das Modell eine monatliche Miete von rund CHF 2'450. Winterthur ist eine gut erschlossene Stadt mit moderaten Mietpreisen. Beachte, dass Faktoren wie Zustand der Wohnung und Mikrolage nicht im Modell enthalten sind und den tatsächlichen Preis beeinflussen können."}
```

---

## 6. End-to-End Pipeline

1. Nutzer gibt deutschen Wohnungswunsch ein (z.B. «Ich suche eine 3.5-Zimmer-Wohnung mit 85 m² in Winterthur.»)
2. `extract_preferences()`: LLM (Claude Haiku) extrahiert `rooms`, `area_m2`, `town` als JSON
3. `parse_json_response()` validiert das JSON und prüft alle Pflichtfelder
4. `run_pipeline()` prüft auf `null`-Werte und gibt Fehlermeldung bei fehlenden Angaben
5. `match_town()` ordnet den Ortsnamen einem kanonischen BFS-Gemeindenamen zu
6. `predict_apartment_price()` lädt Gemeindedaten und übergibt alle 7 Features an das Modell
7. `generate_explanation()`: LLM generiert eine deutsche Erklärung des Preises
8. Die App gibt drei Ausgaben zurück: extrahierte JSON-Parameter, numerische Mietschätzung, natürlichsprachliche Antwort

---

## 7. Test Cases

| Test Input | Extraktion korrekt? | Preis zurückgegeben? | Erklärung zurückgegeben? | Anmerkungen |
|------------|---------------------|----------------------|--------------------------|-------------|
| `ich suche eine 5 zimmer wohnung in uster mit mindestens 500 quadratmeter` | Ja – rooms: 5, area_m2: 500, town: "Uster" | Ja – CHF 3'757/Monat | Ja | Auch ohne Grossschreibung und formale Sätze werden alle Felder korrekt extrahiert |
| `ich suche eine 2 zimmer wohnung in uster mit mindestens 50 quadratmeter` | Ja – rooms: 2, area_m2: 50, town: "Uster" | Ja – CHF 1'578/Monat | Ja | Kleinere Wohnung, plausibel tieferer Preis |
| `2 zimmer 100 quadratmeter uster` | Ja – rooms: 2, area_m2: 100, town: "Uster" | Ja – CHF 2'461/Monat | Ja | Sehr kurze Eingabe ohne Satzstruktur, LLM extrahiert trotzdem korrekt |
| `Ich möchte eine Wohnung in Zürich.` | Teilweise – town: "Zürich", rooms: null, area_m2: null | Nein | Nein | Fehlermeldung: fehlende Angaben Zimmer und Fläche |
| `Ich suche etwas in Tokio.` | Ja – town: "Tokio" | Nein | Nein | match_town() findet keinen Match, klare Fehlermeldung ausgegeben |

---

## 8. Errors and Problems

**Problem 1: LLM gibt Markdown zurück statt reines JSON**  
- **Ursache:** Manche Claude-Varianten umhüllen JSON mit ` ```json ... ``` `  
- **Fix:** `parse_json_response()` erkennt Markdown-Blöcke und entfernt sie vor dem Parsen

**Problem 2: Ortsname aus LLM stimmt nicht exakt mit BFS-Namen überein**  
- **Ursache:** Nutzer schreibt z.B. «Zürich» statt «Zürich (Kreis 1)» oder «Zuerich»  
- **Fix:** `match_town()` versucht zunächst exakten Lowercase-Match, dann Teilstring-Matching

**Problem 3: Fehlende API-Keys im Deployment**  
- **Ursache:** Hugging Face Secrets nicht gesetzt  
- **Fix:** `call_llm_json()` wirft expliziten `ValueError` mit Hinweis auf fehlende Env-Variable; die App zeigt eine verständliche Fehlermeldung

**Problem 4: Modell-Feature-Namen müssen exakt übereinstimmen**  
- **Ursache:** Das gespeicherte Modell erwartet Spalten `area_m2`, nicht `area` wie im Projekt 1  
- **Fix:** Features in `predict_apartment_price()` exakt nach Vorgabe des Modells benannt (`rooms, area_m2, pop, pop_dens, frg_pct, emp, tax_income`)

---

## 9. Deployment Notes

### 9.1 Files included

- `app.py`
- `random_forest_regression.pkl`
- `bfs_municipality_and_tax_data.csv`
- `requirements.txt`
- `README.md`
- `documentation.md`

### 9.2 Secrets / Environment Variables

- `OPENAI_API_KEY` – Pflicht: OpenAI API Key
- `OPENAI_MODEL` – Optional: Modell-ID (Standard: `gpt-4o-mini`)

### 9.3 Deployment Result

Der Space läuft erfolgreich auf Hugging Face. Die Gradio-App startet korrekt, das Modell und die CSV werden beim Start geladen. Alle drei Ausgaben (extrahierte Parameter, Preis, Erklärung) werden korrekt angezeigt.

### 9.4 Screenshots

**Beispiel 1:** Grosse Wohnung mit informeller Eingabe

![Beispiel 1](screenshot1.png)

Eingabe: «ich suche eine 5 zimmer wohnung in uster mit mindestens 500 quadratmeter» – Das LLM extrahiert korrekt `rooms: 5`, `area_m2: 500`, `town: "Uster"`, obwohl die Eingabe keine formale Satzstruktur hat. Das Modell schätzt CHF 3'757/Monat; die Erklärung weist auf Unsicherheiten durch Zustand und Mikrolage hin.

**Beispiel 2:** Kleine Wohnung, gleicher Ort

![Beispiel 2](screenshot2.png)

Eingabe: «ich suche eine 2 zimmer wohnung in uster mit mindestens 50 quadratmeter» – Alle drei Felder werden korrekt erkannt (`rooms: 2`, `area_m2: 50`, `town: "Uster"`). Der deutlich tiefere Preis von CHF 1'578/Monat im Vergleich zu Beispiel 1 ist plausibel und zeigt, dass das Modell auf Fläche und Zimmerzahl reagiert.

---

## 10. Reflection

Die Kombination aus Regressionsmodell und LLM funktioniert gut für strukturierte Vorhersagen mit natürlichsprachiger Ein- und Ausgabe. Der stärkste Vorteil ist die Flexibilität: Nutzer müssen keine Formulare ausfüllen, sondern können sich frei ausdrücken. Die fragile Stelle ist die Ortsnamenerkennung – das Modell kennt nur Gemeinden im Kanton Zürich, und der LLM extrahiert möglicherweise Ortsnamen aus anderen Kantonen oder Ländern. Die deutsche Eingabe ist wichtig, da das Modell auf deutschsprachigen Schweizer Kontext ausgerichtet ist. Fehlende Informationen wie Etage, Baujahr, Ausstattung oder genaue Strassenlage beeinflussen den Preis stark, sind aber nicht im Modell enthalten. Als nächste Verbesserung würde sich ein Fuzzy-Matching für Ortsnamen sowie eine Validierung der Wertebereiche (z.B. Fläche > 0) eignen.

---

## 11. Responsible Use Note

Die Preisschätzung ist ein statistischer Schätzwert auf Basis historischer Daten und dient nur zur Orientierung – sie ersetzt keine professionelle Immobilienberatung. Das Modell kennt keine aktuellen Marktentwicklungen, den Zustand der Wohnung, die Etage oder die genaue Mikrolage. Das LLM kann Angaben aus dem Text falsch interpretieren, insbesondere bei unklaren oder mehrdeutigen Formulierungen. Alle ausgegebenen Preise sollten daher kritisch hinterfragt und mit aktuellen Inseraten verglichen werden.