Premier League predictor: Dixon-Coles + Monte Carlo + FPL recommender (TUI / web / CLI).
Project description
pl-winner
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
Quickstart
1. Install
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]'
2. Run
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 :8501
3. Or run with Docker
docker compose up
# → http://localhost:8501
What you get
| 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 options
How it works
Match outcomes — Dixon-Coles
Each team has an attack rating $\alpha_i$ and a defense rating $\delta_i$. Expected goals are
$$\lambda_{home} = e^{\alpha_h + \delta_a + h}, \qquad \mu_{away} = e^{\alpha_a + \delta_h}$$
A correlation term $\tau(\cdot, \rho)$ corrects 0-0 / 1-0 / 0-1 / 1-1 dependence that pure independent Poissons miss. Fit by weighted MLE with exponential time decay (default half-life 180 days, cross-validated optimum 270 days).
Title race — Monte Carlo
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.
FPL squads — ILP
Maximize $\sum_i \text{proj}_i \cdot x_i$ subject to:
- £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.
Honest framing
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.
Configuration
| 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).
Layout
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)
Data sources
- 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.
Caveats
- 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.
Privacy
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.
Releases
Tag a commit with vX.Y.Z to publish to PyPI via GitHub Actions
(uses PyPI Trusted Publishing — no
API tokens stored anywhere).
make release-check # build + twine check locally
git tag v0.2.0 && git push --tags
See CHANGELOG.md for release notes.
Contributing
See CONTRIBUTING.md. PRs welcome for modeling, FPL features,
tests. Run make test lint before opening a PR.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pl_winner-0.2.0.tar.gz.
File metadata
- Download URL: pl_winner-0.2.0.tar.gz
- Upload date:
- Size: 57.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c08a53c895c56fa60592c7666311cd98e58060c5290a587f47409558fdf1178c
|
|
| MD5 |
b27a447bbf6124cc93db142482de1936
|
|
| BLAKE2b-256 |
0e26aefd4339474867215ee84d57b4e770d4c53aac80eedde352a8b1e77e18f5
|
Provenance
The following attestation bundles were made for pl_winner-0.2.0.tar.gz:
Publisher:
release.yml on t-rhex/pl-winner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pl_winner-0.2.0.tar.gz -
Subject digest:
c08a53c895c56fa60592c7666311cd98e58060c5290a587f47409558fdf1178c - Sigstore transparency entry: 1481267828
- Sigstore integration time:
-
Permalink:
t-rhex/pl-winner@90fabd920155e26ebe229fa4bc1a6760c3969921 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/t-rhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@90fabd920155e26ebe229fa4bc1a6760c3969921 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pl_winner-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pl_winner-0.2.0-py3-none-any.whl
- Upload date:
- Size: 62.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4faccc067281871c96d071d29fe71c53cf8462e50bf1ea90034e02e5cb003b16
|
|
| MD5 |
75dbe63f3c8f7c89a113025abf325b60
|
|
| BLAKE2b-256 |
6ea665f48d113bd9c732801708f7ef1e2421bd1d32f475c899ffdd0cc7fd0a35
|
Provenance
The following attestation bundles were made for pl_winner-0.2.0-py3-none-any.whl:
Publisher:
release.yml on t-rhex/pl-winner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pl_winner-0.2.0-py3-none-any.whl -
Subject digest:
4faccc067281871c96d071d29fe71c53cf8462e50bf1ea90034e02e5cb003b16 - Sigstore transparency entry: 1481267918
- Sigstore integration time:
-
Permalink:
t-rhex/pl-winner@90fabd920155e26ebe229fa4bc1a6760c3969921 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/t-rhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@90fabd920155e26ebe229fa4bc1a6760c3969921 -
Trigger Event:
push
-
Statement type: