import json import numpy as np import pandas as pd import joblib from flask import Flask, request, jsonify app = Flask(__name__) model = joblib.load("model.joblib") FEATURE_COLS = ['rooms', 'area', 'pop_dens', 'frg_pct', 'tax_income', 'room_per_m2', 'luxurious', 'temporary', 'furnished', 'area_cat_ecoded', 'zurich_city', '(ATTIKA)', '(LOFT)', '(SEESICHT)', '(LUXURIÖS)', '(POOL)', '(EXKLUSIV)', 'log_pop_dens', 'log_tax_income', 'area_per_room', 'has_premium_keyword'] TOWNS = [{"town": "Adlikon b. Regensdorf", "median_price": 2272.5, "count": 6, "lat": 47.451836903889976, "lon": 8.464237054189047, "tax_income": 73522.0, "pop_dens": 1268.1258549932, "frg_pct": 35.5717367853}, {"town": "Adliswil", "median_price": 2085.0, "count": 7, "lat": 47.31021772112165, "lon": 8.522809846060616, "tax_income": 89522.0, "pop_dens": 2438.0952380952, "frg_pct": 37.7850506757}, {"town": "Affoltern am Albis", "median_price": 2089.0, "count": 7, "lat": 47.278944287981304, "lon": 8.45288862500872, "tax_income": 72583.0, "pop_dens": 1161.7563739377, "frg_pct": 28.7003169959}, {"town": "Andelfingen", "median_price": 1740.0, "count": 3, "lat": 47.594041188557945, "lon": 8.679091453552246, "tax_income": 88960.0, "pop_dens": 333.3827893175, "frg_pct": 13.3511348465}, {"town": "Bassersdorf", "median_price": 2927.5, "count": 4, "lat": 47.43820285797119, "lon": 8.633100032806396, "tax_income": 79477.0, "pop_dens": 1309.4130675526, "frg_pct": 24.949255751}, {"town": "Br\u00fcttisellen", "median_price": 2130.0, "count": 3, "lat": 47.42356745402018, "lon": 8.633115450541178, "tax_income": 84772.0, "pop_dens": 1005.9343434343, "frg_pct": 27.325216518099996}, {"town": "B\u00fclach", "median_price": 2150.0, "count": 15, "lat": 47.519343566894534, "lon": 8.5398281733195, "tax_income": 78194.0, "pop_dens": 1328.2784338098, "frg_pct": 28.0320044919}, {"town": "Dielsdorf", "median_price": 1825.0, "count": 5, "lat": 47.479800415039065, "lon": 8.454447555541993, "tax_income": 75326.0, "pop_dens": 1016.3543441227, "frg_pct": 31.847133757999995}, {"town": "Dietikon", "median_price": 2143.5, "count": 28, "lat": 47.40354074750628, "lon": 8.404910496303014, "tax_income": 71348.0, "pop_dens": 2143.7092144887497, "frg_pct": 35.57692254225}, {"town": "Dietlikon", "median_price": 2940.0, "count": 10, "lat": 47.42664413452148, "lon": 8.620266914367676, "tax_income": 84564.5, "pop_dens": 2234.7808319589503, "frg_pct": 28.390703560650003}, {"town": "D\u00fcbendorf", "median_price": 2230.0, "count": 11, "lat": 47.40156243064187, "lon": 8.612197962674228, "tax_income": 79563.0, "pop_dens": 2151.4684287812, "frg_pct": 35.9451250725}, {"town": "Eglisau", "median_price": 2660.0, "count": 3, "lat": 47.575480143229164, "lon": 8.517463684082031, "tax_income": 87906.0, "pop_dens": 589.293598234, "frg_pct": 24.143097958399995}, {"town": "Elsau", "median_price": 2525.0, "count": 4, "lat": 47.504170417785645, "lon": 8.802351474761963, "tax_income": 79356.0, "pop_dens": 451.9206939281, "frg_pct": 17.3293117631}, {"town": "Embrach", "median_price": 2400.0, "count": 4, "lat": 47.509860038757324, "lon": 8.595427751541138, "tax_income": 73330.0, "pop_dens": 741.811023622, "frg_pct": 28.0437320879}, {"town": "Fehraltorf", "median_price": 1740.0, "count": 5, "lat": 47.38572616577149, "lon": 8.757724952697753, "tax_income": 80449.0, "pop_dens": 684.7940865892, "frg_pct": 18.2266769468}, {"town": "Feuerthalen", "median_price": 1490.0, "count": 5, "lat": 47.69002685546875, "lon": 8.644001007080078, "tax_income": 76356.0, "pop_dens": 1457.2580645161, "frg_pct": 22.4958494743}, {"town": "F\u00e4llanden", "median_price": 2850.0, "count": 3, "lat": 47.374595642089844, "lon": 8.637135823567709, "tax_income": 90510.0, "pop_dens": 1361.9122257053, "frg_pct": 24.2145241109}, {"town": "Glattbrugg", "median_price": 1386.0, "count": 6, "lat": 47.434794108072914, "lon": 8.567777792612711, "tax_income": 71979.0, "pop_dens": 3759.2128801431004, "frg_pct": 44.9129151994}, {"town": "Gossau ZH", "median_price": 1875.0, "count": 10, "lat": 47.30550994873047, "lon": 8.767629241943359, "tax_income": 80977.0, "pop_dens": 562.9791894852, "frg_pct": 14.5719844358}, {"town": "Greifensee", "median_price": 1860.0, "count": 3, "lat": 47.369075775146484, "lon": 8.683858553568522, "tax_income": 89901.0, "pop_dens": 2372.2466960352, "frg_pct": 20.1671309192}, {"town": "Hedingen", "median_price": 2620.0, "count": 4, "lat": 47.29815101623535, "lon": 8.450382471084595, "tax_income": 94456.0, "pop_dens": 578.5604900459, "frg_pct": 16.4107993647}, {"town": "Hinwil", "median_price": 2267.5, "count": 4, "lat": 47.30254364013672, "lon": 8.83609652519226, "tax_income": 72124.0, "pop_dens": 506.1490125673, "frg_pct": 17.105613195}, {"town": "Hombrechtikon", "median_price": 1812.0, "count": 8, "lat": 47.251842975616455, "lon": 8.775048613548279, "tax_income": 79414.0, "pop_dens": 720.4433497537, "frg_pct": 20.6723646724}, {"town": "Kloten", "median_price": 2354.0, "count": 26, "lat": 47.445955129770134, "lon": 8.58423034961407, "tax_income": 71103.5, "pop_dens": 2401.9209185355, "frg_pct": 39.175285220149995}, {"town": "Langnau am Albis", "median_price": 3100.0, "count": 3, "lat": 47.287497202555336, "lon": 8.529908498128256, "tax_income": 96048.0, "pop_dens": 891.2341407151001, "frg_pct": 27.7856865536}, {"town": "Lufingen", "median_price": 2790.0, "count": 3, "lat": 47.48583094278971, "lon": 8.591005643208822, "tax_income": 94684.0, "pop_dens": 466.60231660229994, "frg_pct": 19.1146048821}, {"town": "Meilen", "median_price": 2500.0, "count": 3, "lat": 47.27062861124674, "lon": 8.64123821258545, "tax_income": 139797.0, "pop_dens": 1200.5862646566, "frg_pct": 23.1880013952}, {"town": "M\u00e4nnedorf", "median_price": 2500.0, "count": 3, "lat": 47.256217956542976, "lon": 8.689555168151855, "tax_income": 103577.0, "pop_dens": 2381.3417190776, "frg_pct": 20.3803151686}, {"town": "Neerach", "median_price": 2240.0, "count": 4, "lat": 47.51153087615967, "lon": 8.47110652923584, "tax_income": 110502.0, "pop_dens": 525.6622516556, "frg_pct": 13.3228346457}, {"town": "Niederglatt ZH", "median_price": 1690.0, "count": 3, "lat": 47.492113749186196, "lon": 8.501464207967123, "tax_income": 74085.0, "pop_dens": 1386.9444444444, "frg_pct": 26.056479070700004}, {"town": "Niederhasli", "median_price": 2070.0, "count": 5, "lat": 47.48185348510742, "lon": 8.495015335083007, "tax_income": 72919.0, "pop_dens": 826.9911504425002, "frg_pct": 25.7998929909}, {"town": "Niederweningen", "median_price": 2700.0, "count": 5, "lat": 47.508734130859374, "lon": 8.37294273376465, "tax_income": 86604.0, "pop_dens": 448.61313868609994, "frg_pct": 17.0517409697}, {"town": "Oberengstringen", "median_price": 1839.5, "count": 8, "lat": 47.407087326049805, "lon": 8.464881896972656, "tax_income": 81948.0, "pop_dens": 3956.4740716056, "frg_pct": 33.2520893901}, {"town": "Oberglatt ZH", "median_price": 2010.0, "count": 5, "lat": 47.47789306640625, "lon": 8.518560981750488, "tax_income": 65147.0, "pop_dens": 876.1212121212, "frg_pct": 37.1748754842}, {"town": "Oetwil am See", "median_price": 1917.5, "count": 8, "lat": 47.27392053604126, "lon": 8.720972299575806, "tax_income": 70794.0, "pop_dens": 792.1440261866, "frg_pct": 29.2561983471}, {"town": "Oetwil an der Limmat", "median_price": 1740.0, "count": 5, "lat": 47.425846099853516, "lon": 8.40000286102295, "tax_income": 65532.0, "pop_dens": 2970.6638115632, "frg_pct": 46.2264830967}, {"town": "Opfikon", "median_price": 2418.0, "count": 5, "lat": 47.427667236328126, "lon": 8.566677856445313, "tax_income": 71979.0, "pop_dens": 3759.2128801431, "frg_pct": 44.9129151994}, {"town": "Pfaffhausen", "median_price": 2448.0, "count": 6, "lat": 47.36277389526367, "lon": 8.620045026143393, "tax_income": 103450.0, "pop_dens": 1035.3230729068, "frg_pct": 22.85907711085}, {"town": "Pfungen", "median_price": 1730.0, "count": 7, "lat": 47.51750564575195, "lon": 8.639920507158552, "tax_income": 71575.0, "pop_dens": 781.5631262525, "frg_pct": 24.8461538462}, {"town": "Pf\u00e4ffikon ZH", "median_price": 1700.0, "count": 5, "lat": 47.36850814819336, "lon": 8.782468223571778, "tax_income": 80310.0, "pop_dens": 623.4751409534, "frg_pct": 19.0891154226}, {"town": "Richterswil", "median_price": 2067.0, "count": 5, "lat": 47.20718002319336, "lon": 8.705493354797364, "tax_income": 90081.0, "pop_dens": 1810.3585657371, "frg_pct": 19.4762323944}, {"town": "Russikon", "median_price": 2571.0, "count": 3, "lat": 47.394126892089844, "lon": 8.774710337320963, "tax_income": 91275.0, "pop_dens": 308.7139845397, "frg_pct": 13.316640109300002}, {"town": "R\u00fcti ZH", "median_price": 1470.0, "count": 6, "lat": 47.258008321126304, "lon": 8.85670264561971, "tax_income": 66676.0, "pop_dens": 1221.2723658052, "frg_pct": 24.8412827609}, {"town": "Schlatt ZH", "median_price": 2700.0, "count": 3, "lat": 47.46413803100586, "lon": 8.845952033996582, "tax_income": 71604.0, "pop_dens": 87.9598662207, "frg_pct": 7.9847908745}, {"town": "Schlieren", "median_price": 2227.5, "count": 14, "lat": 47.400221143450054, "lon": 8.442373888833183, "tax_income": 65678.0, "pop_dens": 2851.4415781487, "frg_pct": 46.1178223618}, {"town": "Schwerzenbach", "median_price": 2940.0, "count": 3, "lat": 47.38499450683594, "lon": 8.64946715037028, "tax_income": 79405.0, "pop_dens": 1973.2824427481, "frg_pct": 27.1373307544}, {"town": "St\u00e4fa", "median_price": 3725.0, "count": 6, "lat": 47.24422264099121, "lon": 8.727231820424398, "tax_income": 103206.0, "pop_dens": 1713.3876600698998, "frg_pct": 19.3232776192}, {"town": "Thalwil", "median_price": 1900.0, "count": 5, "lat": 47.28451690673828, "lon": 8.573451232910156, "tax_income": 113194.0, "pop_dens": 3298.0, "frg_pct": 29.0313688737}, {"town": "Unterengstringen", "median_price": 2624.0, "count": 3, "lat": 47.410074869791664, "lon": 8.444167137145996, "tax_income": 96737.0, "pop_dens": 1177.8443113773, "frg_pct": 25.826131164199996}, {"town": "Uster", "median_price": 1875.0, "count": 18, "lat": 47.349968168470596, "lon": 8.719801743825277, "tax_income": 79839.0, "pop_dens": 1228.7469287469, "frg_pct": 23.0096837775}, {"town": "Volketswil", "median_price": 2490.0, "count": 7, "lat": 47.38727460588728, "lon": 8.66897201538086, "tax_income": 82135.0, "pop_dens": 1327.6353276353, "frg_pct": 24.597639485}, {"town": "Wald ZH", "median_price": 1610.0, "count": 4, "lat": 47.276848793029785, "lon": 8.911797046661377, "tax_income": 66453.0, "pop_dens": 399.525128611, "frg_pct": 26.5649762282}, {"town": "Wallisellen", "median_price": 3315.0, "count": 4, "lat": 47.41270351409912, "lon": 8.59876537322998, "tax_income": 86973.0, "pop_dens": 2623.2087227414, "frg_pct": 31.0135977674}, {"town": "Wetzikon ZH", "median_price": 2094.5, "count": 20, "lat": 47.32592258453369, "lon": 8.802443408966065, "tax_income": 68951.0, "pop_dens": 1486.6151100535, "frg_pct": 25.994397759100003}, {"town": "Winterthur", "median_price": 2167.0, "count": 96, "lat": 47.50012377897898, "lon": 8.735338846842447, "tax_income": 70966.0, "pop_dens": 1662.5973262818, "frg_pct": 24.535003932}, {"town": "Zollikerberg", "median_price": 2590.0, "count": 3, "lat": 47.34665171305338, "lon": 8.599929491678873, "tax_income": 171602.0, "pop_dens": 1664.5859872612, "frg_pct": 26.0580087243}, {"town": "Zollikon", "median_price": 4950.0, "count": 3, "lat": 47.343475341796875, "lon": 8.57410717010498, "tax_income": 171602.0, "pop_dens": 1664.5859872612, "frg_pct": 26.0580087243}, {"town": "Zweidlen", "median_price": 2090.0, "count": 4, "lat": 47.56949996948242, "lon": 8.4723961353302, "tax_income": 74507.0, "pop_dens": 424.1463414634, "frg_pct": 24.5351734713}, {"town": "Z\u00fcrich", "median_price": 2877.5, "count": 262, "lat": 47.38310058244312, "lon": 8.53003583427604, "tax_income": 85446.0, "pop_dens": 5229.091582652058, "frg_pct": 32.4584678868}] ITER1 = [{"model": "Ridge (\u03b1=10)", "MAE": 445.8, "RMSE": 671.2, "R2": 0.607}, {"model": "Lasso (\u03b1=10)", "MAE": 448.3, "RMSE": 677.7, "R2": 0.5994}] ITER2 = [{"model": "RandomForest (n=300, depth=12, min_leaf=4)", "MAE": 438.7, "RMSE": 659.4, "R2": 0.6183}, {"model": "GradientBoosting (n=400, lr=0.04, depth=5, sub=0.8)", "MAE": 422.1, "RMSE": 641.0, "R2": 0.6377}] FI = {"area": 0.45728226097807934, "zurich_city": 0.11430298224257547, "pop_dens": 0.0780246036087376, "log_pop_dens": 0.054507714070176565, "rooms": 0.049764240815849056, "room_per_m2": 0.0436824204066582, "area_per_room": 0.04151442713929889, "tax_income": 0.03480695081719218, "log_tax_income": 0.029919650085885676, "has_premium_keyword": 0.027519254096562112} @app.route("/predict", methods=["POST"]) def predict(): d = request.get_json(force=True) town_name = d.get("town", "").strip() ti = next((t for t in TOWNS if t["town"] == town_name), TOWNS[0]) area = float(d.get("area", 80)) rooms = float(d.get("rooms", 3)) premium = int(any([d.get("attika"), d.get("loft"), d.get("seesicht"), d.get("luxurios"), d.get("pool"), d.get("exklusiv")])) row = { "rooms": rooms, "area": area, "pop_dens": ti["pop_dens"], "frg_pct": ti["frg_pct"], "tax_income": ti["tax_income"], "room_per_m2": rooms / area if area else 0, "luxurious": int(d.get("luxurios", 0)), "temporary": int(d.get("temporary", 0)), "furnished": int(d.get("furnished", 0)), "area_cat_ecoded": 0 if area < 50 else (1 if area < 100 else 2), "zurich_city": 1 if "rich" in town_name and ti["pop_dens"] > 2000 else 0, "(ATTIKA)": int(d.get("attika", 0)), "(LOFT)": int(d.get("loft", 0)), "(SEESICHT)":int(d.get("seesicht", 0)), "(LUXURIÖS)":int(d.get("luxurios", 0)), "(POOL)": int(d.get("pool", 0)), "(EXKLUSIV)":int(d.get("exklusiv", 0)), "log_pop_dens": np.log1p(ti["pop_dens"]), "log_tax_income": np.log1p(ti["tax_income"]), "area_per_room": area / rooms if rooms else 0, "has_premium_keyword": premium, } price = float(model.predict(pd.DataFrame([row])[FEATURE_COLS])[0]) price = max(500, round(price / 10) * 10) return jsonify({"prediction": price, "low": round(price * 0.88 / 10) * 10, "high": round(price * 1.12 / 10) * 10, "sqm": round(price / area)}) @app.route("/") def index(): return HTML HTML = """ ZH Rent Predictor
ZH Apartment Rent Predictor AI Applications HS24

Location



Apartment

Rooms 3.0
Area (m²) 80

Features

← fill in the details and click predict
Canton median
CHF 2'370
804 listings
Town median
select a town
Avg. tax income
municipal avg.
Pop. density
per km²

Model comparison — 5-fold CV

Iter.ModelMAERMSE

Selected: Gradient Boosting (iter. 2). New feature added: has_premium_keyword — 1 if listing mentions Attika, Loft, Seesicht, Pool etc.

Feature importance

""" if __name__ == "__main__": app.run(host="0.0.0.0", port=7860, debug=False)