frc-opr

Compute FRC power ratings (OPR, DPR, CCWM) from match data by least squares. Typed, zero runtime dependencies, runs in the browser and in Node.
Install
npm install frc-opr
30-second example
import { powerRatings } from "frc-opr";
const { opr, dpr, ccwm } = powerRatings([
{ red: ["111", "222", "333"], blue: ["444", "555", "666"], redScore: 88, blueScore: 64 },
{ red: ["111", "444", "555"], blue: ["222", "333", "666"], redScore: 95, blueScore: 71 },
]);
opr.get("111");
dpr.get("111");
ccwm.get("111");
Why this exists
OPR and its relatives are the standard derived statistics in FRC scouting, computed by
least squares over the alliance-membership matrix. Scouting tools are almost all web
apps, but the only reusable implementations are Java and Python scripts, so teams paste
ad hoc least-squares code into their frontends. frc-opr is the first TypeScript library
for it: a correct, tested solve with no dependencies, usable directly in a dashboard.
Comparison
| Language | TypeScript | Java, Python | service |
| Runs in the browser | yes | no | n/a (network) |
| Works on your own match data | yes | yes | no, serves its own |
| Zero dependencies | yes | varies | n/a |
| OPR, DPR, CCWM | yes | varies | EPA and more |
Statbotics is excellent for precomputed ratings over official events; frc-opr computes
ratings locally from whatever matches you give it, including practice or custom data.
API
powerRatings(matches) returns { opr, dpr, ccwm }, each a Map from team key to rating.
defensivePowerRatings(matches) returns a Map<string, number> of DPR per team. Equivalent
to the dpr field of powerRatings, but as a focused function when you only need DPR.
winningMarginRatings(matches) returns a Map<string, number> of CCWM per team. Equivalent
to the ccwm field of powerRatings, but as a focused function when you only need CCWM.
Guaranteed to equal OPR minus DPR elementwise (the linearity of the system ensures this).
powerRatingsRidge(matches, ridge) is the ridge-regularized (MMSE) variant: it stays
solvable on early-event data that is not yet well determined and biases ratings toward
zero, approaching powerRatings as ridge approaches 0.
fromTbaMatches(tbaMatches) converts The Blue Alliance match objects into Match values,
skipping matches that have not been played.
solve(matrix, b), luDecompose(matrix), luSolve(factorization, b): the dense linear
solver, exported for reuse.
A Match is { red: string[], blue: string[], redScore: number, blueScore: number }.
Team keys are any non-empty strings.
DPR and CCWM in depth
DPR and CCWM solve the same normal-equations system as OPR, differing only in the
right-hand side:
| OPR | own alliance score |
| DPR | opponent alliance score |
| CCWM | own score minus opponent score (margin) |
Because the margin is the difference of the other two right-hand sides, CCWM = OPR - DPR
holds elementwise for every team. winningMarginRatings builds the margin right-hand side
directly and verifies this identity in the test suite to within 1e-9.
The math
Each alliance appearance gives one equation: the sum of its members' ratings equals the
alliance score (OPR) or the opponent score (DPR). That overdetermined system is solved
through its normal equations, whose matrix is the symmetric team co-appearance matrix.
OPR and DPR share that matrix, so it is factorized once. CCWM is OPR minus DPR, and the
CCWM values over an event sum to zero. If the data is rank-deficient (too few matches to
separate teams), powerRatings throws a clear error rather than returning unstable
numbers.
Roadmap
- Component ratings from labeled score breakdowns.
Examples
npm run example
Testing
npm test
Tests cover exact recovery of known ratings from consistent synthetic events, the
CCWM = OPR - DPR identity, the zero-sum CCWM property, solver unit tests, and the
rank-deficient error path.
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md.
License
MIT. See LICENSE.