Published 26 June 2026 pythonoptimizationmachine-learning

Can operations research beat gut-feel in Fantasy Premier League?

Applying Mixed-Integer Linear Programming and machine learning to FPL squad selection — and discovering that the best predictor doesn't always win.

Every Monday morning, millions of FPL managers stare at the transfer page and ask the same question: who do I bring in this week? Most rely on form, fixtures, and a vague sense that a player is “due a haul.” I wanted to see what happens when you replace that intuition with a solver.

The idea is straightforward. Fantasy Premier League is a constrained optimization problem. You have a budget, positional requirements, a limit of three players per club, and a transfer economy that penalizes churn. If you can predict how many points each player will score over the next few gameweeks, you can hand those predictions to a Mixed-Integer Linear Program and let it find the mathematically optimal squad.

The optimization model

We formulate FPL squad selection as a Mixed-Integer Linear Program. The solver maximises expected points over a rolling horizon of gameweeks, subject to all FPL game rules: budget, squad composition, and transfer costs.

Show code
In [1]:
formulation = """\
Objective
  max  Z = \u03a3_w  \u03b3^w \u00d7 (SquadPoints_w \u2212 TransferCost_w)

  SquadPoints_w = \u03a3_i  xp_iw \u00d7 xi_iw  +  xp_cw \u00d7 captain_w   (captain scores double)
  TransferCost_w = transfer_cost \u00d7 max(0, transfers_w \u2212 free_transfers_w)

Subject to
  \u03a3_i squad_iw = 15                                    (squad size)
  \u03a3_i xi_iw   = 11                                     (starting XI)
  squad composition: 2 GKP, 5 DEF, 5 MID, 3 FWD        (positional limits)
  \u03a3_{i \u2208 team_t} squad_iw \u2264 3    \u2200t                      (max 3 per club)
  \u03a3_i cost_i \u00d7 squad_iw \u2264 budget_w                       (budget)
  xi_iw \u2264 squad_iw                                      (can only start squad members)
  captain_w \u2208 {xi_iw}                                   (captain must be in XI)

  \u03b3 = 0.95  (decay),  transfer_cost = 4 pts,  horizon = 5 GWs
"""
print(formulation)
Objective
  max  Z = Σ_w  γ^w × (SquadPoints_w − TransferCost_w)

  SquadPoints_w = Σ_i  xp_iw × xi_iw  +  xp_cw × captain_w   (captain scores double)
  TransferCost_w = transfer_cost × max(0, transfers_w − free_transfers_w)

Subject to
  Σ_i squad_iw = 15                                    (squad size)
  Σ_i xi_iw   = 11                                     (starting XI)
  squad composition: 2 GKP, 5 DEF, 5 MID, 3 FWD        (positional limits)
  Σ_{i ∈ team_t} squad_iw ≤ 3    ∀t                      (max 3 per club)
  Σ_i cost_i × squad_iw ≤ budget_w                       (budget)
  xi_iw ≤ squad_iw                                      (can only start squad members)
  captain_w ∈ {xi_iw}                                   (captain must be in XI)

  γ = 0.95  (decay),  transfer_cost = 4 pts,  horizon = 5 GWs

The decision variables are all binary: is player *i* in the squad in week *w*? In the starting XI? Captain? The solver explores the full combinatorial space and returns a provably optimal plan — which transfers to make, who to start, who to captain — across the entire horizon.

Show code
In [2]:
import pandas as pd

backtest = pd.read_csv("data/backtests/comparison_20260215_233723/backtest_summary.csv")
backtest[["predictor", "point_spearman", "point_rmse", "point_mae", "point_top_n_points"]]
PredictorSpearman ρRMSEMAETop-N pts
OpenFPL (self-trained)0.7631.410.853249
LightGBM0.6691.991.02984
Heuristic0.6532.241.15965
OpenFPL (pre-trained)0.6502.001.15983

OpenFPL dominates on prediction quality: Spearman 0.76 vs. 0.67 for LightGBM. But better predictions don't automatically mean more FPL points.

Show code
In [3]:
strategy = pd.read_csv("data/backtests/comparison_20260215_233723/strategy_summary.csv")
strategy[["predictor", "total_raw_points", "total_hit_points", "total_net_points",
          "total_transfers", "total_paid_transfers"]].sort_values("total_net_points", ascending=False)
PredictorRaw ptsHit ptsNet ptsTransfersPaid transfers
LightGBM107041066251
Heuristic11411889537147
OpenFPL (self-trained)9791248555531
OpenFPL (pre-trained)8410841240

The heuristic predictor produces high expected-point predictions that tempt the solver into excessive transfers — 47 paid transfers costing 188 points in hits. LightGBM's more conservative predictions lead to only 1 paid transfer across 24 gameweeks, finishing with the highest net score despite ranking second in raw prediction accuracy.

The solver uses PuLP to formulate the problem as a MILP. Every decision — is this player in my squad, in my starting XI, my captain? — is a binary variable. The objective maximizes expected points across a multi-gameweek horizon, with a decay factor that discounts future weeks (because predictions get less reliable further out) and a penalty for paid transfers.

The constraints encode the FPL rulebook: exactly 15 players in the squad (2 goalkeepers, 5 defenders, 5 midfielders, 3 forwards), 11 in the starting XI, at most 3 from any single club, and a budget that tracks buying and selling prices. Free transfers roll over between gameweeks up to a cap of 5; additional transfers cost 4 points each.

What makes this a multi-period problem rather than a simple one-shot is that the solver plans transfers across the full horizon simultaneously. It might hold off on a transfer this week if the player becomes cheaper next week, or bring someone in early to captain them for a favorable fixture.

Prediction matters — but not the way you’d expect

The solver is only as good as its input predictions. I tested four predictors against the 2024/25 season:

  • Heuristic: a weighted average of recent points and fixture difficulty — the kind of logic most FPL managers use implicitly.
  • LightGBM: a gradient-boosted model trained on rolling player statistics.
  • OpenFPL (self-trained): a per-position Random Forest and XGBoost ensemble, following the approach from the OpenFPL paper, trained on five seasons of historical data enriched with Understat xG/xA metrics.
  • OpenFPL (pre-trained): the same architecture using pre-trained model weights from the OpenFPL GitHub repository.
Prediction accuracy: Spearman rank correlation (x100)

OpenFPL’s self-trained ensemble clearly produces the best predictions. A Spearman correlation of 0.76 means it ranks players by expected output substantially better than the alternatives.

But prediction quality alone does not determine squad performance. The solver acts on those predictions, and the interaction between prediction characteristics and the optimizer’s transfer behavior is where things get interesting.

Season backtest: net points after transfer costs (24 GWs)

LightGBM finishes first with 1,066 net points despite ranking second in prediction accuracy. The heuristic predictor actually generates the most raw points (1,141) but burns 188 of them on transfer hits — 47 paid transfers across 24 gameweeks. The solver, faced with the heuristic’s inflated predictions, sees large expected gains from swapping players every week and churns aggressively.

This is a well-known dynamic in operations research: the quality of an optimization solution depends not just on the model but on the quality and calibration of its inputs. A predictor that systematically overestimates player scores will cause the solver to over-trade, because the apparent gain from each transfer exceeds the 4-point hit cost. LightGBM’s more conservative, better-calibrated predictions lead to only a single paid transfer across the entire season.

Prediction accuracy over time

The aggregate Spearman numbers hide how volatile week-to-week accuracy is. The line chart below shows each predictor’s rolling accuracy across the season.

Spearman correlation by gameweek

OpenFPL is consistently the best ranker — it starts strong and stays above 0.7 all season. LightGBM has a terrible GW2 (0.21 — effectively random) because it hasn’t seen enough in-season data yet, but recovers quickly. The heuristic converges with LightGBM by mid-season — it ranks players almost as well, but its magnitude estimates are wildly off, which is what causes the transfer churn.

The transfer cost trap

The numbers tell a clear story about the tension between prediction and action. The heuristic generates predictions with a mean error of -44 points per gameweek — it consistently overestimates. When the solver sees predicted XI scores of 80-100+ points, even a 4-point transfer cost looks trivially small. So it trades 3-5 players per week, racking up hits that eat nearly 17% of its raw output.

OpenFPL’s self-trained model sits in between: better-calibrated than the heuristic (mean error -8) but still confident enough to drive 31 paid transfers. LightGBM, with a mean error near zero, gives the solver little incentive to trade beyond free transfers.

The lesson for anyone building an optimization pipeline: the objective function and the prediction model are not independent components. A well-calibrated predictor is worth more than a highly accurate one, because it produces decisions the optimizer can trust.

What’s next

The current system runs as a CLI tool. Before each gameweek, it scrapes the latest data, generates predictions, solves the MILP, and outputs a recommended squad. I’ve been running it for my team (“Jim Simons,” team 7127411) since the start of the 2024/25 season.

There is more to explore: multi-horizon strategy with configurable decay, chip optimization (Triple Captain, Bench Boost, Wildcard, Free Hit), and risk-averse formulations that penalize high-variance players. The backtest framework makes it possible to evaluate these variants systematically.

Source code: fpl-analysis on GitHub.