Complete an admin-shares table to the full year range of its extent and fill the pre-seam years by modulating each unit's seam-year share with its own land extent, the plan's decision-7 back-cast:
$$\tilde{s}_u(t) = s_u(t_0) \frac{E_u(t)}{E_u(t_0)}, \qquad s_u(t) = \frac{\tilde{s}_u(t)}{\sum_v \tilde{s}_v(t)}$$
The first step is fill_proxy_growth() with the extent as proxy, the
same machinery .fill_pre_faostat() (R/build_production.R:3008) uses
to back-cast the national totals these shares split, so the two
compose. The second is a per-year renormalisation over the t0 unit
set, the residual pseudo-unit included when the shares carry one.
Nothing here is a fallback: a year the rules cannot resolve is refused
with a diagnostic and keeps share = NA, never a carried value.
Two things the plan's seam section names are deliberately not here.
Pre-seam irrigation needs no separate treatment: the unit irrigated
target is built from the engine's own annual ir_potential
(R/spatialize.R:420), whose LUH2 irrigated_ha varies every year,
pre-seam years included, so it moves with the same extent that
modulates the area share. And the within-unit weight regime per
(unit, item, year) is the allocation's to record, not this
function's: treatment here says how a share was obtained, while the
regime says which weight then spread it inside the unit.
Usage
backcast_admin_shares(
shares,
extent,
seam = NULL,
binding_indicator = "area_harvested",
settings = list()
)Arguments
An admin-shares table (see
admin_shares_schema()). Minimum columns:area_code,level,item_prod_code,indicator_used,level_polity_code,year,share,treatment_year. Every other contract column present is carried onto the produced rows from their anchor row.- extent
A per-unit extent table from
aggregate_unit_extent(). Its year range is the range the shares are completed to.- seam
Optional tibble overriding
t0per series:area_code,item_prod_code,t0, optionallylevel. An override is the temporal hold-out lever: every year below the givent0becomes a back-cast year even where an observation exists, and the superseded observations are counted (seam_supersedes_observed).- binding_indicator
The indicator that binds,
"area_harvested"by the plan's decision 8.t0is the series' first observed year carrying it; a series with none is refused (no_binding_anchor).- settings
Named list of tuning knobs, each defaulting as below. An unknown key aborts, and what the call ran with comes back in the returned
settingstibble:max_gap(Inf): passed tofill_proxy_growth(), its own default.max_gap_linear(0): passed tofill_proxy_growth(); a placeholder pending T31(e), see the interior-gaps section.zero_policy("hold_outside"): how case (a) is handled. Only"hold_outside"is signed off (decision 9); the key exists so a later decision can add a value without changing call sites.repair(FALSE): on a flagged extent jump,TRUEapplies the unit-keyed analogue of.fix_luh2_crop_collapse()and refuses only what stays flagged;FALSE, pending T31, refuses the affected series outright. Either way the choice is insettingsand the outcome indiagnostics.ratio_bounds(c(0.55, 1.6)): plausible band forcheck_extent_jumps().collapse_ratio(0.02): fraction of the adjacent-year mean below which a year counts as an isolated collapse, as in.fix_luh2_crop_collapse()(R/build_production.R:629-630).min_neighbour_ha(100): both neighbours must exceed this for a collapse to be repaired – that function'smin_neighbor_mha = 0.001Mha expressed in hectares.tolerance(1e-9): absolute tolerance for the share and extent arithmetic.
Value
A list of four tibbles:
shares: the input rows plus the completed year set for thet0unit sets, with two added columns.treatmentis"observed"for a row that arrived inshares,"backcast_t0_geometry"for a row this function produced,"luh2_clamped"wheret0lies beyond the extent's last year soE_u(t0)is the clamped last slice, andNAfor a completed row nothing filled, whose reason is indiagnostics.gap_rulerecords the interior-gap setting. A produced row carries its anchor's provenance columns,value = NA(no reported value exists) andtreatment_year = NA(reserved for the interior-gap rule), so produced rows deliberately sit outside the closedadmin_shares_schema(); the"observed"subset on the contract columns still conforms.diagnostics: one row per event, with the series and unit keys,yearwhere the event is year-specific,diagnosticfrom a closed vocabulary and free-textdetail.counters: every vocabulary member with its count, zeros included.settings: the one-row record of what this call was run with.
Territory basis
Shares and E_u are defined on the t0 unit set with t0 geometry
held fixed, the read_luh2_landuse() snapshot convention and the
analogue of mapping_status == "backcast_anchor". Every row produced
that way carries treatment = "backcast_t0_geometry", so a unit whose
containment edge starts after a back-cast year still gets a row at that
year, on its t0 cells.
Translating those rows onto the partition valid at t through the
containment-edge lineage – an earlier unit that later split taking the
sum of its t0-descendants' modulated shares – happens before the
rows enter the engine and is not implemented here. It is a
follow-up, because the relation it needs is succession (which unit
became which), while polity_containment carries containment
(member_code, container_code, start_year, end_year, basis)
only: nothing in the delivered data says a unit starting in one year
descends from one that ended the year before. Until it lands, a
container whose partition changed inside the back-cast window is
back-cast on its t0 partition, which treatment states on every row.
Zero cases
Post-processing applied after the fill, which itself leaves such
positions NA (.fg_fill_backward_vec() keeps a fill only where the
result is finite and positive). The plan's four cases, with precedence
(a) over (b):
(a)
E_u(t0) == 0withs_u(t0) > 0: holds_u(t0)outside the renormalisation, siblings renormalised to1 - s_u(t0). Holding it inside would anti-modulate the unit. Counterzero_extent_t0_held.(b)
E_u(t) == 0at a back-cast year of a unit not held by (a): share0there, its mass going to the siblings through the renormalisation. Counterzero_extent_year_zeroed.(c) the renormalisable sum is zero while there is mass to distribute: the year is refused for that series and all its units left
NA. Counterzero_sum_year_refused.(d)
E_u(t0) == 0with no positives_u(t0): nothing to hold and no mass to move, so the unit contributes0. Counterzero_extent_t0_no_share.
Case (b) counts every zero-extent back-cast year, including the ones inside a year (c) then refuses: both statements are true of such a year, and (c) is what decides the output.
A zero year also breaks the growth chain: fill_proxy_growth()
telescopes a product of consecutive growth factors, so it cannot reach
the years beyond the break and leaves them NA even though the ratio
above is perfectly well defined there. Those positions are completed
from that ratio directly – the same expression, not a second method –
and every one is counted (direct_ratio_repair). Where the chain is
intact the two agree to machine precision, which the tests assert.
Interior gaps
The fill window ends at t0, so no gap between two observed years is
ever filled: such rows are completed, left NA and counted
(interior_gap_refused, trailing_gap_refused). That is the
placeholder default until T31(e) decides the rule; it is recorded on
every row in gap_rule and in settings. max_gap and
max_gap_linear are passed straight to fill_proxy_growth() so that
decision needs no API change. Today max_gap bounds the back-cast run
itself – a longer run is left unfilled and counted
(max_gap_exceeded), because fill_proxy_growth()'s gap test governs
the leading run as well as the interior ones – while max_gap_linear
cannot bite, there being no interior run inside the window.
Examples
shares <- tibble::tibble(
area_code = 900L,
level = 1L,
item_prod_code = 15L,
indicator_used = "area_harvested",
level_polity_code = c("A1", "A2"),
year = 1902L,
share = c(0.6, 0.4),
treatment_year = "observed"
)
extent <- tibble::tibble(
area_code = 900L,
level_polity_code = rep(c("A1", "A2"), each = 3),
level = 1L,
year = rep(1900:1902, times = 2),
extent_ha = c(800, 900, 1000, 600, 550, 500)
)
out <- backcast_admin_shares(shares, extent)
out$shares[, c("level_polity_code", "year", "share", "treatment")]
#> # A tibble: 6 × 4
#> level_polity_code year share treatment
#> <chr> <int> <dbl> <chr>
#> 1 A1 1900 0.5 backcast_t0_geometry
#> 2 A1 1901 0.551 backcast_t0_geometry
#> 3 A1 1902 0.6 observed
#> 4 A2 1900 0.5 backcast_t0_geometry
#> 5 A2 1901 0.449 backcast_t0_geometry
#> 6 A2 1902 0.4 observed
