← StrikerAPICONTENT HISTORY
WHAT CHANGED
Update to StrikerAPI
Snapshot Oct 30, 2026 · 00:06 UTC · version 1.0.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save StrikerAPI and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Builds soccer score models: independent Poisson goals from attack and defence strengths, the Dixon-Coles low-score correction with time decay, scoreline matrices, and the home/draw/away, over/under and handicap probabilities that fall out of them, validated against closing odds. Use when the user wants to predict scores, price totals or handicaps, fit team strengths, or turn xG or Elo into match probabilities. Do not use for rating-only win probabilities, see elo-ratings. Do not use for goalscorer probabilities, see goalscorer-prop-modeling.",
"included_files": [],
"name": "poisson-score-models",
"skill_md_contents": "---\nname: poisson-score-models\ndescription: \"Builds soccer score models: independent Poisson goals from attack and defence strengths, the Dixon-Coles low-score correction with time decay, scoreline matrices, and the home/draw/away, over/under and handicap probabilities that fall out of them, validated against closing odds. Use when the user wants to predict scores, price totals or handicaps, fit team strengths, or turn xG or Elo into match probabilities. Do not use for rating-only win probabilities, see elo-ratings. Do not use for goalscorer probabilities, see goalscorer-prop-modeling.\"\nmetadata:\n version: 1.0.0\n author: StrikerAPI\n---\n\n# Poisson Score Models\n\n> **Default data tool:** StrikerAPI (`strikerapi-tool`).\n> `list_games` (2 credits per call, up to 200 matches) supplies scores. `get_team_stats` (5) supplies xG for and against. `get_odds` (5 per match, 2 per match by season) supplies the benchmark.\n\nGoals in soccer are rare, roughly independent events, which is why a Poisson distribution fits them tolerably well. A score model predicts a goal rate for each side and turns the two rates into a full scoreline matrix. From that one matrix you get every market: home/draw/away, totals, handicaps and exact score.\n\n## When to Use\n\n- \"What is the probability Arsenal win 2-1?\" / \"P(over 2.5)?\"\n- \"Fit attack and defence strengths for the league\"\n- \"Turn xG or Elo into match probabilities\"\n- Producing a model probability to compare with a devigged market\n\n## When NOT to Use\n\n- A single rating-based expected score: `elo-ratings`\n- Anytime goalscorer pricing: `goalscorer-prop-modeling` (player-level, different structure)\n- Evaluating a finished model: `walk-forward-validation`, `probability-calibration`\n\n## Commands Available\n\n| Command | What It Does | Credits |\n|---------|-------------|---------|\n| `list_games` | Scores by `season`, `status: \"final\"`, `limit` 200, `offset`; the training data | 2 |\n| `get_team_stats` | `goals_for`, `goals_against`, `xg_for`, `xg_against` per match, all clubs | 5 |\n| `get_ratings` | Elo input | 2 |\n| `get_odds` | Closing three-way prices, handicap and total, for benchmarking | 5 or 2 per match |\n| `get_schedule` | Fixtures to predict, with the median line | 2 |\n\n## Commands That Do NOT Exist\n\n| Not Available | Use Instead |\n|--------------|-------------|\n| `get_prediction`, `get_forecast` | Fit the model yourself with the code below |\n| `get_btts`, `get_correct_score_odds` | Not available. Both-teams-to-score and correct-score prices are not provided, so those outputs cannot be benchmarked here |\n| `get_expected_lineup` | Not available |\n\n## The Model\n\nFor home club `i` against away club `j`:\n\n```\nlambda_home = exp(attack_i + defence_j + home_advantage) goals the home side is expected to score\nmu_away = exp(attack_j + defence_i) goals the away side is expected to score\nP(home = x, away = y) = Poisson(x; lambda) * Poisson(y; mu) * tau(x, y)\n```\n\n`attack` is high for strong attacks. `defence` is high for leaky defences (it adds to the opponent's goals). Fix the average of each set to zero so the model is identified.\n\n**Independent Poisson** sets `tau = 1`. It underestimates 0-0 and 1-1 and overestimates 1-0 and 0-1.\n\n**Dixon-Coles** corrects the four low scores with a single parameter `rho`:\n\n```\ntau(0,0) = 1 - lambda * mu * rho tau(0,1) = 1 + lambda * rho\ntau(1,0) = 1 + mu * rho tau(1,1) = 1 - rho\ntau = 1 otherwise\n```\n\n`rho` is usually slightly negative: more 0-0 and 1-1 than independence implies.\n\n**Time decay** weights each match by `exp(-xi * days_ago)`. A half-life of a year or so (`xi` near 0.0019 per day) is a common starting point; tune it.\n\n## Fitting\n\nTraining data is cheap. A Premier League season is 380 matches: two `list_games` calls, 4 credits.\n\n```python\nrows = []\nfor off in (0, 200):\n rows += call(\"list_games\", season=2025, status=\"final\", limit=200, offset=off)[\"games\"]\n```\n\nThen fit by weighted maximum likelihood. Tested against simulated data (recovered attack strengths with 0.95 correlation):\n\n```python\nimport numpy as np\nfrom scipy.optimize import minimize\nfrom scipy.special import gammaln\n\ndef tau(x, y, lam, mu, rho):\n t = np.ones_like(lam)\n t = np.where((x == 0) & (y == 0), 1 - lam * mu * rho, t)\n t = np.where((x == 0) & (y == 1), 1 + lam * rho, t)\n t = np.where((x == 1) & (y == 0), 1 + mu * rho, t)\n t = np.where((x == 1) & (y == 1), 1 - rho, t)\n return t\n\ndef fit_dixon_coles(home_idx, away_idx, hg, ag, days_ago, n_teams, xi=0.0019):\n hg, ag = np.asarray(hg), np.asarray(ag)\n w = np.exp(-xi * np.asarray(days_ago))\n\n def unpack(p):\n att = p[:n_teams] - p[:n_teams].mean()\n dfn = p[n_teams:2 * n_teams] - p[n_teams:2 * n_teams].mean()\n return att, dfn, p[-2], p[-1]\n\n def nll(p):\n att, dfn, home, rho = unpack(p)\n lam = np.exp(att[home_idx] + dfn[away_idx] + home)\n mu = np.exp(att[away_idx] + dfn[home_idx])\n t = tau(hg, ag, lam, mu, rho)\n if np.any(t <= 0):\n return 1e10\n ll = (np.log(t) + hg * np.log(lam) - lam - gammaln(hg + 1)\n + ag * np.log(mu) - mu - gammaln(ag + 1))\n return -(w * ll).sum()\n\n p0 = np.zeros(2 * n_teams + 2)\n p0[-2] = 0.25\n res = minimize(nll, p0, method=\"L-BFGS-B\",\n bounds=[(None, None)] * (2 * n_teams) + [(-0.5, 1.0), (-0.3, 0.3)])\n return unpack(res.x)\n```\n\n## From Rates to Markets\n\n```python\ndef score_matrix(lam, mu, rho, max_goals=10):\n g = np.arange(max_goals + 1)\n ph = np.exp(-lam + g * np.log(lam) - gammaln(g + 1))\n pa = np.exp(-mu + g * np.log(mu) - gammaln(g + 1))\n m = np.outer(ph, pa)\n m[0, 0] *= 1 - lam * mu * rho\n m[0, 1] *= 1 + lam * rho\n m[1, 0] *= 1 + mu * rho\n m[1, 1] *= 1 - rho\n return m / m.sum()\n\ndef one_x_two(m):\n return np.tril(m, -1).sum(), np.trace(m), np.triu(m, 1).sum() # home, draw, away\n```\n\nWith `m[i, j]` the probability of home `i`, away `j`:\n\n| Market | Formula |\n|--------|---------|\n| Home / draw / away | `one_x_two(m)` |\n| Over 2.5 | `1 - sum(m[i, j] for i + j <= 2)` |\n| Exact score 2-1 | `m[2, 1]` |\n| Home -1 (win by 2+) | `sum(m[i, j] for i - j >= 2)` |\n| Asian handicap -0.5 | Same as the home win probability; whole and quarter lines need push and half-stake handling |\n| Both teams to score | `1 - m[0, :].sum() - m[:, 0].sum() + m[0, 0]` (model only; no price here to compare) |\n\nQuarter-total and handicap lines such as 2.75 settle as two half-stakes at 2.5 and 3.0. Price each half separately and handle the push at the whole line.\n\n## Worked Example (back-of-envelope, from `get_team_stats`)\n\nA quick multiplicative version, before any fitting, using the 2025-26 per-match xG from `get_team_stats {\"season\": 2025}`. League average xG for per club per match was 1.47. Arsenal: 1.90 for, 0.83 against. Chelsea: 1.73 for, 1.36 against.\n\n```\nlambda_ARS = 1.90 * 1.36 / 1.47 = 1.76 mu_CHE = 1.73 * 0.83 / 1.47 = 0.98\n```\n\nPassing 1.76 and 0.98 to `score_matrix` (no home bonus, because season averages mix home and away matches) with `rho` of 0 and then -0.05:\n\n```\nrho home win draw away win over 2.5 1-1 2-1 1-0\n0.00 0.557 0.234 0.208 0.515 0.111 0.098 0.114\n-0.05 0.552 0.246 0.203 0.515 0.117 0.098 0.109\n```\n\nA small negative `rho` moves about 1.2 points of probability into the draw and leaves the total alone, which is the expected pattern. This is a sketch, not a forecast: it ignores the home bonus, lineups and the opposition faced. A real fit (above) estimates strengths jointly.\n\n## Using xG and Elo as Inputs\n\n- **xG-based rates.** Replace goals with xG in the strength estimates (`get_team_stats` gives `xg_for` and `xg_against` per match). xG is less noisy than goals over a short window. Fit on the same likelihood, treating xG as the scored-goals analogue, or blend goals and xG.\n- **Elo-based rates.** Map the Elo gap to a goal difference with a regression fit on history, set the total from the league mean, and solve for `lambda` and `mu`.\n- **Shrinkage.** For clubs with few matches (promoted sides, early season), shrink attack and defence toward the league mean in proportion to matches played.\n\n## Validation\n\n1. Walk forward. Fit on matches before date `t`, predict the matches on `t`, step forward. Never random-split. See `walk-forward-validation`.\n2. Score with the ranked probability score (RPS) and log loss across home/draw/away, not accuracy. See `probability-calibration`.\n3. Benchmark against the devigged closing prices from `get_odds` (see `odds-analysis`). If you cannot beat the market's RPS, you have a model of what the market already knows. That is the usual outcome for a goals-only model; the useful question is where it differs and whether those differences are signal.\n4. Check totals separately: calibrate P(over) by decile against the actual over rate.\n\n## Pitfalls\n\n1. **Leakage.** Using season-end strengths for early-season matches. Refit as you go, or use the time-decayed fit frozen at the prediction date.\n2. **Home advantage differs by league and era.** Fit it per competition. Neutral games (finals, some European ties) have none.\n3. **Cup and European matches.** Mixed-competition fits need strength links between leagues; avoid unless you model it.\n4. **Small samples.** Fewer than about 15 matches for a club makes its strengths unreliable.\n5. **Overdispersion.** Real totals have a fatter right tail than Poisson. A negative binomial or a mixture can help totals pricing.\n6. **Score effects.** Teams ahead take their foot off. Pure Poisson ignores game state.\n7. **Lineups.** Strengths describe the average squad, not tonight's.\n\n## Anti-patterns\n\n| Rationalization | Why It's Wrong | Do This Instead |\n|----------------|---------------|-----------------|\n| \"Random 80/20 split\" | Uses the future to predict the past | Walk forward by date |\n| \"Poisson says 3-1 is the pick\" | Most likely scoreline is rarely likely | Report probabilities |\n| \"Accuracy 52%\" | Draw and close calls hide quality | RPS and log loss against the closing line |\n| \"Use goals_for as the attack rate\" | Opponent quality is ignored | Fit attack and defence jointly |\n\n## Output Format\n\n```\n[Home] v [Away] - model lambda [x.xx], mu [x.xx], rho [x.xxx], fit through [date]\nHome / Draw / Away: [p] / [p] / [p] (market no-margin: [p] / [p] / [p])\nOver 2.5: [p] BTTS (model only): [p] Most likely scores: 1-1 [p], 1-0 [p], 2-1 [p]\nBacktest RPS: model [x.xxx], market [x.xxx], n = [matches]\n```\n\n## What to Do Next\n\n| What You Found | Next Action | Skill |\n|----------------|-------------|-------|\n| Model probabilities | Check calibration | `probability-calibration` |\n| Need a proper test | Walk-forward harness | `walk-forward-validation` |\n"
}SHA-256 of public snapshot: 952478841623b96ae4207380b9679e365af0fe241b9b0d3e5ab44b7bf60f88c8