Skip to content

Repository files navigation

FPL Optimizer

A Fantasy Premier League squad picker with a measurement harness attached, so model changes are judged on evidence rather than plausibility.

The project started as two files: a form/PPG scoring proxy and a MILP squad solver. The solver was already sound. The scoring half was weak, and there was no way to tell whether any change to it helped — so the first thing built here was the backtest, and everything after was decided by it.

Quick start

# tonight's squad, 90 minutes before the deadline
python pick_team.py --confidence 0.85

# ad-hoc squad
python fpl_optimizer.py --model ep --horizon 1

# is a model actually any good?
python fpl_backtest.py --compare --season 2025-26
python fpl_backtest.py --selftest          # prove the harness first

# interactive
streamlit run fpl_dashboard.py

Requires pulp pandas numpy requests streamlit altair beautifulsoup4 lxml.

Modules

File Role
fpl_data.py Live API, season snapshots, per-gameweek history; caches to data/
fpl_features.py One canonical feature frame, built identically live and historically
fpl_model.py Model registry: proxy, ppg, ep, v2, blend
fpl_backtest.py Walk-forward IC harness, squad simulation, and a self-test
fpl_optimizer.py The MILP: squad, XI, captain, transfers, forced picks
fpl_lineups.py Predicted-lineup adapter, plus the Fantasy Football Scout scraper
fpl_league.py Mini-league: effective ownership, Monte Carlo, P(win the week)
pick_team.py The pre-deadline run — scrape, rebuild, rank captains, diff
fpl_dashboard.py Streamlit front end over the same functions

What the backtest found

Measured over 2023-24 and 2024-25 (37 gameweeks each), scored by mean rank information coefficient — the Spearman correlation between predicted and actual points, recomputed each gameweek.

Model 2023-24 2024-25 Squad sim (GW6-38, 2023-24)
proxy (the original) 0.342 0.346 1820 pts
v2 (rules decomposition) 0.453 0.449 2179 pts
ep (FPL's own estimate) 0.650 0.619 2350 pts

FPL's published ep_next beats everything built here, by 530 points a season over the original proxy. Three separate attempts to beat it failed: a hand-tuned convex blend, a weight sweep, and a fitted ridge stack with ep as a feature (0.621 against ep alone at 0.643). There is no more signal to extract by recombining the existing data.

Much of ep's edge is that FPL zeroes it for unavailable players — real pre-deadline information. Any blend must preserve those zeros; backfilling them with a history-based score promotes exactly the players about to score nothing, and cost 0.2 IC when tried.

Exception — pre-season. ep_next compresses to 26 distinct values capped at 4.0, which flattens the premiums (Haaland and a £4.5m goalkeeper score identically). For gameweek 1 only, use --model v2 --ep-blend 0. From GW2, use ep.

Where the remaining headroom is

Replacing estimated minutes with perfect team news takes v2 from 0.449 to 0.725 — bigger than the gap between any two models here, and past ep. The break-even against ep sits at only ~65% lineup accuracy; public feeds run 80-90%.

History-based minutes prediction already reaches ~0.78 IC against actual minutes. The oracle needs 1.00. That gap is information, not modelling — which is why fpl_lineups.py exists and why four internal improvements were tested and rejected:

Idea Result
Shorter minutes window +0.059 IC, but an artefact of the pool filter; squad sim split across seasons. Reverted.
Fitted ridge stack 0.621 vs ep's 0.643 out-of-sample
Team strength ratings over FDR 0.349 vs FDR's 0.361, and that was an optimistic upper bound
FPL's own news as a lineup source Already wired into avail; changed 0 squad picks

Winning a mini-league is a different problem

Your points relative to the field from one player are (your_multiplier - EO) * points, where EO is effective ownership. Summed over a squad, the EO term is a constant that does not depend on your picks — so maximising expected relative points has the identical optimum to maximising expected points. Ownership cannot matter through the mean.

It matters through variance: a high-EO player moves you with the field, a differential moves you against it. So fpl_league.py simulates player scores and rival squads and estimates P(finish top) directly.

Caveat: before a deadline no rival's picks are public, so rivals are synthesised from global ownership and are weaker than real managers. Absolute P(win) reads far too high; only the comparison between options is meaningful. After a deadline the tool switches to real picks automatically.

Trusting the harness

A backtest that quietly reads the future produces beautiful numbers. Three checks run under --selftest:

  1. No target leakage — scrambling every stat from gameweek t onward must leave the features for t bit-identical. (The assertion inside build_features only restates the filter above it and can never fire; this perturbation test is the one that would actually catch leakage.)
  2. Sensitivity — a model that peeks at the answer must score IC ≈ 1.0, proving the harness can see signal at all.
  3. No false signal — that same model against a shuffled target must score ≈ 0.

Known limitations

  • GW1 is unmeasurable. merged_gw carries no prior-season data, so the backtest starts at GW2. Everything said about gameweek 1 rests on inspection, not measurement.
  • The lineup scraper is unvalidated. Its accuracy cannot be checked until matches are played. --confidence is an assumption until then.
  • Scrapers are brittle. A site redesign breaks the parse; it fails loudly on a zero-row scrape rather than returning garbage.
  • defensive_contribution only exists from 2025-26, so that season is the primary validation set and earlier ones are robustness checks.
  • Squad simulation simplifies: players sell at current price (no 50% sell-on fee) and chips are never played.

Not built yet

Multi-week transfer planning (currently one-week greedy), chip timing (Bench Boost and Triple Captain are live from GW1 and completely ignored), bookmaker-odds integration, and per-position models.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages