Parents, children, double counting
Numbers checked against the live data on
Many publishers split a total into parts: a fuel into combustion and upstream, a food product into farm, transport and retail, a building product into EN 15804 modules. Each part is a real factor you can select, and each one says which total it belongs to. Two rules keep you safe: in any aggregate over many factors, keep only is_top_parent rows (with the default value filters beside them); and add parts only when they are the direct sums_to_parent children of one parent, never together with that parent.
How a decomposition is stored#
Everything lives on the child factor.
| column | on | meaning |
|---|---|---|
parent_id |
emission_factor, factors_flat |
The factor this one is a part of. NULL on a factor with no parent. |
parent_relation |
emission_factor, factors_flat |
How it relates to that parent. Set exactly when parent_id is set. |
root_id |
emission_factor |
The top of the chain. A top-level factor is its own root. |
root_slug |
factors_flat |
The root's slug: every factor of one decomposition shares it. |
is_top_parent |
factors_flat |
True when the factor has no parent. |
boundary_family |
factors_flat |
Which taxonomy the factor's boundary code belongs to: lifecycle, energy_stage, asset_stage, contribution or unknown. Where the code comes from, not how much it covers. |
boundary_role |
factors_flat |
Whether the number may be reported alone: whole, part, credit or unknown. This is the one to filter on. |
is_derived |
emission_factor, factors_flat |
True when no publisher printed this total and open_ef summed its parts. |
parent_relation takes three values:
parent_relation |
meaning | add to its siblings? | add to its parent? |
|---|---|---|---|
sums_to_parent |
One part of the parent's total. | Yes, with the other direct sums_to_parent children of the same parent. Small rounding gaps are normal. |
No. The parent already contains it. |
contained_by_parent |
A narrower envelope nested inside the parent. | No. The envelopes overlap. | No. |
reported_beside_parent |
Published next to the total and never part of it: EN 15804 module D, the credit for recycling beyond the product's life, or the avoided-emissions credit ADEME prints beside an end-of-life total. | No. | No. |
None of the libraries these docs use for examples uses contained_by_parent. Other libraries in factor search do: a publisher that prints a narrower envelope inside a wider one (the combustion part of a well-to-use total, say) is loaded that way so the nested figure is never mistaken for a partition.
Per library, how many default rows are totals and how many are parts, by the relation to their parent and the kind of boundary.
Show the SQL
SELECT f.library, coalesce(f.parent_relation, '(top)') AS parent_relation, f.boundary_family, count(*) AS factors
FROM open_ef.factors_flat f
WHERE f.library IN ('ademe','agribalyse','desnz','okobaudat')
AND f.is_default_indicator AND f.is_latest_activity_year
GROUP BY 1, 2, 3
ORDER BY 1, 2, 3
| library | parent_relation | boundary_family | factors |
|---|---|---|---|
| ademe | reported_beside_parent | contribution | 101 |
| ademe | sums_to_parent | contribution | 884 |
| ademe | sums_to_parent | energy_stage | 1273 |
| ademe | (top) | energy_stage | 1944 |
| ademe | (top) | lifecycle | 618 |
| ademe | (top) | unknown | 205 |
| agribalyse | sums_to_parent | contribution | 14706 |
| agribalyse | sums_to_parent | lifecycle | 12255 |
| agribalyse | (top) | lifecycle | 2451 |
| desnz | sums_to_parent | energy_stage | 988 |
| desnz | (top) | energy_stage | 882 |
| desnz | (top) | lifecycle | 250 |
| okobaudat | reported_beside_parent | contribution | 3706 |
| okobaudat | sums_to_parent | asset_stage | 22009 |
| okobaudat | sums_to_parent | lifecycle | 9207 |
| okobaudat | (top) | asset_stage | 1 |
| okobaudat | (top) | energy_stage | 57 |
| okobaudat | (top) | lifecycle | 4131 |
AIB, EPA, EXIOBASE, MITECO, Open CEDA and USEEIO publish no decompositions: every factor is top-level.
Where a publisher prints the parts but no total, open_ef sums one and flags it is_derived: 491 of DESNZ's 1,132 top-level factors and 3,912 of ÖKOBAUDAT's 4,189 are totals open_ef computed.
For DESNZ and ÖKOBAUDAT, how many top-level factors exist and how many of those totals open_ef computed from the parts.
Show the SQL
SELECT f.library, count(*) AS top_level, count(*) FILTER (WHERE f.is_derived) AS derived
FROM open_ef.factors_flat f
WHERE f.library IN ('desnz','okobaudat')
AND f.is_top_parent AND f.is_default_indicator AND f.is_latest_activity_year
GROUP BY 1
ORDER BY 1
| library | top_level | derived |
|---|---|---|
| desnz | 1132 | 491 |
| okobaudat | 4189 | 3912 |
A fuel: two parts of one total#
DESNZ prints diesel's combustion and its upstream (well-to-tank) figure, but no total. open_ef sums the two into a well_to_use parent and marks it is_derived:
DESNZ diesel: the well-to-use total open_ef computed, and the combustion and upstream parts it sums.
Show the SQL
SELECT system_boundary, parent_relation, is_top_parent, is_derived, value_co2e_native
FROM open_ef.factors_flat
WHERE library = 'desnz'
AND root_slug = 'ef-desnz-liquid_fuels_diesel_100-gb-l-well_to_wheel-99d9d558'
AND is_default_indicator
AND is_latest_activity_year
ORDER BY is_top_parent DESC, system_boundary
| system_boundary | parent_relation | is_top_parent | is_derived | value_co2e_native |
|---|---|---|---|---|
| well_to_use | NULL | true | true | 3.28564 |
| combustion | sums_to_parent | false | false | 2.66155 |
| upstream_fuel | sums_to_parent | false | false | 0.62409 |
2.66155 + 0.62409 = 3.28564. The parts exist because standards draw the line differently: the GHG Protocol reports combustion in Scope 1 and upstream in Scope 3 category 3, apart; ISO 14083 wants the sum. is_top_parent AND NOT is_derived gives the first view, and is_top_parent AND system_boundary = 'well_to_use' the second. Check that a decomposition sums to its parent runs the same check on ADEME's butane, where the publisher prints all three numbers.
Note. The slug still says
well_to_wheel: the boundary code was renamed after the slug was minted, and a slug never changes. Read the boundary fromsystem_boundary, not from the slug. Identifiers and stability explains why.
A food product: a chain of gates and stages#
AGRIBALYSE publishes each food product's footprint at the consumer and its contribution by life-cycle stage. open_ef stores it as a chain: each gate is the previous gate plus one stage.
AGRIBALYSE artichoke: every gate and stage in the chain, with the boundary each one hangs under and its value.
Show the SQL
SELECT f.system_boundary, f.boundary_family, f.parent_relation,
p.system_boundary AS parent_boundary, f.value_co2e_native
FROM open_ef.factors_flat f
LEFT JOIN open_ef.emission_factor p ON p.id = f.parent_id
WHERE f.library = 'agribalyse'
AND f.root_slug = 'ef-agribalyse-artichoke_globe_raw-fr-kg-cradle_to_consumer-7718cba3'
AND f.is_default_indicator
AND f.is_latest_activity_year
ORDER BY f.is_top_parent DESC, f.value_co2e_native DESC, f.system_boundary
| system_boundary | boundary_family | parent_relation | parent_boundary | value_co2e_native |
|---|---|---|---|---|
| cradle_to_consumer | lifecycle | NULL | NULL | 2.56 |
| consumption | contribution | sums_to_parent | cradle_to_consumer | 1.29 |
| cradle_to_store_gate | lifecycle | sums_to_parent | cradle_to_consumer | 1.266166 |
| cradle_to_distribution_gate | lifecycle | sums_to_parent | cradle_to_store_gate | 1.211266 |
| cradle_to_packaging_gate | lifecycle | sums_to_parent | cradle_to_distribution_gate | 0.615266 |
| cradle_to_processing_gate | lifecycle | sums_to_parent | cradle_to_packaging_gate | 0.615266 |
| agriculture | contribution | sums_to_parent | cradle_to_farm_gate | 0.615 |
| cradle_to_farm_gate | lifecycle | sums_to_parent | cradle_to_processing_gate | 0.615 |
| transport | contribution | sums_to_parent | cradle_to_distribution_gate | 0.596 |
| distribution | contribution | sums_to_parent | cradle_to_store_gate | 0.0549 |
| processing | contribution | sums_to_parent | cradle_to_processing_gate | 0.000266 |
| packaging | contribution | sums_to_parent | cradle_to_packaging_gate | 0.0 |
Every parent is the sum of its direct children: cradle_to_consumer 2.56 is cradle_to_store_gate 1.266166 plus consumption 1.29, within the publisher's rounding. Here is what the other sums give:
The same artichoke summed four ways: the top parent alone, every row, the stages only, and the intermediate gates only. Only the first is the footprint.
Show the SQL
SELECT round((sum(value_co2e_native) FILTER (WHERE is_top_parent))::numeric, 6) AS top_parent,
round(sum(value_co2e_native)::numeric, 6) AS every_row,
round((sum(value_co2e_native) FILTER (WHERE boundary_family = 'contribution'))::numeric, 6) AS contributions,
round((sum(value_co2e_native) FILTER (WHERE boundary_family = 'lifecycle' AND NOT is_top_parent))::numeric, 6) AS gates
FROM open_ef.factors_flat
WHERE library = 'agribalyse'
AND root_slug = 'ef-agribalyse-artichoke_globe_raw-fr-kg-cradle_to_consumer-7718cba3'
AND is_default_indicator
AND is_latest_activity_year
| top_parent | every_row | contributions | gates |
|---|---|---|---|
| 2.560000 | 9.439130 | 2.556166 | 4.322964 |
Adding every row of the decomposition gives 9.44, 3.7 times the real footprint. The six stages add up to 2.556166, the total of 2.560000 within the publisher's rounding, because in this chain they are its finest partition. The five intermediate gates add up to nothing meaningful: each already contains the ones below it.
Watch out. A stage such as
transportcarries a real number, 0.596 kg CO2e per kg here, and it matches a query for "artichoke". Returned in place of the product's total, it under-reports by three quarters. Use a contribution row only when you want that stage on purpose.
A building product: modules and module D#
An ÖKOBAUDAT declaration is loaded as one top-level factor per product, with its EN 15804 modules below. Where the publisher prints no whole-life total, open_ef sums one; module D sits beside it.
One ÖKOBAUDAT brick product: under each parent, the modules it holds, what they sum to and the parent's own value.
Show the SQL
SELECT p.system_boundary AS parent_boundary, c.parent_relation,
string_agg(c.system_boundary, ', ' ORDER BY c.system_boundary) AS children,
round(sum(c.value_co2e_native)::numeric, 5) AS children_sum,
round(max(pf.value_co2e_native)::numeric, 5) AS parent_value
FROM open_ef.factors_flat c
JOIN open_ef.emission_factor p ON p.id = c.parent_id
JOIN open_ef.factors_flat pf ON pf.factor_id = p.id AND pf.is_default_indicator AND pf.is_latest_activity_year
WHERE c.library = 'okobaudat'
AND c.root_slug = 'ef-okobaudat-brick_slips-de-m2-cradle_to_grave_embodied-29abe7f2'
AND c.is_default_indicator
AND c.is_latest_activity_year
GROUP BY 1, 2
ORDER BY 1, 2
| parent_boundary | parent_relation | children | children_sum | parent_value |
|---|---|---|---|---|
| cradle_to_grave_embodied | reported_beside_parent | D | -0.04480 | 6.47249 |
| cradle_to_grave_embodied | sums_to_parent | A4, A5, cradle_to_gate, end_of_life | 6.47249 | 6.47249 |
| end_of_life | sums_to_parent | C1, C2, C3, C4 | 0.13574 | 0.13574 |
The whole-life total is the cradle-to-gate modules, transport and installation (A4, A5) and end of life; end of life is itself C1 to C4. Module D, a credit of -0.0448 kg CO2e per m², is reported beside the total and never folded into it. System boundaries explains the modules.
The is_default_indicator on those modules does a second job: where the EPD prints several EN 15804 scenarios for a module, it elects one of them, so each row above carries a single scenario rather than a blend of the alternatives (EPD scenarios and other qualifiers).
Two filters, not the same#
| filter | keeps | use it for |
|---|---|---|
WHERE is_top_parent |
Only factors with no parent: one factor per decomposition, but still several rows per factor. | Any aggregate over many factors, and default matching. |
WHERE boundary_role = 'whole' |
Only rows whose boundary is a whole answer: gates, full life cycles, energy chains. It drops every part, and every credit. | Comparing or listing numbers that stand on their own. |
Neither filter is enough on its own.
is_top_parent chooses one factor per decomposition, not one row per factor. A factor keeps a row for every GWP characterization in every activity year, so an aggregate needs the two default filters beside it:
EXIOBASE with only the top-parent filter: how many factors, how many rows that leaves per factor, and how many rows the two default filters keep.
Show the SQL
SELECT count(DISTINCT factor_id) AS top_level_factors,
count(*) AS rows_under_is_top_parent,
count(*) / count(DISTINCT factor_id) AS rows_per_factor,
count(*) FILTER (WHERE is_default_indicator AND is_latest_activity_year) AS rows_with_the_default_filters
FROM open_ef.factors_flat
WHERE library = 'exiobase'
AND is_top_parent
| top_level_factors | rows_under_is_top_parent | rows_per_factor | rows_with_the_default_filters |
|---|---|---|---|
| 8064 | 620928 | 77 | 8064 |
A sum over is_top_parent alone counts each EXIOBASE sector 77 times. The query above omits the two conditions so that the count is visible; every worked example earlier on this page carries is_default_indicator AND is_latest_activity_year, and so should any aggregate of your own. Choosing a value: the axes explains what the extra rows are.
boundary_role = 'whole' is not a double-counting guard either. On the artichoke above it keeps the five intermediate gates, and an aggregate over them counts the same farm emissions five times: they are each a whole answer, at five different gates. It says whether one number stands alone, not whether two of them overlap. For totals, reach for is_top_parent, and use both together.
Do not reach for boundary_family <> 'contribution', which is the filter this page used to teach. boundary_family records which taxonomy a boundary code comes from, so that filter keeps A1-A3 and combustion, parts of a total that nobody reports alone, while dropping module D, whose 3,706 ÖKOBAUDAT rows are the reported_beside_parent contributions in the census above, and dropping a top-level factor whose own boundary happens to be a contribution code. boundary_role answers the question that filter was reached for.