Financial services / Data science·Nedbank (national competition, via Zindi)

3rd of 251: forecasting bank transaction volumes in the Nedbank ML Challenge

3rd / 251
final private leaderboard
0.3631
private RMSE (log-space)
18M
transactions modelled

Context

The Nedbank Machine Learning & Data Science Challenge 2026 asked entrants to predict how many transactions each customer would make over the next three months, the Nov 2015 to Jan 2016 holiday window, from historical transactions, demographics, and financial data. It matters commercially because transaction forecasts drive resource planning, cash management, and customer engagement. It is hard because the target is heavily skewed (most customers transact rarely, a few hundreds of times) and the prediction window is exactly when behaviour diverges from the norm. The competition is judged purely on measurable model performance, which is the evidence-led standard Nullius is built on.

The problem

Turn 18 million raw transaction rows across roughly 12,000 customers into an accurate, honest forecast, under a strict objective metric (RMSE in log space), without leaking future information, and against 251 entrants including established teams. The real difficulty was the skewed target and the holiday-period behaviour shift, where naive models fall apart.

Approach

We built a reproducible end-to-end pipeline. A temporal panel was constructed over nine historical cutoff dates so the model learned from many points in time, with strict date filtering to prevent any future-information leakage. From that we engineered 266 features: temporal lags, rolling statistics, year-on-year seasonal comparisons, trend slopes, demographics, financials, and calendar seasonality. The breakthrough was leave-one-cutoff-out target encoding on high-cardinality categories (such as residential city, 550 levels) with smoothing to prevent overfitting. We trained a diverse ensemble, LightGBM, XGBoost, and CatBoost plus a Tweedie-objective model for diversity, each bagged across seeds, tuned with Optuna. The final model used segmented blending: customers were split by recent activity (low, mid, high) and per-segment blend weights were optimised with SLSQP against a held-out month, because sparse and heavy users need different model mixes. Everything runs from one command in about 30 minutes using Polars lazy evaluation.

Tech stack

PythonPolarsLightGBMXGBoostCatBoostOptunascikit-learn

Outcome

A 3rd-place finish out of 251 active entrants on the final private leaderboard, with a private RMSE of 0.3631 in log space (public 0.3753) from the per-segment blend. Just as important: the whole result reproduces from code and data in about 30 minutes, with built-in validation checks on every prediction, no manual steps, no fragile notebooks. It is proof that we take a messy, real-world data problem to a model that performs under objective scrutiny, and that we can prove it again on demand.

“Judged on numbers against 251 teams, the model finished third. That is the only kind of proof that counts.”

Nullius Technologies

Published June 2026

Book a similar engagement

This was a data science engagement. Let's talk about yours.

Book a discovery call