KOUKKU Research · White Paper
A literature-anchored fishing-weather distance metric. Bounded Manhattan distance in OSS-normalised feature space. 11 metrics. 8 species profiles. Zero AI cost. Evaluated entirely in SQL.
The WSI maps two weather snapshots into a single 0–100 similarity score. 100 = "indistinguishable on every fishing-relevant axis." 0 = "different to the saturation cap of every axis." The maths is bounded city-block (L1) distance in OSS-normalised feature space, evaluated entirely in SQL at zero marginal cost — the dual of the WFI.
waterTemp is the
highest-weighted axis for made, WSI says the same metric carries the
largest per-species multiplier when comparing two burbot-fishing-day
snapshots.
A naive similarity score takes two weather snapshots, computes Euclidean distance in raw units, and calls it done. This fails for fishing in three ways a biology-aware distance corrects.
A 4 °C swing in air temperature might be an entire feeding regime change for warm-water pike, near-irrelevant for cold-water burbot, and routine diurnal noise in spring. A 10 hPa pressure step is one variable's daily range and another variable's full synoptic regime. WSI's per-metric OSS (One Significance Step) normalises each axis to fishing-relevant units before computing any distance.
Pure Euclidean distance lets one metric with a large gap consume the entire similarity budget. WSI caps each per-metric contribution at a fixed number of OSS-units (the saturation cap), consistent with Stevens' power law — perceived stimulus magnitude saturates. Two otherwise-identical days that differ wildly in one metric still get the other axes counted fairly.
Two perfect-twin atmospheric snapshots — same pressure, wind, temperature — are fundamentally different fishing days if one is midsummer and one is early autumn. Photoperiod, water temperature, prey availability, and species life-stage all shift seasonally. WSI's seasonal-proximity sibling closes this hole with a geometric-mean composition.
For each metric m present (non-NULL) in both snapshots a and b. When fewer than the minimum metric count can be compared, the function returns NULL rather than a misleading number.
The canonical Gower coefficient handles per-variable normalisation, missing-value skipping, and weighting — exactly what WSI needs. WSI makes three deliberate departures: (1) per-metric saturation cap instead of Gower's unbounded normalised difference; (2) L1 city-block instead of Euclidean; (3) hard floor at 0.
Each ecological axis is a roughly independent signal: pressure trend ⊥ wind direction ⊥ water temperature. Per-axis contributions are additive, not orthogonally composed. This matches the L1 "salient differences add" intuition anglers actually report: "it was the same except much windier and a bit colder" — not "it was √(windier² + colder²) different".
Symmetry: WSI(a,b) = WSI(b,a) —
enforced by abs(), asserted in smoke tests. Identity: WSI(s,s) = 100 for
fully-populated snapshots. NULL-skip: missing
metrics are omitted from both the sum and the count — not bluffed
with 0 or NULL-propagated. Compare on the intersection; refuse
below minimum pair count.
Wind direction is biologically meaningless at very low wind speeds (no surface waves, no fetch, no airborne prey delivery). Below the calm threshold the metric is skipped entirely; between calm and moderate wind the effective weight ramps via smoothstep. At moderate-and-above wind speeds the full configured weight applies. Thresholds anchored to WMO Beaufort scale.
Every OSS value and weight either cites a peer-reviewed source, references a published meteorological scale, or names itself a placeholder pending calibration data. Three metrics carry honest-zero weights. OSS (One Significance Step) is the threshold below which two snapshots are indistinguishable on that axis from a fish's perspective.
temp_c — Air temperature
pressure_hpa — MSLP state
wind_speed_ms — Sustained 10 m wind
wind_dir_deg — Wind direction
cloud_pct — Cloud cover
precip_mm_h — Precipitation rate
pressure_delta_2h_hpa — 2 h pressure trend
temp_delta_2h_c — 2 h temp trend
wind_delta_2h_ms — 2 h wind trend
app_area_wsi_observations shows wind-trend matters
independently of wind state, the weight moves up. Until then: zero
and documented.
cloud_delta_2h_pct — 2 h cloud trend
water_temp_c — Water temperature (v2)
public.app_config and can be adjusted without a code deploy.
WSI is atmospherically myopic by design. Two snapshots that match perfectly on all atmospheric metrics may sit at opposite ends of the angling year. The seasonal-proximity sibling closes this hole.
fn_wsi_seasonal_proximity(date_a, date_b) → numeric in [0,
1]:
In Finland (60° N), day length changes noticeably per 30 days outside the solstice plateau. A 60-day shift moves the angler into an unambiguously different photoperiod regime. Borg (1994) establishes photoperiod as the dominant seasonal teleost cue; Bromage et al. (2001) documents response timescales on the order of weeks for photoperiod-mediated annual cycles.
Koukku's phenology system (public.app_areas_phenology) partitions the year into 8
bands — each spanning roughly 4–8 weeks. Two snapshots 60 days
apart almost always cross a phenology band; two snapshots 14 days
apart almost always do not. A linear ramp anchored at 60 days
matches this empirical regime boundary.
SP near zero collapses the composite regardless of WSI. WSI near zero collapses the composite regardless of SP. The semantics are right: two days that share both weather AND season are good fishing analogues; either alone is not enough. Return NULL when WSI is NULL or either date is NULL — no bluffing.
The WSI multiplier table derives from the WFI per-species weight matrix. When WFI says a component is highly important for a species, WSI boosts the same axis when computing distance for that species. The multiplier direction is the same as the WFI priority; only the scale differs.
| Species | Air temp | Pressure state | Wind speed | Cloud | Precipitation | Pressure trend Δ | Temp trend Δ | Water temp |
|---|---|---|---|---|---|---|---|---|
| General | Medium | Half (all) | Medium–high | Medium | Low | High | Low | Base |
| Hauki | Low | Half (all) | Medium–high | High | Very low | Highest group | Very low | Low |
| Kuha | Low | Half (all) | Medium–high | Medium | Low | High | Very low | Very low |
| Ahven | Medium | Half (all) | High | Low | Very low | Highest registry | Very low | Medium |
| Taimen | Low | Half (all) | Medium | Low | Medium | Base | Medium–high | Base (cold-skewed) |
| Lohi | Low | Half (all) | Medium–high | Low | Low | Base | Medium | Base (cooler) |
| Siika | Low | Half (all) | High | Low | Low | Medium | Medium | High |
| Made | Zeroed | Half (all) | Very low | Medium | Very low | Very low | Zeroed | Highest registry |
| Silakka | Low | Half (all) | High | Low | Very low | Base | Very low | Base (coastal) |
Multipliers above base: the metric contributes more than the default weight when comparing two snapshots for that species. Zeroes (made: air temp and temp trend) match the WFI zero weights — surface air temperature is biologically irrelevant for a deep-water benthic species. The water-temperature column shows the widest species spread, reflecting the dominance of thermal biology across cold-stenothermal species.
The v2 empirical foundation is collecting — one row per (user, digest)
in public.app_area_wsi_observations since 2026-05-03. The
calibration plan is straightforward once sufficient observations
accumulate.
app_area_wsi_observations captures one row per unique
area weather digest: the full 11-metric snapshot, the WSI score
against the area's rolling average, and the date. This telemetry
feeds the v2.x re-calibration loop. Zero compute cost — the
trigger fires on the read path already in place for
MorningAnalysis.
Once per-species catch counts reach 500–1 000 with metadata: (1)
plot WSI decile at time-of-catch versus CPUE. (2) Check whether
current OSS values produce a monotone relationship — high WSI
(similar to past-good-days) → higher CPUE. (3) If not monotone:
adjust J or per-metric OSS. Every parameter lives in app_config.
Mahalanobis distance (requires a reliable covariance matrix — needs years of data). Per-user OSS personalisation. Species-interaction modelling. ML-fitted weights. These are not excluded on principle; they are excluded because the honest-zero discipline says "don't add until the evidence justifies it."
| Date | Version | Change | Note |
|---|---|---|---|
| 2026-05-03 | v1 baseline | Initial ship. 6 metrics, uniform species weights, no water temp, no seasonal proximity. |
All baseline calibration scenarios defined in wsi_v1_calibration.sql pass.
|
| 2026-05-18 | v2 release | Wind OSS corrected to match Beaufort step; water_temp_c metric added; seasonal-proximity sibling; per-species multiplier table; telemetry hooks for empirical band calibration. | Quick-look audit identified Beaufort-step mismatch on wind, structural over-weighting of state pressure, missing water-temperature axis, missing seasonal-proximity sibling, and uniform species weights vs literature. New scenarios added for seasonal composite collapse and species divergence. |
The honesty section. Every white paper has one.
pressure_hpa as a partial correction; ideally state
pressure would be zero-weighted like solunarMajorMinor in the WFI General profile. Deferred to
v3 pending calibration evidence.
wind_delta_2h_ms and cloud_delta_2h_pct
carry honest-zero weights until telemetry shows they carry
independent signal. Open-Meteo cloud at hourly resolution is
genuinely noisy; the honest call is zero until we have evidence
otherwise.
water_temp_c is v2's most important new metric by
WFI-derived species weights — but it is only populated when both
compared snapshots are catch rows with user-logged temperature.
MorningAnalysis area-confidence comparisons do not yet populate
this axis.
Selected citations anchoring the WSI formula family, OSS values, and
species multiplier design. Full bibliography in docs/wsi_white_paper.md §9.