Transferring a Land-Use Regression Model to a New Metro Area
This guide, part of Land-Use Regression for Air Pollution Exposure, covers the decision a health department faces when it has no measurement campaign of its own and a well-documented model exists for a neighbouring city: can that model be used here, and how would you know?
Problem Context & Constraints
A land-use regression model is a set of coefficients on predictors like road length within 100 m and industrial area within 1 km. Nothing in the coefficients is intrinsically local — they are numbers — so transferring the model is technically trivial. What makes it fail is that each coefficient absorbs everything about the source city that was constant during its campaign: the vehicle fleet and its emission factors, how the road network classifies a “primary” road, what the land-cover taxonomy calls industrial, the background concentration, and the meteorological regime.
Move the model 300 km and several of those change at once. A “primary” road in one state’s network may carry 30,000 vehicles a day and in another’s 8,000, so the same predictor value means a different emission. A land-cover product that lumps warehousing with light industry in one vintage and separates them in another shifts the industrial predictor by a factor. The transferred model responds to none of this; it applies the same slope to a differently-scaled input.
Two constraints shape a defensible transfer. Some local measurement is required — not a full campaign, but enough sites to estimate whether the transferred predictions are biased and whether they track local variation at all. Ten to twenty sites, deliberately spread across the predictor range, is a workable minimum. And the predictors must be rebuilt with the source model’s definitions, not the local convention: same road classes, same buffer radii, same transformations, same land-cover mapping. A transfer that changes the predictor definitions is not a transfer, it is a new model wearing borrowed coefficients.
Prerequisites
python3.11numpy1.26.4,pandas2.2.2,scikit-learn1.5.0,geopandas0.14.4- The source model’s full specification: terms, coefficients, buffer radii, class definitions, transformations and the measurement period
- Local validation measurements at 10–20 sites covering the predictor range, not just the geography
- Local road, land-cover and population layers rebuilt to the source model’s definitions
Step-by-Step Solution
Score the transfer, then choose between using it as-is, recalibrating it, or rejecting it. The decision rule below is deliberately conservative: recalibration is allowed only when the transferred predictions still track local variation.
# Evaluate and recalibrate a transferred land-use regression model.
# Pinned: numpy==1.26.4, pandas==2.2.2, scikit-learn==1.5.0
import logging
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("lur.transfer")
MIN_TRACKING_R = 0.5 # below this the transfer carries no local information
def apply_source_model(local_predictors: pd.DataFrame, spec: dict) -> np.ndarray:
"""Evaluate the source model. spec = {'intercept': float, 'terms': {name: coef}}."""
missing = [t for t in spec["terms"] if t not in local_predictors.columns]
if missing:
raise KeyError(f"predictors missing locally: {missing} — rebuild with source "
"definitions before transferring")
z = np.full(len(local_predictors), float(spec["intercept"]))
for name, coef in spec["terms"].items():
z += float(coef) * local_predictors[name].to_numpy(float)
return z
def evaluate_transfer(observed: np.ndarray, predicted: np.ndarray) -> dict:
"""Bias, scatter and tracking of a transferred model against local measurements."""
resid = observed - predicted
r = float(np.corrcoef(observed, predicted)[0, 1])
# Regress observed on predicted: intercept is the offset, slope is the scaling.
cal = LinearRegression().fit(predicted.reshape(-1, 1), observed)
out = {"n": int(len(observed)),
"bias": float(resid.mean()),
"rmse": float(np.sqrt((resid ** 2).mean())),
"pearson_r": r,
"cal_intercept": float(cal.intercept_),
"cal_slope": float(cal.coef_[0])}
log.info("transfer n=%d bias=%+.3f rmse=%.3f r=%.3f slope=%.3f intercept=%+.3f",
out["n"], out["bias"], out["rmse"], r, out["cal_slope"], out["cal_intercept"])
return out
def transfer_decision(ev: dict) -> str:
"""Use as-is, recalibrate, or reject — with the reason recorded."""
if ev["pearson_r"] < MIN_TRACKING_R:
log.warning("REJECT: transferred predictions do not track local variation "
"(r=%.3f) — recalibration would only rescale noise", ev["pearson_r"])
return "reject"
if abs(ev["bias"]) < 0.1 * abs(np.nan_to_num(ev["cal_intercept"]) + 1e-9) \
and 0.9 <= ev["cal_slope"] <= 1.1:
log.info("USE AS-IS: slope near one and bias negligible")
return "as_is"
log.info("RECALIBRATE: apply observed = %.3f + %.3f x predicted",
ev["cal_intercept"], ev["cal_slope"])
return "recalibrate"
def recalibrate(predicted: np.ndarray, ev: dict) -> np.ndarray:
"""Two-parameter correction. Never refit the source coefficients on 15 sites."""
return ev["cal_intercept"] + ev["cal_slope"] * predicted
The design point in that code is that recalibration touches two parameters, not the model. With fifteen local sites there is no possibility of refitting a five-term model — that is a new campaign with a fifth of the sites it needs. Correcting the intercept and slope of the predictions, on the other hand, is estimating two parameters from fifteen observations, which is a reasonable ask, and it repairs the two failures that transfers most often exhibit: a background offset and a mismatched gradient.
The rejection branch exists because recalibration is seductive. A linear correction can always be fitted, and it will always improve the apparent fit at the validation sites. If the transferred predictions do not correlate with local measurements, that correction is rescaling noise, and applying it produces a surface that looks calibrated and carries no information. The correlation threshold is the guard, and it should be declared before the validation data are seen.
Validation & Edge Cases
Validation sites used twice. Fitting the calibration on all fifteen sites and then reporting the fit at those same sites is the flat-validation error again, in miniature. Split them, or use leave-one-out for the calibration statistics.
Predictor definitions quietly localised. The most common cause of the no-relationship panel is a road classification that was mapped to local conventions rather than the source model’s. Rebuild with the source definitions and re-score before concluding the transfer has failed.
A predictor absent locally. A distance-to-port term in a source model cannot be evaluated in an inland city. Setting it to zero applies the coefficient to a value outside the sampled range. The term must be dropped and the model refitted at source, or the transfer abandoned.
A different measurement period. A model fitted in 2014 transferred to 2024 measurements carries a decade of fleet change, which appears mostly as a slope below one. Recalibration absorbs it, but the corrected model is then valid for the new period only, and must be labelled accordingly.
A city with a genuinely different source mix. Transfer assumes the same sources drive the contrast in both places. A model fitted where traffic dominates, moved to a city where residential wood burning drives winter concentrations, will track poorly in winter and acceptably in summer, and a single annual validation hides that entirely. Validate by season where the source mix is seasonal, and report the seasonal statistics separately rather than pooling them into one reassuring annual number.
Recalibration parameters reused across years. The intercept and slope correction is estimated against measurements from one period. Carrying it forward unchanged across a five-year cohort assumes the background and the gradient both stayed put. Re-estimate the two parameters whenever new local measurements exist, keep the history, and treat a drifting slope as evidence that the transfer is decaying rather than as noise to average over.
Validation sites all in one part of the predictor range. Fifteen background sites will show an excellent correlation and estimate the near-road behaviour not at all. Distribute the local sites by predictor value, not by map coverage.
INFO lur.transfer transfer n=16 bias=+3.104 rmse=5.221 r=0.741 slope=0.612 intercept=+7.885
INFO lur.transfer RECALIBRATE: apply observed = 7.885 + 0.612 x predicted
Recalibration buys back accuracy only where the transferred predictions already track local variation, which shows clearly in the before-and-after error at the held-out sites:
Compliance Notes
Document the transfer as a distinct artifact from both the source model and a local model. The record needs the source model citation and full specification, the local predictor build confirming the source definitions were used, the local validation site list with how they were chosen across the predictor range, the evaluation statistics, the decision taken with the threshold that drove it, and the recalibration parameters where one was applied.
Where the transferred model feeds a published exposure estimate, the uncertainty reported must be the local validation error, not the source model’s original cross-validated error. Those numbers routinely differ by a factor of two, and quoting the source figure attributes to your city a validation that was never performed there.
Related Topics
- Land-Use Regression for Air Pollution Exposure — the parent guide and what the coefficients actually encode.
- Avoiding Overfitting in Land-Use Regression Variable Selection — why an unstable source model transfers worst.
- Handling Non-Stationary Variograms Across a State — the same “one model, two places” problem in the geostatistical family.