A fantasy lineup is downstream of uncertain player outcomes. I model that uncertainty directly, preserve the direction of time through training and evaluation, then hand the resulting forecasts to a solver that can respect the rules of the slate.
The architecture
Several sources enter through ingestion, integrity auditing and deduplication. NBA and WNBA then follow league-specific feature and inference paths while sharing the same pipeline. Forecasts feed a PuLP MILP solver, which selects an optimal lineup under hard constraints for either a single night or multiple nights.
The feature row is the hard part
Fitting a model is straightforward compared with assembling a truthful feature row for one player on one night. The local SQLite store is about 1.2 GB: 32 tables and roughly 7 million rows. It includes 2.24 million individual shot events, 1.97 million defender-assignment matchups and 1.46 million player game logs across 48 columns, plus multiple seasons of player tracking, hustle, play types, lineups, advanced box scores and betting odds. That detail lets the system reason about role and opponent, not merely recent averages. Injury, absence and role changes are explicit because expected output moves when a teammate sits. Ingestion also runs integrity audits and deduplication because sources disagree. The roughly 2,700-line feature layer joins it all without admitting anything that was unknown at tip-off.
Predict a range, not a point
The forecasting layer is 39 LightGBM quantile models: 13 correlated target statistics, each predicted at P10, P50 and P90. The output is a distribution rather than a single answer, so the model states its own uncertainty instead of hiding it behind one estimate.
The models train on an RTX 3090 GPU, with Optuna handling hyperparameter search. Statistics do not all behave alike. Bursty targets get their own settings: a lower learning rate, higher minimum child samples and lower feature fraction.
Time is part of the contract
Train, validation and test windows are separated by date and never overlap. Walk-forward temporal validation ensures that no future information can reach a fitted model. That separation is the point. A random split in a time-series problem silently leaks the future and flatters the model.
One practical constraint is encoded explicitly. Leaf count is capped at 31, because larger LightGBM models overflow the stack on Windows. The cap belongs in the system design, not in tribal knowledge about which training runs happen to survive.
Changes earn their way through
A backtest harness scores every change before it ships, and accuracy-gated tests guard the pipeline. The same checks cover the shared flow while leaving each league's feature and inference logic separate. The repository is private.
Ken Faiman · Applied AI, agent & evaluation systems · faiman.com