Chapter 17 Advanced ~60 min read

RAPTOR and Modern Composite Metrics

FiveThirtyEight's RAPTOR system and the evolution of modern all-in-one basketball metrics.

The RAPTOR Revolution

RAPTOR (Robust Algorithm using Player Tracking and On/Off Ratings) emerged from FiveThirtyEight in 2019 as one of the most sophisticated publicly available player evaluation metrics. Developed by Nate Silver's team, RAPTOR represents the current frontier of all-in-one metrics, incorporating player tracking data alongside traditional statistics and plus-minus information.

The distinguishing feature of RAPTOR is its integration of player tracking data from the NBA's Second Spectrum system. While previous metrics relied exclusively on box scores or plus-minus data, RAPTOR incorporates detailed spatial and movement information: where shots are taken, how players defend different areas, movement speeds, and other granular details invisible to traditional statistics.

The RAPTOR Architecture

RAPTOR combines three distinct information sources. The first component uses box score statistics processed through a framework similar to BPM. The second component incorporates player tracking data to enhance the box score estimate—shots are evaluated by difficulty, rebounds by contested-ness. The third component uses plus-minus data processed through regularized adjusted plus-minus methodology.

The combination weights these components based on sample size: limited-minute players rely heavily on the tracking-enhanced box score while high-minute players see more influence from actual on-court results.

WAR: Wins Above Replacement

RAPTOR converts to Wins Above Replacement (WAR) using methodology paralleling baseball's sabermetric standard. The conversion translates per-100-possession impact to seasonal win contribution by accounting for possessions played and the points-per-win relationship.

WAR provides cumulative value measurement comparable to Win Shares but derived from the plus-minus framework. A player with 8 WAR contributed approximately 8 wins more than a replacement-level alternative would have in the same minutes.

Comparison Across Modern Metrics

The modern metric landscape features several sophisticated options: EPM from Dunks and Threes, LEBRON from BBall-Index, and various proprietary systems from NBA teams. These metrics typically correlate highly with each other—the best players by RAPTOR are usually the best by EPM and LEBRON as well. Divergences occur at the margins where methodological differences produce different rankings.

Interpreting RAPTOR Values

RAPTOR interpretation follows the plus-minus framework with zero as league average and standard deviation around 3-4 points. Elite players reach +8 to +10 total RAPTOR; very good players fall in the +4 to +7 range; average starters cluster around +1 to +3.

The offensive and defensive splits often prove more informative than total RAPTOR. A player with +6 total RAPTOR could achieve it through +8 offense / -2 defense or +3/+3—very different profiles with different implications for team construction.

Implementation in R

# Understanding RAPTOR (Robust Algorithm using Player Tracking and On/off Ratings)
library(tidyverse)

# Simplified RAPTOR calculation
calculate_raptor <- function(player_stats, tracking_data) {
  player_stats %>%
    left_join(tracking_data, by = "player_id") %>%
    mutate(
      # Box score component
      box_offense = 0.5 * pts_100 + 0.3 * ast_100 - 0.2 * tov_100,
      box_defense = 0.3 * stl_100 + 0.2 * blk_100 + 0.1 * drb_100,

      # Tracking component (if available)
      tracking_offense = ifelse(!is.na(rim_fg_diff),
                                0.3 * rim_fg_diff + 0.2 * open_3_rate, 0),
      tracking_defense = ifelse(!is.na(dfg_diff),
                                -0.4 * dfg_diff, 0),

      # Plus-minus component (if on/off available)
      pm_component = ifelse(!is.na(on_off_rtg),
                            0.3 * on_off_rtg, 0),

      # Combined RAPTOR
      raptor_offense = box_offense + tracking_offense + 0.5 * pm_component,
      raptor_defense = box_defense + tracking_defense + 0.5 * pm_component,
      raptor_total = raptor_offense + raptor_defense
    )
}

players <- read_csv("player_stats.csv")
tracking <- read_csv("player_tracking.csv")

raptor <- calculate_raptor(players, tracking)

# Top RAPTOR players
top_raptor <- raptor %>%
  filter(min >= 1500) %>%
  arrange(desc(raptor_total)) %>%
  select(player_name, raptor_total, raptor_offense, raptor_defense) %>%
  head(15)

print(top_raptor)

Implementation in Python

# Understanding RAPTOR
import pandas as pd

def calculate_raptor(player_stats, tracking_data):
    """Simplified RAPTOR calculation"""
    merged = player_stats.merge(tracking_data, on="player_id", how="left")

    # Box score component
    merged["box_offense"] = (
        0.5 * merged["pts_100"] + 0.3 * merged["ast_100"] - 0.2 * merged["tov_100"]
    )
    merged["box_defense"] = (
        0.3 * merged["stl_100"] + 0.2 * merged["blk_100"] + 0.1 * merged["drb_100"]
    )

    # Tracking component
    merged["tracking_offense"] = (
        0.3 * merged["rim_fg_diff"].fillna(0) + 0.2 * merged["open_3_rate"].fillna(0)
    )
    merged["tracking_defense"] = -0.4 * merged["dfg_diff"].fillna(0)

    # Plus-minus component
    merged["pm_component"] = 0.3 * merged["on_off_rtg"].fillna(0)

    # Combined RAPTOR
    merged["raptor_offense"] = (
        merged["box_offense"] + merged["tracking_offense"] + 0.5 * merged["pm_component"]
    )
    merged["raptor_defense"] = (
        merged["box_defense"] + merged["tracking_defense"] + 0.5 * merged["pm_component"]
    )
    merged["raptor_total"] = merged["raptor_offense"] + merged["raptor_defense"]

    return merged

players = pd.read_csv("player_stats.csv")
tracking = pd.read_csv("player_tracking.csv")

raptor = calculate_raptor(players, tracking)

top_raptor = raptor[raptor["min"] >= 1500].nlargest(15, "raptor_total")[
    ["player_name", "raptor_total", "raptor_offense", "raptor_defense"]
]
print(top_raptor)

Implementation in R

# Understanding RAPTOR (Robust Algorithm using Player Tracking and On/off Ratings)
library(tidyverse)

# Simplified RAPTOR calculation
calculate_raptor <- function(player_stats, tracking_data) {
  player_stats %>%
    left_join(tracking_data, by = "player_id") %>%
    mutate(
      # Box score component
      box_offense = 0.5 * pts_100 + 0.3 * ast_100 - 0.2 * tov_100,
      box_defense = 0.3 * stl_100 + 0.2 * blk_100 + 0.1 * drb_100,

      # Tracking component (if available)
      tracking_offense = ifelse(!is.na(rim_fg_diff),
                                0.3 * rim_fg_diff + 0.2 * open_3_rate, 0),
      tracking_defense = ifelse(!is.na(dfg_diff),
                                -0.4 * dfg_diff, 0),

      # Plus-minus component (if on/off available)
      pm_component = ifelse(!is.na(on_off_rtg),
                            0.3 * on_off_rtg, 0),

      # Combined RAPTOR
      raptor_offense = box_offense + tracking_offense + 0.5 * pm_component,
      raptor_defense = box_defense + tracking_defense + 0.5 * pm_component,
      raptor_total = raptor_offense + raptor_defense
    )
}

players <- read_csv("player_stats.csv")
tracking <- read_csv("player_tracking.csv")

raptor <- calculate_raptor(players, tracking)

# Top RAPTOR players
top_raptor <- raptor %>%
  filter(min >= 1500) %>%
  arrange(desc(raptor_total)) %>%
  select(player_name, raptor_total, raptor_offense, raptor_defense) %>%
  head(15)

print(top_raptor)

Implementation in Python

# Understanding RAPTOR
import pandas as pd

def calculate_raptor(player_stats, tracking_data):
    """Simplified RAPTOR calculation"""
    merged = player_stats.merge(tracking_data, on="player_id", how="left")

    # Box score component
    merged["box_offense"] = (
        0.5 * merged["pts_100"] + 0.3 * merged["ast_100"] - 0.2 * merged["tov_100"]
    )
    merged["box_defense"] = (
        0.3 * merged["stl_100"] + 0.2 * merged["blk_100"] + 0.1 * merged["drb_100"]
    )

    # Tracking component
    merged["tracking_offense"] = (
        0.3 * merged["rim_fg_diff"].fillna(0) + 0.2 * merged["open_3_rate"].fillna(0)
    )
    merged["tracking_defense"] = -0.4 * merged["dfg_diff"].fillna(0)

    # Plus-minus component
    merged["pm_component"] = 0.3 * merged["on_off_rtg"].fillna(0)

    # Combined RAPTOR
    merged["raptor_offense"] = (
        merged["box_offense"] + merged["tracking_offense"] + 0.5 * merged["pm_component"]
    )
    merged["raptor_defense"] = (
        merged["box_defense"] + merged["tracking_defense"] + 0.5 * merged["pm_component"]
    )
    merged["raptor_total"] = merged["raptor_offense"] + merged["raptor_defense"]

    return merged

players = pd.read_csv("player_stats.csv")
tracking = pd.read_csv("player_tracking.csv")

raptor = calculate_raptor(players, tracking)

top_raptor = raptor[raptor["min"] >= 1500].nlargest(15, "raptor_total")[
    ["player_name", "raptor_total", "raptor_offense", "raptor_defense"]
]
print(top_raptor)
Chapter Summary

You've completed Chapter 17: RAPTOR and Modern Composite Metrics.