Spaces:
Sleeping
Sleeping
| from flask import Flask, jsonify, request | |
| from flask_cors import CORS | |
| import joblib | |
| import pandas as pd | |
| import numpy as np | |
| import json | |
| import math | |
| app = Flask(__name__, static_folder='static', static_url_path='') | |
| CORS(app) | |
| # ── Lazy loading ── | |
| _models = {} | |
| def get_models(): | |
| if not _models: | |
| print("Chargement des modèles V5...") | |
| _models['model_v5'] = joblib.load('src/model_v5.pkl') | |
| _models['modeles_segment'] = joblib.load('src/modeles_segment_v5.pkl') | |
| _models['le'] = joblib.load('src/label_encoder_v5.pkl') | |
| _models['features'] = joblib.load('src/features_v5.pkl') | |
| _models['insee'] = joblib.load('src/insee_commune.pkl') | |
| _models['prix_median_commune'] = joblib.load('src/prix_median_commune.pkl') | |
| _models['prix_median_dept'] = joblib.load('src/prix_median_dept.pkl') | |
| with open('src/centres_villes.json') as f: | |
| _models['centres_villes'] = json.load(f) | |
| print("Modèles V5 chargés !") | |
| return _models | |
| # ── Données communes ── | |
| _df_ref = None | |
| def get_df_ref(): | |
| global _df_ref | |
| if _df_ref is None: | |
| _df_ref = pd.read_csv( | |
| 'data/valeursfoncieres-2025-s1.txt/ValeursFoncieres-2025-S1.txt', | |
| sep='|', low_memory=False, | |
| usecols=['Commune', 'Code departement'] | |
| ).dropna() | |
| _df_ref['Commune'] = _df_ref['Commune'].str.strip().str.upper() | |
| _df_ref['Code departement'] = _df_ref['Code departement'].str.strip() | |
| return _df_ref | |
| # ── Helpers ── | |
| def assigner_segment(type_bien, prix): | |
| t = 'appart' if type_bien == 'Appartement' else 'maison' | |
| if prix < 100000: g = 'bas' | |
| elif prix < 250000: g = 'moyen_bas' | |
| elif prix < 500000: g = 'moyen_haut' | |
| else: g = 'haut' | |
| return f"{t}_{g}" | |
| precision_map = { | |
| 'appart_bas': 79.2, 'appart_moyen_bas': 84.5, | |
| 'appart_moyen_haut': 88.7, 'appart_haut': 84.4, | |
| 'maison_bas': 76.5, 'maison_moyen_bas': 81.9, | |
| 'maison_moyen_haut': 87.6, 'maison_haut': 83.2 | |
| } | |
| def calculer_dist_centre(lat, lon, centres_villes): | |
| """Calcule la distance au centre-ville le plus proche en km (Haversine)""" | |
| min_dist = float('inf') | |
| for ville, coords in centres_villes.items(): | |
| clat, clon = coords[0], coords[1] | |
| R = 6371 | |
| dlat = math.radians(clat - lat) | |
| dlon = math.radians(clon - lon) | |
| a = math.sin(dlat/2)**2 + math.cos(math.radians(lat)) * \ | |
| math.cos(math.radians(clat)) * math.sin(dlon/2)**2 | |
| dist = R * 2 * math.asin(math.sqrt(a)) | |
| if dist < min_dist: | |
| min_dist = dist | |
| return min_dist | |
| def construire_code_insee(dept, code_postal): | |
| dept = str(dept).zfill(2) | |
| cp = str(code_postal).split('.')[0].zfill(5) | |
| if dept == '75': | |
| return '75' + str(int(cp[-3:]) + 100).zfill(3) | |
| elif dept == '69' and cp.startswith('6900'): | |
| return '6938' + cp[-1] | |
| elif dept == '13' and cp.startswith('1300'): | |
| return '132' + cp[-2:] | |
| else: | |
| return dept + cp[2:].zfill(3) | |
| # ── Routes ── | |
| def index(): | |
| return app.send_static_file('index.html') | |
| def get_departements(): | |
| df = get_df_ref() | |
| depts = sorted(df['Code departement'].unique().tolist()) | |
| return jsonify(depts) | |
| def get_communes(departement): | |
| df = get_df_ref() | |
| communes = sorted( | |
| df[df['Code departement'] == departement]['Commune'].unique().tolist() | |
| ) | |
| return jsonify(communes) | |
| def predict(): | |
| m = get_models() | |
| model_v5 = m['model_v5'] | |
| modeles_segment = m['modeles_segment'] | |
| le = m['le'] | |
| features = m['features'] | |
| insee = m['insee'] | |
| prix_median_commune = m['prix_median_commune'] | |
| prix_median_dept = m['prix_median_dept'] | |
| centres_villes = m['centres_villes'] | |
| data = request.json | |
| type_bien = data['type_bien'] | |
| surface = float(data['surface']) | |
| nb_pieces = float(data['nb_pieces']) | |
| nb_lots = float(data['nb_lots']) | |
| surface_terrain = float(data['surface_terrain']) | |
| commune = data['commune'].upper() | |
| departement = data['departement'] | |
| latitude = float(data.get('latitude', 0)) | |
| longitude = float(data.get('longitude', 0)) | |
| code_postal = data.get('code_postal', '00000') | |
| type_encode = 0 if type_bien == 'Appartement' else 1 | |
| try: | |
| commune_encode = le.transform([commune])[0] | |
| except: | |
| commune_encode = 0 | |
| try: | |
| dept_encode = le.transform([departement])[0] | |
| except: | |
| dept_encode = 0 | |
| # Target Encoding | |
| prix_med_commune = prix_median_commune.get( | |
| commune, prix_median_dept.get(departement, 200000) | |
| ) | |
| # Distance centre-ville | |
| if latitude != 0 and longitude != 0: | |
| dist_centre = calculer_dist_centre(latitude, longitude, centres_villes) | |
| else: | |
| dist_centre = 50.0 | |
| # INSEE | |
| code_insee = construire_code_insee(departement, code_postal) | |
| insee_row = insee[insee['code_commune'] == code_insee] | |
| if len(insee_row) > 0: | |
| revenu_median = float(insee_row['revenu_median'].values[0]) | |
| taux_pauvrete = float(insee_row['taux_pauvrete'].values[0]) \ | |
| if not pd.isna(insee_row['taux_pauvrete'].values[0]) else 15.0 | |
| indice_gini = float(insee_row['indice_gini'].values[0]) | |
| else: | |
| revenu_median = 21000.0 | |
| taux_pauvrete = 15.0 | |
| indice_gini = 0.28 | |
| X_pred = pd.DataFrame([[ | |
| type_encode, surface, nb_pieces, nb_lots, surface_terrain, | |
| commune_encode, dept_encode, prix_med_commune, dist_centre, | |
| latitude if latitude != 0 else 46.5, | |
| longitude if longitude != 0 else 2.5, | |
| 2025, | |
| revenu_median, taux_pauvrete, indice_gini | |
| ]], columns=features) | |
| # Prédiction globale pour déterminer le segment | |
| prix_estime = model_v5.predict(X_pred)[0] | |
| segment = assigner_segment(type_bien, prix_estime) | |
| if segment in modeles_segment: | |
| prix_final = float(modeles_segment[segment].predict(X_pred)[0]) | |
| else: | |
| prix_final = float(prix_estime) | |
| precision = precision_map.get(segment, 80.0) | |
| return jsonify({ | |
| 'prix': round(prix_final), | |
| 'prix_m2': round(prix_final / surface), | |
| 'fourchette_bas': round(prix_final * 0.85), | |
| 'fourchette_haut': round(prix_final * 1.15), | |
| 'segment': segment.replace('_', ' ').title(), | |
| 'precision': precision, | |
| 'revenu_median_zone': round(revenu_median), | |
| 'dist_centre_km': round(dist_centre, 1) | |
| }) | |
| if __name__ == '__main__': | |
| app.run(debug=False, host='0.0.0.0', port=7860, threaded=True) |