Measure the pre-1962 back-cast's hectares on each year's own borders - #776
Draft
lbm364dl wants to merge 5 commits into
Draft
Measure the pre-1962 back-cast's hectares on each year's own borders#776lbm364dl wants to merge 5 commits into
lbm364dl wants to merge 5 commits into
Conversation
…orders The pre-1962 back-cast estimates production as `tonnes = ha * t_ha`. The yield half is already historical -- `.fill_yields()` back-casts `t_ha` against the `international-yields` pin, 1,058,295 usable pre-1962 observations over 1840-1961, and none of that is touched here. The AREA half came from the `luh2-areas` pin, which is LUH2 land pre-aggregated to PRESENT-DAY ISO3, so a row labelled with the 1961 entity was measured on the borders that entity has today. `build_historical_land_areas()` measures it on each year's own borders instead: gridded LUH2 summed inside the polygon of the polity that `area_code` resolves to in that year, resolved UNFLOORED (`backcast_anchor = -Inf`, the switch `.resolve_hist_trade_polities()` already uses for genuinely historical trade). A cell's land is shared among the polygons covering it in proportion to the covered fraction renormalised to one per cell -- the rule `build_cell_polity_fraction()` already uses, and the one that stops a coastal cell being water-discounted twice. The series is NOT re-keyed. `fill_proxy_growth()` groups by `area_code`, so keying on `polity_code` would start a new group at every period boundary and fragment the series silently (whep#589/#632). `(year, area_code)` stays the key and only the values change. At a handover year the previous year is re-measured inside the INCOMING polity's polygon before the ratio is taken, so a territorial change stays a level step instead of compounding as growth down the whole back-cast. Since `fill_proxy_growth()` reads nothing but year-on-year ratios, that rule is expressed by chain-linking the series from the last year backwards, which also makes every ratio immune to a constant overlap between two polygons. Behind `build_primary_production(land_method = )`, default unchanged. The chosen method is recorded in `source` -- `LUH2_polity_cropland` / `LUH2_polity_agriland` -- which is the column this series' provenance already lives in; a parallel `method_land` column would say the same thing while changing the schema of the default path. `.territorial_join_baseline()` gains one row and the ratchet goes 59 -> 60, for the `polity_code` join that makes the land half year-aware at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chain-link accumulated from the front (`cumsum(x)[n] - cumsum(x)`), so a single NA anywhere in a bucket's series made the total NA and wiped out all 112 years, not just the part the break cuts off. Four polities reachable from a reporting area carry no polygon -- BLZ-1800-1886, CAN-1800-1866, CHL-1810-1884, PRY-1811-1870 and CAN-1866-1870 -- so this is reachable, not hypothetical. Accumulating from the END instead breaks only the years before the hole, which is what the warning already claimed happened. Guarded by a test that fails on the old arithmetic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`boundary_step = "none"` read as "no method"; it is a method, and the measured
difference between the two is large enough that the naming has to say so.
Renamed to `"level_step"`: it lets a territorial change through as a level step,
so the 1850 row is scaled to the smaller empire it is labelled with, which is
the reframing this file exists for. `"relink"` removes the step and uses only
within-territory growth, which is safer against a bad polygon and is the
default.
Measured over 1850-1961 against the present-day series, on back-cast crop
tonnage weighted by 1961 FAOSTAT production:
relink level_step
1850 -17.2% net, 19.2% abs -8.7% net, 31.3% abs
1900 -5.5% net, 6.5% abs +2.4% net, 22.9% abs
1950 -1.0% net, 1.3% abs -0.7% net, 1.3% abs
1961 -0.2% net, 0.2% abs -0.2% net, 0.2% abs
and 18.0% of 1850 back-cast tonnage sits between the two rules, 0.07% by 1960.
The toy fixture now carries real rows from that run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`R CMD check` NOTEd `.polity_cell_cover: no visible binding for global variable 'ID'`. Status: OK after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.namespace_available()` was a one-line wrapper around `requireNamespace()` that also shadowed `.sf_namespace_available()` in polities.R. Inlined, which is what footprint_sankey.R already does for the same check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
The pre-1962 back-cast estimates production as
tonnes = ha * t_ha.The yield half is already historical.
.fill_yields()back-castst_hawith
fill_proxy_growth(proxy_col = "yield")against theinternational-yieldspin — 1,058,295 usable pre-1962 observations, 1840-1961, 179 areas, 155 items.
Nothing in this PR touches it.
The area half was not. It came from the
luh2-areaspin, which is LUH2 landpre-aggregated to present-day ISO3, so a row labelled with the 1961 entity
was measured on the borders that entity has today. Land enters through exactly
one seam, the merge in
.fill_pre_faostat(), and that seam is what this PRchanges.
Evidence it reproduced BEFORE the change
Re-measured from scratch, not taken from #761 on trust. Gridded LUH2 summed
inside Ethiopia's polygons against the
luh2-areaspin for area 238:1850 is 2.5x too large on present-day borders; the series converges by 1940 and
sits ~5% higher after 1952, which is Eritrea, correctly included. The grid path
reproduces the pin on Ethiopian cropland to within 0.5% at every year, so the
new producer is measuring the same thing, spatially.
Two things #761 did not know, both found by measurement, both load-bearing:
The pin's aggregation rule is centroid, winner-take-all.
luh2-areasandread_luh2_landuse()both key onspatialize-country-grid, onearea_codeper 0.5° cell with no share column. Weighting LUH2 by a raw fractional
polygon coverage instead loses 12-15% of the land of every island and heavily
coastal country (Philippines 5.92 → 5.12 Mha, Denmark 2.42 → 2.05, Greece
3.31 → 2.92), because LUH2's state fractions are fractions of the whole cell
and already discount open water — weighting them by the cell's land share
discounts it twice. Renormalising the shares to one per cell, which is what
build_cell_polity_fraction()already does, fixes it: global 1961 croplandgoes 1547.9 → 1327.8 Mha against a raw LUH2 total of 1332.4 and a pin total
of 1315.5.
The unfloored resolver does not hand you a territory. In 1961, 62
reporting buckets resolve to the residual
ROW-1850-2025, and dozens moreresolve out of span to a state that did not exist (bucket 185 Russia inside
the USSR, bucket 167 Czechia inside Czechoslovakia). Sharing each cell among
everything that claims it then gave every ROW cell to 62 buckets at 1/62
each — which is what shrank Belgium-Luxembourg to 0.026 Mha of cropland
in the first working version. Neither case is borders, so neither is
measured.
What I changed
R/historical_land_areas.R—build_historical_land_areas()emits theseam's own shape,
(year, area_code, Cropland, Pasture, agriland)in Mha, withthe hectares summed from gridded LUH2 inside the polygon of the polity that
area_coderesolves to in that year, resolved unfloored(
backcast_anchor = -Inf, the switch.resolve_hist_trade_polities()alreadyuses). Polygons come from
get_polity_geometries(), rasterised once per polityonto the LUH2 grid with
terra::extract(exact = TRUE).The series is not re-keyed.
fill_proxy_growth()groups byfill_cols,which includes
area_code; keying onpolity_codewould start a new group atevery period boundary and fragment the series silently, which is the
whep#589/#632 failure.
(year, area_code)stays the key and only the valueschange.
The boundary rule is
boundary_step, and both options are implemented andmeasured, because
fill_proxy_growth()reads nothing but year-on-year ratios,so a change of territory can only reach the back-cast as a ratio:
"relink"(default) re-measures the previous year inside the incomingpolygon before taking the ratio, so only within-territory growth is ever used.
"level_step"takes the ratio between the two polygons as measured, so theterritorial change passes through as a level step.
On Ethiopia at 1952, when Eritrea joins, the land ratio is +1.9% under
"relink"and +8.0% under"level_step". Expressed as a series,"relink"is a chain-link from the last year backwards, which has a second property worth
naming: both sides of every ratio are then measured with the same year's
polity set, so a constant overlap between two polygons cancels exactly.
Behind an argument, default unchanged.
build_primary_production(land_method = c("present_day", "historical_polity")).The default path is untouched —
.extend_historical()still reads theluh2-areaspin through the same code, now extracted into.land_wide_from_areas(). The chosen method is recorded insource, the columnthis series' provenance already lives in:
LUH2_polity_cropland/LUH2_polity_agriland. See "the open decision" for why not amethod_landcolumn.
data-raw/historical_land_areas.Rmaterialises the full 1850-1961 table.A pin is out of scope for me (I cannot upload to the board), so the script is
the reproducible generator and names the provenance a pin would need to record.
How I verified
Ethiopia, the easy case. Reproduced every checkpoint above, then the
handover: growth at 1952 is 1.0795 under
"level_step"and 1.0193 under"relink", against 1.0205 in 1951 and 1.0198 in 1953. The step lasts exactlyone year.
A fully dissolved federation. All four are reached without
federation_land = "successor_union", because each has a polygon of its own:Belgium-Luxembourg and the USSR over the full 1850-1961, Czechoslovakia and
Yugoslavia over 1917-1961 (before 1917 they resolve out of span — correctly:
they were Austria-Hungary). The USSR walks its own three-period chain —
F228-1800-1856,F228-1856-1905,F228-1945-1991— 107.7 Mha of croplandin 1850 rising to 237.9 in 1961.
A chain with gaps. Czechoslovakia and Yugoslavia are exactly that case,
and the boundary rule fires inside them: 1918, 1919 and 1944-1946 for
Czechoslovakia, 1918-1919 for Yugoslavia, each moving the level by >1%.
Global, not one country. At 1961 the historical series sits within 5% of
the present-day pin for 70 of 106 buckets, median ratio 0.980.
Four guards, each proven load-bearing by reintroducing the defect,
confirming the failure, and restoring:
The first of those is not hypothetical:
polity_areas[year == year]shadowsthe argument with the column and silently keeps every row. It made the polity
set never change, so every boundary step vanished — and the test caught it
before the real run did.
A defect the tests found in my own code. The chain-link accumulated from
the front, so a single unmeasurable year made the running total NA and wiped
out a bucket's whole 112-year series instead of just the part the break cuts
off. Four polities reachable from a reporting area carry no polygon, so it was
reachable. Fixed to accumulate from the end; guarded.
Full suite green (no failures, only the usual
WHEP_*/LPJmL skips);air format .,devtools::document(),lintrclean, pkgdown reference complete.Moves published values
Nothing moves by default.
land_method = "present_day"is the default andits code path is byte-for-byte the old one.
Under
land_method = "historical_polity", on back-cast crop tonnage weighted by1961 FAOSTAT production (6,288 Mt over 176 buckets):
"relink"net / abs"level_step"net / absThe two boundary rules differ from each other by 18.0% of back-cast tonnage
at 1850 and 0.07% by 1960. 56 of 105 buckets move by more than 1% at 1850
between them, the largest being Poland (+294%), Greece (+130%), Ethiopia
(+114%), Egypt (−84%) and Austria (−81%).
Coverage cost. 177 of 262 buckets can be measured on their own borders over
1850-1961. Weighted by tonnage that is small: 5 buckets carrying 1961 crop
tonnage lose their back-cast entirely, 9 Mt = 0.1% of the total — Syria (6.9
Mt, its 1961 polity is shared with another bucket), Eswatini, Equatorial Guinea,
New Caledonia, the Faroe Islands. A bucket that cannot be measured is simply not
back-cast, which is what already happens to an area with no LUH2 match.
What I deliberately did not do
.fill_yields()is not in the diff.polity_code. See above.data-raw/and says what a pinwould need to record (LUH2 vintage +
politiessnapshot). Without it,land_method = "historical_polity"recomputes: ~7 s/year of gridded LUH2 plusone pass of polygon rasterisation, so ~15 minutes for the full span.
.land_in_polygons()keys onpolity_codewith no year, and a polity code already names its own period, soit is classified
time_invariantand the ratchet goes 59 → 60 with the reasonwritten into the row — the "rise that is a fall" case the test's own comment
describes. Nothing was rewritten as
match().below.
The open decision
The two boundary rules answer different questions, and #761 asks for both.
Its evidence section wants 1850 Ethiopia to come out small — "the current
figure credits pre-expansion Ethiopia with cropland in regions annexed in the
1880s-90s". That is
"level_step". Its design section wants the 1952 Eritreastep not to propagate. That is
"relink". Because the consumer reads onlyratios, you cannot have both: either a territorial change scales the earlier
years or it does not, and the 1880s annexation and the 1952 federation are
structurally the same event.
Measured on Ethiopia,
"relink"puts 1850 cropland at 3.24 Mha — within 0.6% ofthe present-day pin's 3.22 — while
"level_step"puts it at 1.52 Mha. So onthat country the approved rule reproduces the framing the issue set out to
change. Globally it does not (it still moves 19.2% of 1850 tonnage), but the
tension is real and it is the owner's call. I shipped
"relink"as the defaultbecause that is the rule the issue approves, and
"level_step"selectable anddocumented beside it, with the numbers above.
Three smaller calls, all reversible:
source, not amethod_landcolumn. CLAUDE.md's multi-method rule asksfor
method_<quantity>; the requirement that the default reproduce today'soutput exactly rules out adding a column to it.
sourcealready carriesLUH2_cropland/LUH2_agriland, so it is this pipeline's method channel.Say the word and it becomes a real column.
conservative reading. The alternative — give a bucket its residual's land and
let its back-cast follow world growth — is defensible and would restore the
85 buckets, at the cost of saying something false about their borders.
build_cell_polity_fraction()andbecause the coastal double-discount is measurable. The centroid,
winner-take-all alternative is what the deployed pin uses and would reproduce
its levels more closely at the cost of dropping coastal cells entirely.
What this makes load-bearing upstream. Polygon quality now moves numbers
where it was cosmetic. Concretely observed here: present-day polity polygons
overlap on 17,495 of 68,704 cells, up to 3 deep, so any sum across buckets
double-counts by ~17% (individual buckets are unaffected, and the chain-link
makes a constant overlap cancel in the ratio — a changing one does not).
Five polities reachable from a reporting area have no polygon at all —
BLZ-1800-1886,CAN-1800-1866,CAN-1866-1870,CHL-1810-1884,PRY-1811-1870— so Canada's series starts in 1870, Chile's in 1884, Paraguay'sin 1870 and Belize's in 1886, and nothing earlier is back-cast for them. That is
whep-polities#155, #123 and #100 becoming real.
Refs #761. Part of the polity migration epic #458.
🤖 Generated with Claude Code