Spaces:
Sleeping
Sleeping
| # Documentation | |
| ## Week 2: Apartment Predictor (Saved Regression Model + LLM Workflow) | |
| --- | |
| ## 1. Project Summary | |
| The app accepts a free-text German apartment request from the user, for example "Ich suche eine 3.5-Zimmer-Wohnung mit 85 m2 in Winterthur." A first LLM call extracts the rooms, area in m2, and town from the text and returns them as structured JSON. The extracted values are then combined with municipality statistics from a CSV file and fed into a pre-trained Random Forest regression model, which predicts the monthly rent in CHF. A second LLM call generates a short German explanation of the prediction, including an uncertainty note. | |
| --- | |
| ## 2. Files Used | |
| | File | Purpose | | |
| |------|---------| | |
| | `ai_applications_exercise2.ipynb` | Notebook work and testing | | |
| | `app_student.py` | Student implementation (template) | | |
| | `app.py` | Final deployable app | | |
| | `random_forest_regression.pkl` | Saved regression model | | |
| | `bfs_municipality_and_tax_data.csv` | Municipality features used for prediction | | |
| | `requirements.txt` | Python dependencies | | |
| | `documentation.md` | Written documentation for the submission | | |
| --- | |
| ## 3. Numeric Prediction Part | |
| ### 3.1 Reused Model | |
| **Which saved model did you use?** | |
| `random_forest_regression.pkl` | |
| **What does the model predict?** | |
| The model predicts the monthly rent in CHF for a Swiss apartment based on structural features (rooms, area) and municipality-level statistics (population, population density, foreigner percentage, employment, tax income). | |
| **Which input features are used for prediction?** | |
| 1. `rooms` | |
| 2. `area_m2` | |
| 3. `pop` | |
| 4. `pop_dens` | |
| 5. `frg_pct` | |
| 6. `emp` | |
| 7. `tax_income` | |
| ### 3.2 Prediction Logic | |
| The user's town name is matched to the BFS municipality database using exact lowercase matching first, then a relaxed contains-match as fallback. Once matched, the corresponding municipality row is looked up to retrieve the five statistical features. These are combined with the user-provided rooms and area values into a NumPy array with shape (1, 7) and passed to the scikit-learn Random Forest model via `model.predict()`. | |
| --- | |
| ## 4. LLM Extraction Part | |
| ### 4.1 Goal | |
| The LLM had to extract three structured values from free German text: the number of rooms (`rooms`), the apartment size in square meters (`area_m2`), and the town name (`town`). | |
| ### 4.2 Prompt Design | |
| A system prompt instructed the LLM to act as an assistant that extracts apartment information from German text. The prompt required strict JSON output without any Markdown, code blocks, or explanations. It specified the exact output format and told the model to set `null` for any value it could not find. | |
| - System instruction was used | |
| - Strict JSON output was required | |
| - Required keys: `rooms`, `area_m2`, `town` | |
| - German input was expected | |
| - No Markdown or code fences allowed | |
| ### 4.3 Expected Output Format | |
| ```json | |
| {"rooms": 3.5, "area_m2": 85, "town": "Winterthur"} | |
| ``` | |
| ### 4.4 Validation | |
| After receiving the LLM response, a `parse_json_response()` function strips any accidental Markdown fences, then parses the text with `json.loads()`. If parsing fails a `ValueError` is raised with the raw response. After successful parsing the function checks that all required keys are present and raises a `ValueError` listing any missing keys. The `run_pipeline()` function additionally checks that none of the three extracted values are `null` before proceeding. | |
| --- | |
| ## 5. LLM Explanation Part | |
| ### 5.1 Goal | |
| The second LLM step generates a short German explanation of the predicted rent. The LLM must mention the key input factors (rooms, area, town) and add an uncertainty note about factors not captured by the model. The LLM must not calculate or suggest a different price. | |
| ### 5.2 Prompt Design | |
| The system prompt instructed the LLM to act as a helpful Swiss apartment assistant, explain the prediction in plain German, mention rooms, area and location as key factors, add one uncertainty note (e.g. condition, exact location, amenities), and explicitly not recalculate the price. Output was required as strict JSON with a single `answer` key. | |
| ### 5.3 Expected Output Format | |
| ```json | |
| {"answer": "Fuer eine 2.5-Zimmer-Wohnung mit 55 m2 in Zuerich schaetzt das Modell rund 2639 CHF pro Monat. Eine Unsicherheit ist, dass Zustand und Mikrolage nicht direkt im Modell enthalten sind."} | |
| ``` | |
| --- | |
| ## 6. End-to-End Pipeline | |
| 1. User enters a German apartment request in the Gradio text box. | |
| 2. The first LLM call extracts `rooms`, `area_m2`, and `town` from the text and returns strict JSON. | |
| 3. Python validates the extracted values: checks for valid JSON, required keys, and non-null values. | |
| 4. The town name is matched to the BFS municipality database to retrieve population and tax statistics. | |
| 5. The seven features are assembled into a NumPy array and passed to the Random Forest model, which returns the predicted monthly rent in CHF. | |
| 6. The second LLM call receives the preferences and prediction and returns a short German explanation with an uncertainty note as JSON. | |
| 7. The app displays the extracted JSON, the predicted rent, and the final explanation to the user. | |
| --- | |
| ## 7. Test Cases | |
| | Test Input | Extracted Output Correct? | Prediction Returned? | Explanation Returned? | Notes | | |
| |------------|----------------------------|----------------------|-----------------------|-------| | |
| | `2.5 Zimmer, 55m2, Zuerich` | Yes | Yes | Yes | rooms=2.5, area_m2=55, town=Zuerich extracted correctly. Prediction: 2639.47 CHF | | |
| | `Ich suche eine 4.5-Zimmer-Wohnung mit 120 m2 in Bern.` | Yes | Yes | Yes | rooms=4.5, area_m2=120, town=Bern extracted correctly. Prediction: 2827.90 CHF | | |
| | `Ich brauche etwas Guenstiges in Luzern, ca. 60m2 und 2 Zimmer.` | Yes | Yes | Yes | LLM correctly identified rooms=2, area_m2=60, town=Luzern from informal text | | |
| --- | |
| ## 8. Errors and Problems | |
| **Problem:** scikit-learn version warning on startup | |
| **Cause:** The model was pickled with scikit-learn 1.6.1 but Hugging Face Spaces runs 1.8.0 | |
| **Fix:** Pinned `scikit-learn==1.6.1` in requirements.txt to ensure version compatibility | |
| **Problem:** LLM occasionally returns JSON wrapped in Markdown code fences | |
| **Cause:** Some LLM responses include ```json ... ``` formatting despite instructions | |
| **Fix:** The `parse_json_response()` function strips Markdown fences before parsing | |
| **Problem:** Town names with umlauts (e.g. Zuerich vs Zuerich) sometimes failed matching | |
| **Cause:** Case and umlaut differences between user input and BFS database | |
| **Fix:** Implemented relaxed contains-matching as fallback after exact lowercase match | |
| --- | |
| ## 9. Deployment Notes | |
| ### 9.1 Files included | |
| - `app.py` | |
| - `requirements.txt` | |
| - `random_forest_regression.pkl` | |
| - `bfs_municipality_and_tax_data.csv` | |
| - `documentation.md` | |
| ### 9.2 Secrets / Environment Variables | |
| - `OPENAI_API_KEY` | |
| - `OPENAI_MODEL` (optional, defaults to `gpt-4.1-mini`) | |
| ### 9.3 Deployment Result | |
| The Space ran successfully after pinning the scikit-learn version. Both LLM calls and the regression model work correctly. The app correctly handles German free-text input and returns structured extraction, a numeric prediction, and a natural language explanation. | |
| ### 9.4 Screenshots | |
|  | |
| Test 1: The user entered "2.5 Zimmer, 55m2, Zuerich". The LLM correctly extracted all three values and the model predicted a monthly rent of 2639.47 CHF. The explanation mentions the key factors and includes an uncertainty note about condition and amenities. | |
|  | |
| Test 2: The user entered "Ich suche eine 4.5-Zimmer-Wohnung mit 120 m2 in Bern." The LLM correctly extracted rooms=4.5, area_m2=120, town=Bern and the model predicted 2827.90 CHF. The explanation correctly references all input factors and notes potential price variation. | |
| --- | |
| ## 10. Reflection | |
| The combination of a regression model and an LLM worked well: the structured model provides a reliable numeric estimate based on objective features, while the LLM bridges the gap between free text and structured input. The most fragile part of the system is the LLM extraction step, since the model may misread ambiguous German phrasing or fail to produce valid JSON. German input is important because Swiss renters naturally write in German, and forcing English would create an unnecessary barrier. Important apartment information still missing from the model includes floor level, building age, renovation status, and proximity to public transport. As a next step, adding more structured features or allowing the LLM to also estimate qualitative factors could improve the prediction quality. | |
| --- | |
| ## 11. Responsible Use Note | |
| The predicted rent is only an estimate based on a limited set of structural and municipality-level features and should not be used as a binding price reference. The regression model does not capture all factors that influence real rental prices, such as apartment condition, exact micro-location, or current market demand. The LLM extraction step may occasionally misinterpret user input, leading to incorrect features being passed to the model. Users should treat the output as a rough orientation and consult official rental platforms or a professional for accurate pricing information. | |