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)