French DVF Real-Estate → Analytics Schema¶
What
Reshape France's official "Demandes de valeurs foncières" (DVF) raw real-estate transaction export into a clean analytics CSV with ISO dates, proper euro amounts, and a repaired postal code.
Why interesting¶
DVF is the canonical open dataset for French property
prices (every registered sale, published by the tax authority DGFiP), and the
raw file is a textbook example of a European-government CSV that quietly breaks
US-centric tooling: (1) it is pipe-delimited (|), not comma-delimited, so
a default read_csv sees one giant column; (2) monetary values use a comma
decimal — 346,50 means 346.50 €, but a parser that assumes , is a
thousands separator reads it as 34650 (a 100× overvaluation) or splits
the field; (3) dates are DD/MM/YYYY, so 02/01/2024 is 2 January, not
1 February; (4) the Code postal column was stored as a number by the
producer, so Ain's 01230 already arrives as 1230 — its leading zero is
gone before you even open the file.
Edge cases sourced from.
- DGFiP — "Notice descriptive du fichier DVF" — the official field/format description (pipe delimiter, comma decimals, column meanings)
- French postal codes are five digits with a leading zero for départements 01–09; storing them numerically (a recurring open-data defect) drops it
Data source. DGFiP — Demandes de valeurs foncières (data.gouv.fr) (this slice: 9 real transactions from the 2024 file, département 01 / Ain, hand-picked to show a comma-decimal price, each property type, and several postal codes that lost their leading zero). Licence Ouverte / Etalab (CC-BY-compatible).
At full scale¶
The committed sample.csv is a 9-row teaching slice; the
real 2024 file is every property transaction registered in France that year.
Pull it and run the same template against the whole thing:
bash fetch-full.sh # downloads + extracts ./full/ValeursFoncieres-2024.txt (~446 MB)
bxp-cli --config full.json # processes all ~3.5M transactions
Measured on the reference machine (ReleaseFast, 8 cores):
| metric | value |
|---|---|
| input | 3,499,931 rows / 446 MB (pipe-delimited) |
| output | 3,499,931 rows / 213 MB (1:1) |
| wall time | ~4.9 s |
| peak RSS | ~22 MB (flat — does not grow with the file) |
The tricks¶
(see inline comments in sample.json):
- Pipe delimiter —
csv_delimiter_in: "|". - Comma decimal —
csv_decimal_separator_in: ","so346,50arrives as346.5instead of34650or a split field. - DD/MM/YYYY → ISO —
DATE_CONVERT([Date mutation], 'DD/MM/YYYY', 'YYYY-MM-DD'). - Leading-zero département preserved —
[Code departement]is stored as text (01) and passes through verbatim. - Damaged postal code repaired —
[Code postal]already lost its zero in the source (1230);LPAD([Code postal], 5, '0')re-pads it back to01230. (This only emits the correct value since the bxp leading-zero fix — previously the padded result was re-canonicalised straight back to1230.)
Final result¶
A pipe-delimited row with a comma decimal and a broken postal code becomes a clean analytics row:
raw 02/01/2024 | 346,50 | CHALEY | 01 | 1230
clean 2024-01-02 , 346.5 , CHALEY , 01 , 1230 (raw) , 01230 (fixed)
346,50 → 346.5. A naive importer reads that comma as a thousands separator and logs the sale at €34,650 — a 100× error that silently corrupts every price aggregate downstream.1230 → 01230. Join the raw value against a postal-code reference table and every département 01–09 row misses. The repaired column joins.
Trace it in the GUI
Click the price_eur cell: the trace pane shows [Valeur fonciere]
resolving the comma-decimal field to 346.5. Click postal_fixed to watch
LPAD rebuild the zero the source threw away.
Sample data¶
Run it with bxp-cli --config ./sample.json --template dvf_realestate_to_analytics:
{
// French DGFiP "Demandes de valeurs foncières" (DVF) — the raw open-data
// real-estate transaction export. A textbook European-government CSV:
// pipe-delimited, comma-decimal money, DD/MM/YYYY dates, and a postal-code
// column the producer already stored as a number (leading zero lost).
conversion_templates: {
dvf_realestate_to_analytics: {
data_dir: ".",
file_pattern_in: ".csv",
file_pattern_out: ".csvx",
// TRICK 0 — the file is pipe-delimited, not comma-delimited.
csv_delimiter_in: "|",
// TRICK 1 — monetary values use a comma decimal: "346,50" = 346.50 €.
// A US-centric parser reads "346,50" as 34650 (comma = thousands) — a
// 100x error — or splits the field. Declaring the decimal separator
// makes [Valeur fonciere] arrive as a clean 346.5.
csv_decimal_separator_in: ",",
csv_delimiter_out: ",",
csv_text_quote_out: "double",
input_schema: {
// TRICK 2 — French dates are DD/MM/YYYY; convert to ISO-8601.
$date: "DATE_CONVERT([Date mutation], 'DD/MM/YYYY', 'YYYY-MM-DD')",
$sale_type: "[Nature mutation]",
$price_eur: "[Valeur fonciere]",
$commune: "[Commune]",
// TRICK 3 — `Code departement` is stored as text and keeps its
// leading zero ("01" = Ain). bxp passes it through verbatim.
$dept: "[Code departement]",
// TRICK 4 — `Code postal`, by contrast, was stored as a NUMBER by the
// producer, so its leading zero is already gone ("01230" → "1230").
// Re-pad it back to five digits with LPAD, which says the intent
// outright — the older concat-and-slice spelling RIGHT('00000' & x, 5)
// makes the reader simulate it. (Either way the value only survives as
// "01230" since the bxp leading-zero fix — before it, the padded value
// was re-canonicalised back to "1230".)
$postal_raw: "[Code postal]",
$postal_fixed: "LPAD([Code postal], 5, '0')",
$property_type: "[Type local]",
$area_m2: "[Surface reelle bati]",
$rooms: "[Nombre pieces principales]"
},
row_rules: [ { when: "1 = 1", rows: [ {} ] } ],
output_schema: {
date: "$date",
sale_type: "$sale_type",
price_eur: "$price_eur",
commune: "$commune",
dept: "$dept",
postal_raw: "$postal_raw",
postal_fixed: "$postal_fixed",
property_type: "$property_type",
area_m2: "$area_m2",
rooms: "$rooms"
}
}
}
}
Identifiant de document|Reference document|1 Articles CGI|2 Articles CGI|3 Articles CGI|4 Articles CGI|5 Articles CGI|No disposition|Date mutation|Nature mutation|Valeur fonciere|No voie|B/T/Q|Type de voie|Code voie|Voie|Code postal|Commune|Code departement|Code commune|Prefixe de section|Section|No plan|No Volume|1er lot|Surface Carrez du 1er lot|2eme lot|Surface Carrez du 2eme lot|3eme lot|Surface Carrez du 3eme lot|4eme lot|Surface Carrez du 4eme lot|5eme lot|Surface Carrez du 5eme lot|Nombre de lots|Code type local|Type local|Identifiant local|Surface reelle bati|Nombre pieces principales|Nature culture|Nature culture speciale|Surface terrain
|||||||000001|02/01/2024|Vente|346,50||||B020|LE DELIVRE|1230|CHALEY|01|76||B|514||||||||||||0||||||P||99
|||||||000001|03/01/2024|Vente|329500,00|29||PL|0500|DU JURA|1170|GEX|01|173||AI|551||175||29||||||||2|3|Dépendance||0|0|||
|||||||000001|03/01/2024|Vente|329500,00|9001||PL|0500|DU JURA|1170|GEX|01|173||AI|551||50||||||||||1|3|Dépendance||0|0|||
|||||||000001|03/01/2024|Vente|329500,00|29||PL|0500|DU JURA|1170|GEX|01|173||AI|551||175||29||||||||2|2|Appartement||89|4|||
|||||||000001|03/01/2024|Vente|220000,00|5043|||B152|AU MOLLARD|1640|BOYEUX-SAINT-JEROME|01|56||C|2523||||||||||||0|1|Maison||40|1|S||488
|||||||000001|05/01/2024|Vente|330000,00|6189|||B037|LA POLETTE|1800|VILLIEU-LOYES-MOLLON|01|450||B|2056||||||||||||0|1|Maison||88|4|S||594
|||||||000001|04/01/2024|Vente|5000,00||||B001|LES ABERROUX|1100|APREMONT|01|11||A|598||||||||||||0||||||T||1027
|||||||000001|02/01/2024|Vente|130000,00|177||PL|0300|DE LA CROIX BLANCHE|1390|SAINT-ANDRE-DE-CORCY|01|333||AN|270||2|61,64|||||||||1|4|Local industriel. commercial ou assimilé||61|0|||
|||||||000001|09/01/2024|Vente en l'état futur d'achèvement|615000,00||||B025|GRAND PRE|1630|ST-GENIS-POUILLY|01|354||AB|587||||||||||||0||||||AB||395
date,sale_type,price_eur,commune,dept,postal_raw,postal_fixed,property_type,area_m2,rooms
2024-01-02,Vente,346.5,CHALEY,01,1230,01230,,,
2024-01-03,Vente,329500,GEX,01,1170,01170,Dépendance,0,0
2024-01-03,Vente,329500,GEX,01,1170,01170,Dépendance,0,0
2024-01-03,Vente,329500,GEX,01,1170,01170,Appartement,89,4
2024-01-03,Vente,220000,BOYEUX-SAINT-JEROME,01,1640,01640,Maison,40,1
2024-01-05,Vente,330000,VILLIEU-LOYES-MOLLON,01,1800,01800,Maison,88,4
2024-01-04,Vente,5000,APREMONT,01,1100,01100,,,
2024-01-02,Vente,130000,SAINT-ANDRE-DE-CORCY,01,1390,01390,Local industriel. commercial ou assimilé,61,0
2024-01-09,Vente en l'état futur d'achèvement,615000,ST-GENIS-POUILLY,01,1630,01630,,,
Full-scale & binary files (run it on the complete dataset): fetch-full.sh · full.json.