Skip to content

feat(horizon): fixed-range Gower normalization (+ explain site-name) - #400

Merged
johannesparty merged 4 commits into
mainfrom
feat/fixed-range-horizon-gower
Sep 30, 2026
Merged

johannesparty merged 4 commits into
mainfrom
feat/fixed-range-horizon-gower

Conversation

@johannesparty

Copy link
Copy Markdown
Contributor

What

Normalize each numeric horizon feature in the per-slice Gower distance by its fixed plausible span (GLOBAL_HORIZON_PROP_BOUNDS / global_prop_bounds) instead of the per-slice data spread. Supersedes the #377 "spread, floored at 10% of the span" hybrid — the fixed span is now the full denominator, not just a floor.

Effect: a candidate's horizon distance is now a stable, local function of (candidate, pit) — it no longer depends on which other candidates are in the pool (a distant outlier can't rescale everyone) and no longer varies by depth.

Why

The old data-spread denominator meant the same |Δ| scored very differently by depth and pool. Real example (Humic Alisols, pit rock-fragment 0% vs candidate 16%): Δ 0.727 at 0–20 cm (pool spread 22) but 0.258 at 20–30 cm — where the 62 came from a Leptosol 26 km away. Soil properties have known physical ranges, so normalizing to them is the principled fix.

Bulk test (NAS, paired before/after)

  • Global, isolated (v2.6.0 → v2.7.0): ~neutral — recall@1 +0.3 pt, recall@3 +0.6 pt, 187 improved / 178 worsened, recall unchanged. Not an accuracy driver; a correctness/robustness change.
  • US: cumulative vs old-main shows no regression and no crash increase.

Also in this PR

  • render_explain.py: horizon caption rewritten to describe fixed-range (the numbers already flow from gower's denom); optional site_name renders (HTML-escaped) as the report title, and the CLI wrapper passes its label.
  • test_gower_distance.py: floor tests replaced with the fixed-range invariant.
  • Snapshots regenerated (US + global rankings + explain trace) in the pinned GDAL runner; determinism verified (175 passed twice).
  • Version 2.6.0 → 2.7.0 (result-affecting → client cache flush).

Review notes

  • The bound values are now the full denominator (not a floor), so they matter more — kept as-is pending soil-scientist confirmation.
  • The site_name title change is logically independent of fixed-range (bundled here by request) — easy to split if preferred.

🤖 Generated with Claude Code

johannesparty and others added 4 commits September 29, 2026 13:44
…loor)

The per-slice horizon Gower distance normalized each numeric feature by the
per-slice data spread (max-min across the candidates + pit at that depth),
floored at 10% of a fixed plausible span (#377). That made a candidate's
distance depend on which OTHER candidates were in the pool and vary by depth:
e.g. a 16% vs 0% rock-fragment gap scored 0.727 at 0-20 cm (pool spread 22) but
0.258 at 20-30 cm (a Leptosol 26 km away pushed the spread to 62).

Soil properties have known physical spans, so normalize each numeric feature by
its fixed plausible range (GLOBAL_HORIZON_PROP_BOUNDS / global_prop_bounds)
instead. A candidate's distance is now a stable, local function of (candidate,
pit) — independent of the rest of the pool and constant across depths. The fixed
span is now the full denominator, not just a floor.

Change is isolated to gower_distances (denom = theoretical span when provided);
US + global already pass their bounds in. Bound VALUES are unchanged for now
(pending soil-scientist review). Result-affecting -> snapshots must be
regenerated and validated on the bulk-test suite; version 2.6.0 -> 2.7.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the fixed-range Gower change (065cb20):
- render_explain.py: the horizon caption described the old "range = spread of the
  candidates at this depth, floored at 10% of the plausible span, varies with
  depth" behavior. It now normalizes by the fixed plausible span at every depth,
  so the caption is rewritten to say so (and to note the distance no longer
  depends on the other candidates in the pool). Display-only; the numbers already
  flow from gower's denom, and the HTML snapshot test only smoke-checks render.
- test_gower_distance.py: the two range tests pinned the floor semantics. One
  keeps its assertions (comment fixed: denom = fixed span, not floored to 8); the
  other is replaced with an invariant test that the fixed-range distance is
  independent of the per-slice spread (the point of the change).

No ranking change beyond 065cb20; version stays 2.7.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
render_html now accepts an optional site_name; when provided it's shown
(HTML-escaped) as the report title above the "Soil ID explanation — REGION"
heading. Default None keeps existing output byte-identical, so snapshots and
callers that don't pass a name are unaffected. The CLI wrapper passes the
site/export label it already extracts, so local reports get the name too.

(Unrelated to the fixed-range change on this branch; bundled here per request.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
US + global test_soil_location rankings and the explain trace snapshot move under the fixed-range change (065cb20). Regenerated in the GDAL runner against the pinned soil-id-db image (CI-matching); determinism verified (175 passed twice). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@johannesparty
johannesparty merged commit bb51fa5 into main Sep 30, 2026
4 checks passed
@johannesparty
johannesparty deleted the feat/fixed-range-horizon-gower branch September 30, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants