The pipeline computes fourteen metrics. Twelve pass, one is borderline, one fails on a dimension nobody downstream uses. Somebody has to decide whether the release ships, and the fourteen numbers do not answer that on their own.
Part of Realism Metrics & Evaluation: that page covers which metrics detect which defects. This one is about assembling them into an artifact a reviewer can act on, and the two ways that assembly is usually done wrong.
Two failure modes dominate, and they are opposites.
The first is the scatter. Fourteen numbers in a log, each with its own tolerance, no grouping and no summary. A reviewer reads the first three, sees green, and approves; the failure sits at position eleven. Nothing is wrong with any individual number and the artifact is unusable, because it asks the reviewer to do the aggregation and gives them no basis for it.
The second is the average. Somebody collapses the fourteen into a single realism score, usually a weighted mean of normalised deviations. It is immediately readable and it is worse than the scatter, because it lets a catastrophic failure on one dimension be offset by excellent scores elsewhere. A release whose geometry is invalid and whose statistics are perfect scores well, and the one number that mattered has been averaged away.
The resolution is neither: group the metrics by dimension, take the worst verdict within each dimension rather than the mean, and require every dimension to pass independently.
The same fourteen metrics under three aggregation rules: only the third refuses to let a strong dimension pay for a failing one.
from dataclasses import dataclass
from enum import Enum
classDimension(Enum):
STRUCTURAL ="structural"# is the geometry well-formed at all
DISTRIBUTIONAL ="distributional"# do the marginals match
SPATIAL ="spatial"# does the spatial structure match
RELATIONAL ="relational"# do the attributes relate correctly
INTEGRITY ="integrity"# duplicates, nulls, referential rules@dataclass(frozen=True)classMetric:
name:str
dimension: Dimension
value:float
tolerance:float
higher_is_better:bool=False@propertydefverdict(self)->str:
ok =(self.value >= self.tolerance if self.higher_is_better
else self.value <= self.tolerance)
margin =abs(self.value - self.tolerance)/max(abs(self.tolerance),1e-9)ifnot ok:return"fail"return"borderline"if margin <0.15else"pass"
The one-dimension rule is what makes the aggregation meaningful. A metric assigned to two dimensions is counted twice and can rescue a dimension it does not really speak to; a metric assigned to none is a metric nobody looks at.
The borderline verdict earns its place: a metric passing by two per cent of its tolerance carries very different information from one passing by two hundred, and collapsing both to “pass” throws away the only early warning the scorecard produces.
defdimension_verdict(metrics:list[Metric], dimension: Dimension)->dict:
members =[m for m in metrics if m.dimension is dimension]ifnot members:return{"verdict":"not covered","metrics":[]}
verdicts =[m.verdict for m in members]
worst ="fail"if"fail"in verdicts else("borderline"if"borderline"in verdicts else"pass")return{"verdict": worst,"driver":next(m.name for m in members if m.verdict == worst),"metrics":[{"name": m.name,"value": m.value,"tolerance": m.tolerance,"verdict": m.verdict}for m in members],}defscorecard(metrics:list[Metric])->dict:
dims ={d.value: dimension_verdict(metrics, d)for d in Dimension}
overall ="fail"ifany(v["verdict"]in("fail","not covered")for v in dims.values())else("borderline"ifany(v["verdict"]=="borderline"for v in dims.values())else"pass")return{"overall": overall,"dimensions": dims}
Two decisions in that function are worth stating explicitly, because they are the ones that get argued.
“Not covered” is a failure, not a gap. A dimension with no metric assigned to it is a dimension nobody is checking, and a scorecard that reports it as neutral invites a release to ship with an entire class unexamined. Treating it as a failure means adding a dimension forces somebody to add a metric for it.
The worst verdict wins, and the driver is named. A reviewer reading “spatial: fail (driver: Moran’s I)” knows what to look at. One reading “spatial: 0.62” does not.
The scorecard as a reviewer sees it: five dimensions, one verdict each, and the driving metric named — with the detail available underneath rather than instead.
A borderline pass is the case the scorecard exists to surface, and a verdict alone does not tell a consumer what to do with it. The addition that makes it actionable is a small sensitivity table: what the verdict would be at nearby tolerances.
python
defsensitivity(metrics:list[Metric], factors=(0.8,1.0,1.25))->dict:"""How the overall verdict moves if every tolerance were tightened or loosened."""
out ={}for f in factors:
scaled =[Metric(m.name, m.dimension, m.value, m.tolerance * f, m.higher_is_better)for m in metrics]
out[f"×{f}"]= scorecard(scaled)["overall"]return out
A release that passes at every factor is robustly fine. One that passes at the declared tolerance and fails at eighty per cent of it is telling a consumer with tighter requirements exactly what they need to know, and telling the producer that the tolerance is doing more work than it should.
deftest_every_dimension_is_covered(metrics):
covered ={m.dimension for m in metrics}
missing =sorted(d.value for d in Dimension if d notin covered)assertnot missing,f"no metric assigned to: {missing}"deftest_no_metric_has_two_dimensions(metric_registry):
dupes =[name for name, dims in metric_registry.items()iflen(dims)>1]assertnot dupes,f"metrics in more than one dimension: {dupes}"deftest_release_passes_the_scorecard(metrics):
card = scorecard(metrics)
failing ={k: v["driver"]for k, v in card["dimensions"].items()if v["verdict"]in("fail","not covered")}assertnot failing, failing
deftest_scorecard_is_recorded_with_the_release(manifest):assert"scorecard"in manifest["validation"],("the verdict must ship with the artifact — a scorecard in a CI log is not evidence")
The last test is the one that turns the scorecard from a build step into an artifact. A verdict that lives only in a CI log disappears when the log rotates, and the release it justified stays in production for years afterwards.
A dimension nobody downstream uses. Tempting to drop, and worth resisting. A dimension no current consumer uses is one the next consumer will, and dropping it means the release history has a gap exactly where somebody later wants a comparison. Keep the metric, and if the tolerance is genuinely irrelevant, widen it deliberately and record why.
A waiver is a record with an expiry; a widened tolerance is indistinguishable from a design choice.
Metrics that are not independent. Several distributional metrics on the same attribute will move together, so a dimension containing five of them is not five times better covered than one containing one. Worth noticing when reading a green dimension: coverage is about the kinds of defect represented, not the count.
A failing metric that is known and accepted. It happens — a legacy tolerance, a known limitation. Record it as an explicit waiver with an expiry rather than by widening the tolerance, because a widened tolerance is indistinguishable from one that was always that wide.
The scorecard passing while a consumer complains. The dimension set is incomplete, and the complaint is the specification for the missing metric. That is the most useful outcome a complaint can have, and it is worth asking for the failing case rather than only the report.
The artifact has three readers and they want different things from it, which is worth designing
for rather than discovering.
The release reviewer wants a verdict and, when it is not a clean pass, one sentence naming
what to look at. They are reading it under time pressure and they will read the top of it only.
That argues for the overall verdict and the five dimension rows being the whole of the first
screen, with everything else below.
The consumer wants to know whether this release is suitable for what they do, which is a
different question from whether it met the producer’s tolerances. The sensitivity row is written
for them: a consumer whose requirements are tighter than the contract’s can see immediately that
the release would not satisfy them, without having to reason about individual metrics.
The auditor, later wants to know what was checked and what was accepted, possibly years
afterwards and possibly about a release that has since been withdrawn. They need the full detail
and they need it attached to the artifact rather than to a build log. That argues for the
scorecard being recorded in the manifest and retained with it, which costs a few kilobytes.
Designing for all three is mostly a matter of ordering rather than content: verdict first, then
sensitivity, then the per-metric detail, and all of it in the manifest. A scorecard that is a
rendered dashboard and nothing else serves the first reader and fails the other two.