Skip to main content

Premier League predictor: Dixon-Coles + Monte Carlo + FPL recommender (TUI / web / CLI).

Project description

pl-winner

CI PyPI Python License: MIT

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-winner CLI.

$ 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 --runs for 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pl_winner-0.2.0.tar.gz (57.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pl_winner-0.2.0-py3-none-any.whl (62.9 kB view details)

Uploaded Python 3

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

Hashes for pl_winner-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c08a53c895c56fa60592c7666311cd98e58060c5290a587f47409558fdf1178c
MD5 b27a447bbf6124cc93db142482de1936
BLAKE2b-256 0e26aefd4339474867215ee84d57b4e770d4c53aac80eedde352a8b1e77e18f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for pl_winner-0.2.0.tar.gz:

Publisher: release.yml on t-rhex/pl-winner

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pl_winner-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4faccc067281871c96d071d29fe71c53cf8462e50bf1ea90034e02e5cb003b16
MD5 75dbe63f3c8f7c89a113025abf325b60
BLAKE2b-256 6ea665f48d113bd9c732801708f7ef1e2421bd1d32f475c899ffdd0cc7fd0a35

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page