log in
tennis methodology

Tennis Methodology

Surface-specific Elo + an age curve per court + draw-aware Monte Carlo. Grass-Elo gets a sample-size shrinkage toward overall Elo, but capped at 50% — Sinner's correction is preserved while small-sample players (Fils, Tien, Fonseca) no longer get their hard-court rating laundered through grass.

Tennis projections use a separate Elo per surface (hard / clay / grass). Each court has its own aging curve. Each tournament's bracket runs Monte Carlo over the published draw with the matchup-by-matchup probabilities the model assigns. The /wimbledon and /wimbledon/match pages surface those drivers explicitly — what's the grass Elo, what's the aging contribution — so the model's reasoning is auditable on every match.

Surface Elo (capped shrinkage) + an age curve per court + draw-aware MC

Each player carries a separate Elo per surface. Grass-Elo on small samples is shrunk toward the player's cross-surface overall Elo with strength K/(K+n_grass), capped at 50% pull — so a player with very few grass matches can borrow from overall but never to the point that overall replaces grass entirely. Each court has its own aging curve, and a player's age sets where they sit on it. A playing-style term (how one style of player fares against another) is currently switched off: it did not hold up on matches held out of the fit, so it moves no probability we publish. Tournament projections run Monte Carlo (10k sims) over the actual published draw.

Where the inputs come from

sources
ATP + WTA match results (Jeff Sackmann mirror), official Wimbledon draw JSON (wimbledon.com MS.json / LS.json), Wikidata for WTA DOB backfill, Tennis Abstract Match Charting Project for every per-player stroke rating
training
Multi-year per-surface Elo fit; aging curves fit on multi-decade player histories
holdout
Per-match Brier + tournament champion Brier evaluated post-tournament. The grass-Elo shrinkage cap (w_max=0.5) was set after Wimbledon 2026 audit found K=40 alone over-shrank n<20 players; 5 of 7 flagged calls flipped to the intuitive winner with the cap applied.

Out-of-sample performance

Measured on 5,288 matches from the 2024 season, held out of the fit.

63.5%
Matches called correctly
5,288 matches, both tours
64.0%
Men
2,875 matches
62.9%
Women
2,413 matches
Whether the confidence means anything: how often the pick actually won, grouped by how sure we said we were.
We saidIt happenedMatches
50-60%54%2,171
60-70%64%1,747
70-80%74%914
80%+84%456

Calling 63.5% of tennis matches is not the point — the favorite wins most of them. The point is the table: when the model says 74%, it happens 74% of the time. A confident number is worth what it claims to be worth.

metric
Per-match Brier + per-tournament champion Brier. Point-in-time OOS for surface shrinkage parameters.
value
Grass-Elo shrinkage K=40, w_max=0.5 (cap added 2026-06-28 after audit)

The shrinkage cap is the lesson from the K=40 lever: K=40 alone correctly un-suppressed Sinner (39 grass matches, raw 1843 → shrunk 1986), but it over-credited cross-surface form on players with <20 grass matches (Fils 11 matches got pulled 78% toward overall). Grass is a specialist surface — overall Elo is a noisy prior, not a clean substitute. The 50% cap keeps Sinner's correction (he was already at w=0.506) and stops the laundering. User explicitly rejected steepening the aging tail as an alternative Sinner fix — that would have been survivorship bias.

What controls the projection

Surface-specific Elo
Hard / clay / grass Elo fit separately. Players are not equally strong across surfaces — surface-specific Elo captures the gap.
Grass-Elo shrinkage K=40, capped at 50%
Grass tournaments are sparse per player per year. Players with few grass matches get their grass Elo pulled toward their overall (cross-surface) Elo, but never more than halfway. The cap is the honest bound — overall isn't a valid substitute for grass data on a specialist surface; you can borrow at most half your strength.
An age curve per court
Each court has its own aging curve, fit on multi-decade player histories; a player's age sets where they sit on it. Rejecting in-sample fits that steepen the tail to fix one player is a principle, not a debatable choice.
Playing-style matchup — switched off
A term for how one style of player fares against another (a big server against a counter-puncher, say) was built and tested, and is currently switched off: what it found in the fitting years did not hold up on later matches, so it moves no probability we publish. The one exception is Wimbledon 2026, published before it was switched off, where it shifts the chances on some pairings in the women's draw. It touched no men's match in either 2026 draw.
Draw-aware Monte Carlo
Tournament projections sim the actual draw, not an abstract bracket. Every potential matchup is simulated; the /wimbledon page's 'if they meet' picker and per-player champion% both come from the same 10k-sim distribution.
Stroke ratings (advanced)
A separate descriptive layer, not part of the match-projection engine: per-player ratings from Match Charting Project shot-by-shot data, z-scored within tour with tau=10 shrinkage and a 20-charted-match floor. Describes how a player wins points; not a beyond-Elo predictor. The ATP + WTA boards on /advanced carry the three original ratings — serve, forehand and backhand. The player and matchup cards carry five more built the same way from the same source: return, net, pressure, rally and variety. Variety is a style measure, the spread of shot types a player uses, so a high place means the most varied rather than the best.

Common questions

What does a percentile on a player or match card mean?
Every point on the serve, return, groundstroke and game diamonds is a place against the men in that event's draw, not against the whole tour and not against only the players who are hand-charted. Each group is weighted by ATP ranking band so that its middle — the 50th percentile — is the draw's own middle player, about the world #71 at a Slam. Hand-charted stats (return by wing, groundstrokes, long rallies, variety) reach only part of a draw, so the men we have not charted are estimated from their official ATP stats to fill out the group. An estimate is only ever part of the group a real figure is placed against; it never appears on a card.
What is a rating?
A rating is an Elo: a single number, already relative to everyone else, earned from results. We keep one per court. Hard and clay ratings drift back toward the tour average the longer a player goes without a match there; a grass rating built on few matches is pulled up to halfway toward the player's every-court rating. The percentile beside a rating is its place among the players in the draw who hold one on that court.
How does the surface switch work?
A player card opens on all courts together; a match card opens on the court the match is played on. Choosing another court recomputes every number that depends on it from that court's matches only — the rating, the serve and return win rates, hold and break rates, the hand-charted strokes, pressure, net play and variety — each placed against the draw on that court. A dash means the player has too few matches on that court to place fairly (for example fewer than ten matches in the official record, or fewer than 300 charted shots on a wing). The official ATP record does not include the Grand Slams, so grass figures there rest on the grass events outside Wimbledon.
Where do a match card's numbers on another court come from?
On the court they are really playing, the number is the one we publish. On any other court the same two players are run through the same formula with that court's ratings and that court's age curve: the rating gap converted to a probability and, over five sets, amplified; plus the age curve; then the whole thing calibrated. "All courts" uses each player's every-court rating and the age curve averaged across the three courts. Anything else the published number carries, such as form earned in this draw, is kept the same on every court. The parts shown under "What moves the number" add up to the headline figure.
How is a match split into scorelines?
From one number: the chance of winning any single set, solved so that sets played independently give the match probability we publish. That gives every way the match can end — straight sets, four, five — and they add to exactly one match. Nothing about the scoreline itself is modeled, so a love-and-love set and a tiebreak count the same.
Why does the model sometimes pick a less-accomplished player over a name?
Usually because of the small-sample shrinkage + aging-curve interaction. A young player with few grass matches gets their grass Elo pulled up toward their hard/clay overall (capped at 50%), and the aging curve credits players in the grass-peak window (roughly age 22-27). Combined, this can tip a close matchup toward the younger, less-grass-experienced player. The /wimbledon/match page shows exactly which driver is doing the work for any given match — if grass Elo is favoring the veteran but aging flips it, you'll see both contributions side by side.
Why grass-Elo shrinkage K=40 with a 50% cap?
Without shrinkage, sparse grass samples produce artifacts — Sinner's 39 grass matches gave a raw Elo ~280 points below his cross-surface overall, leaving him underrated entering 2026 Wimbledon despite being world #1. K=40 fixed that. But uncapped, K=40 over-corrected on players with <20 grass matches: Fils (11 matches) got pulled 78% toward overall, ending up higher on grass than Shelton (80 matches) — wrong. The 50% cap preserves Sinner's correction (his w was already 0.506) and stops the laundering for everyone else.
Why not steepen the aging tail to handle these cases?
Because that would be survivorship bias — steepening the aging tail to fix one player's projection is fitting noise. The shrinkage cap is OOS-testable and principled. The aging tail stays where the data put it.
How does the model update after each match?
Per-surface Elo updates with each completed match (standard Elo update with surface-specific K). Aging curves are stable across the tournament. The Wimbledon refresh script (D:/Tennis Projections/scripts/refresh_during_wimbledon.py) pulls the previous day's results, re-derives Elo, re-runs the bracket MC, and re-deploys the bracket + per-pair probabilities + projected JSON. The /wimbledon page picks up the updated probabilities on the next page load.
What does the model NOT account for?
Injuries (a player carrying a thigh strain isn't surfaced in the projection — we only see match results, not in-tournament withdrawals or load management). Weather (grass plays differently in heat / humidity / under the roof). Match scheduling and accumulated fatigue (back-to-back five-setters). Court assignment effects (Center vs Court 1 vs outside courts — bounce and feel differ). Recent off-court news (coaching changes, personal). Doubles partnerships (singles only for now). These all matter, but we don't ingest them and we won't pretend otherwise.
How should I read counterintuitive calls?
Open the /wimbledon/match page for that pairing. It shows the model's full breakdown: grass Elo (raw + shrunk + sample size), age + aging contribution. If a call surprises you, one of those drivers is doing unusual work — usually a small-sample shrinkage on a younger player. The model is making a probabilistic statement, not a prediction; a 55% call still loses 45% of the time.
How was the K=40 shrinkage cap derived?
After Wimbledon 2026 draw posted, an audit of seven flagged matches found 5 of them shared the same defect: K=40 was overpowered on small-grass-sample players, hauling their grass Elo 60-80% toward hard-court overall. Capping at w_max=0.5 was the smallest change that fixed all 5 without disturbing the n>=40 specialists. Verified with re-run MC: 5 of 7 calls flipped to the intuitive winner; Sinner's champion% unchanged at 47.6%.
Is WTA the same fidelity as ATP?
Yes. Same engine class, same surface-specific Elo + cap, same age curve per court, same draw-aware sim. Equity-first ship — women's tennis is not a junior partner.
What are the stroke ratings on the advanced page?
A descriptive layer, separate from the match-projection model. From Match Charting Project shot-by-shot logs we rate each player's serve (ace rate + first-serve win rate + short-point dominance minus double faults), forehand and backhand (winners minus unforced errors per non-serve point), each z-scored against its own tour with sample-size shrinkage and a 20-charted-match floor. The player and matchup cards carry five more from the same logs, built the same way: return, net, pressure, rally, and variety — the spread of shot types a player uses, which is a style measure, so first means the most varied rather than the best. They describe HOW a player wins points — they are not used to predict matches (that's the surface-Elo engine). ATP and WTA sit on separate boards because the z-scores are computed within tour and aren't comparable across tours, and the board defaults to all-time with an active-only toggle.
What about doubles?
Singles only for now. Doubles projection is on the roadmap but not yet shipped — when it ships, it'll have its own page + OOS calibration in release notes.