Case file · AML alert triage WIUT Hackathon 2026 · Team 348471A6

Fintech · AI in Fintech

Which alerts deserve an investigation?

A bank's monitoring system raises thousands of automated alerts. A specialist dismisses most of them and escalates a few. We used each customer's transaction history to rank alerts by their chance of escalation, so the likely ones get looked at first.

Triage
Alert ID
Alert date
History≈ 6 months of transactions
Model outputP(escalation) ∈ [0, 1]
Illustration only — no real alert is shown on this site.
14,000
training alerts
17.2%
escalated
7.0M
training transactions
0.648
CV ROC-AUC, final model
Approach

Rank the alerts, not just label them

Each alert is either dismissed or escalated. For every test alert we predict the probability of escalation. The score is ROC-AUC: it only checks the ordering. It asks how often a random escalated alert is ranked above a random dismissed one. 0.5 is a coin flip, 1.0 is perfect.

  1. Explore the data and answer basic questions with numbers: size, balance, time, leakage.
  2. Summarise each alert's transactions (hundreds per alert) into one row of features.
  3. Validate honestly: stratified 5-fold cross-validation, repeated on 5 different random splits. A feature group is kept only if it adds more than 0.002 AUC.
  4. Explain the final model with SHAP and check that it makes sense in anti-money-laundering (AML) terms.

Confidentiality. The data is under NDA. This site shows only charts and aggregated statistics — no raw rows, no alert IDs, no data files.

Dataset

Twenty thousand alerts, ten million transactions

TrainTest
Alerts14,0006,000
Transactions6,987,6633,027,575
Alert dates2025-01-01 … 2026-12-312025-01-01 … 2026-12-31
Labelescalated (1) / dismissed (0)— (to predict)
  • Each transaction has a timestamp, a direction, a type and a standardised amount index. As a share of all train transactions: incoming 75.7%; card 53.8%, bank transfer 39.4%, cash 6.3%, international 0.5%.
  • 0 missing values. Every alert has at least one transaction. No alert appears in both train and test.
  • Transactions per alert: min 1, median 461, max 2,279. History length: median 179 days, so almost every alert comes with a full ~6-month window. Only 157 alerts have less than 30 days.
  • Leakage check: 0 transactions after the alert day. 840 transactions fell later on the alert day, in just 21 train alerts. We dropped them because their timing relative to the alert is ambiguous.
  • Shared customers? Across all 20,000 alerts there are 0 exact duplicate transactions, and no pair of alerts shares more than 3 timestamps outside the pre-alert burst. No sign of one customer behind several alerts, so plain stratified folds are safe.
Target & time

One in six alerts is escalated, and that rate barely moves

Horizontal bars: 11,595 dismissed and 2,405 escalated training alerts
Imbalanced classes. 2,405 of 14,000 alerts (17.2%) were escalated. A model that always says "dismiss" would be right 83% of the time and useless, which is why the metric is ROC-AUC, not accuracy.
Line chart of escalation rate by quarter, between 15.8% and 18.9%
No drift over time. The quarterly escalation rate stays between 15.8% and 18.9%.
Monthly share of alerts, train and test lines overlapping across 2025–2026
Test is not "the future". Train and test alerts cover the same two years with the same monthly shape. So random (stratified) cross-validation is a fair estimate of the test score, and a time-based split is not needed.
The pre-alert burst

Almost every alert ends with a three-minute flurry

The most striking structure in the data: 551,899 transactions (7.9% of all) sit in the last 15 minutes before midnight of the alert date. 13,848 of 14,000 alerts (98.9%) have this burst, with a median of 31 transactions. It is extremely tight: 548,712 transactions fall within 5 minutes of midnight, only 44 in the 5–10 minutes before that, and a typical burst lasts 172 seconds. It looks like the event that triggered the alert.

Histogram on log scale of transactions in the 72 hours before the alert, with a huge spike at zero
A spike at the end. Log-scale count of transactions in the last 72 hours. The final 15-minute bin towers over everything else.
Bar chart of transactions by hour; hour 23 is about three times higher than the rest
The same burst, seen by hour. Hour 23 holds 11.7% of all transactions, versus about 3.8% for every other hour.

But the burst does not separate the classes. Its size alone has AUC 0.513, and a whole group of 22 burst features added only +0.0003. Almost every alert has one, escalated or not. See what didn't work.

Behavioural patterns

Escalated and dismissed alerts look almost the same, on average

The EDA's main lesson was humbling: simple averages barely tell the classes apart. Every single simple feature had an AUC between 0.502 and 0.548.

Bar chart of single-feature ROC-AUC values, all between 0.50 and 0.55
No silver bullet. The best simple feature (largest amount) reaches AUC 0.548. The signal is weak and spread across many features, so gains had to come from combining them.
Five small bar pairs comparing transaction type and direction shares between classes; nearly identical
Same mix. Here each alert gets its own shares, which are then averaged over alerts in the class (dismissed vs escalated): card 52.1% vs 52.1%, bank transfer 41.2% vs 40.9%, cash 6.2% vs 6.5%, incoming 75.0% vs 74.4%. This differs from the shares of all transactions above (e.g. card 53.8%) because busy alerts weigh more in a pooled share.
Two overlapping amount distributions for dismissed and escalated alerts
Amounts overlap too. Mean amount index −0.182 (escalated) vs −0.123 (dismissed): a small shift, not a separation.
Escalation rate by activity quintile rising from 15.5% to 19.0%
Busier customers are escalated a bit more often: 15.5% in the least active fifth vs 19.0% in the most active.
Average daily transactions over 180 days before the alert, escalated line slightly above dismissed
Consistently a little more active over the whole six months before the alert, not only at the end.

Where the difference really hides

Amounts only mean something within a transaction type: an international transfer averages an index of about 2, an incoming card payment about −0.5. When we compared amounts type by type, a clear pattern appeared. Per-alert medians, dismissed vs escalated:

  • mean outgoing bank transfer: 0.299 vs 0.122;
  • mean incoming bank transfer: 0.125 vs 0.002;
  • number of outgoing cash transactions: 6 vs 7.

Escalated customers move money in smaller pieces. In AML this is the classic structuring typology: splitting a large sum into many small transactions to stay under reporting thresholds.

Features motivated by EDA

From 11 features to 110, one tested group at a time

Every group came from an EDA observation or an AML typology and was added only if it improved cross-validated AUC by more than 0.002. The final set is 110 features in four groups:

GroupWhat it measuresWhy (EDA)Effect
Base (11)count, sum / mean / max / min / std of amounts, type and direction sharesstarting point0.5979
A · amount shape (33)quantiles, spread, skew, kurtosis; shares of very small / large amountsaverages overlap, so look at the shape+0.0108
E · type × direction (24)count, share and mean amount for each of the 8 combinations (e.g. incoming cash)amounts only comparable within a type+0.0232
F · shape within type × direction (42)min, max, median, 10th/90th percentile, spread, skew for 6 combinationsstructuring = many similar small amounts+0.0096

International transactions were skipped in group F: fewer than half of the alerts have 5 or more of them, so their statistics would be noise.

Cross-validated ROC-AUC, mean over 5 random splits

Final model: one LightGBM gradient-boosted tree model with 8 leaves per tree, learning rate 0.01 and 508 trees. CV ROC-AUC 0.6465 ± 0.0160 on the main split, 0.6480 on average over 5 splits. The final notebook rebuilds the submission from the raw files, and a re-run produces a byte-identical file.

What the model learned

It watches how customers move money through bank transfers

SHAP splits each prediction into contributions from individual features. Positive values push an alert towards escalated, negative towards dismissed. By group, 60.3% of the model's attention goes to group F (amount shape within type × direction), 18.4% to E, 15.3% to A and 6.1% to the base features. Seven of the top-15 features describe bank transfers.

SHAP beeswarm plot of the top 15 features
How to read it: each dot is one training alert; colour is the feature value (red = high, blue = low). For the median outgoing bank transfer (top row), red dots sit on the left: small transfers push towards escalation. For incoming cash amounts it is the opposite, and larger cash deposits push towards escalation.
Dependence plot: SHAP falls steadily as median outgoing bank transfer grows
#1 · Median outgoing bank transfer. A clean downhill curve: the smaller the typical transfer, the higher the risk. Mean SHAP by value fifth, from small to large: +0.099, +0.071, +0.006, −0.045, −0.095. This is the structuring signal.
Dependence plot: SHAP falls as mean outgoing bank transfer grows
#3 · Mean outgoing bank transfer. The same story from the mean: correlation between value and SHAP −0.89.
Dependence plot: a sharp step at the very bottom of the amount scale
#2 · Smallest incoming bank transfer: a threshold, not a trend. The effect sits in a narrow band at the bottom of the amount scale. Alerts whose smallest incoming transfer is at or below −2.89 are escalated 13.7% of the time (2,677 alerts). Just above, in (−2.89, −2.85], the rate is 27.9% (953 alerts). We cannot explain this in AML terms; it may reflect how the amount index was built. We report it rather than hide it.

Two individual decisions

Two typical cases: an escalated alert ranked at the 90th percentile of escalated scores, and a dismissed alert at the 10th percentile of dismissed scores. Only the contributions are shown, not the customers' actual values.

Waterfall plot for an escalated alert; most contributions push the score up
Escalated example. Starting from the model's average output (−0.146 in log-odds; close to 0 rather than −1.57 because training weights escalated alerts ×4.82, which balances the classes), almost every top feature pushes the score up. The biggest push, +0.27, comes from the smallest outgoing card amount, followed by the spread of incoming and the median of outgoing bank transfers. Only one of the top ten pulls it down.
Waterfall plot for a dismissed alert; most contributions push the score down
Dismissed example. The incoming bank-transfer features (mean −0.19, 90th percentile −0.10, spread −0.06) pull the score down. Incoming cash pushes the other way (+0.13), but not enough to change the picture.
What didn't work

Good AML ideas that this data did not reward

Each idea below was built, tested on the same folds and dropped because it did not beat the 0.002 bar. We think reporting them matters as much as the wins.

The pre-alert burst +0.0003

22 features on the burst's size, mix and amounts vs history. Re-tested on top of the stronger feature set: −0.0014. Nearly every alert has a burst, so it does not discriminate.

Transit / pass-through −0.0004

Money in, then out within 1 h or 24 h; share of money passed through; running balance. A textbook laundering pattern, but no gain here (−0.0001 in the second pass).

Rhythm & time of day −0.0002

Gaps between transactions, active days, peaks per day and hour, night and weekend shares, 24 hourly shares. −0.0022 in the second pass.

Behaviour change −0.0016

Last 7 / 30 days vs the whole history, second half vs first half, monthly trend. −0.0020 in the second pass.

CatBoost −0.0059

A second gradient-boosting library on the same 110 features: 0.6422, worse than LightGBM on 5/5 splits.

Blending & seed averaging −0.0011 … −0.0033

Rank-averaging LightGBM with CatBoost was worse at every weight (0.3 / 0.5 / 0.7). Averaging 5 LightGBM seeds gave −0.0004.

A lesson in honest validation. Our first baseline stopped training on each fold's own validation data and ended after 1–2 trees. Its score looked fine (0.5804), but the out-of-fold AUC was only 0.5617. We switched to one shared number of trees for all folds. From then on the mean fold score and the out-of-fold score agreed (e.g. 0.6465 vs 0.6460).

Putting every group in together (141 features) scored 0.6273, below the 68-feature set at that stage (0.6318). More features also meant more noise.

Conclusion

Weak signal, honest gains, a pattern that makes sense

  • The task is hard: no simple statistic gets beyond AUC 0.548. The final model reaches 0.648 (from a 0.601 baseline, both averaged over 5 splits). The ranking is better than chance, but far from perfect.
  • The gain came from comparing amounts within each transaction type and direction, not from more data or bigger models.
  • What the model learned matches a known AML typology: escalated customers make smaller, more fragmented bank transfers and larger cash deposits, consistent with structuring.
  • Every kept change was confirmed on 5 random splits. Every dropped idea is listed above. The whole pipeline is reproducible from the raw files with a fixed seed.
References

Credits

  • Libraries: pandas, NumPy, PyArrow, SciPy, scikit-learn, Matplotlib, Jupyter.
  • LightGBM — Ke et al., 2017, LightGBM: A Highly Efficient Gradient Boosting Decision Tree, NeurIPS.
  • CatBoost — Prokhorenkova et al., 2018, CatBoost: unbiased boosting with categorical features, NeurIPS (tested, not in the final model).
  • SHAP — Lundberg & Lee, 2017, A Unified Approach to Interpreting Model Predictions, NeurIPS.
  • American Express — Default Prediction, Kaggle competition (2022): aggregating customer history into per-customer features. kaggle.com
  • Feedzai — Anti-Money Laundering Alert Optimization Using Machine Learning with Graphs. arXiv:2112.07508
  • Dataiku — AML Alerts Triage solution. knowledge.dataiku.com
  • FATF — money-laundering typology reports (structuring, pass-through, cash and cross-border activity). fatf-gafi.org
  • AI tools: Claude Code (Anthropic) was used as a coding assistant for analysis, experiments and this site.