Questions - Does coaches challenge review process include “enhanced video”?

Takeaways - - Reviewable out of bounds are incorrectly called 7% of the time - Shooting fouls are missed or incorretly called 6% of the time while other personal fouls are missed or incorrectly called 3% of the time.

library(tidyverse)
library(httr)
library(rvest)
library(dplyr)
library(reactable)
library(reactablefmtr)

Sys.setenv("VROOM_CONNECTION_SIZE" = 131072 * 2)
url <- "https://raw.githubusercontent.com/atlhawksfanatic/L2M/master/0-data/official_nba/official_nba_l2m_api.csv"
data <- read.csv(url)

data$GameDateOut <- as.Date(data$GameDateOut, format = "%B %d, %Y")
data1 <- data %>%
  rename(Player = CP,
         Opponent = DP,
         Date = GameDateOut) %>%
  select(-c(ImposibleIndicator, Qualifier,teamIdInFavor, errorInFavor, GameDate, L2M_Comments,)) 
data1 <- data1 %>%
  mutate(type = case_when(
    str_detect(CallType, "Foul: Offensive") ~ "Offensive Foul",
    str_detect(CallType, "Foul: Loose Ball") ~ "Loose Ball Foul",
    str_detect(CallType, "Foul: Personal") ~ "Defensive Foul",
    str_detect(CallType, "Foul: Shooting") ~ "Defensive Foul",
    str_detect(CallType, "Stoppage: Out-of-Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Traveling") ~ "Possession",
    str_detect(CallType, "Turnover: Lost Ball Out of Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Out of Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Traveling") ~ "Possession",
    str_detect(CallType, "Foul: Personal Take") ~ "Defensive Foul",
    str_detect(CallType, "Turnover: Out of Bounds - Bad Pass Turn") ~ "Possession",
    str_detect(CallType, "Turnover: Stepped out of Bounds") ~ "Possession",
    str_detect(CallType, "Foul: Away from Play") ~ "Defensive Foul",
    str_detect(CallType, "Violation: Defensive Goaltending") ~ "Goaltending",
    str_detect(CallType, "Violation: Offensive Goaltending") ~ "Goaltending",
    str_detect(CallType, "Foul: Offensive Charge") ~ "Offensive Foul",
    str_detect(CallType, "Foul: Flagrant Type 1") ~ "Defensive Foul",
    str_detect(CallType, "Foul: Flagrant Type 2") ~ "Defensive Foul",
    str_detect(CallType, "Turnover: Lost ball") ~ "Possession",
    str_detect(CallType, "Turnover: Bad Pass") ~ "Possession",
    str_detect(CallType, "Turnover: Offensive Foul") ~ "Offensive Foul",
    TRUE ~ "other"
  )) %>%
  mutate(result = case_when(
    CallRatingName == "IC" ~ 1,
    CallRatingName == "INC" ~ 1,
    TRUE ~ 0
  ))

data2 <- data %>%
  rename(Player = CP,
         Opponent = DP,
         Date = GameDateOut) %>%
  select(-c(ImposibleIndicator, Qualifier,teamIdInFavor, errorInFavor, GameDate, L2M_Comments,)) 
data2 <- data2 %>%
  mutate(type = case_when(
    str_detect(CallType, "Foul: Offensive") ~ "Offensive Foul",
    str_detect(CallType, "Foul: Loose Ball") ~ "Loose Ball Foul",
    str_detect(CallType, "Foul: Personal") ~ "Defensive Foul",
    str_detect(CallType, "Foul: Shooting") ~ "Defensive Foul",
    str_detect(CallType, "Stoppage: Out-of-Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Traveling") ~ "Possession",
    str_detect(CallType, "Turnover: Lost Ball Out of Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Out of Bounds") ~ "Possession",
    str_detect(CallType, "Turnover: Traveling") ~ "Possession",
    str_detect(CallType, "Foul: Personal Take") ~ "Defensive Foul",
    str_detect(CallType, "Turnover: Out of Bounds - Bad Pass Turn") ~ "Possession",
    str_detect(CallType, "Turnover: Stepped out of Bounds") ~ "Possession",
    str_detect(CallType, "Foul: Away from Play") ~ "Defensive Foul",
    str_detect(CallType, "Violation: Defensive Goaltending") ~ "Goaltending",
    str_detect(CallType, "Violation: Offensive Goaltending") ~ "Goaltending",
    str_detect(CallType, "Foul: Offensive Charge") ~ "Offensive Foul",
    str_detect(CallType, "Foul: Flagrant Type 1") ~ "Defensive Foul",
    str_detect(CallType, "Foul: Flagrant Type 2") ~ "Defensive Foul",
    str_detect(CallType, "Turnover: Lost ball") ~ "Possession",
    str_detect(CallType, "Turnover: Bad Pass") ~ "Possession",
    str_detect(CallType, "Turnover: Offensive Foul") ~ "Offensive Foul",
    TRUE ~ "other"
  )) %>%
  mutate(result = case_when(
    CallRatingName == "IC" ~ 1,
    TRUE ~ 0
  ))

Summary Tables

reg_24_e <- data1 %>%
  filter(Date > "2023-10-10") %>%
  group_by(type) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  filter(type != "other") %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Error Rate' = error_rate)


reg_24_b <- data2 %>%
  filter(Date > "2023-10-10") %>%
  group_by(type) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  filter(type != "other") %>%
  rename('Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Bad Call Rate' = error_rate)
right_join(reg_24_b, reg_24_e) %>%
  select(type,
         Occurrences,
         'Error Rate',
         'Bad Call Rate') %>%
  reactable( theme = espn()) %>%
  add_title("Referee Analysis for the 2024 Regular Season")

Referee Analysis for the 2024 Regular Season

data1 %>%
  group_by(type) %>%
  filter( Date > "2021-10-01") %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  filter(type != "other") %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Incidences' = total_challenges,
         'Error Rate' = error_rate) %>%
  reactable( theme = espn()) %>%
  add_title("Summary of 'Last 2 Minute' reports from last 3 Regular Seasons")

Summary of 'Last 2 Minute' reports from last 3 Regular Seasons

data1 %>%
  group_by(type) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  filter(type != "other") %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Incidences' = total_challenges,
         'Error Rate' = error_rate) %>%
  reactable( theme = espn()) %>%
  add_title("Error Rate from last 5 Regular Seasons")

Error Rate from last 5 Regular Seasons

data2 %>%
  group_by(type) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  filter(type != "other") %>%
  rename('Missed Calls' = challenges_won,
         'Incidences' = total_challenges,
         'Error Rate' = error_rate) %>%
  reactable( theme = espn()) %>%
  add_title("Bad Call Rate from last 5 Regular Seasons")

Bad Call Rate from last 5 Regular Seasons

Difficulty

data1 %>%
  group_by(type, Difficulty) %>%
  summarize(
    total_challenges = n()
  ) %>%
  filter(Difficulty == c("Difficult", "Observable", "Undetectable")) %>%
  mutate(percent = case_when(
    type == "Defensive Foul" ~ total_challenges/ (14 + 28270 + 1),
    type == "Goaltending" ~ total_challenges/ (192 + 2),
    type == "Offensive Foul" ~ total_challenges/ (4 + 9856 ),
    type == "Loose Ball Foul" ~ total_challenges/ (1 + 4616),
    type == "Possession" ~ total_challenges/ (1 + 2300 + 302)
  )) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Incidences' = total_challenges) %>%
  reactable( theme = espn()) %>%
  add_title("Summary of 'Last 2 Minute' reports from the last Regular Season")

Summary of 'Last 2 Minute' reports from the last Regular Season

data_difficulty <- data1 %>%
  filter(Difficulty == "Undetectable")

Defensive Fouls

def_fouls <- data1 %>%
  filter(type == "Defensive Foul") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Error Rate' = error_rate)

def_fouls2 <- data2 %>%
  filter(type == "Defensive Foul") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Bad Call Rate' = error_rate) 

right_join(def_fouls, def_fouls2) %>%
  rename('Specific Call' = CallType) %>%
  select('Specific Call',
         Occurrences,
         'Error Rate',
         'Bad Call Rate') %>%
  filter(Occurrences > 20) %>%
  reactable( theme = espn()) %>%
  add_title("Analysis of Defensive Fouls from last 5 Regular Seasons")

Analysis of Defensive Fouls from last 5 Regular Seasons

Offensive Fouls

off_fouls1 <- data1 %>%
  filter(type == "Offensive Foul") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Occurences' = total_challenges,
         'Error Rate' = error_rate) 

off_fouls2 <- data2 %>%
  filter(type == "Offensive Foul") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Missed Calls' = challenges_won,
         'Occurences' = total_challenges,
         'Bad Call Rate' = error_rate) 

right_join(off_fouls1, off_fouls2) %>%
  rename('Specific Type' = CallType) %>%
  select('Specific Type',
         Occurences,
         'Error Rate',
         'Bad Call Rate') %>%
  filter(Occurences > 20) %>%
  reactable( theme = espn()) %>%
  add_title("Analysis of Offensive Fouls from last 5 Regular Seasons")

Analysis of Offensive Fouls from last 5 Regular Seasons

Possession

poss1 <- data1 %>%
  filter(type == "Possession") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Incorrect/Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Error Rate' = error_rate)

poss2 <- data2 %>%
  filter(type == "Possession") %>%
  group_by(CallType) %>%
  summarize(
    challenges_won = sum(result),
    total_challenges = n()
  ) %>%
  mutate(error_rate = challenges_won/total_challenges) %>%
  mutate_at(vars(4), round, digits = 4) %>%
  rename('Missed Calls' = challenges_won,
         'Occurrences' = total_challenges,
         'Bad Call Rate' = error_rate)
right_join(poss1,poss2) %>%
  rename('Specific Call' = CallType) %>%
  select('Specific Call',
         Occurrences,
         'Error Rate',
         'Bad Call Rate') %>%
  filter(Occurrences > 20) %>%
  reactable( theme = espn()) %>%
  add_title("Analysis of Possesion based calls from last 5 Regular Seasons")

Analysis of Possesion based calls from last 5 Regular Seasons

Full Table

data1 %>%
  reactable()