Live demo

14 series in. 4 alerts out.

An institution monitoring transaction behaviour by customer segment. 60 days, 14 series, four segments. On day 45 a structuring pattern starts inside one of them. Total alert volume never moves, because that segment is 4% of the queue. Find it.

Every number on this page is computed in your browser right now, by the same CUSUM and Bayesian changepoint code that runs in production. Nothing here is a screenshot.

The comparison that matters

Against the rule most teams actually ship

The standard configuration is a percentage or sigma threshold against a trailing mean, evaluated per metric. Run it over the exact same 14 series and count what lands in the channel.

Naive rule
73
alerts sent · alert if |Δ| > 20% vs 7-day mean
Of those, false
39
fired before anything was wrong
This engine
4
alerts sent, all correct
Noise reduction
95%
fewer messages, same catch

The false-alarm count is not a judgement call. The incident was injected at a known index, so every naive alert before that index is wrong by construction.

What the engine found

Detection run

Detection run4 alerts14 series tested · 4 fired · 0 suppressed by BH at FDR=0.05
MetricTrendBaselineNowChangeConfidenceMethod
Structuring score
segment=money-services-business
18.24548.911+168%99.9%CUSUM
Cash deposits per account per day
segment=money-services-business
3.4567.150+107%99.9%CUSUM
Cross-border transaction share
segment=money-services-business
11.9%25.7%+117%99.9%CUSUM
Average transaction amount
segment=money-services-business
$8.9k$5.1k−42.6%99.9%CUSUM

Show your work

Structuring score · segment=money-services-business

Baseline
18.245
After changepoint
48.911
Change
+168%
Confidence
99.9%
CUSUM (Page 1954)
The series60 daysdashed line = baseline mean · red = detected changepoint
11.77834.36556.952changepointJun 29Aug 27
The CUSUM accumulatorsmall deviations stack slowly; a real shift stacks fast
h = 0.633 (decision boundary)1.660

Baseline mean μ = 0.0055 and σ = 0.1582, both computed from the first two thirds of the day-over-day percentage changes. The slack k = 0.0791 is half a sigma, and the decision boundary h = 0.6329 is four. The accumulator runs on percentage changes rather than raw levels so a healthy growing series cannot drift across the boundary on its own.

What the naive rule did on this same series

alert if |Δ| > 20% vs 7-day mean fired 8 times across this one metric, including 3 before anything was actually wrong. The statistical pipeline sent one message, on the day the regime actually changed.

What lands in Slack

One message, with the work already done

A
AML TriageAPP#eng-alerts
🔴 Structuring score jumped +168%segment=money-services-business
Baseline
18.245
Now
48.911
Confidence
99.9%
Started
Aug 13
Institution
Segment monitoring, 14 series
Segment
money-services-business
Accounts in segment
2,140
Share of alert queue
4.1%
Likely cause · Four independent behavioural measures on segment=money-services-business changed within two days of each other on Aug 13: deposit velocity roughly doubled while average ticket fell 42%, and cross-border share more than doubled. Splitting deposits and moving them out is the classic shape of structuring. No transaction-monitoring rule fired on it in isolation, and total alert volume is flat because this segment is 4.1% of the queue. This is a ranked lead for review, not a filing determination.
  1. 1.Pull the 2,140 MSB accounts ranked by contribution to the deposit-velocity shift.
  2. 2.Review the top decile against the existing structuring rule to see why it stayed silent.
  3. 3.Log the suppressed low-rank alerts with their q-values so the sample is auditable.
AcknowledgeFalse positiveCUSUM · q < 0.0001 · BH-corrected

The written cause is generated only after the statistics confirm the change. The model never decides whether something is an anomaly — it explains one that has already been established. Getting that order backwards is how these products hallucinate.