Primary competition visual

June Study Jam Series: Bank Transaction Volume Forecasting Challenge

Helping South Africa
500 Points
Under code review
Feature Engineering
Time-series
Forecast
265 joined
74 active
Starti
Jun 10, 26
Closei
Jun 30, 26
Reveali
Jun 30, 26
#4 Solution write-up
1 Jul 2026, 12:07 Β· 0

Thanks to Nedbank and Zindi for an interesting problem. Sharing my approach in case it helps others. Full code + a single self-contained notebook that reproduces the submission is here: https://github.com/Roebou1989/public-zindi-june-study-jam-transaction-volume-forecasting-challenge

The thing that shaped everything: train and test customers are different people

The train and test customers are disjoint, so normal cross-validation is misleading - it tells you how well you predict people like the ones you trained on, not unseen customers. For a long time my better-CV models were my worse-leaderboard models. Once I accepted that, the whole strategy became "do the things that help you generalise to strangers, and ignore the things that only help CV."

What actually moved the needle: a multi-anchor panel

Instead of one row per customer as of Oct 2015, I slid the "as-of" month backwards through history and made one row per (customer, month), each with the realized next-3-month count as its target (you can just count it, since those windows are in the past). That turns ~8k rows into ~250k and forces the model to learn a general "given this recent history, expect this many next quarter" rule instead of memorising individual customers. I also included all customers (train + test) at the historical anchors - completely legitimate, since only the Oct 2015 future label is hidden - which puts the test customers' own past behaviour into training and directly attacks the disjoint-customer problem. Small calendar features per anchor (does the next quarter include Nov/Dec/Jan, etc.) keep the festive seasonality, and I weighted the real Oct-2015 anchor more heavily.

The model: boring on purpose

A single LightGBM, L1 objective, trained on log1p of the target, heavily regularised. No blend, no deep learning. My consistent experience was: more data (anchors, customers) helped the leaderboard; more capacity (fancy features, heavy tuning, correlated ensembles) helped CV but hurt the leaderboard. So the only extra lever I trusted at the end was pure variance reduction - averaging the same model over 36 random seeds, which nudged the score down to its floor (0.369841).

Things that looked good but lost on the leaderboard

Sharing these because the negative results were the most useful part:

  • EWMA / decay recency features: better CV, worse LB.
  • Heavier hyperparameter tuning: looked like a win at 3 seeds, evaporated at 24 (it was seed luck).
  • Passing missing values as NaN instead of filling count features with 0: clearly worse (0 really does mean "no activity" here).
  • Closing-balance "capacity" features: worse.

Thanks again to Nedbank and Zindi, and congrats to everyone who stuck with this one :)

Discussion 0 answers