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 ── @app.route('/') def index(): return app.send_static_file('index.html') @app.route('/api/departements') def get_departements(): df = get_df_ref() depts = sorted(df['Code departement'].unique().tolist()) return jsonify(depts) @app.route('/api/communes/') def get_communes(departement): df = get_df_ref() communes = sorted( df[df['Code departement'] == departement]['Commune'].unique().tolist() ) return jsonify(communes) @app.route('/api/predict', methods=['POST']) 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)