Each manufacturer answers "how much does this car cost?" with different fields. This guide shows how to read real configurator fields and map them to a common schema without mixing tax bases.
If you extract prices from several configurators and put them in one column, you are most likely comparing different things. One manufacturer publishes the price with VAT, another without; one already applies the promotion and another shows the list price; some include registration tax and others leave it out. If you do not tell the base of each amount, any comparison across brands, countries or dates is contaminated.
Normalizing is not about "cleaning" the data but about declaring, for each amount, what it is: list or discounted, gross or net, with or without special taxes. That declaration is what lets you use the data later in a dashboard, a pricing model or a contract. Behind every brand page of the OEM configurators hub there is a field dictionary for exactly this purpose.
Before looking at fields, it helps to fix the vocabulary. There are three independent axes, and every amount in a configurator sits at some position on each:
The best way to see the difference is to look at real rows. The table shows one row per brand, taken from the samples of our actors for the Spanish market, in euros. These are not aggregate figures or comparisons: they are the fields exactly as each configurator delivers them.
| Brand / row | Field | Value | What it represents |
|---|---|---|---|
| BMW 118d (ES) | gross_list_price | 42768.1 | Gross list price |
| BMW 118d (ES) | net_list_price | 34010.4 | Net list price |
| BMW 118d (ES) | total_taxes | 8757.678 | Taxes between gross and net |
| BMW 118d (ES) | emission_tax_pct | 0.0475 | Emissions-based percentage |
| Kia EV6 Air (ES) | msrp_before_discount | 48995.01 | List price before discount |
| Kia EV6 Air (ES) | discount_amount | 13465 | Discount in euros |
| Kia EV6 Air (ES) | msrp_dc_price | 35530.01 | Price after discount |
| Kia EV6 Air (ES) | is_net_price | False | The displayed amount is not net |
| Kia EV6 Air (ES) | net_price | 29363.64 | Net of the discounted price |
| Toyota Aygo X Cross (ES) | price_list | 27800 | List price |
| Toyota Aygo X Cross (ES) | price_discount | 3300 | Discount |
| Toyota Aygo X Cross (ES) | price_with_discount | 24500 | Price after discount |
| Toyota Aygo X Cross (ES) | price_net | 22975.21 | Net of the list price |
| Toyota Aygo X Cross (ES) | price_tax | 4824.79 | Tax on the list price |
In BMW, net and gross are sibling fields and the difference between them is published in total_taxes. In that row, 8757.678 divided by the net 34010.4 gives 0.2575, which is 21% VAT plus the 4.75% in emission_tax_pct. The practical conclusion: total_taxes is not just VAT; it groups VAT and the emissions tax, and if you subtract it assuming it is VAT you will get a wrong net.
Kia has a different structure: a list price, a discount and a final price, plus a boolean, is_net_price, that tells you whether the displayed amount is net. Here the check is arithmetic: 48995.01 minus 13465 is 35530.01, and 35530.01 divided by 1.21 is 29363.64. So net_price is the net of the discounted price, not of the list price.
Toyota is the subtlest trap. price_net is 22975.21, which is the net of the list price (27800 divided by 1.21), not of the discounted price. The post-discount net lives in another field, price_ex_vat, at 20247.93. Two brands with a field called "net" and two different bases: that is why you should never map by field name, only by what each field represents, verified against a row.
The goal is a schema where every amount carries its base explicitly. A minimal proposal, which we use as a starting point in data normalization projects, is the following:
| Common field | Meaning | How it is obtained |
|---|---|---|
| price_list_gross | List price with VAT | BMW: gross_list_price; Kia: msrp_before_discount; Toyota: price_list |
| discount_amount | Discount in local currency | Kia: discount_amount; Toyota: price_discount; null if absent |
| price_final_gross | Final price with VAT | Kia: msrp_dc_price; Toyota: price_with_discount |
| price_final_net | Final price without VAT | Kia: net_price; Toyota: price_ex_vat; BMW: net_list_price |
| vat_rate | VAT rate applied | Source field or derived and flagged as derived |
| registration_tax_amount | Registration tax, if known | Only if the source separates it; otherwise null with a flag |
| price_basis_flag | Quality of the tax basis | ok, derived or unknown |
A rule that saves trouble: always keep the original field next to the normalized one and add a quality flag instead of dropping rows. If the net was derived by dividing by VAT, mark it as derived; if the source does not let you separate the registration tax, leave the field null and flag it. Whoever consumes the data decides whether to accept derived rows. Some of our actors already do this natively: Toyota delivers tax_calculation_status and price_quality, and Volkswagen delivers price_net_status and registration_tax_status.
This philosophy, flag and let through, avoids the opposite mistake, which is silently filtering so the analyst never learns that data was missing. With the flag, a dashboard can show verified-net coverage versus derived, and a model can decide whether to exclude the derived rows.
Normalization is not only about prices. To compare electric cars with hybrids and combustion models you need to classify the powertrain, and the temptation is to detect it from the model or trim name. It is a classic mistake. If you search the text for the substring "ev", a trim called "Clio evolution" is classified as electric, as is any name containing "ev" anywhere, from "Level" to "Revolution".
The fix has three layers. First: use the fuel or powertrain fields the configurator delivers, such as fuel_type in BMW, fuel_code and is_hybrid in Toyota or engine_category_name, before the commercial name. Second: if you must use text, tokenize and compare whole words with word boundaries, never substrings. Third: store the signal you used, for example propulsion_source = fuel_field | token | unknown, so uncertain classifications can be audited later. And a unit test with the "Clio evolution" case keeps the bug from returning with a code change.
Summarized as a working order you can follow:
Each brand has its own page with coverage, real rows and a field dictionary. For the examples in this guide: BMW, Kia, Toyota and Dacia and Renault, the latter with price_retail, price_type and price_without_tax. The commercial hub is new car pricing data, and if you would rather receive the data already homogenized, you can request a normalized dataset.
Because each configurator publishes the amount on a different basis: with or without VAT, list or discounted, with or without registration tax. Until you declare the basis of each field and map it to a common schema, the comparison mixes concepts.
The list price (MSRP) is the tariff amount with VAT; net is that amount without VAT, and the discounted price is the one after applying the manufacturer promotion. They are three different axes and one field can combine two of them, such as the net of an already discounted price.
No. It depends on the configurator and the country. Some fold it into the displayed price and others leave it out or publish only a percentage, such as `emission_tax_pct` in BMW. That is why the common schema treats it as an optional field with a quality flag.
Do not detect the powertrain from substrings of the name: "Clio evolution" contains "ev" and is not an electric car. Use the source fuel fields, and if you must parse text, compare whole words and record which signal you used.
Yes. Besides the samples and the actor for each brand, there is a normalization service that delivers a common schema with a quality flag. You can request brands, markets and period from the datasets page.
Download a real sample of up to 100 rows, already sanitized, or tell us which sources you need.