Skip to content

Validate modelled habitat against fish observations (#283) - #289

Merged
NewGraphEnvironment merged 10 commits into
mainfrom
283-validate-modelled-habitat-against-fish
Sep 26, 2026
Merged

NewGraphEnvironment merged 10 commits into
mainfrom
283-validate-modelled-habitat-against-fish

Conversation

@NewGraphEnvironment

@NewGraphEnvironment NewGraphEnvironment commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #283. Relates to NewGraphEnvironment/sred#24.

This is the scorer for #284 step 5. #284 stays open until default_tuned is modelled and scored.

What

lnk_habitat_validate() scores a persisted run against fish observations rather than against bcfishpass. For each WSG × species × stage (any / spawn / rear) it reports:

  • Capture: the share of observation locations on accessible, spawning, stream-rearing and any-rearing segments.
  • Cost: accessible_km, spawning_km and rearing_km, from lnk_rollup_wsg().
  • Miss reasons: the bundle's own fresh::frs_habitat_predicates() are re-evaluated with gradient and width relaxed, so fails_gradient means the gradient maximum is binding.
  • Absences (optional), as a false-positive check.

Any observation source. observations takes bcfishobs (the default), another table, or a data frame. Only species_code, watershed_group_code, blue_line_key and downstream_route_measure are required, so crew data, eDNA or a held-out subset can be scored. A source can give its own is_spawn / is_rear. The bcfishobs filters (match_types, source_exclude) default on. When the source lacks the column a filter needs, the function errors instead of skipping it. With the defaults, the bcfishobs path reproduces the committed baseline exactly (120 of 120 summary rows).

data-raw/habitat_validate.R --bundles=<config>:<schema>,... runs one or two bundles, at buffers 0 and 100 m. It writes aggregate CSVs, per-bundle and shared-WSG totals, and a diff.

The method, the caveats and the step-5 command are in research/habitat_validation.md.

Findings that changed the design

  • fresh#218 is a join artifact. The persist tables are unique on (id_segment, watershed_group_code). Naver Creek gives 187 rows on the full key and 4,301 on bare id_segment. The issue body is corrected.
  • schema is required. default declares pipeline.schema: fresh, which the bcfishpass bundle builds. The function stops when <schema>.log records a WSG under a different config.
  • CH spawn-staged capture is the overlay. In the 51 shared WSGs, 237 of 245 spawn-staged CH locations sit in user_habitat_classification spawning reaches, which default forces to habitat. share_*_outside_uhc reports the part the observations did not decide.
  • BT rearing 0.1349 would score in-sample. It is the P95 of these same locations. The observations argument accepts a held-out table.

Baseline (read-only; default:fresh_default vs bcfishpass:fresh, 51 shared WSGs, buffer 0)

n bcfishpass default
BT rear-staged, any rearing 1,047 63.6 % / 76,872 km 66.5 % / 75,280 km
CH any-stage, stream rearing 1,734 86.6 % / 21,570 km 87.7 % / 27,602 km
CH spawn-staged, spawning 245 84.5 % 98.4 % (overlay)

Reconciliation: on its own 55 WSGs, fresh_default retains 5,104 BT+DV and 1,745 CH locations, exactly #284's counts. Build vintage differs between the two schemas (4 of 55 and 39 of 59 WSGs are logged), so the diff is not a pure config effect.

This PR models nothing (scope decided at the plan gate). Scoring default_tuned needs its own run decisions, stated at launch.

Verification

  • 105 tests in test-lnk_habitat_validate.R, including a DB fixture: two WSGs share id_segment, and there are cases for exclusions, releases, DV where BT is absent, a buffer, UHC indicators, absences, a log mismatch, and an unattached point.
  • Mutation check: a planted bare-id_segment habitat join fails 6 tests.
  • Full suite: FAIL 3 / PASS 2042. All 3 are the :63333 bcfp tunnel being down (test-lnk_db_conn, test-lnk_wsg_resolve ×2), files this branch does not touch. devtools::check(): its one ERROR is those same tests; the 3 WARNINGs and 2 NOTEs are in pre-existing files.
  • The full suite caught one more defect. The preflight drift guard needed frs_habitat_predicates declared, and checking that showed the pinned fresh@v0.33.0 has no model argument, so the call as written would have failed on every host running 0.33.0. It is fixed, and a test that fails with the argument restored guards it.
  • /code-check: 6 rounds, plus a plan review, so 7 reviewer agents, past the usual ~5. Round 6 covered the source-agnostic change, and its five findings are fixed and pinned by tests (key handling, exclusions without a key, code normalisation, empty prefix, 0/1 flags). Round 2 found a defect inside a round-1 fix, so from then on only an enumeration could end the loop. Round 3 named three mechanisms and enumerated 59 rows, and round 4 re-walked them. Round 5 reviewed the late outside-UHC delta; its one finding is fixed and pinned by a test. Findings are in planning/archive/2026-09-issue-283-habitat-validation/review-*.md.
  • Found by running the driver, not by review: toupper(NULL) is character(0), so an absent --wsgs selected zero WSGs.

Not done

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx

NewGraphEnvironment and others added 10 commits September 26, 2026 12:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
Scores a persisted run against fish observations: capture on accessible,
spawning and rearing segments, cost in km, miss reasons from the bundle's own
habitat predicates with gradient and width relaxed, and optional absences.
All joins on the full (id_segment, watershed_group_code) key. schema is
required: default's pipeline.schema names the schema bcfishpass builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
With the overlay on, 237 of 245 spawn-staged CH locations in the 51 shared
WSGs sit in reaches default forces to spawning, so the headline capture is
mostly decided by construction. The *_outside_uhc shares are the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
With buffer_m > 0 capture credits habitat upstream of the point, so a UHC
reach just above an observation forced its capture while the point tested
as outside UHC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
default vs bcfishpass on 51 shared WSGs at 0c19e0a. CH spawn-staged capture is
mostly the user_habitat_classification overlay (237 of 245); BT's 0.1349 would
score in-sample. research/habitat_validation.md carries the method, caveats and
the #284 step-5 command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
The preflight drift guard requires every fresh:: symbol link calls to be
declared. And the pinned minimum's frs_habitat_predicates() has no `model`
argument, so passing model = "cw" would have failed on every host running
fresh 0.33.0; cw is the default in 0.34.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
observations is now a table or a data frame. Only the location columns and
species_code are required. The bcfishobs filters become arguments
(match_types, source_exclude) that default on, and a filter whose column the
source lacks is an error rather than a skip. A source's own is_spawn /
is_rear win over bcfishobs wording. A data frame without observation_key is
keyed by its own row numbers, and then refuses non-NULL exclusions. Codes are
compared upper-cased and trimmed.

With defaults, the bcfishobs path reproduces the committed baseline: 120 of
120 summary rows on 5 WSGs. Review round 6 (in the #283 archive) found the
key, exclusion, normalisation and 0/1-flag issues fixed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJE2szSCkeV9R3qKhuSwyx
@NewGraphEnvironment
NewGraphEnvironment merged commit e35f421 into main Sep 26, 2026
1 check passed
@NewGraphEnvironment
NewGraphEnvironment deleted the 283-validate-modelled-habitat-against-fish branch September 26, 2026 22:09
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.

Validate modelled habitat against fish observations per species and watershed group

1 participant