2026-08-15
Elo tells you who tends to win. Ratings tell you how they do it through their offense and defense.
Elo is great at ranking based on wins, losses, and score, but it is knows nothing about playstyle and how those results were achieved. Two teams can have the same Ele while has an incredible offense and weak defense and the other has a horrible offense and great defense.
Ratings are a second layer: one offense score, one defense score, and a combined average. Scaled 0–100. No win probability. No black box.
If you want the Elo deep dive first, start with Our Elo Model. The helpers on this page are already loaded — open the console like we show in Try out our Models.
Pick a few sport-specific stats. Map each one onto a 0–100 score with a good and bad anchor. Blend those scores with percent weights that add to 100%.
function clamp(x, lo = 0, hi = 100) {
return Math.max(lo, Math.min(hi, x));
}
// Higher is better
function score_up(raw, good, bad) {
return clamp(
(raw - bad) / (good - bad) * 100
);
}
// Lower is better
function score_down(raw, good, bad) {
return clamp(
(bad - raw) / (bad - good) * 100
);
}
Why percents instead of mystery multipliers like ×200? Because "40% Strike, 60% Surge" is something a human can argue with. The anchors still matter — they set what "great" and "poor" look like — but the blend is readable.
Meet the Racoons. They play Arena Ball. We track four rates:
const OFF_W = { strike: 0.55, surge: 0.45 };
const DEF_W = { stops: 0.60, leak: 0.40 };
function offense_rating(strike, surge) {
const strike_s = score_up(strike, 0.55, 0.35);
const surge_s = score_up(surge, 12, 6);
return Math.round(
OFF_W.strike * strike_s
+ OFF_W.surge * surge_s
);
}
function defense_rating(stops, leak) {
const stops_s = score_up(stops, 18, 8);
const leak_s = score_down(leak, 4, 10);
return Math.round(
DEF_W.stops * stops_s
+ DEF_W.leak * leak_s
);
}
function combined_rating(off, def) {
return Math.round((off + def) / 2);
}
Game 1 is loud. Game 2 is quieter. Same formulas either way.
const GAME1 = {
strike: 0.50,
surge: 10.5,
stops: 15,
leak: 5.5
};
const GAME2 = {
strike: 0.45,
surge: 9,
stops: 13,
leak: 7
};
offense_rating(GAME1.strike, GAME1.surge);
>>> 75
defense_rating(GAME1.stops, GAME1.leak);
>>> 72
offense_rating(GAME2.strike, GAME2.surge);
>>> 50
defense_rating(GAME2.stops, GAME2.leak);
>>> 50
The tables below total those games. Season averages run through the same functions. When scores sit away from the 0/100 clamps, averaging game ratings and rating the averaged inputs stay close (ordinary rounding aside).
Cumulative season ratings after each game:
Do not turn these into prices. Use Elo (or the market) for that. Use ratings to ask better questions when the styles clash.
Arena Ball was the teaching toy. Live site weights:
Offense Pass net yds45% + Rush yds35% + 3rd conv20%
Defense Rush yds allowed30% (lower better) + Yards allowed25% (lower better) + Sacks25% + INTs20%
Offense Total Shots attempted50% + Hits25% + Corsi%25%
Defense Shots allowed50% (lower better) + Corsi Tot. (Opp.)50% (lower better)