Haider TohaProjects

fpl analyser

by ·

an advanced fantasy premier league analytics platform combining machine learning, monte carlo simulations and mathematical optimization for data-driven fpl decisions.

live sitegithub

---

the problem

fantasy premier league presents a multi-period stochastic optimization problem. you have £100m to pick 15 players. each gameweek, you field 11 and they earn points based on real-life performance. traditional approaches rely on intuition and basic statistics. i wanted to take a quantitative approach and solve three fundamental challenges:

traditional approachfpl analyser approach
"this player looks good"expected points model with 50+ features
pick players you likeilp solver guarantees mathematical optimum
gut feel on transfersmulti-gameweek rolling horizon planning
hope for the bestprobability distributions and confidence intervals

---

system architecture

---

data pipeline

the fpl api provides all player, team and fixture data. the pipeline ingests raw json, normalises types and foreign keys, calculates derived metrics via ml models, caches with ttl-based expiration and serves via json responses.

endpointdataupdate frequencycache ttl
bootstrap-staticall players, teams, gwsdaily15-30 min
element-summary/{id}player history and fixturesdaily30 min
fixturesmatch schedule and resultsdaily15 min
event/{gw}/livelive scoresevery few min60 sec
entry/{id}manager squad and historyon request5 min

caching uses request deduplication (concurrent requests coalesce), conditional fetching with etags and tiered ttl (live data = short, static data = long).

---

expected points model

the model uses gradient boosting (xgboost) with 100-200 trees, max depth 4-6, l1/l2 regularization to prevent overfitting and learning rate decay. cross-validation tunes hyperparameters. separate models for each position capture position-specific patterns (goalkeepers score via saves and clean sheets, forwards via goals).

---

bayesian player modeling

early in the season, limited data means high uncertainty. bayesian updating shrinks estimates toward position averages (priors), then progressively trusts individual performance as more gameweeks accumulate.

hierarchical pooling: league average informs position priors, which inform individual player estimates.

---

fixture difficulty rating

fdr combines attack difficulty (how hard to score against this team) and defense difficulty (how likely opponent will score). metrics include goals conceded/scored per game, xG conceded/created, shot volume and quality, with home/away splits.

multi-gameweek aggregation uses geometric mean with time discounting. near fixtures weighted more heavily (gw+1 gets weight 1.0, gw+6 gets weight 0.75). this identifies favorable fixture runs for transfer targeting.

---

integer linear programming

squad selection is formulated as an ilp. let x_i \in \{0,1\} indicate whether player i is selected:

$\max \sum_{i=1}^{n} \mathbb{E}[\text{pts}_i] \cdot x_i$

subject to:

$\sum_{i=1}^{n} c_i \cdot x_i \leq 100 \quad \text{(budget)}$

$\sum_{i \in T_j} x_i \leq 3 \quad \forall j \quad \text{(max 3 per club)}$

plus position constraints (2 gk, 5 def, 5 mid, 3 fwd). the solution space is astronomical: C(700, 15) = 10^30+ possibilities. brute force is impossible.

ilp constraints define a convex polytope. lp relaxation + branch and bound finds the mathematically optimal solution in under 1 second. the pulp library with cbc solver handles the full ~700 player pool. unlike heuristics, ilp guarantees the best squad.

---

transfer planning (multi-period)

rolling horizon optimization plans transfers across 4-8 gameweeks. the objective maximizes total points minus transfer costs (4 points per extra transfer):

$\max \sum_{gw} (\text{points}[gw] - 4 \times \text{extra\_transfers}[gw])$

subject to squad validity each gameweek, transfer continuity between gameweeks and free transfer accumulation (max 2). output is an optimal transfer sequence: "gw 20: hold (bank transfer), gw 21: salah → saka, watkins → haaland (2 ft), gw 22: hold..."

---

monte carlo simulation

point predictions are uncertain. a player expected to score 6 might score anywhere from 0 to 20. the simulation engine runs 10,000 gameweeks:

FOR iteration = 1 to 10,000:
    For each player:
        sample points from player's distribution
    Apply captain multiplier (2x)
    Sum starting XI points
    Handle auto-substitutions
    Record: total_points[iteration]

AGGREGATE:
    • Mean (expected points)
    • Median
    • Standard deviation
    • 5th/95th percentiles (90% CI)
    • Full histogram

why negative binomial distribution?

normal distributionnegative binomial (actual)
symmetricright-skewed
allows negative pointsnon-negative
thin tailsheavy tails (hauls)

fpl points exhibit over-dispersion (variance exceeds mean) and heavy right tails (occasional 15+ point hauls). negative binomial captures this better than normal or poisson.

performance: naive python loops take ~30 seconds. numpy vectorization completes 10,000 simulations in ~0.1 seconds (300x speedup).

---

value over replacement (vor)

traditional view: "haaland: 8 pts/game, watkins: 5 pts/game, haaland is 3 pts better."

vor view considers opportunity cost. bench fwd averages 3 pts. haaland vor: 8 - 3 = 5. watkins vor: 5 - 3 = 2. per-million efficiency: haaland (14m) = 0.36 vor/m, watkins (8m) = 0.25 vor/m. haaland is more efficient despite the higher price.

---

core features

featureml modeloptimizersimulatorlive data
player analysis
squad optimizer
transfer planner
captain picker
gameweek sim
live tracking
league analysis
chip strategy
vor rankings
fixture analysis

transfer predictions: fixture swing analysis identifies teams whose fixtures are improving or worsening. transfer recommendations include urgency levels (immediate, soon, plan ahead), expected point gains and reasoning. rotation pairs find players who complement each other's fixtures for smart bench rotation.

live gameweek: real-time scores, bonus point predictions from bps standings, fixture status. polling every 60 seconds during active matches.

chip strategy: when to use bench boost, triple captain, free hit, wildcard based on fixture patterns and double gameweeks.

---

performance

operationtargettypicalmethod
player list< 200ms~100mscached data
player detail< 200ms~150mscached + computed
squad optimization< 1s~300msilp solver
simulation (10k)< 3s~1.5svectorized numpy
live scores< 500ms~200msshort-ttl cache

async event loop handles concurrent requests without blocking on network i/o. cpu-bound work (optimization, simulation) uses concurrency limiting to prevent overload.

---

results

consistently finished top 100k (out of ~10m players) without spending hours on team selection. the edge comes from discipline: the model doesn't get attached to players or chase last week's haul. beat my manual decisions in 75% of gameweeks.

---

stack: python 3.11, fastapi, pydantic, pulp + cbc, xgboost, numpy, httpx, uvicorn | next.js 14, react 18, typescript, tailwindcss, tanstack query, recharts, radix ui | render