Local-first ratings & match predictions for FRC, powered by Microsoft TrueSkill and The Blue Alliance
This API runs locally on 127.0.0.1:5000, ingests official FRC match data from The Blue Alliance (TBA), computes TrueSkill ratings for every team, and serves win-probability predictions for any alliance matchup (single or batch). It’s designed to be clean, fast, and integration-friendly—returning plain JSON so your apps, scripts, or dashboards can consume it directly.
- Local only by default (no public exposure).
- Full-season ingestion (
/updatewith{"year": YYYY}) processes all events and all matches in that year. - Accurate, analytical predictions using the canonical TrueSkill CDF.
- Stateful when needed:
/upload_datasaves totrueskill_data.json,/load_datarestores—no re-fetch required. - Live updates via
/push_resultswithout hitting TBA. - Informative outputs: μ, σ, conservative rating (μ − 3σ), confidence% (from σ shrinkage), and prediction confidence for a matchup.
python -m venv .venv
. .venv/bin/activate # Windows: .\.venv\Scripts\activate
pip install flask flask-cors requests trueskill
# REQUIRED: TBA read key
export TBA_AUTH_KEY="YOUR_TBA_READ_KEY" # Windows PowerShell: $env:TBA_AUTH_KEY="YOUR_TBA_READ_KEY"
# Optional: choose a save path
# export TRUESKILL_DATA_PATH="/path/to/trueskill_data.json"
python trueskill_api_v2.py
# → API on http://127.0.0.1:5000Check health:
curl -s http://127.0.0.1:5000/health
# {"ok": true, "teams_indexed": 0}- TEAM_RATINGS (memory):
{ "frc####": Rating(mu, sigma), ... } - Snapshot file (
trueskill_data.json): includes metadata (environment, context) + all teams with μ, σ, conservative μ, confidence%. - Context tracks last
event_keyoryearused to build the snapshot.
Each team’s skill is modeled as a Gaussian:
- μ (mu): estimated skill
- σ (sigma): uncertainty in that estimate
Default prior (TrueSkill environment): $ \mu_0 = 25,\quad \sigma_0 = \frac{25}{3} \approx 8.33 $ As matches occur, winners’ μ tends to increase, losers’ μ decreases, and σ shrinks (more data → more certainty).
Let Alliance 1 teams be (i \in A_1), Alliance 2 teams be (j \in A_2).
- Aggregate means: $ \mu_1 = \sum_{i \in A_1} \mu_i,\quad \mu_2 = \sum_{j \in A_2} \mu_j $
- Aggregate variance from all players: $ \Sigma_{\sigma^2} = \sum_{k \in A_1 \cup A_2} \sigma_k^2 $
- Let (
$N = |A_1| + |A_2|$ ) and ($\beta$ ) be the environment’s performance variance scale.
Effective standard deviation for the skill difference: $ \text{denom} = \sqrt{N \cdot \beta^2 + \Sigma_{\sigma^2}} $
Define:
$
\Delta = \frac{\mu_1 - \mu_2}{\text{denom}}
$
Then the win probability for Alliance 1 is:
$
P(\text{A1 wins}) = \Phi(\Delta)
$
where ( denom), yields more decisive probabilities.
-
Conservative skill: $ \text{conservative_mu_3sigma} = \mu - 3\sigma $ A strict lower-bound estimate; useful for risk-aware rankings.
-
Confidence percent (how much uncertainty has been reduced vs prior): $ \text{confidence%} = 100 \times \left(1 - \left(\frac{\sigma}{\sigma_0}\right)^2\right) $ Starts near 0% with no data; climbs toward 100% as σ shrinks.
Base URL: http://127.0.0.1:5000
| Method | Path | Summary |
|---|---|---|
| GET | /health |
Service check; returns teams_indexed |
| POST | /update |
Rebuild ratings from TBA by event ({"event_key":"YYYYxxxx"}) or year ({"year":YYYY}) |
| POST | /push_results |
Incrementally apply match results you provide (no TBA call) |
| GET | /predict_team |
Return μ, σ, conservative μ, confidence% for one team |
| POST | /predict_match |
Win probability for one matchup (also returns a prediction confidence%) |
| POST | /predict_batch |
Win probabilities for many matchups |
| POST | /upload_data |
Save current ratings + metadata to trueskill_data.json |
| POST | /load_data |
Load ratings from trueskill_data.json into memory |
| POST | /recalculate |
Refresh derived fields; optionally load from JSON first |
curl -s http://127.0.0.1:5000/health{ "ok": true, "teams_indexed": 158 }Rebuild from event or year (fresh start).
Event:
curl -s -X POST http://127.0.0.1:5000/update \
-H "Content-Type: application/json" \
-d '{"event_key":"2025nyny"}'Year:
curl -s -X POST http://127.0.0.1:5000/update \
-H "Content-Type: application/json" \
-d '{"year":2025}'Response:
{ "status": "rankings updated", "year": 2025, "teams_indexed": 634 }Notes
- Gathers all events for the year, then all matches for each event; skips unplayed (scores
null/-1). - Resets in-memory ratings.
- Requires
TBA_AUTH_KEY.
Apply live results, no TBA call. Body: JSON array of matches:
curl -s -X POST http://127.0.0.1:5000/push_results \
-H "Content-Type: application/json" \
-d '[
{"teams1":["frc254","frc1678","frc118"],"teams2":["frc1323","frc2056","frc148"],"score1":120,"score2":95},
{"teams1":["frc1678","frc254","frc118"],"teams2":["frc2056","frc1323","frc148"],"score1":87,"score2":87}
]'{ "status": "results incorporated", "applied": 2 }curl -s "http://127.0.0.1:5000/predict_team?team=frc3173"{
"team": "frc3173",
"mu": 35.67,
"sigma": 5.19,
"conservative_mu_3sigma": 20.10,
"confidence_percent": 61.0
}curl -s -X POST http://127.0.0.1:5000/predict_match \
-H "Content-Type: application/json" \
-d '{"teams1":["frc254","frc1678","frc118"],"teams2":["frc1323","frc2056","frc148"]}'{
"team1_win_prob": 0.6429187735,
"team2_win_prob": 0.3570812265,
"prediction_confidence_percent": 28.58
}
prediction_confidence_percent = |2*P-1|*100— how decisive the model sees this particular matchup.
curl -s -X POST http://127.0.0.1:5000/predict_batch \
-H "Content-Type: application/json" \
-d '[
{"teams1":["frc254","frc1678","frc118"],"teams2":["frc1323","frc2056","frc148"]},
{"teams1":["frc1114","frc2056","frc1241"],"teams2":["frc33","frc217","frc910"]}
]'[
{
"teams1": ["frc254","frc1678","frc118"],
"teams2": ["frc1323","frc2056","frc148"],
"team1_win_prob": 0.6429187735,
"team2_win_prob": 0.3570812265
},
{
"teams1": ["frc1114","frc2056","frc1241"],
"teams2": ["frc33","frc217","frc910"],
"team1_win_prob": 0.513922184,
"team2_win_prob": 0.486077816
}
]Save snapshot to trueskill_data.json:
curl -s -X POST http://127.0.0.1:5000/upload_data{ "status": "saved", "file": "trueskill_data.json", "teams_indexed": 634 }Load snapshot into memory. Optional body:
{ "path": "custom/path.json", "use_env_from_json": true }curl -s -X POST http://127.0.0.1:5000/load_data \
-H "Content-Type: application/json" \
-d '{"use_env_from_json":true}'{
"status": "loaded",
"file": "trueskill_data.json",
"use_env_from_json": true,
"teams_indexed": 634,
"context": { "event_key": null, "year": 2025 }
}Refresh derived fields and re-save.
- From memory (default):
curl -s -X POST http://127.0.0.1:5000/recalculate- Load from JSON, then recalc + save:
curl -s -X POST http://127.0.0.1:5000/recalculate \
-H "Content-Type: application/json" \
-d '{"source":"json"}'{
"meta": {
"generated_at": "2025-10-14T02:32:17.119288+00:00",
"source": "The Blue Alliance (processed locally)",
"env": {
"mu": 25.0,
"sigma": 8.3333333333,
"beta": 4.1666666667,
"tau": 0.0833333333,
"draw_probability": 0.0
},
"context": {
"event_key": "2025nyny",
"year": null,
"teams_indexed": 60
}
},
"teams": [
{
"team_key": "frc1",
"mu": 28.54,
"sigma": 7.91,
"conservative_mu_3sigma": 4.81,
"confidence_percent": 9.0
}
]
}- meta.env documents the TrueSkill environment used.
- meta.context indicates whether this snapshot covers an event or a full year.
- teams contains μ, σ, conservative μ, and confidence% for each team.
Season baseline → query → save → restore
POST /updatewith{"year": 2025}- Use
/predict_matchor/predict_batch POST /upload_datato persist- Next session:
POST /load_datato be ready instantly
Event + live
POST /updatewith{"event_key": "2025xxxx"}- After each match:
POST /push_results - Query
/predict_matchfor upcoming schedule POST /upload_dataat day/event end
-
Predictions stuck at 50/50 Ensure you’ve run
/updateor/load_dataand/healthshows a non-zeroteams_indexed. Identical alliances or fully default ratings also yield 50/50. -
/load_data“works” but ratings look default Inspect the JSON: if μ≈25 and σ≈8.33 everywhere, it’s a default snapshot. Try/load_datawith{"use_env_from_json": true}to mirror saved env. -
Year ingestion missing teams This build iterates every event in the year and fetches all matches per event. Unplayed matches (scores
null/-1) are skipped by design. -
ImportError: partially initialized module 'trueskill' Don’t name your file
trueskill.py. Usetrueskill_api_v2.py.
- Deterministic rebuilds:
/updatereplays matches chronologically to produce a consistent ratings table. - Local-first: Defaults to localhost; add security before exposing beyond your machine.
- Minimal coupling: Pure JSON in/out; easy to script, test, and integrate.
- Explainable outputs: μ, σ, conservative μ, confidence%, and transparent math for predictions.
- Fixed /load_data to hydrate in-memory ratings reliably (with optional environment recreation).
- Correct TrueSkill probability computation in /predict_match and /predict_batch.
- Full-year ingestion ensures all events and all teams are included.
- Added prediction confidence% for single-match predictions.
- Robust save/load cycle via /upload_data, /recalculate, /load_data.