Skip to main content

Method: Simple Z-Score

For each (project, connector, metric, date), we compute:
Where:
  • rolling_mean and rolling_stddev are computed over a trailing 30-day window (configurable: ANOMALY_BASELINE_WINDOW, default 29 days preceding + current row)
  • Source: fact_daily_kpi single-canonical-dimension totals (anti-double-count guard)
The exact SQL is in dbt/models/marts/metric_baselines.sql and dbt/models/marts/anomalies_daily.sql.

Anti-Double-Count Guard

fact_daily_kpi contains parallel breakdown_dimension series (e.g. device_category AND country) that EACH independently total the day. Summing across ALL breakdown rows would produce values 2-5x too large. metric_baselines.sql uses the canonical_dim pattern (same as cross_source_conversions.sql) to pick ONE breakdown_dimension per (project, connector, metric) deterministically (MIN(breakdown_dimension)), then aggregates only over that dimension’s rows. This is an architectural requirement (AD-13, review-1-6 lesson).

Threshold

An anomaly is flagged when |zscore| >= ANOMALY_Z_THRESHOLD (default 3.0).
  • |z| in [3.0, 5.0) → severity: warning
  • |z| >= 5.0 → severity: error
ANOMALY_Z_THRESHOLD is configurable via environment variable (see .env.example).

Why Z-Score?

  • Transparent: no model parameters to tune, no black box. The exact formula is visible in the dbt model SQL (AD-9 requirement).
  • Robust for marketing KPIs at day grain.
  • Testable: the seed generator (generate_seed.py) injects a 3x sessions spike at day 75 (AI-09), producing a z-score well above 3.0 for deterministic test validation.

Limitations

  • Requires at least 2 days of history (sample stddev is undefined with 1 row). Rows with rolling_stddev IS NULL OR = 0 are excluded — no division by zero, no false alerts during the first day of data.
  • Sensitive to sudden baseline shifts (e.g. new campaign launch). Use context_events to annotate known shifts.
  • Does not detect gradual drift (use declared thresholds for that: Story 5.3).

Context Event Citations (AD-9)

Anomalies never assert a cause. When context events exist for the anomaly date, they are listed as candidate context only. When absent, “missing context” is stated. This is an AD-9 architectural requirement. Prohibited language in any generated message:
  • “caused by”, “due to”, “because of”, “as a result of”
A unit test (test_causal_language_absent in server/tests/core/test_anomaly_alerts.py) enforces this constraint.

Pull ID Provenance for Anomaly Firings

Anomaly firings have pull_ids = [] (empty array). Unlike business threshold firings which trace to specific raw data pull batches, anomalies are derived signals computed by the anomalies_daily dbt model from historical data. Provenance is the dbt model itself (documented method above), not a raw data event.

Environment Variables