Skip to content

Validate modelled habitat against fish observations per species and watershed group #283

Closed

Description

@NewGraphEnvironment

If we do it: "is this threshold set better?" gets a number per species and watershed group, so candidate CH/BT thresholds can be compared rather than eyeballed. If we never do: threshold changes are judged against bcfishpass parity, which measures agreement with a model rather than with fish.

Problem

link compares its output with bcfishpass (compare_*, wsg_compare.R, parity_crosssection.R) but never with fish observations. The only observation-vs-model comparison is the Babine SK work in research/default_vs_bcfishpass.md §6-7, done by hand for one species against user_habitat_classification.

Proposed Solution

A function (working name lnk_habitat_validate()) and a driver that, for a run schema, species set and WSG set, report:

  • Capture: share of observations (after observation_exclusions, releases removed) on or within a buffer of modelled spawning / rearing / accessible segments, split by life stage where known.
  • Cost: km of modelled spawning and rearing, so capture can't be raised by just calling everything habitat.
  • Misses: gradient, channel width and edge type of the segments under observations that fell outside modelled habitat. This is the direct evidence for which threshold is binding.
  • Absences (optional input): sites sampled with no fish captured (FISS no-fish-captured records), used as a false-positive check.

Output is a tidy table per bundle × species × WSG, so two bundles can be diffed.

Corrected 2026-09-26: this said to use the per-WSG schemas because fresh.streams_habitat_* is duplicated per group (fresh#218). It is not: fresh.streams_habitat_ch is 2,329,201 rows and 2,329,201 distinct (id_segment, watershed_group_code). Naver Creek joins 187 rows on the full key and 4,301 on bare id_segment, because id_segment repeats across groups (#203). The persist schemas are correct when joined on the full key, so the validator reads them directly.

Known biases to report alongside the numbers, not correct silently:

  • observation points often sit at the downstream end of a sampling site;
  • sampling clusters near road access;
  • observations exist only where fish have access, so observed gradients are cut off at the access limit.

Scope decided (2026-09-26)

  • lnk_habitat_validate() returns list(summary, observations). summary has one row per WSG × species × stage (any / spawn / rear), with capture counts and shares, cost km from lnk_rollup_wsg(), and absence columns when absences are supplied. observations has one row per retained location, with its segment's attributes and a diagnostic miss_reason.
  • Any observation source (added during review): observations takes bcfishobs, another table, or a data frame with species_code, watershed_group_code, blue_line_key and downstream_route_measure. The bcfishobs filters (match_types, source_exclude) default on, and a filter whose column the source lacks is an error rather than a skip. Snapping points to the network stays outside the validator; a bulk snap in fresh is proposed separately.
  • CH spawn-staged capture is mostly the overlay: 237 of 245 locations sit in user_habitat_classification spawning reaches, so the result reports capture outside those reaches (*_outside_uhc).
  • Accessible = streams_access.access_<sp> IN (1, 2), the definition lnk_rollup_wsg()'s accessible_km uses, so capture and cost agree. Research: calibrate CH and BT gradient and channel-width thresholds from observations #284 used streams_habitat.accessible.
  • Accessible capture is partly circular. Observations are pipeline inputs: they lift barriers (observation_threshold), they are break points, and user_habitat_classification forces habitat. Spawning and rearing capture is the score; accessible capture is a sanity check, and observations on inaccessible segments are reported as access misses.
  • Buffer: capture can extend upstream along the same stream (buffer_m), since observation points often sit at the downstream end of a site. The driver reports 0 and 100 m.
  • This PR runs read-only against fresh_default and fresh (51 shared WSGs). Scoring default_tuned is Research: calibrate CH and BT gradient and channel-width thresholds from observations #284 step 5, with its own run decisions stated at launch.

Result: method, caveats and the default vs bcfishpass baseline are in research/habitat_validation.md (on main once the PR merges).

Activity

  1. added 3 commits that reference this issue on Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions