TestingGENERALDOMAIN-SPECIFICCONTESTED

Robustness Testing

Missing features, extreme values, noise, rare segments and corrupted inputs — the test is not whether the model stays accurate under damage but whether it does what the design says it should: degrade gracefully, refuse, or fall back.

Target & dataWhat to measureWhat must stay true

The problem, the obvious approach, and why it breaks

Every lesson starts where the work starts: someone has a problem, and the first model that comes to mind looks fine offline.

The question

When the inputs are damaged — missing, extreme, noisy, corrupted, from a segment the model barely saw — what should the model do, what does it actually do, and how do you test the gap?

The problem

A logistics company's delivery-time model went badly wrong during a regional outage of their GPS provider. For six hours every shipment in one region arrived at the model with a null location and a stale last-known position, and the model confidently predicted two-hour deliveries for parcels three days away. Dispatch trusted it.

The obvious approach

The model handles missing values — the imputation is in the pipeline — and the validation metric is good across the board. Robustness is what the imputation is for.

Why it breaks

The imputation was designed for occasional, brief GPS gaps. Under a six-hour outage location_age_minutes is a value the model never saw, and the tree ensemble treats it as its largest training value, where a small age meant the parcel was moving normally. The model has no concept of "stale beyond experience" and extrapolates confidently.

How it breaks — usually after the offline metric looked fine
  • The imputation was designed for occasional, brief GPS gaps. Under a six-hour outage location_age_minutes is a value the model never saw, and the tree ensemble treats it as its largest training value, where a small age meant the parcel was moving normally. The model has no concept of "stale beyond experience" and extrapolates confidently.
  • The validation metric is computed on the training distribution, where the outage does not exist. It is good "across the board" on a board that does not contain the failure.
  • There was no designed behaviour for the case. The model produced a number, the number had the same shape as every other number, and dispatch acted on it. The failure is not that the prediction was wrong but that nothing said it should not be trusted (Human Oversight).
  • A month later a truck with a corrupted load field — a sensor sending negative weight — received similarly confident predictions, for the same reason: no test asked what the model does outside the range it saw.
ProblemTargetDataRepresentationSplitModelTrainingEvaluationValidationDeploymentInferenceMonitoringDriftRetraining

What is being predicted, and from what data

This domain leads with these two. A target nobody defined precisely is a label nobody can trust, and a dataset nobody can describe is a model nobody can debug.

Target
  • The model predicts delivery time in hours from route, load and location features (Regression). This lesson's target is the model's behaviour under damaged inputs, specified per kind of damage: what it *should* do, tested against what it does.
  • "Should" is a design decision, not a model property: for a null location the design might say "fall back to the route-average estimate and flag low confidence", and the test checks that the deployed function does exactly that.
Data
  • Features per shipment: origin, destination, current location, distance remaining, load, weather, hour of day. Training data is dense with fresh locations because the GPS provider is reliable most of the time.
  • Null location appears in a fraction of a percent of training rows and was imputed with the last-known position and a location_age_minutes feature that is almost always small.
  • The robustness fixtures: a base set of shipments with each feature independently nulled, set to its training minimum and maximum and beyond, perturbed with noise, and drawn from the rarest segments — remote regions, oversize loads.

How it actually works

Precisely enough to predict its behaviour — not a framework API.

  • Robustness testing has two halves, and the first is a design step. For each kind of damage — a missing feature, an extreme or out-of-range value, noise, a corrupted or impossible value, an input from a rare segment — the design states what the deployed function should do: degrade (predict, with a wider interval or a lowered confidence flag), refuse (return no prediction, with a reason), or fall back (return a simpler estimate, marked as such). The second half tests the deployed function against that specification.
  • Models behave differently under damage by family. Tree ensembles clamp: an out-of-range value falls into the terminal split for the extreme, and the model predicts as if the value were the training maximum. Linear models extrapolate: an out-of-range value moves the output linearly without bound. Networks do whatever the geometry of their learned function does in a region with no training data, which is usually confident and arbitrary. None of them knows it is outside its experience unless something outside the model checks (Anomaly Detection is one way to build that check).
  • So the robustness mechanism lives largely *around* the model: an input validator that knows the training range per feature and the null policy, a confidence or out-of-distribution signal that flags inputs far from training, and a decision layer that turns the flag into degrade, refuse or fall back. Testing robustness tests that layer as much as the weights.
  • Rare segments are the case where the model is in range and still unreliable: the remote region has a few dozen training rows, the model's error there is several times the average, and the aggregate metric hides it. Slice evaluation is the test (Evaluation Slices); the design decision is whether to serve the segment at all, and with what confidence.

Decide what it should do before testing what it does

A robustness test without a specification measures a metric under damage, which tells you the model gets worse — it always does — and not whether that is acceptable. The specification comes first: for each kind of damage, what the deployed function must do and what signal it must send. Only then is there something to assert.

The three answers are degrade, refuse and fall back, and they belong to different kinds of damage in different products. The choice is a conversation with whoever acts on the prediction, because a refusal is only better than a wrong number if the consumer has something to do instead.

Per damage kind, the designed behaviour

What should the deployed function do with this input?

Degrade

when The model can still say something useful but with less confidence — a slightly stale location, a mildly out-of-range load. Predict, widen the interval, set the flag.

cost Needs a confidence signal the consumer honours; a flag that is ignored is a confident prediction.

Refuse

when The input is outside anything the model can reason about — null location during an outage, an impossible negative weight. Return no prediction and a reason.

cost The consumer must have a manual or default path; a refusal with nothing behind it is an outage.

Fall back

when A simpler, robust estimate exists — the route average, the population rate — and a marked worse answer beats no answer. Return it, marked.

cost The fallback needs its own tests and its own monitoring; an unmarked fallback is indistinguishable from a prediction (Serving Fallbacks).

What each family does with an input it never saw

A tree ensemble sends an out-of-range value down the branch for the training extreme and predicts as if the value were that extreme: a location_age_minutes of six hours is treated as the largest age in training, which was twenty minutes and meant "moving normally". A linear model extrapolates without bound. A network follows its learned surface into a region with no data, and the surface there is smooth, confident and meaningless.

None of them raises a flag. The knowledge that an input is outside experience lives in the training data's ranges, and the model does not carry it; a validator around the model must. The failure table below is the outage, one row per damaged feature, with what the model did and what the specification says it should have done.

The GPS outage, feature by feature
TriggerSymptomCauseResponse
current_location null for six hoursImputed with the last-known position; the model predicts as if the parcel were there now.Imputation designed for brief gaps; no range check on location_age_minutes.Specification: refuse when age exceeds the training maximum by a margin; fall back to route average, marked.
location_age_minutes far beyond training rangeTree ensemble clamps to the training maximum, where a small age meant normal movement; confident two-hour estimates.Trees cannot represent "beyond experience"; the validator did not exist.Per-feature training range recorded with the artifact; out-of-range triggers the degrade/refuse path.
distance_remaining unchanged for hoursConsistent with a parcel at the depot; no anomaly.Each feature plausible alone; the joint pattern — stale age with unchanged distance — was never a fixture.Joint damage cases from the incident added to the fixtures; a staleness rule across features.
Negative load_kg from a faulty sensor (a month later)Confident predictions for an impossible input.No impossibility check; the model extrapolates below the training minimum.Validator rejects physically impossible values; refusal with a reason.

Rare segments: in range and still unreliable

Damage is not only out-of-range inputs. The remote region has a few dozen training rows; the model's error there is several times the average; every feature is within range. The aggregate metric barely notices. Slice evaluation finds it, and the design decision is the same three-way choice: serve with a flag, refuse, or fall back to the regional average.

What must remain true after deployment is that the deployed function keeps to the specification for every kind of damage the design anticipated, that the validator's idea of "in range" tracks the current artifact, and that the consumer does something different with a flagged prediction. Each of those decays without a commit.

must stay trueDamage produces the designed behaviour

For every anticipated kind of damaged or rare input, the deployed function degrades, refuses or falls back as specified, signals which, and the consumer acts on the signal.

holds when The robustness fixtures pass through the endpoint on every deploy; the validator's ranges are read from the artifact; the flag is rendered and the action rate on flagged predictions is nonzero; joint cases from past incidents are in the fixtures.

breaks when A retrain widens the data and the validator is not updated; a consumer redesign drops the flag; a new upstream source introduces a damage pattern not in the fixtures; the fallback estimate is never refreshed.

how you would know The fixture suite as a deploy gate; production counts of validator refusals, fallbacks and flags, with the downstream action rate; slice error on rare segments tracked per retrain (Evaluation Slices).

respond Add the new damage pattern to the fixtures and the specification; fix the validator or the consumer; do not retrain to make the model "handle" an input it should refuse.

A validator that knows the training range
1def validate(x, ranges, impossible):
2 # ranges: per-feature (min, max, null_ok) recorded with the artifact
3 flags = []
4 for f, (lo, hi, null_ok) in ranges.items():
5 v = x.get(f)
6 if v is None:
7 if not null_ok: return Decision.REFUSE, f"{f} missing"
8 flags.append(f"{f}:null")
9 elif impossible.get(f, lambda _: False)(v):
10 return Decision.REFUSE, f"{f}={v} impossible"
11 elif v < lo or v > hi:
12 margin = (hi - lo) * 0.1
13 if v < lo - margin or v > hi + margin:
14 return Decision.FALLBACK, f"{f}={v} far outside training range"
15 flags.append(f"{f}:edge")
16 return (Decision.DEGRADE if flags else Decision.PREDICT), flags
17
18# The model never sees a refused or fallback input. The consumer sees
19# the decision and the reason on every response.

The ranges are read from the artifact, not from a config, so they change when the model does. The margin is a design choice — a delivery model can tolerate a slightly stale location; a triage model may not — and the test asserts the decision, never the prediction.

How to build it

Most important first.

  • Write the damage specification first: for each feature and each damage kind, the required behaviour — degrade, refuse, fall back — and the signal that must accompany it. This is a product conversation with whoever acts on the prediction.
  • Put an input validator in the serving path with the training range, null policy and cardinality per feature, recorded with the artifact, so "outside training experience" is a computed fact rather than a guess (Preprocessing Lives in the Artifact).
  • Emit a confidence or out-of-distribution flag on every prediction and make the consumer honour it; a prediction without a flag is a prediction that cannot degrade.
  • Build the robustness fixtures — per-feature nulls, extremes, noise, corruption, rare segments — and run them through the deployed function on every candidate and every deploy, asserting the designed behaviour and not a metric.
  • Treat adversarial inputs as a separate concern with a separate test set: robustness to accident is a distribution question; robustness to an attacker is a security question (Adversarial Inputs).

What to measure

Which number actually maps to the decision — and which numbers look relevant and are not.

  • For each damage case, whether the deployed function did what the specification says — a pass/fail per case, not an accuracy.
  • Error by slice on the rare segments, against the aggregate, with the sample size; the ratio says where the model should not be trusted alone.
  • The out-of-distribution flag rate in production and the downstream action rate on flagged predictions — the number that says whether the degrade path is actually being honoured.
  • The validation metric under clean inputs is not a robustness measure. It is computed on the distribution in which the damage does not occur.

What must stay true after deployment

The field this whole domain exists for. A model is a set of assumptions with weights attached; these are the ones a monitor or a test should be checking.

Assumptions
  • For every kind of input damage the design anticipates, the deployed function behaves as the specification says — degrades, refuses or falls back — and signals which it did.
  • The input validator's notion of "inside training experience" — ranges, null policy, cardinality per feature — matches the artifact currently deployed and is updated with it.
  • The consumer of the prediction honours the confidence or out-of-distribution signal; a degraded prediction is acted on differently from a confident one.
How to verify — offline, online, and over time
  • Offline: the robustness fixtures against every candidate through the full deployed function, asserting the specified behaviour per case, including joint damage cases modelled on past incidents.
  • Before deploy: the same fixtures through the endpoint, plus a check that the consumer renders and acts on the flag.
  • Over time: the flag rate and the flagged-prediction action rate in production; an incident review that adds each new damage pattern to the fixtures.

What can go wrong

Failure modes in production
  • The specification says "refuse on null location" and the serving path implements it — and the fallback route-average estimate is never tested, so during the next outage dispatch gets no predictions at all and reverts to guessing.
  • The input validator's training range is recorded once and never updated; after a retrain on wider data the validator refuses inputs the new model handles well.
  • The out-of-distribution flag is emitted and the consumer's UI does not render it, so a flagged prediction looks identical to a confident one.
  • Noise tests perturb features independently and the model is robust to each; the outage perturbs several together — null location *and* stale age *and* an unchanged distance-remaining — and the joint case was never constructed.
What the recommended approach costs
  • A refusal or a fallback is a worse answer than a correct prediction, and the specification will sometimes refuse an input the model would have handled; the choice is between occasional lost predictions and occasional confident nonsense.
  • An input validator and a confidence signal are serving-path components with their own tests, ranges to keep in sync with the artifact, and a consumer that must be changed to honour them.
  • Joint damage cases are combinatorial; the fixtures can cover the ones incidents have shown and the ones the design anticipates, not all of them.
Misreads
  • "The pipeline imputes missing values, so missing values are handled." Imputation replaces a null with a number the model can consume. It does not tell the model that the number is a guess, and under an outage the imputed value is a value the model never learned to distrust.
  • "The model is robust — the metric barely moved when we added noise." Robustness to independent noise on a validation set is one narrow case. A model can be robust to noise and confidently wrong on one out-of-range feature, and the outage was the latter.
  • "Robustness testing means adversarial examples." Adversarial inputs are crafted by an attacker to cross a decision boundary, and they are a security concern with their own tests. Robustness to accident — outages, sensors, rare segments — is the distribution question, and it is the one that failed here.

Where this applies

ML advice is stated as universal far more often than it is. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALThat a model has no notion of its own training range and will produce a shaped output for any input holds for every family; what differs is the failure geometry — trees clamp, linear models extrapolate, networks do something arbitrary — and the validator around the model is needed in every case.
  • DOMAIN-SPECIFICThe right response to damage is a domain decision: a delivery estimate can degrade to a route average with a flag, a medical triage score should refuse rather than guess, and an ad-ranking model can fall back to popularity with no flag at all; the test asserts the chosen behaviour, and the choice is not the model team's alone.
  • CONTESTEDWhether to build an explicit out-of-distribution detector or to rely on the model's own uncertainty estimate is disputed. The strongest case for the model's own estimate — ensembles, predictive intervals, calibrated probabilities — is that it is trained with the model and needs no separate range bookkeeping; the strongest case against is that every family's uncertainty is itself unreliable outside the training distribution, which is exactly where robustness is tested, and a simple per-feature range check is honest about what it does not know.

Where the depth lives

This domain teaches the model and hands the rest off by name.

Domains that do not exist yet
  • Testing & Reliability Engineering — robustness fixtures are fault-injection tests for a model, and chaos-style exercises against the serving path with a failed upstream are the reliability practice this lesson assumes.