immo-predict / src /api.py
Grailleton's picture
Fix port 7860 for Hugging Face
1bbcb12
Raw
History Blame Contribute Delete
6.66 kB
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/<departement>')
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)