Aller au contenu

Premiers pas avec DVF : le prix au mètre carré de votre commune

Objectif

Partir des fichiers bruts des Demandes de valeurs foncières et obtenir, pour chaque commune d’un département, le prix médian au mètre carré des appartements vendus, année par année. En chemin, on rencontre les pièges habituels d’un vrai jeu de données : une ligne n’est pas une vente, les prix extrêmes faussent les moyennes, et il faut décider ce que l’on garde.

Ce tutoriel est un notebook exécuté tel quel : les résultats affichés sont ceux obtenus sur les fichiers publics au moment de l’exécution. Il suppose Python et pandas installés, et rien d’autre.

Charger les données

DVF est publié par département et par année, en CSV compressé. La version géolocalisée d’Etalab ajoute les coordonnées de chaque bien. On charge trois années du Rhône, directement depuis leur adresse publique : changer DEPARTEMENT suffit pour refaire l’analyse ailleurs.

import pandas as pd

pd.set_option("display.notebook_repr_html", False)
pd.set_option("display.width", 110)
pd.set_option("display.max_columns", 12)

DEPARTEMENT = "69"
ANNEES = [2023, 2024, 2025]
URL = "https://files.data.gouv.fr/geo-dvf/latest/csv/{annee}/departements/{dep}.csv.gz"

dvf = pd.concat(
    [pd.read_csv(URL.format(annee=a, dep=DEPARTEMENT), low_memory=False) for a in ANNEES],
    ignore_index=True,
)
dvf.shape
(245873, 40)

Comprendre la structure

Chaque ligne est un lot ou un local concerné par une mutation, pas une vente. Une vente d’appartement avec une cave et un parking occupe trois lignes, qui partagent le même id_mutation et la même valeur_fonciere, celle de l’ensemble.

colonnes = ["id_mutation", "date_mutation", "nature_mutation", "valeur_fonciere",
            "type_local", "surface_reelle_bati", "nombre_pieces_principales", "nom_commune"]
dvf[colonnes].head(8)
   id_mutation date_mutation        nature_mutation  valeur_fonciere   type_local  surface_reelle_bati  \
0  2023-894771    2023-01-03                  Vente         335000.0  Appartement                 65.0   
1  2023-894771    2023-01-03                  Vente         335000.0  Appartement                 70.0   
2  2023-894772    2023-01-03  Vente terrain à bâtir         349000.0          NaN                  NaN   
3  2023-894773    2023-01-03                  Vente         296909.0       Maison                124.0   
4  2023-894774    2023-01-04                  Vente         200000.0   Dépendance                  NaN   
5  2023-894774    2023-01-04                  Vente         200000.0  Appartement                 58.0   
6  2023-894775    2023-01-04                  Vente         511900.0       Maison                 85.0   
7  2023-894775    2023-01-04                  Vente         511900.0       Maison                 85.0   

   nombre_pieces_principales             nom_commune  
0                        3.0  Lyon 4e Arrondissement  
1                        2.0  Lyon 4e Arrondissement  
2                        NaN                Craponne  
3                        5.0                Orliénas  
4                        0.0  Lyon 2e Arrondissement  
5                        3.0  Lyon 2e Arrondissement  
6                        4.0                Craponne  
7                        4.0                Craponne  
dvf["nature_mutation"].value_counts()
nature_mutation
Vente                                 224394
Vente en l'état futur d'achèvement     19020
Echange                                 1709
Adjudication                             493
Vente terrain à bâtir                    222
Expropriation                             35
Name: count, dtype: int64
dvf["type_local"].value_counts(dropna=False)
type_local
Dépendance                                  95937
Appartement                                 60654
NaN                                         51336
Maison                                      21825
Local industriel. commercial ou assimilé    16121
Name: count, dtype: int64

Les dépendances (caves, garages) sont plus nombreuses que les logements, et un sixième des lignes n’ont pas de type de local : ce sont des terrains. Regardons combien de lignes compte une mutation.

dvf.groupby("id_mutation").size().value_counts().head(8)
2    33301
1    29555
3    18070
4     6164
5     2124
6     1308
7      661
8      522
Name: count, dtype: int64

Nettoyer

Trois décisions, à expliciter parce qu’elles changent le résultat :

  1. On ne garde que les ventes (pas les échanges, adjudications ni ventes en l’état futur d’achèvement, dont le prix n’est pas comparable).
  2. On ne garde que les mutations qui contiennent un seul logement. Une mutation avec deux appartements n’a qu’un prix global, impossible à répartir. Les dépendances qui accompagnent le logement restent acceptées : leur valeur est incluse dans le prix, c’est une approximation assumée.
  3. On ne garde que les appartements avec une surface et un prix renseignés.
logements = dvf[dvf["type_local"].isin(["Appartement", "Maison"])]
par_mutation = logements.groupby("id_mutation").size()
mutations_simples = par_mutation[par_mutation == 1].index

appts = logements[
    logements["id_mutation"].isin(mutations_simples)
    & (logements["nature_mutation"] == "Vente")
    & (logements["type_local"] == "Appartement")
    & logements["surface_reelle_bati"].ge(9)
    & logements["valeur_fonciere"].gt(0)
].copy()

appts["prix_m2"] = appts["valeur_fonciere"] / appts["surface_reelle_bati"]
appts["annee"] = pd.to_datetime(appts["date_mutation"]).dt.year
len(appts)
43833

Repérer les valeurs aberrantes

Avant d’agréger, on regarde la distribution. DVF contient des ventes à 1 €, des cessions entre membres d’une famille, des erreurs de saisie et des biens d’exception. La moyenne y est très sensible ; la médiane beaucoup moins, mais autant ne pas laisser l’absurde dans le calcul.

appts["prix_m2"].describe(percentiles=[0.01, 0.05, 0.5, 0.95, 0.99]).round(0)
count     43833.0
mean       3902.0
std        3366.0
min           0.0
1%          840.0
5%         1683.0
50%        3797.0
95%        6231.0
99%        7783.0
max      361702.0
Name: prix_m2, dtype: float64
bas, haut = appts["prix_m2"].quantile([0.01, 0.99])
appts = appts[appts["prix_m2"].between(bas, haut)]
print(f"Bornes conservées : {bas:.0f} à {haut:.0f} €/m², {len(appts)} ventes")
Bornes conservées : 840 à 7783 €/m², 42955 ventes

Agréger par commune et par année

On calcule le nombre de ventes et la médiane du prix au mètre carré, puis on ne garde que les cases qui reposent sur au moins trente ventes : une médiane sur cinq transactions ne veut rien dire.

stats = (
    appts.groupby(["nom_commune", "annee"])["prix_m2"]
    .agg(ventes="size", prix_m2_median="median")
)
fiable = stats[stats["ventes"] >= 30]
tableau = fiable["prix_m2_median"].round(0).unstack("annee")
tableau.sort_values(2024, ascending=False).head(15)
annee                      2023    2024    2025
nom_commune                                    
Lyon 2e Arrondissement   5526.0  5314.0  4946.0
Lyon 6e Arrondissement   5713.0  5204.0  5440.0
Lyon 1er Arrondissement  5346.0  4913.0  4764.0
Lyon 4e Arrondissement   5270.0  4804.0  4787.0
Charbonnières-les-Bains  4536.0  4627.0  4626.0
Lyon 3e Arrondissement   4846.0  4503.0  4488.0
Lyon 7e Arrondissement   4797.0  4438.0  4507.0
Chaponost                4420.0  4436.0  3763.0
Genas                    4726.0  4177.0  3595.0
Lyon 8e Arrondissement   4263.0  4023.0  3965.0
Écully                   4312.0  4000.0  3833.0
Tassin-la-Demi-Lune      4444.0  4000.0  4120.0
Craponne                 4411.0  3942.0  3981.0
Sathonay-Camp            4051.0  3849.0  3681.0
Mions                    3784.0  3808.0  3956.0

Zoom sur Lyon

Les arrondissements de Lyon sont des communes à part entière dans DVF. On calcule l’évolution entre la première et la dernière année chargée.

lyon = tableau[tableau.index.str.startswith("Lyon")].copy()
lyon["evolution_%"] = ((lyon[ANNEES[-1]] / lyon[ANNEES[0]] - 1) * 100).round(1)
lyon.sort_values("evolution_%")
annee                      2023    2024    2025  evolution_%
nom_commune                                                 
Lyon 1er Arrondissement  5346.0  4913.0  4764.0        -10.9
Lyon 2e Arrondissement   5526.0  5314.0  4946.0        -10.5
Lyon 4e Arrondissement   5270.0  4804.0  4787.0         -9.2
Lyon 9e Arrondissement   4088.0  3651.0  3728.0         -8.8
Lyon 5e Arrondissement   4000.0  3790.0  3688.0         -7.8
Lyon 3e Arrondissement   4846.0  4503.0  4488.0         -7.4
Lyon 8e Arrondissement   4263.0  4023.0  3965.0         -7.0
Lyon 7e Arrondissement   4797.0  4438.0  4507.0         -6.0
Lyon 6e Arrondissement   5713.0  5204.0  5440.0         -4.8

Votre commune

Il suffit de l’appeler par son nom tel qu’il apparaît dans le fichier.

tableau.loc["Villeurbanne"]
annee
2023    3840.0
2024    3505.0
2025    3566.0
Name: Villeurbanne, dtype: float64

Ce qu’il faut retenir

  • Une ligne DVF n’est pas une vente. Toujours raisonner par id_mutation.
  • Les filtres sont des choix. Les expliciter, les garder dans le code, et savoir ce qu’ils excluent : ici, les ventes groupées et le neuf.
  • Médiane et effectif minimal protègent contre les valeurs extrêmes et les petits nombres.
  • Le dernier fichier annuel peut être incomplet si l’année n’est pas terminée au moment de sa publication : vérifier la date de mise à jour sur data.gouv.fr avant de commenter une baisse.

Pour aller plus loin

  • Changer DEPARTEMENT, ou passer aux maisons en remplaçant « Appartement » par « Maison » et en ajoutant la surface du terrain à l’analyse.
  • Cartographier le résultat avec les contours communaux d’Admin Express (IGN).
  • Construire un modèle qui estime le prix d’un bien à partir de ses caractéristiques : c’est l’objet du tutoriel Prédire le prix d’un appartement avec DVF et scikit-learn.