Compute FRC power ratings (OPR, DPR, CCWM) from match data by least squares. Typed, zero runtime dependencies, runs in the browser and in Node.
npm install frc-oprimport { 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 },
// ... the rest of the qualification matches
]);
opr.get("111"); // estimated points team 111 contributes
dpr.get("111"); // estimated points it allows the opponent
ccwm.get("111"); // contribution to winning margin (OPR minus DPR)I built this after needing the same rating math in FRC scouting dashboards and small
Node scripts. OPR and its relatives are computed by least squares over the
alliance-membership matrix, but most reusable implementations are Java or Python, so
teams end up pasting one-off solvers into their frontends. frc-opr keeps that solve
small, tested, and usable directly in a dashboard.
| frc-opr | Java / Python scripts | Statbotics API | |
|---|---|---|---|
| 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.
powerRatings(matches)returns{ opr, dpr, ccwm }, each aMapfrom team key to rating.defensivePowerRatings(matches)returns aMap<string, number>of DPR per team. Equivalent to thedprfield ofpowerRatings, but as a focused function when you only need DPR.winningMarginRatings(matches)returns aMap<string, number>of CCWM per team. Equivalent to theccwmfield ofpowerRatings, 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, approachingpowerRatingsasridgeapproaches 0.fromTbaMatches(tbaMatches)converts The Blue Alliance match objects intoMatchvalues, 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 solve the same normal-equations system as OPR, differing only in the right-hand side:
| Rating | Right-hand side per alliance appearance |
|---|---|
| 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.
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.
- Component ratings from labeled score breakdowns.
npm run examplenpm testTests 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.
Issues and pull requests are welcome. See CONTRIBUTING.md.
MIT. See LICENSE.
