Match a spend factor to your spend year
Numbers checked against the live data on
Ask for the activity year of your spend with the year window: a spend-based factor comes back already adjusted to that year's price level. EXIOBASE hotel and restaurant services in Belgium are published per 2019 euro; for 2024 spend the served value is 0.1322 kg CO2e per euro, where the published one is 0.1582.
The query#
A monetary value is served once per activity year, each row carrying that year's adjusted number. This query reads the Belgian and French twins for 2024 and 2026 spend.
Show the SQL
SELECT s.region_code,
y.spend_year,
f.currency_year,
f.price_basis,
round(f.price_index_ratio::numeric, 4) AS price_index_ratio,
round(f.value_co2e_native::numeric, 4) AS kg_co2e_per_eur,
round((f.value_co2e_native / f.price_index_ratio)::numeric, 4) AS as_published,
f.deflation_economy,
f.value_key
FROM (VALUES ('BE', 'ef-exiobase-hotel_and_restaurant-be-eur-cradle_to_gate-d6613af4'),
('FR', 'ef-exiobase-hotel_and_restaurant-fr-eur-cradle_to_gate-38319515')) AS s (region_code, slug)
CROSS JOIN (VALUES (2024), (2026)) AS y (spend_year)
LEFT JOIN open_ef.factors_flat f
ON f.library = 'exiobase'
AND f.slug = s.slug
AND f.is_default_indicator
AND (f.applies_from_year IS NULL OR f.applies_from_year <= y.spend_year)
AND (f.applies_to_year IS NULL OR f.applies_to_year >= y.spend_year)
ORDER BY s.region_code, y.spend_year;
| region_code | spend_year | currency_year | price_basis | price_index_ratio | kg_co2e_per_eur | as_published | deflation_economy | value_key |
|---|---|---|---|---|---|---|---|---|
| BE | 2024 | 2019 | basic | 0.8357 | 0.1322 | 0.1582 | BE | efv1-9a222be9f95285e45b617abca2f8bdd3_2024 |
| BE | 2026 | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
| FR | 2024 | 2019 | basic | 0.8689 | 0.0882 | 0.1015 | FR | efv1-df819bdd12ce28b0ad74047dba1f129c_2024 |
| FR | 2026 | NULL | NULL | NULL | NULL | NULL | NULL | NULL |
How to read it#
currency_yearis the price level the publisher used, 2019 here. It is notreference_year.price_index_ratiois the move from that price level to your spend year. At 0.8357, a Belgian 2024 euro carries about 16% fewer emissions than a 2019 euro, because it buys less.value_co2e_nativeandvalue_co2e_normalizedare already multiplied by the ratio. Divide by it to get the number as published.value_keynames the adjusted row you read: the value'ssource_key, an underscore, and the activity year. Store it if you need to cite the exact number.
Read it in your own currency#
A spend factor is quoted in the money its publisher priced it in: EXIOBASE in euro, USEEIO in US dollars, openCEDA in yen. Ask for a currency and every monetary value comes back restated into it, at the annual average rate for the row's own activity year. The year matters as much as it does for the price index: a 2024 row converts at the 2024 rate, a 2025 row at the 2025 one, and one factor read across several spend years crosses several rates.
The rates are the same open_ef.fx_annual series the price index runs through, so the conversion pivots on the dollar the same way, and a physical factor has nothing to convert and is never touched.
restated value = served value * usd_per_unit[quoted currency][spend year] / usd_per_unit[your currency][spend year]
One Belgian EXIOBASE spend factor read three ways for two spend years: as published in euro, and restated in pounds and in dollars at each year's own average rate.
Show the SQL
SELECT f.applies_from_year AS spend_year,
f.unit_code AS quoted_in,
round(f.value_co2e_native::numeric, 4) AS kg_co2e_per_eur,
round(xf.usd_per_unit::numeric, 6) AS usd_per_eur,
round(xg.usd_per_unit::numeric, 6) AS usd_per_gbp,
round((f.value_co2e_native * xf.usd_per_unit / xg.usd_per_unit)::numeric, 4)
AS kg_co2e_per_gbp,
round((f.value_co2e_native * xf.usd_per_unit)::numeric, 4) AS kg_co2e_per_usd
FROM open_ef.factors_flat f
JOIN open_ef.fx_annual xf
ON xf.currency_code = f.unit_code AND xf.year = f.applies_from_year
JOIN open_ef.fx_annual xg
ON xg.currency_code = 'gbp' AND xg.year = f.applies_from_year
WHERE f.library = 'exiobase'
AND f.slug = 'ef-exiobase-hotel_and_restaurant-be-eur-cradle_to_gate-d6613af4'
AND f.is_default_indicator
AND f.applies_from_year IN (2024, 2025)
ORDER BY 1
| spend_year | quoted_in | kg_co2e_per_eur | usd_per_eur | usd_per_gbp | kg_co2e_per_gbp | kg_co2e_per_usd |
|---|---|---|---|---|---|---|
| 2024 | eur | 0.1322 | 1.082381 | 1.278095 | 0.1119 | 0.1431 |
| 2025 | eur | 0.1289 | 1.129983 | 1.316701 | 0.1106 | 0.1456 |
The dollar column needs no rate of its own, because one dollar is one dollar in every year: usd_per_unit for the dollar is 1, which is why the third reading is the euro value times usd_per_eur alone.
How to ask for it#
- In factor search, the currency sits at the head of the controls above the table and the URL carries it as
cur, so a link shares the money as well as the filters: hotel and restaurant factors in pounds. It is not a filter. The row count, the facet counts and the order are identical with it and without it, and the page you are on does not move when you turn it. - On the API,
currencytakes a lowercase ISO 4217 code on both the list and the single-factor route, spelled the wayunit_codespells a monetary denominator.GET /factors/api/currencieslists the codes that can be served and the year window each one's rates cover. A code outside that list is a 400, never a page of rows quietly left in the publisher's money. - An export carries whatever the screen was in, and the file says so: the restated rows keep the publisher's own currency beside the converted unit, so a reader who was not at the screen can still turn the number back.
- Unset is the default and it means untouched. With no currency named, every number is exactly what the route served before currencies existed.
What a restated row discloses#
Two fields travel with every converted row, whichever columns you chose: original_unit_code is the currency the publisher quoted, and fx_ratio is the number the value was multiplied by. A row that was not converted carries null in both. That pair is the whole audit trail: the as-published value is the served value divided by fx_ratio, and the ratio itself is the quotient of the two usd_per_unit figures above for the row's own spend year.
Nothing else on the row moves. The physical factors keep their units, the price index has already been applied, and value_key still names the same adjusted row.
Where the index comes from#
price_index_ratio is not a published figure. It is computed when the served view is built, from two vendored World Bank series that open_ef keeps as tables of its own, so you can recompute it and show your working.
open_ef.price_indexatindex_type = 'gdp_deflator_usd'is an economy's output price level expressed in US dollars, built from GDP in current US dollars over GDP in constant 2015 US dollars (NY.GDP.MKTP.CDoverNY.GDP.MKTP.KD). It is a GDP deflator, not a consumer price index: a spend factor prices an industry's output, not a household basket.open_ef.fx_annualis the annual average US dollar value of one unit of the currency the factor is quoted in, from the official ratePA.NUS.FCRF, inverted. Annual average rather than year end, because a currency year is a year of production and not an instant.anchor_economynames whose rate it is: the euro's is Germany's, because the euro area shares one rate.
price_index_ratio = usd_price[economy][currency_year] / usd_price[economy][spend_year]
* fx[currency][spend_year] / fx[currency][currency_year]
The dollar is only a pivot and cancels. This recomputes the Belgian 2024 ratio above from the two tables:
Show the SQL
SELECT f.currency_year,
f.applies_from_year AS spend_year,
pf.value AS usd_price_currency_year,
pt.value AS usd_price_spend_year,
xf.usd_per_unit AS usd_per_eur_currency_year,
xt.usd_per_unit AS usd_per_eur_spend_year,
round(((pf.value / pt.value) * (xt.usd_per_unit / xf.usd_per_unit))::numeric, 4) AS recomputed_ratio,
round(f.price_index_ratio::numeric, 4) AS served_ratio
FROM open_ef.factors_flat f
JOIN open_ef.price_index pf
ON pf.economy = f.deflation_economy AND pf.index_type = 'gdp_deflator_usd' AND pf.year = f.currency_year
JOIN open_ef.price_index pt
ON pt.economy = f.deflation_economy AND pt.index_type = 'gdp_deflator_usd' AND pt.year = f.applies_from_year
JOIN open_ef.fx_annual xf
ON xf.currency_code = f.unit_code AND xf.year = f.currency_year
JOIN open_ef.fx_annual xt
ON xt.currency_code = f.unit_code AND xt.year = f.applies_from_year
WHERE f.library = 'exiobase'
AND f.slug = 'ef-exiobase-hotel_and_restaurant-be-eur-cradle_to_gate-d6613af4'
AND f.is_default_indicator
AND f.applies_from_year = 2024
| currency_year | spend_year | usd_price_currency_year | usd_price_spend_year | usd_per_eur_currency_year | usd_per_eur_spend_year | recomputed_ratio | served_ratio |
|---|---|---|---|---|---|---|---|
| 2019 | 2024 | 108.62713 | 125.68262 | 1.1194745 | 1.0823805 | 0.8357 | 0.8357 |
Why the index runs through the dollar#
A published deflator is denominated in the economy's own currency, and a spend factor is denominated in its publisher's. EXIOBASE prices its regions in euro, USEEIO in US dollars, openCEDA every economy in yen. An Argentine openCEDA row is money that moved the way the yen moved, not the way the peso did, so restating it by Argentina's own deflator is wrong, and wrong by an order of magnitude over a few years:
Show the SQL
SELECT f.applies_from_year AS spend_year,
f.deflation_economy,
f.unit_code AS quoted_in,
f.currency_year,
round(f.price_index_ratio::numeric, 4) AS served_ratio,
round((lf.value / lt.value)::numeric, 4) AS local_deflator_ratio,
round((lf.value / lt.value / f.price_index_ratio)::numeric, 2) AS times_too_high
FROM open_ef.factors_flat f
JOIN open_ef.price_index lf
ON lf.economy = f.deflation_economy AND lf.index_type = 'gdp_deflator' AND lf.year = f.currency_year
JOIN open_ef.price_index lt
ON lt.economy = f.deflation_economy AND lt.index_type = 'gdp_deflator' AND lt.year = f.applies_from_year
WHERE f.library = 'openceda'
AND f.slug = 'ef-openceda-accommodation-ar-jpy-cradle_to_gate-2379bd4e'
AND f.is_default_indicator
AND f.applies_from_year IN (2020, 2024)
ORDER BY 1
| spend_year | deflation_economy | quoted_in | currency_year | served_ratio | local_deflator_ratio | times_too_high |
|---|---|---|---|---|---|---|
| 2020 | AR | jpy | 2025 | 2.0977 | 26.3266 | 12.55 |
| 2024 | AR | jpy | 2025 | 1.0137 | 1.3909 | 1.37 |
Where a factor is quoted in its own economy's money the two exchange-rate legs cancel exactly and the whole expression collapses to the local ratio. Every euro-area EXIOBASE row and every USEEIO row reads the same either way, which is why a reconstruction that drops the currency leg looks correct on those examples and fails on every factor priced in someone else's money.
Observed and chained index years#
The index is dense: a hole would drop the factor out of the served view rather than serve it unadjusted, so every economy carries every year the index covers. Where the publisher has no observation for a year, the level is chained instead, from the economy's nearest observed level moved by its macro region's growth. price_index.source is where both of those are disclosed: it names the series the level came from, and it names every chained year.
Three independent things can stand between your spend year and a published statistic, and one column reports each. deflation_economy_hops is geographic and says whose series ran. Chaining is temporal and says a year inside that series was estimated. The series itself can be derived, a GDP-weighted aggregate of a region's member economies rather than anyone's published figure, which is what a regional deflation_economy (GLOBAL, EUROPE, MIDDLE_EAST) carries. They move separately: AE and LB below read hops = 0 and still rest on a chained year, and GLOBAL carries no chaining note at all because there is no observation behind it to be missing.
Show the SQL
SELECT f.deflation_economy,
f.deflation_economy_hops AS hops,
split_part(pi.source, ':', 1) AS series,
nullif(split_part(pi.source, '; ', 2), '') AS chained_year,
pi.snapshot_version,
count(DISTINCT f.slug) AS factors
FROM open_ef.factors_flat f
JOIN open_ef.price_index pi
ON pi.economy = f.deflation_economy
AND pi.index_type = 'gdp_deflator_usd'
AND pi.year = f.applies_from_year
WHERE f.library = 'openceda'
AND f.deflation_economy IN ('AE', 'BE', 'GLOBAL', 'LB')
AND f.applies_from_year = 2025
AND f.is_default_indicator
GROUP BY 1, 2, 3, 4, 5
ORDER BY 1
| deflation_economy | hops | series | chained_year | snapshot_version | factors |
|---|---|---|---|---|---|
| AE | 0 | worldbank | 2025 chained from MIDDLE_EAST | worldbank-2026-07-13 | 400 |
| BE | 0 | worldbank | NULL | worldbank-2026-07-13 | 397 |
| GLOBAL | 1 | derived | NULL | worldbank-2026-07-13 | 400 |
| LB | 0 | worldbank | 2025 chained from MIDDLE_EAST | worldbank-2026-07-13 | 400 |
Read the two together, because NULL in chained_year only says the source string carries no chaining note. On a worldbank row that means the year was observed and the level is the published statistic for that economy. On a derived row it means nothing of the kind: the level is an aggregate open_ef computed, and no year of it is an observation of anything. The newest year of the index is the one most likely to be chained, and it is also the year most readers ask for, so read source for your spend year before you put an index-adjusted number in an audit, and say which years were estimated and which levels were derived.
Watch out#
Watch out. The index is per economy, not per currency. France and Belgium share the euro, yet the French ratio is 0.8689 against 0.8357 for Belgium.
deflation_economysays which series ran, anddeflation_economy_hopssays whether it was the region's own series (0) or a parent region's.
Watch out. 2026 returns NULL. The price index runs to 2025, so there is no adjusted factor for 2026 spend yet. The factor is absent rather than served unadjusted: never replace the NULL with the published value or with zero. Use the latest covered year and say so in your report.
Watch out. Both series carry a
snapshot_version,worldbank-2026-07-13on every row today, and the snapshot is deliberately not part of the value's identity. A re-seeded series can therefore move a served spend number whilesource_key,value_key, the release and the slug all stay put. The ratio on the row is the disclosure: store it, or store the as-published value, which does not move. See What a source_key does not pin.
Watch out. A currency that has no rate for a row's spend year leaves that row in the publisher's own money rather than dropping it or guessing.
original_unit_codeisnullon such a row, so it reads as unconverted, and mixing it into a total denominated in your currency is the error to guard against.GET /factors/api/currenciesgives each code's covered years up front.
Why it works. Deflate in the factor's own currency first, then convert currencies. Doing it in the other order counts the exchange-rate move twice. Also match
price_basis: abasicprice excludes taxes and trade margins, so a purchaser-price invoice needs a factor on the same basis or an adjustment you state.