Premier League title-race predictor and Fantasy Premier League recommender. Dixon-Coles + Monte Carlo for match outcomes. PuLP ILP for FPL squad selection. Terminal UI, web UI, and a single
pl-winnerCLI.
$ pl-winner predict --runs 10000
...
Predicted champion: Arsenal (P = 87.1%)
$ pl-winner fpl
=== ILP-optimal 15-man squad (Β£100m, max 3 per club) ===
cost Β£86.1m squad pts 209.3 XI pts 163.5 captain Cherki vice Doku
pip install pl-winner # core CLI + TUI
pip install 'pl-winner[web]' # + Streamlit web UIβ¦or from source:
git clone https://github.com/t-rhex/pl-winner && cd pl-winner
python -m venv .venv && source .venv/bin/activate
pip install -e '.[web]'pl-winner predict # title race + simulation projections
pl-winner fpl # top picks, captains, ILP squad, chips
pl-winner tui # interactive Textual UI (8 tabs)
pl-winner web # Streamlit web UI on :8501docker compose up
# β http://localhost:8501π https://pl.andrewadhikari.com β same app, deployed to Fly.io, auto-updated on every commit. See DEPLOY.md for how it's wired up.
| Command | Output |
|---|---|
pl-winner predict |
Title / top-4 / relegation probabilities for every team |
pl-winner fixtures |
Every remaining fixture with model H/D/A probs |
pl-winner backtest |
Walk-forward title hit-rate + match log-loss vs Bet365 |
pl-winner fpl |
Top 8 per position, captains, ILP-optimal 15, differentials, chip advice |
pl-winner value |
Brier / log-loss with bootstrap CIs, ROI of edges, break-even odds |
pl-winner league --league-id 314 |
Mini-league finish-position probabilities |
pl-winner track record/score/report |
SQLite log of predictions scored against actuals |
pl-winner tune |
Cross-validate the half-life parameter |
pl-winner tui |
Interactive 8-tab terminal UI |
pl-winner web |
Streamlit web app with the same data + Plotly charts |
pl-winner --help # full subcommand list
pl-winner fpl --help # per-subcommand optionsEach team has an attack rating
A correlation term
For each remaining fixture build the joint score pmf, sample 10k full seasons, count how often each club finishes 1st / top-4 / bottom-3. Vectorized, ~50ms per 1k seasons.
Maximize
- Β£100m budget
- 2 GK / 5 DEF / 5 MID / 3 FWD
- β€ 3 per club
- All players available (injury / suspension filtered)
Solved with PuLP / CBC. The same ILP in Free Hit mode (single-GW) and Wildcard mode (re-pick over remaining GWs) underpins the chip advisor.
The model is well-calibrated (reliability table ticks the diagonal) but doesn't beat Bet365's closing line on Brier or log-loss β we verified this with bootstrap CIs and the diff is statistically significant. Useful as a probability estimator and FPL fixture-difficulty signal; don't treat the break-even odds as a money printer against sharp markets.
| Env var | Purpose | Default |
|---|---|---|
PL_WINNER_DATA_DIR |
Where caches and SQLite live | <repo>/data |
STREAMLIT_SERVER_PORT |
Web UI port | 8501 |
Caches honor TTLs (FPL bootstrap: 6h; player history: 24h; match CSVs: forever β pass --refresh).
src/ # pl_winner package
cli.py # `pl-winner` entry, subparsers
commands/ # one module per subcommand
data.py # match data (E0/E1/SP1/D1/I1/F1/N1/P1)
model.py # Dixon-Coles
simulate.py # Monte Carlo
fpl.py # FPL API client + projections
fpl_optimizer.py # PuLP ILP (squad / Free Hit / Wildcard / transfers)
chips.py # Triple Captain / Bench Boost
league.py # mini-league simulator
value.py # implied probabilities, EV, break-even
calibration.py # Brier, log-loss, bootstrap CIs, reliability
tracker.py # SQLite log
tune.py # half-life CV
elo.py # Elo + DC hybrid (kept for experiments)
http_utils.py # robust HTTP with retries + cache TTL
paths.py # data-dir resolution
tui.py # Textual TUI
app/
streamlit_app.py # web UI
tests/ # pytest suite (~50 tests)
- Match results / odds: football-data.co.uk β free CSVs, no API key
- FPL data: official FPL public API β no API key
- Live odds for unplayed matches: intentionally not scraped (ToS-grey, fragile per-bookmaker)
All requests retry with exponential backoff, cache to disk with TTLs, and degrade gracefully when the API is unavailable or a season hasn't been published.
- Dixon-Coles is symmetric across clubs β doesn't model transfers/managerial changes/fatigue beyond the time-decay weight.
- Promoted clubs have little prior history; ratings stabilize as the season progresses.
- The mini-league simulator uses Normal samples around player projections (Ο β β(ΞΌ+1)) β adequate for ranking but conservative on tail outcomes.
- 10k Monte Carlo simulations: title-probability SE β 0.5pp at pβ0.5. Bump
--runsfor tighter intervals. - ILP is "optimal under the projection" β the projection itself has noise, so don't read Β£0.1m / 0.05-pt differences as meaningful.
pl-winner makes no telemetry calls. The only network traffic is to
football-data.co.uk for match CSVs and
fantasy.premierleague.com/api
for FPL data. Caches stay on your machine. Streamlit usage stats are disabled.
See SECURITY.md for the full posture and how to report vulnerabilities.
Three workflows automate the entire release flow β no API tokens stored anywhere (uses PyPI Trusted Publishing).
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
push, PR | tests + ruff + smoke on Python 3.10/3.11/3.12 |
cut-release.yml |
manual (Actions tab) | bumps version + CHANGELOG, commits, tags, pushes |
release.yml |
tag v* push |
builds, twine-checks, smoke-installs, publishes to PyPI, creates a GitHub Release |
deploy.yml |
CI passes on main | deploys the Streamlit app to Fly.io at pl.andrewadhikari.com |
Run the Cut release workflow
with a bump type (patch / minor / major / explicit 0.4.2):
Actions β Cut release β Run workflow β bump: patch β Run
This handles the full chain: version bump β CHANGELOG roll β commit β tag β
which triggers release.yml β which publishes to PyPI and drafts a GitHub
release. End-to-end, ~3 minutes.
make release-check # build + twine check locally
python tools/bump_version.py patch
git commit -am "Release v$(grep '^version' pyproject.toml | cut -d'"' -f2)"
git tag "v$(grep '^version' pyproject.toml | cut -d'"' -f2)"
git push origin HEAD --tagsSee CHANGELOG.md for release notes.
See CONTRIBUTING.md. PRs welcome for modeling, FPL features,
tests. Run make test lint before opening a PR.
MIT β see LICENSE.