Our Ratings

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.

What These Are (and Are Not)

  • They are not a replacement for Elo
  • They are not market odds
  • They are a mismatch lens: strong attack vs soft defense, and the reverse

The Recipe

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.

A Notional Sport: Arena Ball

Meet the Racoons. They play Arena Ball. We track four rates:

  • Offense: Strike% (finishing), Surge (pace pressure)
  • Defense: Stops (disruptions per game), Leak (mistakes allowed — lower is better)
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);
}

Two Games, Then a Season

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:

How We Use Them

  • Preview hero: season-to-date O68 · D58 under Elo%
  • Box score / game page: Off / Def from that game’s metrics only
  • Key Insights, standings, and the upcoming board (season-to-date)

Do not turn these into prices. Use Elo (or the market) for that. Use ratings to ask better questions when the styles clash.

Production Weights by Sport

Arena Ball was the teaching toy. Live site weights:

MLB

Offense OBP40% + SLG40% + RE2420%

Defense RE2445% + Whiffs/G35% + Walks/G20% (lower better)

NFL

Offense Pass net yds45% + Rush yds35% + 3rd conv20%

Defense Rush yds allowed30% (lower better) + Yards allowed25% (lower better) + Sacks25% + INTs20%

NBA

Offense eFG%50% + ORB%30% + FT rate20%

Defense Opp eFG%50% (lower better) + Def reb30% + Blocks20%

NHL

Offense Total Shots attempted50% + Hits25% + Corsi%25%

Defense Shots allowed50% (lower better) + Corsi Tot. (Opp.)50% (lower better)