Expected Points Added (EPA) rating system for FIRST Tech Challenge (FTC), ported from Statbotics.
Pulls match data from api.ftcscout.org/graphql, computes EPA ratings via an EWMA learning loop, and serves them through a FastAPI REST API.
Live deployment: https://scoutkick.onrender.com (docs at /docs)
$env:PYTHONPATH="."
python backend/main.py # Start API server (http://127.0.0.1:8000)First run triggers the pipeline to fetch & process matches. Cached data stored in cache/epa_data.db.
To populate the database:
curl -X POST "http://127.0.0.1:8000/v1/data/run?season=2025"
curl http://127.0.0.1:8000/v1/data/status # Check progressZero-dependency client (pure urllib + json):
pip install git+https://github.com/Cicchy/scoutkick.git#subdirectory=scoutkick-pythonfrom scoutkick_api import ScoutKick
sk = ScoutKick() # defaults to https://scoutkick.onrender.com
# or locally: ScoutKick(base_url="http://127.0.0.1:8000")
sk.get_team(26914)
sk.predict(red=[26914, 32736], blue=[23400, 24599])
sk.get_teams(season="2025", limit=10)
sk.compare(teams=[26914, 32736])All list endpoints return {"value": [...], "count": N}. Single-resource endpoints return the object directly.
| Endpoint | Description |
|---|---|
POST /v1/data/run?season=2025 |
Trigger EPA pipeline (background task) |
GET /v1/data/status |
Pipeline state: running, done, pending |
| Endpoint | Description |
|---|---|
GET /v1/seasons |
List cached seasons |
GET /v1/season/{season} |
Season metadata (score_mean, score_sd, num_matches, num_teams) |
| Endpoint | Description |
|---|---|
GET /v1/teams?season=2025 |
Paginated team list, sorted by any metric |
GET /v1/team/{team}?season=2025 |
Team season stats + team_matches array (EPA evolution) |
GET /v1/team/{team}/events?season=2025 |
Per-event EPA stats |
GET /v1/team/{team}/matches?season=2025 |
Per-match EPA history |
| Endpoint | Description |
|---|---|
GET /v1/events?season=2025 |
All events with aggregate EPA stats |
GET /v1/event/{code}?season=2025 |
Event detail with team list |
GET /v1/event/{code}/matches?season=2025 |
Event match list |
| Endpoint | Description |
|---|---|
GET /v1/matches?season=2025 |
Global match list (filterable by team, event, elim) |
GET /v1/match/{event}/{match}?season=2025 |
Single match detail |
| Endpoint | Description |
|---|---|
GET /v1/predict?red=26914,32736&blue=23400,24599 |
Predict match outcome + scores |
GET /v1/compare?teams=26914,32736,23400 |
Side-by-side team comparison |
scoutkick/
├── backend/
│ ├── main.py # FastAPI entry point
│ ├── deploy/
│ │ └── render.yaml # Render deployment config
│ ├── src/
│ │ ├── core/
│ │ │ ├── math.py # SkewNormal, unit_sigmoid
│ │ │ └── constants.py # Tunable EPA parameters
│ │ ├── data/
│ │ │ ├── cleaner.py # Season-specific ETL (CleanerRegistry)
│ │ │ ├── ftcscout_api.py # GraphQL fetcher
│ │ │ └── read_ftcscout.py # Cache layer
│ │ ├── services/
│ │ │ ├── epa_service.py # EPAEngine (prediction + update)
│ │ │ ├── pipeline_service.py # EPAPipeline orchestrator
│ │ │ ├── calibrate.py # score_sd calibration
│ │ │ ├── init_epa.py # Initial rating logic
│ │ │ ├── complementarity.py # Team synergy analysis
│ │ │ ├── trajectory.py # Rating trajectory projection
│ │ │ └── clustering.py # Team clustering
│ │ ├── storage/
│ │ │ ├── __init__.py # get_db_path(), create_storage() factory
│ │ │ ├── base_storage.py # Abstract base (Template Method)
│ │ │ ├── sqlite_storage.py # SQLite implementation
│ │ │ └── postgres_storage.py # PostgreSQL implementation
│ │ └── api/
│ │ ├── router.py, deps.py
│ │ ├── season.py, team.py, event.py, match.py
│ │ ├── predict.py, data.py, cluster.py
│ ├── tests/
│ │ ├── test_engine.py # Unit tests (~82)
│ │ ├── test_integration.py # Integration tests (20)
│ │ ├── test_api.py # API endpoint tests (20)
│ │ ├── test_init_epa.py # Init EPA unit tests
│ │ ├── test_calibrate.py # Calibration tests
│ │ └── ...
│ ├── cache/ # Runtime data (gitignored)
│ ├── pyproject.toml
│ └── requirements.txt
├── scoutkick-python/ # pip-installable client package
│ └── src/scoutkick_api/client.py
├── CLAUDE.md
├── README.md
├── LICENSE
└── .todos.md
- Fetch — Pulls match data from
api.ftcscout.org/graphqlusing per-season GraphQL fragments. Cached incache/ftcscout/*.p. - Clean —
BaseCleanersubclasses map alliance scores to a fixed 32-dim EPA vector. 7 seasons supported (2019–2025). - Learn — EWMA loop. Each team is a
SkewNormaldistribution. Prediction error split equally across 2 alliance members, updates mean/variance/skewness. - Calibrate — Computes
score_sdand component-mean baselines from the match dataset. - Serve — Results stored in SQLite (or PostgreSQL), served via FastAPI.
| Parameter | Value |
|---|---|
| Learning rate | alpha = 1 / (1 + n * 0.1) |
| Error attribution | Split equally across 2 teammates |
| Elimination weight | 0.33 |
| Initial mean (total) | 20.0 |
| Initial variance | 100.0 |
| Norm mean (for display) | 1500 |
| Index | Component |
|---|---|
| 0 | Total EPA (preFoulTotal) |
| 1 | Auto EPA |
| 2 | Teleop EPA |
| 3 | Endgame EPA |
| 4–6 | Ranking Points (binary, RP1–RP3) |
| 7 | Auto Classified |
| 8 | Teleop Classified |
| 9 | Teleop Depot |
Seasons 2019–2023 use 4 dims, 2024 uses 10, 2025 uses 10 active dims in a 32-slot vector.
$env:PYTHONPATH="."
python -m pytest backend/tests/ -v # All ~197 tests
python -m pytest backend/tests/ -v -k "api" # API tests onlyDeployed via render.yaml on Render. A git push to master auto-deploys. Persistent disk at /opt/render/project/cache/ stores both epa_data.db and ftcscout/ API cache.
All match data sourced from FTC Scout via their public GraphQL API (api.ftcscout.org/graphql).