Raw vs clean DVF: bulk sales, price outliers and off-plan sales
DVF, published by France's tax authority (DGFiP), is a register of property transactions, not a price database. Of the 21,199,016 lines in the 2014-2025 files, Normi drops 3.3% that are not sales, 8.1% with no usable surface and 3.2% with an aberrant price per m². Of the 18,119,513 lines it keeps, 6.7% are bulk sales and 2.6% are off-plan (VEFA) sales, excluded from statistics by default but still queryable.
These figures are measured on our own corpus, with the query shown. Depending on the commune, the cleaning raises the median price of flats by almost 5% or leaves it exactly where it was: both cases are shown below.
Published 24 September 2026 · measured in production the same day · every figure links to its query at the bottom of the page · Lire en français
Why not use raw DVF directly?
The “Demandes de valeurs foncières” files and Etalab's DVF API are free, complete and faithful to the source. That is exactly their job: to return transactions as they were declared, without interpreting them. To display the sales on a parcel, that is what you want.
To compute a price per m², a median or a trend, though, every line first has to answer three questions. Is it a sale? Does the price refer to this property alone? Does the surface allow it to be expressed per m²? The file doesn't say. The answer has to be computed, and Normi does that work once, at ingestion.
What is dropped at ingestion
Three categories of line cannot produce a reliable price per m². They are not loaded into the database, so they cannot be retrieved through the API.
3.3%
Transactions that are not sales
Exchanges, auctions and expropriations are mixed in with sales in the transaction file. Their “price” says nothing about the market. Normi keeps only the transaction types that are sales.
690,160 lines out of 21,199,016
8.1%
Lines with no surface
Without a Carrez, built or land area above 9 m², no price per m² can be computed. These lines are dropped rather than filled in with an invented surface.
1,708,261 lines out of 21,199,016
3.2%
Aberrant prices per m²
Token €1 transfers, data-entry errors, outbuildings sold on their own: for built property, a price under €500 or over €100,000 per m² is dropped. The bounds are fixed and wide: they remove impossible values, not expensive homes.
681,082 lines out of 21,199,016
What is kept, but flagged
Two further categories carry a real price, but one that doesn't describe the resale market for a home. Normi keeps and flags them, so you decide.
Bulk sales: 6.7% of published lines (1,208,430)
A transaction covering a building, a block of parking spaces or a portfolio of flats has a single overall price, repeated on each of its lines. A line is flagged
is_bulk_salewhen it has more than 3 lots, or when its summed Carrez areas exceed 1.8 times its built area, a sign that several properties were aggregated.Off-plan (VEFA) sales: 2.6% of published lines (468,970)
A VEFA sale (vente en l'état futur d'achèvement) is a new-build purchased off-plan, at a developer's price. Mixed with resales, it pushes medians up in neighbourhoods under construction. The line is flagged
is_vefafrom its transaction type.
In total, 1,648,774 lines (9.1%) are bulk or off-plan. Separately, 99.7% of published lines have coordinates, which makes them queryable by radius.
How Normi applies these filters
| Category | Default | Adjustable |
|---|---|---|
| Non-sales, missing surfaces, price outliers | Dropped at ingestion | No |
| Bulk sales | Excluded | exclude_bulk_sales=false on search_property_transactions, analyze_market_statistics, analyze_price_trends, find_property_comparables |
| Off-plan (VEFA) sales | Excluded | exclude_vefa=false on search_property_transactions, analyze_market_statistics, analyze_price_trends |
The valuation and comparison tools (estimate_property_value, compare_locations, get_zonal_price_distribution) exclude off-plan sales with no option. Two tools don't exclude them yet: analyze_market_activity, which measures volumes, and score_market_health. The exclude_outliers parameter is deprecated: price outliers are never loaded, so it has nothing to exclude. It is still accepted so existing calls don't break, but it is ignored.
To compare both versions yourself, run the same request with and without the filters. The API limits commune-level queries to the last two years, so its figures cover a shorter window than the table below and differ slightly from it.
# Median with the default filters curl -H "X-API-Key: $NORMI_API_KEY" \ "https://mcp.normi.fr/v1/stats/market?commune=MONTPELLIER&type_local=Appartement" # The same, bulk and off-plan sales included curl -H "X-API-Key: $NORMI_API_KEY" \ "https://mcp.normi.fr/v1/stats/market?commune=MONTPELLIER&type_local=Appartement&exclude_bulk_sales=false&exclude_vefa=false"
Before and after: the median price of flats, 2024-2025
Six large communes, every one we measured. “Before” counts every published line (so price outliers are already absent from both), “after” excludes bulk and off-plan sales.
| Commune | Lines before | Median before | Lines after | Median after | Change |
|---|---|---|---|---|---|
| Bordeaux | 4,584 | €4,250 | 4,021 | €4,286 | +0.8% |
| Lille | 3,420 | €3,773 | 2,617 | €3,860 | +2.3% |
| Montpellier | 4,720 | €3,433 | 3,168 | €3,600 | +4.9% |
| Nantes | 4,390 | €3,477 | 3,057 | €3,597 | +3.5% |
| Paris 15th | 3,510 | €9,445 | 1,773 | €9,443 | 0.0% |
| Toulouse | 7,413 | €3,358 | 5,310 | €3,441 | +2.5% |
exclude_bulk_sales=false.Query used
SELECT commune,
count(*),
percentile_cont(0.5) WITHIN GROUP (ORDER BY prix_m2),
count(*) FILTER (WHERE NOT is_bulk_sale AND NOT is_vefa),
percentile_cont(0.5) WITHIN GROUP (ORDER BY prix_m2)
FILTER (WHERE NOT is_bulk_sale AND NOT is_vefa)
FROM dvf_transactions
WHERE commune IN ('BORDEAUX','LILLE','MONTPELLIER','NANTES','PARIS 15','TOULOUSE')
AND computed_type_local = 'Appartement' AND prix_m2 IS NOT NULL
AND date_mutation >= '2024-01-01' AND date_mutation < '2026-01-01'
GROUP BY commune;FAQ
Frequently asked questions about DVF data quality
The questions that come up before choosing between raw DVF and a cleaned API.
Why not use raw DVF, or Etalab's DVF API, directly?
You can, and to look up the sales on one parcel it is usually enough. For a price per m² or a median, you first have to redo the cleaning: of 21,199,016 DVF lines read from 2014 to 2025, 3.3% are not sales, 8.1% have no usable surface and 3.2% have an out-of-bounds price per m². Of the remaining lines, 6.7% belong to a bulk sale and 2.6% to an off-plan sale. The public files ship these lines as they were declared; filtering them is left to the user.
What is a bulk sale in DVF?
A single transaction covering several lots for one overall price: a building, a block of parking spaces, a portfolio of flats. DVF repeats that price on every line of the transaction, so a price per m² computed line by line no longer means anything. Normi flags a line is_bulk_sale when it has more than 3 lots, or when its summed Carrez areas exceed 1.8 times its built area.
Can I still get bulk or off-plan sales if I need them?
Yes. They stay in the database and are only excluded by default. Pass exclude_bulk_sales=false or exclude_vefa=false on transaction search, market statistics or price trends. Price outliers, however, are dropped at ingestion and cannot be retrieved through the API.
Does the cleaning actually change the median?
It depends on the market, and this page shows every commune we measured, including the one where nothing moves. For flats sold in 2024-2025, excluding bulk and off-plan sales raises the median by 4.9% in Montpellier and 3.5% in Nantes, and leaves it unchanged in Paris 15th, even though half the lines there are set aside. The filter always changes the volume; its effect on price is local.
Are these figures up to date?
They were measured on 2026-09-24 against the production DVF corpus, which covers transactions from 2014 to the end of 2025. Every figure is listed at the bottom of the page with the SQL query or count that produced it.
Where the figures come from
Lines dropped at ingestion never reach the database: they are counted in the cleaning step's reject files, whose kept-row files match the production table line for line. The other figures were measured in production by SQL query. Data source: DVF (DGFiP), under an open licence.
- 21,199,016 DVF lines read, 2014 to 2025 · measured on 2026-09-24
python3: count rows by reject_reason over mcp-server/pipeline/temp/dvf/cleaned/*_rejects.csv (DVF 2014-2025) + rows of *_clean.csv — 3 079 503 rejected + 18 119 513 kept
- 690,160 mutations that are not sales (exchanges, auctions, expropriations) · measured on 2026-09-24
python3: count rows by reject_reason over mcp-server/pipeline/temp/dvf/cleaned/*_rejects.csv (DVF 2014-2025) — reject_reason = 'nature_mutation'
- 1,708,261 lines with no usable surface (no Carrez, built or land area above 9 m²) · measured on 2026-09-24
python3: count rows by reject_reason over mcp-server/pipeline/temp/dvf/cleaned/*_rejects.csv (DVF 2014-2025) — reject_reason = 'missing_surface'
- 681,082 built-property prices per m² out of bounds (under €500 or over €100,000) · measured on 2026-09-24
python3: count rows by reject_reason over mcp-server/pipeline/temp/dvf/cleaned/*_rejects.csv (DVF 2014-2025) — reject_reason LIKE 'price_outlier:%' (670 025 extreme_value_low + 11 057 extreme_value_high)
- 18,119,513 lines published in the API · measured on 2026-09-24
SELECT count(*), count(*) FILTER (WHERE is_bulk_sale), count(*) FILTER (WHERE is_vefa), count(*) FILTER (WHERE is_bulk_sale OR is_vefa) FROM dvf_transactions;
- 1,208,430 bulk-sale lines (is_bulk_sale) · measured on 2026-09-24
SELECT count(*), count(*) FILTER (WHERE is_bulk_sale), count(*) FILTER (WHERE is_vefa), count(*) FILTER (WHERE is_bulk_sale OR is_vefa) FROM dvf_transactions;
- 468,970 off-plan (VEFA) sale lines (is_vefa) · measured on 2026-09-24
SELECT count(*), count(*) FILTER (WHERE is_bulk_sale), count(*) FILTER (WHERE is_vefa), count(*) FILTER (WHERE is_bulk_sale OR is_vefa) FROM dvf_transactions;
- 1,648,774 lines that are bulk or VEFA (28,626 are both) · measured on 2026-09-24
SELECT count(*), count(*) FILTER (WHERE is_bulk_sale), count(*) FILTER (WHERE is_vefa), count(*) FILTER (WHERE is_bulk_sale OR is_vefa) FROM dvf_transactions;
- 18,070,857 published lines with coordinates · measured on 2026-09-24
SELECT count(*) FILTER (WHERE latitude IS NOT NULL AND longitude IS NOT NULL) FROM dvf_transactions;
Going further
- Data quality reference (in French): every field and rule in technical detail.
- DVF API comparison: which services document their cleaning, and which don't.
- Quickstart: a first call in under five minutes.