id: evaluation_contingency_table
name: Contingency Table
description: >
  Compute the 2x2 forecast verification contingency table (TP/FP/FN/TN) and
  standard skill scores (Hit Rate, False Alarm Ratio, Hanssen-Kuipers,
  acting-in-vain) from aligned binary forecast and observation timeseries.

  TP (Hit): forecast=1 AND observed=1 — alarm raised, event happened.
  FP (False Alarm / "acting in vain"): forecast=1 AND observed=0.
  FN (Miss): forecast=0 AND observed=1.
  TN (Correct Rejection): forecast=0 AND observed=0.

  Hit Rate (POD) = TP / (TP + FN); fraction of events correctly forecast.
  False Alarm Ratio = FP / (TP + FP); fraction of alarms that were false.
  Hanssen-Kuipers (TSS) = HR - FP / (FP + TN); discrimination skill, [-1, 1].
  Acting-in-vain rate = FAR (same arithmetic; published vocabulary for the
  anticipatory-action / forecast-based-financing field — Anticipation Hub
  2024 Global Overview, IFRC/DRK/Climate Centre FbF Practitioner Manual).
  Both names are exposed so AA recipes can read either.

  The standard product-evaluation primitive for anticipatory action triggers.

version: 1.0.0
category: evaluation
type: timeseries-to-record
default_implementation: native

inputs:
  - name: forecast
    type: timeseries
    description: Binary forecast event series (0/1 or boolean), aligned on time with observed.
    required: true
  - name: observed
    type: timeseries
    description: Binary observed event series (0/1 or boolean), aligned on time with forecast.
    required: true

requires: {}

outputs:
  - name: table
    type: record
    description: "{tp, fp, fn, tn, total} with integer counts."
  - name: skill
    type: record
    description: >
      {hit_rate, false_alarm_ratio, hanssen_kuipers, acting_in_vain} as floats.
      Undefined ratios return NaN (see op uncertainty for details).

params:
  alignment_check:
    type: select
    options: [strict, intersect]
    default: strict
    description: >
      strict = inputs must share identical time coordinate (raise on mismatch).
      intersect = trim to the common time coordinate before evaluating.

uncertainty:
  error_model: unknown
  factors:
    - name: degenerate_skill_scores
      description: >
        Hit Rate is undefined when no events were observed (TP+FN=0).
        False Alarm Ratio and acting-in-vain are undefined when no alarms
        were raised (TP+FP=0). Hanssen-Kuipers is undefined when either of
        those base populations is empty. The op returns NaN for undefined
        ratios; counts remain well-defined integers.
      severity: medium
      mitigations:
        - "Inspect the count record before consuming skill scores."
        - "Use a longer evaluation period containing both events and non-events."
    - name: small_sample_skill_variance
      description: >
        Skill scores from short timeseries (< ~30 paired observations) have
        wide confidence intervals not reflected in the point estimate.
      severity: medium
      mitigations:
        - "Bootstrap confidence intervals (out of v0 scope; planned follow-up)."
        - "Report sample size (`total`) alongside skill scores in the Evidence Bundle."
  limitations:
    - "Binary (2x2) only — multi-category contingency tables out of scope."
    - "Point-wise / aggregated only — no spatial per-cell verification in v0."
    - "Deterministic forecasts only — Brier / ROC / reliability are separate ops."
  methodology_reference: "docs/spec/FORECAST_VERIFICATION.md"

# backends: audited 2026-08-14 (defect 50). A key means a runtime that DISPATCHES this op —
# folia-engine `dispatch_op` (products/sdk/folia-engine/src/lib.rs), a `registerOp`/OP_TABLE
# entry in packages/compute, `_BUILTIN_OP_MAP` in folia/compute.py, or a backend manifest
# (folia/backends/*/backend.yaml).
backends:
  native:
    function: folia.domains.temporal.transforms.evaluation.contingency_table

display_hints:
  table:
    renderer: tanstack-table
  info:
    fields: [hit_rate, false_alarm_ratio, hanssen_kuipers, acting_in_vain]
    format: "{value}"
    section: Verification
