KOUKKU Research · White Paper

Weather Similarity Index

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.

Version v2 · May 2026 Production · Shipped CodeHan Oy
§1

Executive Summary

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.

11
Metrics declared
6 state metrics + 4 centred 2 h deltas + water temperature. Most carry non-zero default weight; 3 honest-zero placeholders.
8+1
Species profiles
Hauki, kuha, ahven, taimen, lohi, siika, made, silakka + species-neutral General. Per-species multiplier rows seeded for all.
<1 ms
Per pair (SQL)
One PL/pgSQL function call iterating ≤11 metrics. Pure SQL — no Edge Function on the hot path.
60 days
Seasonal window
The seasonal-proximity sibling decays to SP = 0 at ±60 days on the calendar-year circle. Anchored to Finnish photoperiod regime changes.
L1
Distance family
City-block (L1), not Euclidean (L2). Each ecological axis adds independently. Gower (1971) lineage. Saturation cap prevents single-outlier dominance.
WFI vs WSI — two sides of the same coin. WFI answers "how good is this hour for fishing this species?" — an absolute score. WSI answers "how similar are these two hours on fishing-relevant axes?" — a relative distance. Both share their citations: when WFI says 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.
§2

Why Fishing Similarity Must Be Biology-Aware

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.

Failure 1

Variables don't scale equivalently

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.

Failure 2

One outlier shouldn't dominate

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.

Failure 3

Same atmosphere ≠ same fishing day in June vs October

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.

§3

The Formula — Bounded Manhattan in OSS-Normalised Space

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.

-- Per-metric delta
delta_m = |value_a_m − value_b_m|
delta_m = min(delta_m, 360° − delta_m) -- angular only (wind_dir_deg)

-- Cap at saturation_cap OSS-units
coef_m = min(saturation_cap, delta_m / OSS_m)

-- Weighted contribution
contribution_m = weight_m × coef_m

-- Sum and scale
Sigma = Σ_m contribution_m
WSI = max(0, 100 − J · Sigma) -- J tunable via app_config
Mathematical lineage

Gower (1971) — founding parent

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.

L1 vs L2

City-block — the right geometry

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".

Guarantees

Symmetry · Identity · NULL-skip

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 gate

Strength-gated angular metric

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.

§4

Per-Metric OSS Values & Priorities

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
OSS 2.0 °CLive
Base priority. OSS set to approximately half the WFI thermal-preference plateau width — the threshold at which the next fish would notice a different thermal window. Air temp is a proxy for water temp; species multipliers express this differential priority (e.g. deep-water burbot gets no air-temp multiplier; surface-hunting pike gets a moderate one). Sources: Magnuson et al. (1979); Coutant (1977).
pressure_hpa — MSLP state
OSS 4.0 hPaLive · halved
Halved base priority. State pressure barely correlates with feeding behaviour — WFI explicitly weights only the trend, not the state. v2 applies a uniform half-weight multiplier for all species, reflecting Stoner (2004): "trend, not state, drives the biological signal."
wind_speed_ms — Sustained 10 m wind
OSS 2.0 m/sLive · v2 corrected
Base priority. OSS set to one Beaufort step in the 0–8 m/s range dominating Finnish fishing. v1's OSS overestimated this by ~50%, mismatching the Beaufort-step biology — the only OSS value that changed between v1 and v2. Source: WMO Resolution 9 (1962); Met Office Beaufort documentation.
wind_dir_deg — Wind direction
OSS 25.0 °Live · gated
Base priority, strength-gated. OSS set to one 16-point compass notch — the threshold anglers report as meaningful. Angular wraparound enforced. Gated: skipped below calm threshold, smoothstep ramp through moderate, full weight at strong. Source: WMO Beaufort; compass geometry.
cloud_pct — Cloud cover
OSS 25.0 %Live
Slightly reduced priority. OSS set to two oktas — the four practical sky regimes anglers report (clear / scattered / broken / overcast) collapse to quarter-sky bins. Source: WMO (2017) International Cloud Atlas; Met Office oktas standard.
precip_mm_h — Precipitation rate
OSS 1.0 mm/hLive
Reduced priority. OSS set to the Met Office "light rain" lower bound — below this threshold precipitation is trace or drizzle, largely behaviourally invisible to fish; above it represents real rain with measurable river-input and surface-disturbance effects. Source: Met Office; UK Environment Agency thresholds.
pressure_delta_2h_hpa — 2 h pressure trend
OSS 1.0 hPaLive · key axis
The single most diagnostic axis in v2. Stoner (2004) concluded that trend, not state, drives the biological signal. Species multipliers span the widest range of any metric in the registry, reflecting the WFI pressure-trend spread from near-zero priority (deep-water benthic species) to the highest priority in the registry (daylight sight-feeders).
temp_delta_2h_c — 2 h temp trend
OSS 1.0 °CLive
Low priority. Dawn/dusk warming in Finnish summer routinely reaches 1–2 °C/2h; the OSS is set at the threshold above which the trend becomes synoptic or convective rather than purely diurnal. Secondary trend signal — lower priority than pressure delta.
wind_delta_2h_ms — 2 h wind trend
OSS 2.0 m/sHonest zero
Computed but contributes nothing. If telemetry from 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
OSS 15.0 %Honest zero
Open-Meteo's hourly cloud cover is too noisy at 2 h resolution for a reliable trend signal in v1/v2. Same honest-zero discipline as wind delta. Will be revisited with telemetry evidence.
water_temp_c — Water temperature (v2)
OSS 2.0 °Cv2 · declared
Highest species-multiplier spread in the registry. Deep cold-water benthic species receive the largest multiplier (water temp dominates burbot biology); surface-hunting piscivores receive lower multipliers. v2 caveat: metric is declared but only active when both compared snapshots are catch rows with user-logged water temperature. Sources: Brett (1971); Magnuson et al. (1979); Coutant (1977).
Global parameters are tunable: the scaling factor J (controls how steeply WSI falls as distance accumulates), the saturation cap (maximum OSS-units any single metric can contribute), and the minimum metric pair count (below which the function returns NULL rather than a misleading score). All parameters live in public.app_config and can be adjusted without a code deploy.
§5

Seasonal Proximity — The Sibling That Closes the Photoperiod Hole

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.

The hole: A mid-July afternoon at 18 °C / 1015 hPa / light south-westerly is a very different fishing day from a mid-September afternoon at the same conditions — different photoperiod, different surface-water temperature, different prey availability, different species life-stage. Pure WSI scores those at 100. "Two perfect-twin weather snapshots in June and October are not the same fishing weather."

The sibling formula

fn_wsi_seasonal_proximity(date_a, date_b) → numeric in [0, 1]:

diff_doy = min(|doy_a − doy_b|, 365 − |doy_a − doy_b|) -- circular calendar distance
SP = max(0, 1 − diff_doy / window_days) -- window_days = 60 (default)
Composite fishing-day similarity
FishingDaySimilarity = 100 × √( (WSI/100) × SP )
Geometric mean of two [0, 1] similarities. Both must be high to give a high composite. Equal contributions required. Source: Legendre & Legendre (2012), §7.2.
Why 60 days?

Photoperiod regime change

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.

Phenology validation

Aligned with Koukku's own phenology bands

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.

Geometric mean semantics

Both factors must be high

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.

§6

Per-Species Multipliers — Harmonising WSI with the WFI Matrix

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.

Where the multipliers come from: each multiplier is derived by normalising the WFI per-species weight for the corresponding component against the WFI General weight. The result is a consistent transfer of biological priority from WFI (absolute score) to WSI (distance metric) — both instruments respond to the same underlying fish biology.
§7

The Calibration Loop — Closing the Feedback Gap

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.

What we have now

Telemetry collection active

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.

The calibration plan

Empirical OSS and band validation

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.

Not in v2

What we deliberately deferred

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."

Calibration Log

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.
§8

Limits of Current Confidence

The honesty section. Every white paper has one.

No spatial signal
WSI is purely temporal: two atmospheric snapshots compared. It has no way of knowing whether the two snapshots were collected at the same water body, the same latitude, or even the same country. A Finnish lake in October and a Norwegian fjord in October at the same atmospheric state are not the same fishing day; WSI would score them as if they were.
OSS values are literature-anchored approximations
Every OSS value cites a source, but the source does not directly specify "1 OSS = this magnitude." The OSS is an engineering translation of biology into units — defensible, documented, and calibration-revisable, but not empirically measured against Finnish angling catch rates yet.
Silakka is the weakest species profile
Recreational-angler literature for Baltic herring is thin; most sources are commercial-fishery (ICES WGBFAS, LUKE) or general clupeid biology. WSI v2 multipliers are plausible defaults pending Koukku-internal silakka catch data. Same caveat in the WFI white paper.
Pressure state is a poor proxy
State pressure (1015 hPa vs 1010 hPa) matters far less than the trend. v2 applies a halved species multiplier on 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 and cloud trend signals unvalidated
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 rarely available
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.
§9

Key References

Selected citations anchoring the WSI formula family, OSS values, and species multiplier design. Full bibliography in docs/wsi_white_paper.md §9.

Companion document: WFI White Paper — the Weather Favourability Index, the WSI's biological-axes sibling. Both instruments share their citations; the WFI per-species weight matrix is the direct source for the WSI species multipliers.