Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions docs/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,20 @@ The final per-(jurisdiction, commodity) table produced by the `trace` stage. Ind

Four rows are not crops. Beef, 867 (`Meat of cattle with the bone, fresh or chilled`), is a livestock commodity: it carries its share of pasture's emissions and area, and production like a crop's. `PASTURE` (-3) keeps the rest of pasture's share, with emissions and area but no production. `DROPPED` (-1) and `UNATTRIBUTED` (-2) appear in every leg and are not commodities at all. `UNATTRIBUTED` holds emissions that count toward the jurisdiction's total but that none of the commodities we model takes, so the commodity rows plus `UNATTRIBUTED` add up to that total. `DROPPED` holds carbon released where, as far as our maps show, the land became neither cropland nor pasture; it does not count toward the total and comes on top of it. `PASTURE`, `DROPPED`, `UNATTRIBUTED` and the fodder crops have no production figure, so none of them has an emissions factor. See "Rows that name no commodity" in [`methodology.md`](methodology.md).

| Column | Type | Units | Description |
| ---------------------------------- | ------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jurisdiction_name` | string | — | Display name (e.g. `United States of America \| Iowa`) |
| `commodity_name` | string | — | FAOSTAT's item name for a positive `commodity_id`, our own name for a negative one |
| `commodity_hectares` | float64 | ha | Area the commodity occupies (land occupation), on production's discounted 2000–2020 window; CROPGRIDS' 2020 area for the proxy leg |
| `peatland_commodity_hectares` | float64 | ha | Commodity area on peatland |
| `emissions_mt` | float64 | t CO₂e | Total allocated LUC emissions |
| `peatland_occupation_emissions_mt` | float64 | t CO₂e | Annual peatland-occupation emissions on the commodity's pixels |
| `forest_emissions_mt` | float64 | t CO₂e | Forest-conversion emissions |
| `grassland_emissions_mt` | float64 | t CO₂e | Grassland-conversion emissions |
| `peatland_conversion_emissions_mt` | float64 | t CO₂e | Peatland-conversion (drainage-pulse) emissions |
| `production_kg` | float64 | kg | Production: crops from NASS yield × area (direct leg), MapSPAM (statistical leg) or FAOSTAT split by CROPGRIDS area (proxy leg); livestock as FAOSTAT carcass weight, bone in |
| `yield_kg_per_ha` | float64 | kg/ha | `production_kg / commodity_hectares` |
| `emissions_factor_kgco2e_per_kg` | float64 | kg CO₂e / kg | `1000 × emissions_mt / production_kg` — the headline factor |
| Column | Type | Units | Description |
| ---------------------------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jurisdiction_name` | string | — | Display name (e.g. `United States of America \| Iowa`) |
| `commodity_name` | string | — | FAOSTAT's item name for a positive `commodity_id`, our own name for a negative one |
| `commodity_hectares` | float64 | ha | Area the commodity occupies (land occupation): the crop map in the assessment year (direct leg), MapSPAM's snapshots weighted like production (statistical leg), CROPGRIDS' 2020 area (proxy leg) |
| `peatland_commodity_hectares` | float64 | ha | Commodity area on peatland |
| `emissions_mt` | float64 | t CO₂e | Total allocated LUC emissions |
| `peatland_occupation_emissions_mt` | float64 | t CO₂e | Annual peatland-occupation emissions on the commodity's pixels |
| `forest_emissions_mt` | float64 | t CO₂e | Forest-conversion emissions |
| `grassland_emissions_mt` | float64 | t CO₂e | Grassland-conversion emissions |
| `peatland_conversion_emissions_mt` | float64 | t CO₂e | Peatland-conversion (drainage-pulse) emissions |
| `production_kg` | float64 | kg | Production: crops from NASS yield × area (direct leg), MapSPAM (statistical leg) or FAOSTAT split by CROPGRIDS area (proxy leg); livestock as FAOSTAT carcass weight, bone in |
| `yield_kg_per_ha` | float64 | kg/ha | `production_kg / commodity_hectares` |
| `emissions_factor_kgco2e_per_kg` | float64 | kg CO₂e / kg | `1000 × emissions_mt / production_kg` — the headline factor |

All three legs emit the same columns.

Expand Down
10 changes: 10 additions & 0 deletions docs/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,16 @@ Where no reference year places the group in a pixel there is no basis for a spli

**Windowed production denominator (a departure from WRI).** The emissions numerator is time-resolved: it links conversion emissions to each crop's *historical* expansion, weighted by recency. Dividing that by a single *current-year* production snapshot — as in WRI's methodology — would place the numerator and denominator on inconsistent time bases. A crop that expanded early in the window and then contracted would carry real conversion emissions against a shrunken (or zero) present-day production, yielding an inflated or undefined emissions factor. Instead, production is reduced over the same 20-year lookback window using the same linear temporal ramp applied to emissions: each conversion year's discount weight is split evenly between the two MapSPAM snapshots of the span holding it, a year after 2020 gives its whole weight to the 2020 snapshot, and production is the snapshots' mean under those weights (`get_snapshot_year_to_weight` in `statistical.py`). This ties emission allocation and production to the same years with the same recency weighting. When production is flat across the window the result reduces exactly to the current-production value; it diverges only when production actually changed. This consistent linkage is a material improvement over attributing historical-expansion emissions to present-day production alone.

The weights depend on the assessment year, because the lookback moves with it and every conversion year after 2020 adds its whole weight to the 2020 snapshot:

| Assessment year | 2000 | 2005 | 2010 | 2020 |
| --------------- | ------ | ----- | ------- | ----- |
| 2020 | 3.125% | 12.5% | 46.875% | 37.5% |
| 2022 | 1.125% | 8% | 39.375% | 51.5% |
| 2024 | 0.125% | 4.5% | 31.875% | 63.5% |

A later assessment year therefore leans further on MapSPAM 2020: a crop whose production grew after 2010 has more production for 2024 than for 2020, and one that shrank has less. It is never the assessment year's own production, since MapSPAM ends in 2020. A commodity's area is the same weighted mean of its snapshots, and so is a herd's production: its heads at each snapshot times FAOSTAT's carcass weight per head in that year.

### Jurisdictional direct proxy (`jurisdictional_direct_proxy.py`)

Each pixel's emissions are split by the land class it became, cropland or pasture, and averaged onto the CROPGRIDS grid. In each cell, each crop takes its share of the 2020 crop area of all 173 CROPGRIDS crops of what cropland is charged, and each livestock commodity takes its 2020 grazing share, as in the statistical leg, of what pasture is charged. CROPGRIDS has one year, so every span is divided by the same shares. A crop's hectares are its CROPGRIDS crop area, and `UNATTRIBUTED` is cropland's charge in cells with no CROPGRIDS crop.
Expand Down
10 changes: 5 additions & 5 deletions jdluc/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,18 @@ def get_year_to_linear_discount_weight(assessment_year: int) -> dict[int, float]
"""The LSR Guidance's linear amortization factor for a conversion in each year (Table 7.11):
9.75% in the assessment year, falling 0.5 points a year to 0.25% nineteen years before it.
"""
# NB: nothing can depart in the lookback's first year, which has no year before it to compare
# with
_, *conversion_years = get_lookback_years_range(assessment_year=assessment_year)
year_to_weight = {
year: (39 - 2 * (assessment_year - year)) / 400
# NB: nothing can depart in the lookback's first year, which has no year before it to
# compare with
for year in get_lookback_years_range(assessment_year=assessment_year)[1:]
year: (39 - 2 * (assessment_year - year)) / 400 for year in conversion_years
}
assert math.isclose(sum(year_to_weight.values()), 1)
return year_to_weight


assert all(
set(get_lookback_years_range(assessment_year=assessment_year)[1:])
set(get_year_to_linear_discount_weight(assessment_year=assessment_year))
<= set(gnw_tcl.LOSS_YEARS)
and set(get_lookback_years_range(assessment_year=assessment_year))
<= set(gpw_grassland.YEARS)
Expand Down
3 changes: 2 additions & 1 deletion jdluc/jurisdictional_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ def it() -> collections.abc.Iterator[dict[str, float | str]]:
}

if data := list(it()):
assert set(data[0]) == set(SCHEMA)
first_record, *_ = data
assert set(first_record) == set(SCHEMA)
return (
pandas.DataFrame.from_records(columns=list(SCHEMA), data=data)
.astype(SCHEMA)
Expand Down
3 changes: 2 additions & 1 deletion jdluc/jurisdictional_direct_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ def it() -> collections.abc.Iterator[dict[str, float | str]]:
}

if data := list(it()):
assert set(data[0]) == set(SCHEMA)
first_record, *_ = data
assert set(first_record) == set(SCHEMA)
return (
pandas.DataFrame.from_records(columns=list(SCHEMA), data=data)
.astype(SCHEMA)
Expand Down
3 changes: 2 additions & 1 deletion jdluc/statistical.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,8 @@ def it() -> collections.abc.Iterator[dict[str, float | str]]:
}

if data := list(it()):
assert set(data[0]) == set(SCHEMA)
first_record, *_ = data
assert set(first_record) == set(SCHEMA)
return (
pandas.DataFrame.from_records(columns=list(SCHEMA), data=data)
.astype(SCHEMA)
Expand Down
20 changes: 12 additions & 8 deletions tools/build-soy-brazil-figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@
logger = logging.getLogger(__name__)

Conversion = emit.Conversion
# The lookback opens at the assessment year less twenty, and nothing can have departed in that
# first year -- a departure needs the year before it to compare against
LOOKBACK_YEARS = emit.get_lookback_years_range(
assessment_year=emit.DEFAULT_ASSESSMENT_YEAR
)[1:]
# The years a conversion can be dated to, each with its discount weight: the lookback less its
# first year, in which nothing can have departed -- a departure needs the year before it
LOOKBACK_YEARS = tuple(
emit.get_year_to_linear_discount_weight(
assessment_year=emit.DEFAULT_ASSESSMENT_YEAR
)
)
FIRST_LOOKBACK_YEAR, *_, LAST_LOOKBACK_YEAR = LOOKBACK_YEARS


# Hotter reads as more carbon released: forest sources red and orange, grassland tan and olive
Expand Down Expand Up @@ -164,7 +167,8 @@ def conversion_artists() -> tuple[
cmap = matplotlib.colors.ListedColormap(
[CONVERSION_COLOR[c] for c in conversions]
).with_extremes(bad=TRANSPARENT)
boundaries = [c.value - 0.5 for c in conversions] + [conversions[-1].value + 0.5]
*_, last_conversion = conversions
boundaries = [c.value - 0.5 for c in conversions] + [last_conversion.value + 0.5]
norm = matplotlib.colors.BoundaryNorm(boundaries, cmap.N)
handles = [
Line2D(
Expand Down Expand Up @@ -206,7 +210,7 @@ def draw_conversion_year(
)
cmap = matplotlib.colors.ListedColormap(colours).with_extremes(bad=TRANSPARENT)
norm = matplotlib.colors.BoundaryNorm(
[*(year - 0.5 for year in LOOKBACK_YEARS), LOOKBACK_YEARS[-1] + 0.5], cmap.N
[*(year - 0.5 for year in LOOKBACK_YEARS), LAST_LOOKBACK_YEAR + 0.5], cmap.N
)
im = ax.imshow(
numpy.where(numpy.isin(layer.data, LOOKBACK_YEARS), layer.data, numpy.nan),
Expand Down Expand Up @@ -359,7 +363,7 @@ def out(name: str) -> str:
im_year,
cax=ax_lc0.inset_axes((0.05, 0.1, 0.5, 0.04)),
orientation="horizontal",
ticks=(LOOKBACK_YEARS[0], *LOOKBACK_YEARS[4::5]),
ticks=(FIRST_LOOKBACK_YEAR, *LOOKBACK_YEARS[4::5]),
)
year_bar.ax.tick_params(labelsize=7)
draw_conversion(ax_lc1, conversion, "")
Expand Down
18 changes: 15 additions & 3 deletions tools/measure-drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,9 @@ def print_worst_rows(
for value, column, row, before_value, after_value in sorted(records, reverse=True)[
:limit
]:
admin_id, commodity_id = row[1], row[2]
level_to_value = dict(zip(baseline.index.names, row, strict=True))
admin_id = level_to_value["admin_id"]
commodity_id = level_to_value["commodity_id"]
print(
f" {commodity_id!s:<16}{admin_id!s:<9}{column:<34}"
f"{before_value:>13.5g}{after_value:>13.5g}{value:>11.3e}"
Expand Down Expand Up @@ -483,8 +485,18 @@ def get_outside(df: pandas.DataFrame) -> pandas.DataFrame:
if not relative.notna().any():
blind += 1
elif float(relative.max()) > worst:
row = typing.cast(tuple[str, ...], relative.idxmax())
worst, where = float(relative.max()), f"{column:s} {row[2]!s}/{row[1]:s}"
level_to_value = dict(
zip(
left.index.names,
typing.cast(tuple[str, ...], relative.idxmax()),
strict=True,
)
)
worst = float(relative.max())
where = (
f"{column:s} {level_to_value['commodity_id']!s}/"
f"{level_to_value['admin_id']:s}"
)
# Three distinct outcomes, and the first two used to print alike: something moved, nothing was
# comparable, and everything was comparable and sat exactly still
measured = (
Expand Down
Loading