Yield correction¶
An optional stage that brings modelled yields into line with an observational reference. It is deliberately rough: a plausibility adjustment, not formal parameter estimation or statistical downscaling.
Mode |
What it changes |
Where it acts |
When |
|---|---|---|---|
|
One multiplicative factor on yield |
Whole domain |
Per year |
|
One multiplicative factor per county |
County resolution |
Per year |
|
One crop growth parameter ( |
Whole domain, via the model |
One value across all years |
Three fixed design rules¶
1. No temporal aggregation. Every quantity carries a year. Simulated seasons are never averaged together: factors are fitted and applied per year, the reference is per year, and all output keeps the year dimension. The only cross-year quantity is the calibrated crop parameter itself, because a crop parameter is not a per-year property — and even then its objective runs over every (year, county) pair rather than a time-averaged field.
2. The model is aggregated to the reference’s native support. The reference
is a set of county polygons with one yield per county per year; it is never
rasterised. Simulation cells are assigned to the county containing their centre
and combined as an area-weighted mean, with weights proportional to
cos(latitude). Comparison, factors, residuals and the calibration objective
all live at county scale. The fine-scale model is reduced to the scale of the
observations, rather than county statistics being spread onto a grid whose
apparent resolution the data does not support.
3. Corrected output stays on the simulation grid. Only the correction is
derived at county scale; it is applied back to the full-resolution field, so the
written NetCDF is (year, y, x) at the model’s own resolution. A county-level
comparison table is written alongside as CSV.
Building the reference¶
build_reference joins any boundary file of reporting regions to any table
of yields per region per year. Nothing about the country, the administrative
level or the data source is assumed – counties, provinces and districts all
work the same way:
python -m geoaquacrop_simulate.build_reference \
--regions provinces.geojson --region-id NAME_LATN \
--table wheat_yields.csv --table-id region \
--year-col year --value-col yield_t_ha \
--out reference.geojson
or from Python:
import geoaquacrop as gac
gac.simulate.build_reference(
regions='provinces.geojson', region_id='NAME_LATN',
table='wheat_yields.csv', table_id='region',
year_col='year', value_col='yield_t_ha', out='reference.geojson')
It writes one GeoJSON feature per region, carrying region_id and a
yield_<year> field per year. Join keys are matched case-, accent- and
whitespace-insensitively, and regions that fail to match are reported rather
than silently dropped – an unmatched region is the usual reason for an
all-NaN reference.
Values are written in the units of the input table, so convert to the units of the summary column being corrected (by default dry-matter t/ha) before building. No rasterisation happens at any stage, so the reference carries exactly the information the statistical agency published: one number per region per year.
Scaling¶
Pure post-processing; no re-simulation.
Global fits one factor per year across the whole domain, so all modelled spatial pattern within a year is preserved and only the level changes. Because the factor is per year, it follows interannual variation in the reference.
Local fits one factor per year per county. Within a county the model’s relative pattern survives untouched; the county’s level is pinned to the reference. Residuals are then exactly zero by construction — the factor is constant within a county and the aggregation is a linear weighted mean, so the county mean of the corrected field equals the reference identically. A reported RMSE of zero is arithmetic, not skill.
Reusing a finished run:
'correction': {
'method': 'scale',
'reference_path': 'reference/high_plains_maize_reference.geojson',
'scale_mode': 'local',
'reuse_results': 'latest',
}
With reuse_results set, should_reuse()
returns True and the driver skips simulation entirely, scaling the saved
summary in seconds.
Calibration¶
Varies one physical crop parameter and lets the model respond.
|
Parameter |
Meaning |
Yield response |
|---|---|---|---|
|
|
Maximum canopy cover |
Smooth, monotonic, concave |
|
|
Normalised water productivity |
Monotonic, near-linear |
Canopy growth rate (CGC) is deliberately not offered: measured yield
sensitivity to it is essentially nil once canopy reaches CCx before season
end. Harvest index is excluded as the least physically defensible lever.
Calibration runs in two phases so the expensive full-grid simulation happens exactly once:
calibration_override()runs a golden-section search (~15–20 evaluations) on a subsample of cells, scoring the area-weighted sum of squared differences over every (year, county) pair.The fitted value is injected as
crop_param_overrideand the full grid is simulated once with it applied.run_correction()then reports on that run without re-searching.
Wiring it into the driver:
from geoaquacrop_simulate.correction import calibration_override
sim_config.config.update(
calibration_override(sim_config.config, ParallelProcessor,
validated_inputs, coords_df, logger))
Interpreting the results¶
compare_corrections.py writes per-year maps, county-polygon difference
maps, a domain-mean time series and a statistics table:
from geoaquacrop_simulate.compare_corrections import main
main([
"--reference", "reference/high_plains_maize_reference.geojson",
"--calibrated", "outputs/yield_calibrated.nc",
"--scaled", "outputs/yield_scaled.nc",
"--summary", "outputs/summary_results_20260807_120000.pkl",
"--out-dir", "outputs/comparison",
])
Questions worth more than “which RMSE is lowest”:
Does the run track interannual variation? Per-year scaling follows the reference by construction; calibration reproduces it only if the model’s own weather response does.
Does the calibrated run reproduce the spatial pattern, or only the level? Pattern agreement under a single global parameter is a genuine result.
Are residuals spatially structured? Structure implies a missing process that no single lever or global factor will fix.
Did the parameter hit a bound? A
CCxpressed against 0.30 or 0.99 means the lever has saturated.Do per-year biases have opposite signs? Then one parameter cannot satisfy all years — a structural limit, not a tuning failure.
Warning
Most published statistics blend irrigated and rainfed production into a
single regional yield. If the run is configured irrigation: 'rainfed',
modelled yields will sit below the reference in heavily irrigated regions
for reasons unrelated to CCx or WP. Any correction absorbs that gap,
flattering the fit statistics while attributing an irrigation signal to a
growth parameter. For a cleaner test, use a reference column that separates
the two where the source provides one, restrict the reference to
predominantly rainfed regions, or run irrigated to match.
Checking the machinery¶
compare_corrections inspects the results of a real run, tabulating
corrected against uncorrected yields per region per year so a correction that
has gone the wrong way is visible immediately:
import geoaquacrop as gac
gac.simulate.compare()