Methodology
The estimator returns (mu, sigma) — a mean MMR and standard deviation. The 90% confidence interval shown to users is mu ± 1.645 * sigma.
Tier 1 — Win behavior
Cheap signal derived from league-v4 only. Cost: 1 API call.
The matchmaker drives most accounts toward a 50% win rate. The deviation from 50%, scaled by sample size, is a noisy but unbiased estimate of how far the player's MMR sits from the matchmaker's target for their bracket.
delta_mmr = k * (winrate - 0.5) * sqrt(n)
mu_tier1 = rank_baseline + delta_mmr
sigma_tier1 = base_sigma / sqrt(n)Where n is games played in the current split, k is a tuning constant calibrated against known accounts, and rank_baseline is the median MMR for the player's visible tier/division.
This tier is fast and always available, but its sigma stays large unless n is high. It is the only signal for accounts under ~20 games this split.
Tier 2 — Lobby composition
The strong signal. Cost: up to ~180 Riot API calls per cold request, mitigated by D1.
For each of the player's last 20 ranked solo/duo games:
- Fetch the match via
match-v5. - For all 9 other participants, look up their current ranked entry.
- Convert each participant's rank to MMR via the
tier/division/lp → MMRmap. - Compute a weighted lobby average:
- Opponents: weight
0.6 - Teammates: weight
0.4 - Recency decay:
exp(-days_since_match / 14)
- Opponents: weight
- Discard games with more than 2 unranked participants — they distort the lobby average too much.
Per-match lobby MMR estimates are aggregated:
mu_tier2 = weighted_mean(lobby_estimates)
sigma_tier2 = weighted_stdev(lobby_estimates) / sqrt(games_used)For steady-state accounts (20+ ranked games this split), Tier 2 dominates the combined estimate. For smurfs and decaying accounts it correctly produces wider intervals because match-to-match lobby variance grows.
Tier 3 — Inverse-variance combination
Both tiers produce (mu, sigma). We combine via:
w_i = 1 / sigma_i^2
mu_final = (w_1 * mu_1 + w_2 * mu_2) / (w_1 + w_2)
sigma_final = sqrt(1 / (w_1 + w_2))This automatically gives more weight to the more confident tier. We then floor sigma_final at 40 MMR to reflect irreducible matchmaking noise — no estimator should claim a ±20 MMR interval on a public-data signal.
What the floor protects against
Without the sigma floor, the estimator would happily report ±15 MMR intervals on accounts with hundreds of recent games. Riot's matchmaker itself fluctuates by more than that on a day-to-day basis. The floor encodes the honesty of "we don't know to that precision, even with infinite data."
Future tier — LP-delta inversion
The cron job at 30 0 * * * snapshots ranked entries daily. Once 7+ days of snapshots exist for an account, observed (LP_delta, opponent_rank) pairs let us solve for MMR directly via the matchmaker's LP gain function. The code path is structured for this — it just needs a snapshot-count gate, which today is the missing piece.