Identifiers and stability
Numbers checked against the live data on
Other systems hold references to this data, so identifier stability is a contract. A factor's identity is anchored in the publisher's own source: its record id, sheet coordinate or process UUID. Region, unit, boundary and activity text are annotations on top of that identity. They can be corrected and refreshed without re-keying the factor. This page lists every identifier, how it is built, what keeps it stable, and which one to reach for.
Which identifier to use#
| you want to | use | lives on | how |
|---|---|---|---|
| Store a durable pointer to a factor: in a mapping, a report, another system | slug (ef-…) |
emission_factor, factors_flat |
Exact match on slug, then on emission_factor_alias.slug. Never parse it. |
| Pin the exact published number, for audit or reproducibility | source_key (efv1-…) |
emission_factor_value, factors_flat |
Exact match. You can recompute it from the source fields. |
| Say which served row you read, including the price year of a spend factor | value_key |
factors_flat |
Exact match. It equals source_key on a physical row. |
| Recognise the same source record across releases, or de-duplicate on import | factor_source_id |
emission_factor |
Unique per publisher. The loader reuses a factor on this key. |
| Collect one source activity's factors: its boundaries, and any region or unit its key does not hold apart | activity_src_id |
emission_factor |
Exact match; it is always a literal prefix of factor_source_id. |
| Detect that a publisher edited a record in place | content_hash (ch1-…) |
source_row, emission_factor_value |
Recompute it over the record and compare. |
Watch out.
factor_id,value_id,release_idand the other UUID columns are join keys inside one database. They are not stable across a rebuild of factor search. Never store or cite one; store theslugor thesource_key.
Every token says what it is#
Each identifier that leaves the database starts with a kind prefix, so a pasted token tells you what it is and how to resolve it.
| prefix | what it is | example |
|---|---|---|
ef- |
factor slug | ef-aib-grid_electricity-fr-kwh-generation-86d757e7 |
efv1- |
a value's key, recipe version 1: a source_key, or the value_key built from it |
efv1-a143fd8320cf95bb70520ea954878c16 |
ch1- |
content hash, recipe version 1 | ch1-497c6c9a166d91c55895797ac9f0bfe2 |
The recipe version lives in the prefix, never in a column. If a hashing recipe ever changes, newly published values get a new prefix (efv2-), and every existing efv1- key stays valid.
The slug#
ef-<publisher>-<activity>-<region>-<unit>[-<boundary>]-<hex8>
ef-aib-grid_electricity-fr-kwh-generation-86d757e7
| | | | | \ first 8 hex of the factor's own id
| | | | \ system_boundary (omitted when unknown)
| | | \ unit_code (at most 12 characters)
| | \ region_code (at most 24 characters)
| \ activity text, folded to ASCII (at most 24 characters)
\ publisher code
The slug is minted once, when the factor is first loaded, and then frozen. Segments are lowercase ASCII with accents stripped; words inside a segment join with _, and the ISO - of a region code becomes _ (FR-34 becomes fr_34). The readable part is display only: it can be truncated, and it does not follow later corrections. Only the trailing hex makes a slug unique.
Note. Because the slug is frozen, its readable part can go stale. The DESNZ diesel total
ef-desnz-liquid_fuels_diesel_100-gb-l-well_to_wheel-99d9d558hassystem_boundary = 'well_to_use'today. Read the columns, never the slug.
When factors are merged, split or re-keyed, each old slug gets a row in emission_factor_alias, so a slug that was ever published keeps resolving. Resolve a slug by exact match first, then through the alias table:
Resolve one slug: first as a current slug, then through the alias table, which is where this retired slug is found.
Show the SQL
SELECT ef.slug AS current_slug, 'current' AS matched_as
FROM open_ef.emission_factor ef
WHERE ef.slug = 'ef-ademe-butane_maritime_included-europe-kwh-well_to_wheel-53be0cf1'
UNION ALL
SELECT ef.slug, a.reason
FROM open_ef.emission_factor_alias a
JOIN open_ef.emission_factor ef ON ef.id = a.emission_factor_id
WHERE a.slug = 'ef-ademe-butane_maritime_included-europe-kwh-well_to_wheel-53be0cf1'
| current_slug | matched_as |
|---|---|
| ef-ademe-butane_maritime_included-europe-kwh-well_to_use-53be0cf1 | key_fix |
The old slug is no longer a factor, so only the alias row matches. Guarantees you can rely on counts the aliases per library.
The factor identity: factor_source_id#
A factor is identified by its publisher code joined to its own source parts, under a frozen recipe per source:
factor_source_id = publisher_code "//" canon(part_0) "//" canon(part_1) ...
canon = Unicode NFC, trim, collapse inner whitespace. Case is never folded.
"//" = the part separator, doubled because a single "/" occurs in source text.
A part that contains "//" is refused at import.
UNIQUE (publisher_id, factor_source_id) is the only identity constraint. Region, unit, boundary and activity text are not part of it, with a few declared exceptions below.
| library | parts after the publisher code | example factor_source_id |
|---|---|---|
| ADEME, a total | identifiant |
ademe//12896 |
| ADEME, a stage | the total's parts, then type_poste, nom_poste_francais |
ademe//twin//Butane//inclus maritime//-//kwh//Europe//-//Combustion//Combustion |
| AGRIBALYSE, a gate | code_agb, gate |
agribalyse//20052//cradle_to_consumer |
| AGRIBALYSE, a stage | code_agb, contrib, stage |
agribalyse//20052//contrib//consumption |
| AIB | electricity, country |
aib//electricity//FR |
| DESNZ | activity path, boundary, unit | desnz//Fuels > Liquid fuels > Diesel (100% mineral diesel)//combustion//l |
| EPA | sheet tag, the row's coordinate on that sheet, unit | epa//stationary//Biomass Fuels - Gaseous//Landfill Gas//mmBtu |
| EXIOBASE | product code, region | exiobase//C_OANP//FR |
| MITECO | sheet tag, the row's coordinate on that sheet, the unit or basis where the sheet prints one | miterd//fuel//B100//l |
| ÖKOBAUDAT | process UUID, EN 15804 module | okobaudat//a9141bc2-9a57-40cc-9a5d-f6345af7bf4e//A4 |
| Open CEDA | sector code, then the ISO 3166-1 alpha-3 country or the literal ROW aggregate |
openceda//311513//USA |
| USEEIO | BEA commodity code | useeio//311513 |
Where a publisher issues a stable code, that code is the identity: an ADEME identifiant, an ÖKOBAUDAT process UUID, a BEA commodity code. Where it does not, the identity is the row's own coordinate in the published file, and a column that renumbers between editions is kept out of it. That is why DESNZ is keyed on the activity path and not on its ID column. Where one library publishes several sheet shapes (EPA, MITECO), the first part is a sheet tag, so two sheets can never share a key.
Three kinds of recipe copy a code open_ef assigns into the key, and each is declared:
- A unit where one row is printed on two bases. DESNZ keys on the canonical unit code. So do ADEME's
twinkeys, which merge the kWh and GJ printings of one fuel into one factor. - A folded name where the source ships no id. MITECO's electricity retailers are keyed on a fold of the retailer's name.
- A total open_ef summed. A derived total adds
_sum, region, unit and target boundary to its activity's parts:okobaudat//a9141bc2-9a57-40cc-9a5d-f6345af7bf4e//_sum//DE//m2//cradle_to_grave_embodied.
On those segments a change to the mapping is a declared re-key, with an alias for every old slug. It is never a silent edit.
ADEME and ÖKOBAUDAT also publish some activities as a year series: a new record id for each edition. Those factors are keyed series, on the activity without its year, and the per-year record id moves into the value's source_key. One factor then carries every year.
Activity families: activity_src_id#
activity_src_id is the activity part of factor_source_id: the same key with its region, unit and boundary tail dropped. Use it to collect a family with one filter:
Four activity families across three libraries: how many factors each holds and how many regions, countries, units and boundaries they span.
Show the SQL
SELECT ef.activity_src_id,
count(*) AS factors,
count(DISTINCT ef.region_code) AS regions,
count(DISTINCT ef.region_code) FILTER (WHERE length(ef.region_code) = 2) AS iso_countries,
count(DISTINCT ef.unit_code) AS units,
count(DISTINCT ef.system_boundary) AS boundaries
FROM open_ef.emission_factor ef
WHERE ef.activity_src_id IN ('openceda//311513', 'useeio//311513', 'agribalyse//20052', 'ademe//twin//Butane//inclus maritime//-//kwh//Europe//-')
GROUP BY 1
ORDER BY 1
| activity_src_id | factors | regions | iso_countries | units | boundaries |
|---|---|---|---|---|---|
| ademe//twin//Butane//inclus maritime//-//kwh//Europe//- | 3 | 1 | 0 | 1 | 3 |
| agribalyse//20052 | 12 | 1 | 1 | 1 | 12 |
| openceda//311513 | 149 | 149 | 148 | 1 | 1 |
| useeio//311513 | 1 | 1 | 1 | 1 | 1 |
Open CEDA's cheese sector covers 148 countries plus a rest-of-world aggregate, 149 regions in all; AGRIBALYSE's artichoke is one product in 12 gates and stages. activity_src_id always starts with the publisher code, so it never groups two libraries, even when they share a sector code as Open CEDA and USEEIO do here.
Watch out. A family is only as wide as the key. Where a recipe folds the unit or the locality into the activity token itself, as ADEME's
twinkeys do, they stay insideactivity_src_id:kwhandEuropeare both part of the butane key above, so that family is three boundaries of one fuel and can never hold a second unit or a second locality, and the same butane forFrance continentaleis a second family, not a second region of this one.
Note. A coarser grouping is a query over these columns, not a stored group id. The only durable pointers are the
slug(a factor) and thesource_key(a value).
The value keys: source_key and value_key#
source_key = "efv1-" + sha256(NFC(field_0) US NFC(field_1) US ...)[:32]
US = U+001F, the unit separator
fields = publisher code, release label, the factor's source parts, then the
value's discriminators (such as calculation approach and grid mix)
NFC = Unicode NFC. Fields are not case-folded; a missing field is "".
digest = hex, the first 32 characters
Because the factor's parts come first, value identity nests inside factor identity. You can recompute a key yourself. This reproduces the AIB France production value:
import hashlib
import unicodedata
US = "\x1f"
def source_key(publisher: str, release_label: str, parts: list[str | None]) -> str:
fields = [publisher, release_label, *parts]
payload = US.join(unicodedata.normalize("NFC", f) if f else "" for f in fields)
return "efv1-" + hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]
print(source_key("aib", "2025", ["electricity", "FR", "location_based", "production"]))
# efv1-a143fd8320cf95bb70520ea954878c16
The exact field list per library is set by its import and does not change for existing keys.
value_key names the served row. On a physical factor it equals source_key. A spend factor is served once per accounting year at that year's price level, so its value_key appends the year. A source_key contains no underscore, so the split is unambiguous:
A USEEIO spend factor and an AIB electricity factor side by side: the value key equals the source key on one and appends the year on the other.
Show the SQL
SELECT f.library, f.slug, f.source_key, f.value_key, f.applies_from_year, f.grid_mix
FROM open_ef.factors_flat f
WHERE f.library IN ('useeio', 'aib')
AND f.slug IN ('ef-useeio-cheese_manufacturing-us-usd-cradle_to_gate-9c6d0a5a', 'ef-aib-grid_electricity-fr-kwh-generation-86d757e7')
AND f.is_latest_activity_year
AND (f.price_basis = 'purchaser' OR f.grid_mix = 'production')
ORDER BY f.library
| library | slug | source_key | value_key | applies_from_year | grid_mix |
|---|---|---|---|---|---|
| aib | ef-aib-grid_electricity-fr-kwh-generation-86d757e7 | efv1-a143fd8320cf95bb70520ea954878c16 | efv1-a143fd8320cf95bb70520ea954878c16 | NULL | production |
| useeio | ef-useeio-cheese_manufacturing-us-usd-cradle_to_gate-9c6d0a5a | efv1-ef09998c37ce579fe15e87d39628973f | efv1-ef09998c37ce579fe15e87d39628973f_2025 | 2025 | NULL |
A mirror stores the slug and watches the source_key: a source_key that changes under an unchanged slug means the served number changed, through a new release, a new default or a corrected figure. Pin a factor value to a release builds on this.
The content hash#
content_hash = "ch1-" + sha256(canonical_json(record))[:32]
canonical_json = json.dumps(record, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, default=str)
source_row.content_hash covers the raw line as read from the file (source_row.raw); emission_factor_value.content_hash covers the value's own source record. The hash is not an identity: it is recomputed on every import, and a changed hash under an unchanged source_key means the publisher edited the record in place. It is not served on factors_flat; read it through source_row_id as in Trace a served value back to its source line.
Suppliers are named, never keyed#
Some factors describe a named company: a Spanish electricity retailer in MITECO, the declaration owner of an ÖKOBAUDAT EPD. The company lives in supplier, and the handles each source ships are mapped to it in supplier_resolution.
- The supplier is an annotation.
supplier_idis not part offactor_source_id,source_keyor the slug, so pointing a factor at the right legal entity never re-keys it. MITECO's electricity sheet is the one place a company name shapes a key, because that sheet publishes one row per retailer. - The supplier code is a fold of the name that keeps legal forms.
supplier.codeis lowercase and alphanumeric only: case is folded, punctuation is dropped, and an accented Latin letter is reduced to its ASCII base, so none of those three splits one company in two. Letters of a script ASCII cannot represent are kept as they are, so a code is not guaranteed ASCII, although across the ten libraries these docs use for examples every one of them is. The code keeps legal-form suffixes on purpose:ACME S.A.andACME S.L.can be two companies, and merging them would assign one company's emissions to another. - A rename is an append. A new spelling or id is added to
supplier_resolutionagainst the samesupplier_id, and no factor moves. Where the source ships an id, as ÖKOBAUDAT does with a contact UUID, a rename needs no new row at all.
Watch out. The fold is not a spelling normaliser. A Latin letter with no ASCII base at all is dropped rather than transliterated, so ÖKOBAUDAT's
Betonverband Straße, Landschaft, Garten e.V.folds tobetonverbandstraelandschaftgartenev, and the same company spelledStrassewould fold to a different code. Two spellings of one company are joined by a row insupplier_resolution, never by the fold alone.
Today 531 MITECO retailer names and 425 ÖKOBAUDAT contact UUIDs resolve to suppliers; 16 ÖKOBAUDAT suppliers carry more than one UUID.
What stays stable#
- A published slug always resolves, directly or through
emission_factor_alias. - A
source_keynever changes. A recipe change mints a new prefix for new values only. - Identity survives better normalization. Improving how a region, unit, boundary or label is derived re-stamps
norm_versionand can leave the slug's readable part stale, but it never changesfactor_source_id, the slug's hex or asource_key. The declared exceptions above are re-keyed with aliases.
You can check the structural rules yourself:
On the public libraries, how many factors break each of three structural rules: none.
Show the SQL
SELECT count(*) AS factors,
count(*) FILTER (WHERE left(ef.factor_source_id, length(ef.activity_src_id)) <> ef.activity_src_id) AS activity_not_a_prefix,
count(*) FILTER (WHERE right(ef.slug, 8) <> left(replace(ef.id::text, '-', ''), 8)) AS slug_hex_not_the_id,
count(*) FILTER (WHERE ef.factor_source_id NOT LIKE p.code || '//%') AS key_without_publisher
FROM open_ef.emission_factor ef
JOIN open_ef.publisher p ON p.id = ef.publisher_id
WHERE p.code IN ('ademe','agribalyse','exiobase','desnz','miterd','aib','epa','useeio','openceda','okobaudat')
| factors | activity_not_a_prefix | slug_hex_not_the_id | key_without_publisher |
|---|---|---|---|
| 146758 | 0 | 0 | 0 |
factors counts every factor row of the ten example libraries, stages and earlier editions included, not only the ones served today.