
Analyser les données DVF en Python, c'est choisir entre un fichier lu par pandas et un JSON d'API. Dans les deux cas, une mutation occupe souvent plusieurs lignes, et la valeur foncière y est répétée.
Réponse courte. Deux voies donnent les ventes DVF en Python. Les fichiers « Demandes de valeurs foncières » et « Demandes de valeurs foncières géolocalisées » se téléchargent sur data.gouv.fr et se lisent avec pandas.read_csv : le CSV géolocalisé est en UTF-8, virgule, point décimal, et code_commune y tient le code INSEE sur 5 caractères. Immo API s'appelle avec requests sur GET /api/mutations ou GET /api/stats, clé dvf_live_ dans x-api-key ou Authorization: Bearer. Le prix au mètre carré divise la valeur foncière par la surface réelle bâtie, après regroupement sur id_mutation.
Ce que le script doit calculer
Le DVF recense des mutations à titre onéreux. Le ratio utile est valeur_fonciere / surface_reelle_bati, sur un appartement ou une maison de surface strictement positive, une fois la mutation réduite à une ligne. Archives, tailles et jeux (brut, géolocalisé, DVF+) sont dans le guide des fichiers DVF.
Lire les fichiers officiels avec pandas
Le 30 octobre 2026, les deux fiches répondent HTTP 200. Demandes de valeurs foncières est le fichier de la DGFiP : texte, séparateur |, mise à jour en avril et en octobre, dernière mise à jour le 12 octobre 2026, couverture du 1er juillet 2021 au 30 juin 2026, sans l'Alsace, la Moselle ni Mayotte. Le champ de licence de l'API data.gouv vaut lov2. Demandes de valeurs foncières géolocalisées en est dérivé : CSV UTF-8, virgule, point décimal, date ISO 8601, coordonnées WGS 84. La fiche décrit code_commune comme le code INSEE actuel sur 5 caractères, et id_mutation comme l'identifiant qui groupe les lignes, non stable d'une livraison à l'autre.
Le champ de couverture de la fiche géolocalisée va du 1er juillet 2020 au 31 décembre 2025. Les titres des ressources disent « DVF 2026 (1er semestre) » et « DVF juillet 2021 - juin 2026 ». On suit les titres. Les URL changent à chaque livraison.
import requests
fiche = requests.get(
"https://www.data.gouv.fr/api/1/datasets/demandes-de-valeurs-foncieres-geolocalisees/",
timeout=60,
)
fiche.raise_for_status()
for res in fiche.json()["resources"]:
if res.get("type") == "main":
print(res["title"], res["format"], res["url"])
Charger le CSV et filtrer le code INSEE
L'en-tête de full-2026-s1.csv.gz, lu le 30 octobre 2026, contient id_mutation, date_mutation, nature_mutation, valeur_fonciere, code_commune, nombre_lots, type_local et surface_reelle_bati. Les lots s'appellent lot1_numero et lot1_surface_carrez, jusqu'au lot 5. code_commune garde un zéro initial : en entier, 01244 devient 1244. pandas décompresse un chemin qui se termine par .csv.gz.
import pandas as pd
df = pd.read_csv(
"full-2026-s1.csv.gz",
dtype={
"code_commune": "string",
"code_postal": "string",
"code_departement": "string",
},
low_memory=False,
)
commune = df[df["code_commune"] == "01244"]
Le texte DGFiP sépare Code departement et Code commune. Sa première ligne, lue le même jour dans ValeursFoncieres-2026-S1.txt, montre le département 01, la commune 244, le code postal 1800, la date 08/01/2026 et la valeur 130000,00. Le CSV géolocalisé écrit cette commune 01244 (Meximieux) et un code postal sur 5 caractères. Le brut se charge avec le séparateur et la virgule décimale.
brut = pd.read_csv(
"ValeursFoncieres-2026-S1.txt",
sep="|",
decimal=",",
dtype={
"Code postal": "string",
"Code departement": "string",
"Code commune": "string",
},
low_memory=False,
)
Trois pièges avant le prix au mètre carré
id_mutation groupe les lignes, et la valeur foncière y est répétée : l'additionner compte le même prix plusieurs fois. nombre_lots égal à 1 n'empêche pas un appartement et une dépendance de partager ce montant, avec une surface de dépendance à 0. Des surfaces, des valeurs ou des coordonnées sont vides. La fiche le dit pour les coordonnées : géométrie absente, erronée, ou parcelle dans le domaine public. pandas met NaN. On garde la mutation seulement si elle n'a qu'une ligne Appartement ou Maison, avec une surface strictement positive et une valeur renseignée.
ventes = commune[commune["nature_mutation"] == "Vente"].copy()
residentiel = ventes[ventes["type_local"].isin(["Appartement", "Maison"])]
residentiel = residentiel[residentiel["surface_reelle_bati"].gt(0)]
residentiel = residentiel[residentiel["valeur_fonciere"].notna()]
nb = residentiel.groupby("id_mutation")["id_mutation"].transform("size")
mono = residentiel[nb.eq(1)].copy()
mono["prix_m2"] = mono["valeur_fonciere"] / mono["surface_reelle_bati"]
print(mono["prix_m2"].median())
Ce résultat diffère de prix_m2_median. GET /api/stats ne garde, pour ce champ, que les ventes Appartement ou Maison de surface supérieure à 9, une ligne par mutation. Les autres agrégats de prix écartent les valeurs qui ne dépassent pas 1000, puis dédupliquent par id_mutation. Le graphique ci-dessous décrit le tableau filtré.
import matplotlib.pyplot as plt
mono.groupby("type_local")["prix_m2"].median().sort_values().plot.barh()
plt.xlabel("Mediane du prix au m2, en euros")
plt.tight_layout()
plt.show()
Interroger l'API avec requests
Le code et la documentation exposent GET /api/mutations, GET /api/mutations/nearby et GET /api/stats. Aucune route ne lit un paramètre code_commune. Pour /api/mutations, le lieu est commune (égalité insensible à la casse, ou le nom suivi d'un espace) ou code_postal. Les autres paramètres sont type_local, annee, prix_min, prix_max, limit et offset. limit vaut 100 par défaut et 1000 au plus. count est le nombre de lignes de la page, pas le total du filtre.
La clé commence par dvf_live_. Le middleware accepte x-api-key, Authorization: Bearer, ou le paramètre d'URL api_key, puis recopie la valeur dans x-api-key. Sans clé, la réponse est 401. Une clé inconnue répond 403. La clé reste dans une variable d'environnement.
import os
import requests
import pandas as pd
reponse = requests.get(
"https://immoapi.app/api/mutations",
params={
"commune": "Paris",
"type_local": "Appartement",
"annee": 2024,
"limit": 100,
"offset": 0,
},
headers={"x-api-key": os.environ["IMMOAPI_KEY"]},
timeout=30,
)
reponse.raise_for_status()
payload = reponse.json()
lignes = pd.DataFrame(payload["data"])
lignes["valeur_fonciere"] = pd.to_numeric(lignes["valeur_fonciere"], errors="coerce")
lignes["surface_reelle_bati"] = pd.to_numeric(
lignes["surface_reelle_bati"], errors="coerce"
)
L'exemple de réponse contient data, count, limit et offset. Chaque ligne reprend les colonnes, dont id_mutation, valeur_fonciere, code_commune, type_local et surface_reelle_bati. Dans cet exemple, valeur_fonciere est une chaîne. La route ne force pas nature_mutation à « Vente ». type_local est une égalité stricte : Appartement, Maison, « Local industriel. commercial ou assimilé » (point après industriel) ou Dépendance, comme l'énumération du serveur MCP.
GET /api/stats prend commune ou code_postal. Si les deux sont envoyés, commune est utilisé. La réponse est un objet plat : total_mutations, prix_moyen, prix_median, prix_p25, prix_p75, prix_min, prix_max, prix_m2_median, surface_moyenne.
stats = requests.get(
"https://immoapi.app/api/stats",
params={"code_postal": "75001"},
headers={"x-api-key": os.environ["IMMOAPI_KEY"]},
timeout=30,
)
stats.raise_for_status()
chiffres = stats.json()
print(chiffres["prix_median"], chiffres["prix_m2_median"])
GET /api/mutations/nearby prend lat et lng, ou code_postal, plus radius en kilomètres (défaut 5, de 0,1 à 50), type_local, annee et limit (défaut 20, maximum 100). Il ajoute center, radius_km et distance_km, et ne garde que les ventes de valeur foncière supérieure à 0. La page API estimation immobilière part de ces comparables. La documentation MCP indique une couverture depuis 2014, hors Alsace-Moselle et Mayotte. La fiche DGFiP du brut affiche, de son côté, la fenêtre du 1er juillet 2021 au 30 juin 2026.
proches = requests.get(
"https://immoapi.app/api/mutations/nearby",
params={
"lat": 48.8566,
"lng": 2.3522,
"radius": 1,
"type_local": "Appartement",
},
headers={"x-api-key": os.environ["IMMOAPI_KEY"]},
timeout=30,
)
proches.raise_for_status()
Fichiers ou API : quand choisir
| Besoin | Fichiers et pandas | API |
|---|---|---|
| Lire un millésime entier | read_csv sur l'archive | Pagination, limit au plus 1000 |
| Filtrer un code INSEE | code_commune dans le CSV géolocalisé | Pas de paramètre code_commune. commune ou code_postal |
| Prix au mètre carré | Calcul après groupby sur id_mutation | prix_m2_median de GET /api/stats |
| Authentification | Aucune. Licence Ouverte 2.0 sur la fiche | Clé dvf_live_, en-tête x-api-key ou Bearer |
| Fraîcheur affichée | Fiche DGFiP : avril et octobre, dernière mise à jour le 12 octobre 2026 | Page tarifs : mises à jour semestrielles |
Le middleware répond 429 au-delà de 100 requêtes par minute et par adresse IP, avec X-RateLimit-Remaining et X-RateLimit-Reset. La page tarifs affiche un essai de 2 jours, Starter à 29 € par mois (1 000 requêtes par jour sur cette page) et Pro à 49 € par mois.
FAQ : DVF Python et pandas
Comment charger le DVF en Python avec pandas ?
Le CSV géolocalisé se lit avec read_csv, y compris en .csv.gz, avec les codes en texte. Le fichier DGFiP se lit avec sep="|" et decimal=",".
Quelle colonne contient le code commune INSEE ?
Dans le CSV géolocalisé, code_commune sur 5 caractères. Dans le texte DGFiP, Code departement et Code commune sont deux colonnes.
Pourquoi diviser chaque ligne fausse le prix au mètre carré ?
La valeur foncière de la mutation est répétée sur chaque ligne du même id_mutation, parfois avec nombre_lots égal à 1. On ne divise qu'une ligne de logement, de surface strictement positive.
L'API accepte-t-elle un code commune en paramètre ?
Non. GET /api/mutations lit commune, code_postal, type_local, annee, prix_min, prix_max, limit et offset. code_commune est un champ de la réponse.
Que contient la réponse de GET /api/mutations ?
Un objet data, count, limit, offset. count est la taille de la page. Les lignes incluent id_mutation, valeur_fonciere, code_commune, type_local et surface_reelle_bati. La route n'impose pas la nature Vente.
Fichiers pandas ou API, pour un script régulier ?
Les fichiers servent à traiter un millésime entier en local. L'API sert à une requête filtrée, ou à prix_m2_median via GET /api/stats. Les conditions sont sur la page tarifs, le contrat HTTP dans la documentation.
Passer du fichier au premier appel
Le CSV géolocalisé sert au code INSEE et au regroupement par id_mutation. L'API sert au JSON filtré et à prix_m2_median. Paramètres : documentation. Formules : tarifs.
Articles connexes
DVF par commune : télécharger ou interroger les ventes d'une ville
DVF par commune : le CSV Etalab par code INSEE, l'application DVF ou l'API Immo API (ventes et prix au m² médian d'une commune). URL, colonnes, pièges.
DVF et cadastre : retrouver le prix de vente d'une parcelle
DVF et cadastre : relier une parcelle à son prix de vente, lire le numéro cadastral et interroger les mutations immobilières avec une API (commune ou GPS).
App DVF Etalab (app.dvf.etalab.gouv.fr) : mode d'emploi, limites et passage à l'API
Comment utiliser l'app DVF d'Etalab pour voir les ventes immobilières par parcelle, ce qu'elle ne permet pas (export, API, Alsace-Moselle) et comment automatiser avec une API DVF.