Parcelledata
Accueil › Méthodologie

DVF × DPE — Methodology

The product is one number: the empirical "brown discount" (décote) — what an F/G home actually sells for versus an equivalent D, computed on real transactions at local granularity, with a confidence band. This document is the provenance trail for that number: every published figure can be traced to (a) the controlled method that produced it, (b) the sample it rests on, and (c) a match set that survives the temporal/renovation checks below.

1. Data sources

DVF — Demandes de Valeurs Foncières (geocoded)

Attribute Value
Publisher DGFiP via data.gouv.fr (geo-dvf)
Licence Licence Ouverte 2.0
URL https://files.data.gouv.fr/geo-dvf/latest/csv/<year>/departements/<dept>.csv.gz
Years used 2022–2025

Only residential rows (type_local ∈ {Appartement, Maison}) with surface and price are downloaded (scripts/download_all.py).

DPE v2 — post-July-2021 certificates (dpe03existant)

Attribute Value
Publisher ADEME
Licence Licence Ouverte 2.0
API https://data.ademe.fr/data-fair/api/v1/datasets/dpe03existant/lines
Size ~15 M dwellings nationally

Why v2 and not the legacy pre-2021 dpe-france dataset. An earlier build matched 2022–2025 sales against pre-July-2021 certificates. Measured consequences on that build: median certificate-to-sale gap of 4.2 years (p75 = 6.1 y), 18 % of matches carrying the blank label "N", and a naive label/price gradient that was essentially flat — maximal renovation-in-between risk plus a label scale (pre-reform) that is not the one the decote must measure. With v2 the median gap is 69 days (dept-69 benchmark) and 99.8 % of matches fall within 3 years.

v2 additionally provides: BAN-geocoded coordinates and address parts (score_ban, identifiant_ban), and numero_dpe_remplace (explicit certificate-replacement chains).

Register property (verified empirically): when a certificate is replaced, ADEME removes the replaced certificate from the open-data register (in dept 69, only 2 of 46 606 replacement targets still exist in the register). The candidate pool therefore cannot contain an officially superseded DPE. The residual renovation risk is renovations without a chained re-filing, which the S2 heuristic below addresses.

2. Mutation-level preparation (multi-lot handling)

DVF publishes one row per (mutation, disposition, local) but valeur_fonciere is the price of the whole mutation. normalize.prepare_dvf_mutations:

  1. keeps nature_mutation == "Vente" only (excludes VEFA, exchanges, adjudications);
  2. drops exact duplicate rows (same local across dispositions);
  3. excludes mutations containing more than one residential local — they have no per-dwelling price. They are counted, not silently collapsed.

Dept-69 funnel (2022–2025): 119 056 raw rows → 111 934 Vente → 98 962 after dedup → 86 913 single-dwelling sales (3 850 multi-local mutations, 12 049 rows, excluded).

Known residual bias: a dwelling sold together with a non-residential lot (garage, cellar) filtered at download keeps the bundled price. This is trimmed by the €/m² [p01, p99] filter in §6.

3. Blocking (recall-oriented)

Candidate pairs (blocking.py) must pass all of:

Block Rule
B1 same INSEE commune
B2 ≤ 50 m between DVF geocode and DPE BAN geocode (0.001° grid + 4-neighbour expansion, then exact distance)
B3 DPE surface ∈ [0.70, 1.50] × DVF surface
B4 DPE date ∈ [sale − 10 y, sale + 30 d]

Blocking is deliberately permissive on time (B4); the publication window is enforced downstream (§6) so that the excluded volume is measurable.

4. Scoring, selection, ambiguity

Pair score (matching.py):

match_score = 0.30·dist + 0.35·surface + 0.15·time + 0.10·type + 0.10·address

Selection: for each sale, the most recent certificate not later than 30 days after the deed, among candidates above threshold ("take latest" — the certificate the buyer saw); ties broken by score. A global one-to-one assignment (Hungarian per postal block, greedy fallback) prevents one DPE from serving several sales; contested certificates are flagged (dpe_contested).

Per-record confidence:

match_confidence = 0.50·structural + 0.20·time_proximity + 0.30·ambiguity
ambiguity = 1 (unique candidate) else 0.5·min(1, margin/0.10) + 0.5·label_consensus

label_consensus is the share of the sale's plausible candidates that carry the same label as the selected one — many same-building candidates that all say "F" are harmless for the decote; a close runner-up with a different label is the dangerous case. Tiers: HIGH ≥ 0.75, MEDIUM ≥ 0.60, LOW below.

Diagnostic columns on every record: n_candidates, score_margin, label_consensus, dpe_contested, gap_days, dist_m, plus each score component.

Dept-69 benchmark: 51 197 matches / 86 911 usable sales = 58.9 %; 21 % of sales have a unique candidate; median 5 candidates; 22 % contested certificates resolved by the assignment.

5. Renovation-in-between detection (renovation.py)

The killer confounder: DPE filed F, renovation, sale — recorded as "F sold at price X" when it was effectively a D.

Signal Rule Action
S1 flag_superseded matched certificate replaced (chain) on/before the sale excluded if the label changed (in practice ~0: the register removes superseded certificates — S1 is a structural guard)
S2 flag_later_dpe different certificate ≤ 20 m, same type, surface ± 10 %, dated between the matched DPE and the sale excluded if the label changed
S3 flag_price_outlier F/G sale above local p90 €/m² reported only — overlaps with genuinely premium F/G stock; excluding it would mechanically inflate the decote

renovation_suspect = S1 or S2 with a label change. Dept-69: 0.42 % of matches; excluded from published aggregates. Renovations with no DPE re-filing before the sale are undetectable case-by-case; their window is bounded by the 3-year publication gap limit (median gap 69 days).

6. Publication filters (the sample behind every figure)

Applied by decote.prepare_analysis_frame, counts logged per run (decote_funnel.json):

  1. valid price/surface/label/date fields;
  2. confidence tier HIGH or MEDIUM;
  3. gap_days ∈ [−30 d, 3 years] — 3 years accepted because v2 keeps 99.8 % of matches inside it anyway and a tighter cap mostly discards rural stock;
  4. not renovation_suspect;
  5. €/m² within the departement's [p01, p99].

Dept-69: 51 197 matched → 40 613 analysis rows (79 %).

7. Decote estimation (hedonic, like-for-like)

Naive median(F) − median(D) measures composition (F homes are older and more rural), not the label. Instead (decote.py):

log(€/m²) = Σ_c β_c·1[class ∈ c] + γ·controls + μ_location + ε

decote = exp(β) − 1. Pooled F+G is the count-weighted combination of β_F, β_G with a delta-method CI.

Finer location FE mechanically shrink the decote (dept-69: FG −6.3 % with dept-level FE → −4.2 % with commune FE) — that shrinkage is the composition bias being removed, and is why level-mixing is never allowed in rankings.

Publication rules. A commune × type figure exists only if the cell has ≥ 80 usable sales, ≥ 10 sales in the class and in the reference D, and a finite SE. Otherwise the lookup falls back commune → departement → national, and says so (geo_level). Every figure carries n_sales_cell, n_sales_classe, n_sales_ref_d and a 95 % CI. No number ships without them.

8. False-match rate (measured, not asserted)

validation.py: matches with identical house number and identical normalised street (the "gold" set) are re-matched blind to all address information (geometry + surface + type + date only), mirroring the production selection rule. Disagreement estimates the wrong-certificate rate of the geometry-driven matching regime.

Dept-69: 19 678 gold pairs (13 542 with ≥ 2 candidates):

Metric All gold Ambiguous (≥2 candidates)
wrong certificate 1.9 % 2.7 %
wrong label 0.20 % 0.30 %

Wrong-label is the harm metric for the decote (same-building errors that agree on the label do not move class coefficients). Caveats: the gold set skews toward well-addressed dwellings, and errors where the true certificate never entered the candidate set (bad geocode, surface outside the window) are not covered — that failure mode is bounded by blocking design, not measured.

9. Deliverable artifacts

File Content
data/output/by_dept/<dept>.parquet matched records + all diagnostics/flags
data/output/decote_figures.parquet every published figure (geo, level, type, class, effect, CI, samples, model version)
data/output/decote_funnel.json filter-by-filter row counts
data/output/decote_ranking_departements.csv / _communes.csv rankings, projections of the same figures
GET /api/decote?q=<address> BAN-geocoded lookup → applicable decote + CI + geo_level
GET /api/decote/ranking same figures, ranked

10. Known limitations