Projet : Estimateur de prix d’un bien immobilier basé sur du Machine Learning
Introduction :
Bonjour, ici je vais vous présenter un projet qui consiste à créer une intelligence artificielle capable de prédire le prix d’un bien immobilier pour une adresse et des caractéristiques données.
Vous pouvez voir le résultat final de ce projet sur cette vidéo youtube : https://youtu.be/W1duupReEFI
Les données utilisées seront les Demandes de valeur foncières diffusées par la DGFIP : https://www.data.gouv.fr/fr/datasets/5c4ae55a634f4117716d5656/
Le traitement des données et la création du modèle seront faits avec python.
Pour le traitement des données et la création des modèles, nous allons nous baser sur ce Google Colab (les données ne sont pas encore disponibles dessus, mais cela peut permettre de voir comment la variable “prix voisinage” a été faite et quel modèle j’ai utilisé): https://colab.research.google.com/drive/1bU1Nx0HcLSSf1U6BVvsiYFfrJeKnhIW2?usp=sharing
Cette présentation de projet se découpe en 8 parties :
- présentation des données
- preprocessing
- feature engineering
- création du modèle
- retouche manuelle de notre modèle
- mise en production du modèle
- pistes d’amélioration de notre modèle/estimateur immobilier
- conclusion
Présentation des données :
Les données sont composées de l’intégralité des ventes de biens immobilier déclarés aux notaires depuis le 1er janvier 2015, à chaque ligne est donc associée la vente d’un bien immobilier avec comme information son adresse, son type (maison / appartement/local), sa taille et le prix auquel il a été vendu. Il n’y pas moins de 15 millions de lignes.
Preprocessing :
Nous allons filtrer notre dataset de la manière suivante :
- nous ne prenons que les types de mutation “Vente” et “Vente en l’état futur d’achèvement”, pour éviter n’inclure dans notre modèle des ventes de terrains / ventes issus d’adjudication.
- nous ne prenons que les ventes dont le nombre de lots est égal à 1 ou 0, car le dataset affiche le même prix de vente global à chaque lot
- nous ne prenons que les ventes dont le nombre de m² bâti est renseigné et différent de zéro
- nous ne prenons que les ventes dont le prix de ventes est renseigné et différent de zéro
Après ce premier filtre, il ne reste que 5 millions de lignes.
Et nous ne prenons que les colonnes suivantes :
- Date de vente/mutation
- Nature mutation (pour séparer les ventes en VEFA et les ventes classiques)
- Valeur foncière (prix de vente)
- Colonnes liées à l’adresse (pour nous permettre de localiser le bien)
- Type local (maison/appartement/Local commercial/Dépendance etc)
- Surface réelle bâtie (nb de mètre carré du bien bâti)
- Surface terrain (nb de mètre carré du terrain associé au bien)
Cependant nos données ne disposent pas d’une latitude et d’une longitude, seulement d’une adresse non utilisable par un algorithme de machine learning. Il nous faut donc géocoder ces adresses, pour cela j’avais à l’époque utilisé le site https://adresse.data.gouv.fr/csv qui permet de géocoder un fichier .csv, le site n’acceptant que les petits datasets, j’avais du fragmenter mon dataset en 15. Heureusement il existe aujourd’hui ce dataset déjà géocodé (et avec une meilleure précision car il est fait par croisement avec le cadastre) disponible juste ici : https://www.data.gouv.fr/fr/datasets/demandes-de-valeurs-foncieres-geolocalisees/
Nous ajoutons donc les colonnes latitude et longitude à notre dataset.
Lien du Kaagle associé : https://www.kaggle.com/arnaudhureaux/estimateur-pr-processing
Feature engineering :
Notre problème est que nous n’avons pas de véritables variables intéressantes pour déduire un prix au m², savoir qu’un bien est une maison, dans la ville de Lyon, de 300 m² peut nous donner une première fourchette de prix, mais ce ne serait pas un estimateur immobilier digne de ce nom.
Le premier critère que va juger un agent immobilier pour estimer un bien, sera le prix du voisinage. Nous allons donc essayer d’obtenir la variable “Prix du voisinage”. Une première étape serait d’associer à chaque bien la moyenne des prix au m² des 10 biens les plus proches.
Pour cela il suffit de calculer pour chaque bien, sa distance à tous les autres biens, puis prendre les 10 biens dont la distance est la plus faible, et calculer la moyenne de leur prix de vente /m². Cette distance se calcule à partir de leur latitude / longitude, et se nomme distance d’Haversine.
Mais avec 5 millions de biens, cela nécessiterait 5 millions x 5 millions = 25 000 milliards de calculs de distances. Autant vous dire que c’est quasi impossible à calculer. Une autre méthode consisterait à fragmenter géographiquement notre dataset (par département par exemple) et à répéter la même opération, mais cela resterait terriblement long, et les biens aux frontières de ces limites géographiques auront un prix moyen du voisinage faussé.
La meilleur méthode est sans conteste l’utilisation d’un algorithme de partitionnement spatial comme BallTree de la librairie Sklearn :
https://en.wikipedia.org/wiki/Ball_tree
https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.BallTree
Pour faire simple, cet algorithme va apprendre par coeur nos données et générer un modèle BallTree; pour un bien immobilier donné, ce modèle sera en mesure d’identifier les 10 biens les plus proches très rapidement.
Sachant que nous allons appliquer ce modèle à nos 5 millions de lignes, nous allons encore faire une étape d’optimisation. Au lieu de créer un modèle pour les 5 millions de lignes, un modèle BallTree sera crée par région, puis pour chaque ligne, on lui appliquera le modèle de sa région.
On crée les 13 modèles (1 par région)
from sklearn.neighbors import BallTree
appart_old[‘Distance moyenne’]=np.zeros(len(appart_old))
appart_old[‘Indices voisins’]=np.zeros(len(appart_old))
models={}
regions=appart_old.Région.unique()
for k in range(len(regions)):
name=’appart_’+regions[k]
data=appart_old[appart_old.Région==regions[k]]
data=data.reset_index(drop=True)
models[k]=BallTree(data[[‘latitude_r’, ‘longitude_r’]].values, leaf_size=2, metric=’haversine’)
save_obj(models[k], name)
On remplit les colonnes voisines “Prix moyen du quartier” et “Distance moyenne” avec nos 13 modèles
for k in range(len(regions)):
data=appart_old[appart_old.Région==regions[k]]
data=data.reset_index(drop=True)
dist, indices = models[k].query(data[[‘latitude_r’,’longitude_r’]].values,k=10)
data[‘Distance moyenne’]=np.mean(dist[:,1:]*6341,1)
a=pd.DataFrame()
a[‘Prix au m²(€)’]=np.zeros(len(data))for i in range(1,10):
a+=pd.DataFrame(data.iloc[indices[:,i],:][‘Prix au m²(€)’]).reset_index(drop=True)
a=a/10
data[‘Prix moyen du quartier’]=a.values
data.to_csv(regions[k],index=False,header=True)
Cette ligne :
dist, indices = models[k].query(data[[‘latitude_r’,’longitude_r’]].values,k=10)
…associera à la valeur dist une matrice de 10 colonnes et 5 000 000 de lignes, la Ke colonne étant la distance en km du Ke bien bien le plus proche
Et associera à la variable indices matrice de 10 colonnes et 5 000 000 de lignes, la Ke colonne étant le numéro de ligne du Ke bien le plus proche
Ces lignes :
for i in range(1,10):
a+=pd.DataFrame(data.iloc[indices[:,i],:][‘Prix au m²(€)’]).reset_index(drop=True)
a=a/10
data[‘Prix moyen du quartier’]=a.values
Rempliront la colonne “Prix moyen du quartier” par la moyenne des prix de ventes des 10 biens les plus proches (dont le numéro de ligne est donné par la variable indices), (le for k in (1,10) assure de ne pas inclure le prix du bien visé dans le prix moyen de son voisinage ;) )
Maintenant traçons la variable “Prix au m²” que nous essayons de prédire en fonction de cette nouvelle variable “Prix moyen du quartier” avec un alpha=0.03 (en enlevant les Prix au m² >10 000) :
Bingo ! On retrouve bel et bien un lien fort entre le prix d’un bien et celui de son voisinage.
Une grosse piste d’amélioration aurait été de donner le prix du voisinage en fonction du type de bien, et du nombre d’une tranche de m². Ainsi un appartement de 100 m² aurait reçu comme prix de voisinage la moyenne du prix de ventes des 10 appartements entre 80 et 120 m² les plus proches, cela aurait été encore plus pertinent. On peut même imaginer une tranche qui varierait en fonction de la densité du quartier (plus il est dense, plus on peut se permettre une tranche fine).
Entre autre nous avons aussi ajouté la densité de la région comme colonne à notre dataset à partir de cette source : https://www.insee.fr/fr/statistiques/3303305?sommaire=3353488
Pour résumer nous avons comme donnée/variables pertinentes :
Maintenant, voyons si l’on peut en tirer un premier modèle prédictif capable de deviner le prix au m² d’un bien à partir de ces variables pertinentes.
Petite infographie pour rappeler ce que signifie “créer un modèle” :
Création du modèle :
Tout d’abord nous allons définir une métrique. Ici nous allons prendre comme métrique “la distance en % à la valeur réelle”. Ainsi si nous prédisons pour un bien un prix au m² de 1200€ alors que le bien s’est vendu à 1000€/m², l’erreur sera de 20%.
La formule étant : ABS(Valeur prédite-Valeur réelle)/Valeur réelle
En python cela donne :
from sklearn.metrics import make_scorer
def custom_pred(y_test,y_pred):
error=np.abs((y_test-y_pred)/y_test)
return np.mean(error)
custom_loss=make_scorer(custom_pred,greater_is_better=False)
A noter que deux preprocessing supplémentaires au premier ont été appliqués dans le notebook visant à ajouter le prix du voisinage et dans celui créant le modèle, respectivement https://www.kaggle.com/arnaudhureaux/estimateur-ball-tree et https://www.kaggle.com/arnaudhureaux/estimateur-appart-old
Après quelques tests peu poussés, il semble que le meilleur modèle soit le RandomForestRegressor, vous aurez sur ce lien une excellente explication du fonctionement de cet algorithme : https://www.youtube.com/watch?v=J4Wdy0Wc_xQ
Ces lignes permettent de créer le modèle à partir de l’algorithme RandomForestRegressor :
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScalery=df[‘Prix au m²(€)’].values
X=df.drop(columns=[‘Valeur fonciere’,’Prix au m²(€)’]).values
scaler=StandardScaler()
X=scaler.fit_transform(X)
forest=RandomForestRegressor(n_estimators=30,min_samples_split=5)forest.fit(X,y)
Ce modèle prends comme input :
[Type local / Surface réelle bati / Latitude_r / Longitude_r /Région Densité /Distance moyenne /Prix moyen du quartier]
Et a comme output :
[Le prix au m² du bien]
Sur les 122265 exemples de notre jeu d’entrainement (données que notre modèle d’entraînement n’a pas vu), notre modèle une erreur moyenne de 20.8% et une erreur médiane de 13.8%. Ci-dessous la distribution de l’erreur du modèle :
Retouches manuels sur notre modèle :
Une étude réalisée par le site MeilleurAgent.com montre que l’étage et la présence d’un ascenceurs peuvent influencer le prix d’un bien jusqu’à 25% !
Source : https://backyard-static.meilleursagents.com/press/270c923805468437e1acc35c1e47266e34f6897f.pdf
Mais nos données ne disposent pas d’une colonne “Présence d’un ascenceur” ou non, ainsi notre modèle ne prendra pas en compte cette information essentielle.
Cependant il est tout à fait possible de faire un ajustement de notre prédiction selon l’étage du bien renseigné et la présence ou non d’un ascenseur.
Il suffit d’appliquer les % affichés dans cette infographie sur notre prédiction finale.
Il existe de nombreuses études dans le genre qui permettent d’améliorer son modèle manuellement, une grosse piste d’amélioration est donc d’agréger ces études et de faire le plus de retouches possibles.
Mise en production du modèle :
Maintenant voyons comment nous pourrions mettre en production ce modèle.
L’idéal serait que l’utilisateur n’ai qu’à renseigner l’adresse du bien, la surface, le type, la présence d’un ascenseur, et l’étage, et que le modèle déduise un prix au m² :
Notre idéal :
Pour ce faire nous allons déployer le pipeline décrit sur le shéma ci-dessous :
- Ici nous utilisons l’API Google Map pour extraire de l’adresse sa latitude, sa longitude et sa région/département.
- Nous utilisons le modèle BallTree entraîné sur nos données pour retrouver le prix moyen du voisinage (pour rappel BallTree est un algorithme qui apprend par coeur des données pour retrouver rapidement le voisin de n’importe quel point donné).
- Nous utilisons le tableau Insee pour retrouver la densité de population du département associé à l’adresse.
- Nous utilisons notre modèle RandomForestRegressor pour déduire un prix des données suivantes : Surface/Type de bien/Latitude/Longitude/Région/Prix moyen des 10 biens les plus proches/Densité
- Enfin nous ajustons notre prédiction en fonction de la présence d’un ascenseur et de l’étage, en reprenant les coefficients données dans l’étude MeilleurAgent.
Ce pipeline a été mis en place sur le framework python Flask, et est testable à cette adresse : http://arnaudjil.pythonanywhere.com/module2
Voici deux screens de ce à quoi cela ressemble :
INPUT :
OUTPUT :
Pistes d’amélioration de l’estimateur immobilier :
Utilisation des données géocodées par croisement avec le cadastre :
Nos données ont été géolocalisé avec le site https://adresse.data.gouv.fr/csv , en plus de prendre beaucoup de temps, cela donne des coordonnées peu précises et nous oblige à nous passer de beaucoup de données. La base de donnée géocodé par croisement avec le cadastre est bien plus précise, et permettra de conserver toutes nos données.
C’est une manière simple de grandement améliorer l’estimateur.
Prix du voisinage moyen selon le type d’appartement : Pour rappel, dans notre Feature Engineering nous avions crée la variable “Prix moyen du voisinage” qui pour chaque bien immobilier, associait la moyenne du prix au m² des 10 biens les plus proches.
Une grosse piste d’amélioration aurait été de donner le prix du voisinage en fonction du type de bien, et du nombre d’une tranche de m². Ainsi un appartement de 100 m² aurait reçu comme prix de voisinage la moyenne du prix de ventes des 10 appartement entre 80 et 120 m² les plus proches, cela aurait été encore plus pertinent.
Le problème que pose cette méthode est que pour les zones les moins denses, il est peu probable de trouver 10 biens similaires qui sont proches du bien, ajouter une distance maximale aux biens du voisinage serait intéressant. On peut même imaginer une tranche en m² qui varierait selon la densité de ventes, si la moyenne des distances des 10 biens les plus proches (trouvables dans la variable dist) est grande, on accepte des appartement avec 50% de m² en plus dans notre calcul du voisinage, à l’inverse si cette distance est faible, on peut se restreindre à une variation de 5–10%.
Prise en compte de la date de vente des biens : Un problème de notre modèle, est qu’il ignore complètement la date de vente des biens. Une solution serait de ne considérer que les biens vendus à une temporalité proche pour estimer un prix de voisinage moyen, ou bien de pondérer l’importance des ventes selon leur âge (plus une vente est vielle moins elle influencera notre modèle), cela peut se faire via le feature engineering du prix du voisinage ou via des méthodes de sampling classique : https://towardsdatascience.com/credit-card-fraud-detection-9bc8db79b956
Croisement avec d’autres sources de données : De nombreuses sources externes de données peuvent être ajoutés à nos données pour améliorer notre modèle.
Par exemple la liste des stations RATP pour ajouter à nos variables la distance au métro/RER/Tram le plus proche : https://www.data.gouv.fr/fr/datasets/positions-geographiques-des-stations-du-reseau-ratp-ratp/
Ou bien la liste des pharmacies (même si je ne pense pas que l’accès à une pharmacie soit très determinant dans la valorisation d’un bien, cela ne coûte rien d’essayer ) : https://www.data.gouv.fr/fr/datasets/carte-des-pharmacies-de-paris-idf/
L’évolution de la criminalité par département : https://www.data.gouv.fr/fr/datasets/chiffres-departementaux-mensuels-relatifs-aux-crimes-et-delits-enregistres-par-les-services-de-police-et-de-gendarmerie-depuis-janvier-1996/
Le taux de chômage , l’activité, l’évolution démographique : https://insee.fr/fr/statistiques/4191029
Cette liste est loin d’être exhaustive, la France est le premier au classement de l’open data mondial : https://www.etalab.gouv.fr/la-france-de-nouveau-sur-le-podium-de-lopen-data-en-2019
En cherchant bien on peut trouver énormément de données à intégrer dans notre modèle.
Avoir des annonces de vente récentes grâce au scraping:
Le gros défaut de notre modèle est qu’il ne possède pas de données fraîche, il ne peut donc pas capter des ventes qui ont eu lieu il y a 3 mois. Ainsi il ne peut pas capter les variations de prix récentes, ce qui en cette période de fort trouble est dommage ! L’open data ne fournissant pas de données aussi fraîche, une solution serait de scrapper les sites d’annonces comme Leboncoin.fr ou Seloger.com.
Ici un tutoriel pour effectuer un scraping quotidien en cloud d’un site avec des données Javascript fait par votre obligé : https://medium.com/@hureauxarnaud/comment-scraper-un-site-quotidiennement-en-cloud-avec-selenium-from-a-to-z-partie-1-4-1337a3c50929
Choix d’un autre algorithme / Tuning des hyperparamètres :
Enfin, plus d’efforts pourraient être fournis dans le choix de notre algorithme et dans le tuning de ses hyperparamètres. Le nombre de données étant très conséquent, il est compliqué de le faire en local, je vous conseille donc d’utiliser des outils comme Kaggle qui fournit leurs propres serveur pour les calculs et propose de pouvoir lancer l’entraînement de plusieurs modèles en parallèle.
Conclusion :
Merci d’avoir suivi cette présentation de projet. Les estimateurs de prix de biens immobiliers peuvent sembler légèrement obscurs dans leur fonctionnement, et j’espère avoir pu vous éclairer sur ce que pouvait être leur squelette.
De plus vous avez pu apercevoir dans ce projet que le grand nombre de données différentes que nous pouvons inclure dans notre modèle, et l’infini possibilité en matière de feature engineering, laisse à penser que l’immobilier sera un des prochains secteurs qui bénéficiera grandement du développement de l’IA.