TimStats's picture
Update app.R
59773a8 verified
Raw
History Blame Contribute Delete
12 kB
# Load required libraries
library(shiny)
library(DT)
library(baseballr)
library(dplyr)
library(xgboost)
library(httr)
Sys.setenv(TZ='EST')
download_private_csv <- function(repo_id, filename) {
url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename)
response <- GET(url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
if (status_code(response) == 200) {
content <- content(response, "text")
con <- textConnection(content)
# Try different read options
data <- read.csv(con,
header = TRUE,
check.names = FALSE, # This prevents R from modifying column names
fileEncoding = "UTF-8",
stringsAsFactors = FALSE)
close(con)
return(data)
} else {
stop("Failed to download dataset")
}
}
download_private_parquet <- function(repo_id, filename) {
library(httr)
library(arrow)
# Create the direct download URL based on your example
url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename, "?download=true")
# Create a temporary file
temp_file <- tempfile(fileext = ".parquet")
# Download directly to file
response <- GET(
url,
add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))),
write_disk(temp_file, overwrite = TRUE)
)
# Check if download was successful
if (status_code(response) == 200) {
tryCatch({
# Read the parquet file
data <- read_parquet(temp_file)
file.remove(temp_file)
return(data)
}, error = function(e) {
file.remove(temp_file)
stop(paste("Error reading parquet file:", e$message))
})
} else {
file.remove(temp_file)
stop(paste("Failed to download file. Status code:", status_code(response)))
}
}
MLB <- download_private_parquet("TimStats/StatcastDataAll", "MLB25.parquet")
AAA <- download_private_parquet("TimStats/StatcastDataAll", "AAA25.parquet")
FSL <- download_private_parquet("TimStats/StatcastDataAll", "FSL25.parquet")
MLB <- MLB %>%
select(
`Pitcher Name`, `Pitcher ID`, pitch_name, start_speed, spin_rate, extension,
IVB, HB, x0, z0
)
# Same selection for AAA before rbind
AAA <- AAA %>%
select(
`Pitcher Name`, `Pitcher ID`, pitch_name, start_speed, spin_rate, extension,
IVB, HB, x0, z0
)
FSL <- FSL %>%
select(
`Pitcher Name`, `Pitcher ID`, pitch_name, start_speed, spin_rate, extension,
IVB, HB, x0, z0
)
# Helper functions
calculate_EAA <- function(extension) {
extension / 6.3
}
calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
inSA <- ifelse(inSA < 0, inSA + 360, inSA)
SADiff <- spinDirection - inSA
SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
return(SADiff)
}
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
}
pitcher_summary <- function(game_pk, date) {
gdate <- as.Date.character(date)
gdate <- as.Date(gdate)
tmilb <- mlb_pbp(game_pk)
tmilb <- tmilb %>%
filter(type == "pitch") %>%
select(matchup.batter.fullName, matchup.batter.id, matchup.pitcher.fullName,
matchup.pitcher.id, matchup.pitchHand.code, details.type.description,
pitchData.startSpeed, pitchData.breaks.spinRate, pitchData.extension,
pitchData.coordinates.x0, pitchData.coordinates.y0, pitchData.coordinates.z0,
pitchData.coordinates.aX, pitchData.coordinates.aY, pitchData.coordinates.aZ,
pitchData.coordinates.vX0, pitchData.coordinates.vZ0, pitchData.coordinates.vY0,
pitchData.coordinates.pfxX, pitchData.coordinates.pfxZ,
pitchData.breaks.breakVerticalInduced, pitchData.breaks.breakHorizontal,
pitchData.breaks.spinDirection)
colnames(tmilb) <- c("Batter Name", "Batter ID", "Pitcher Name", "Pitcher ID",
"phand", "pitch_name", "start_speed", "spin_rate", "extension",
"x0", "y0", "z0", "ax", "ay", "az", "vx0", "vz0", "vy0",
"pfxX", "pfxZ", "IVB", "HB", "spinDirection")
tmilb <- tmilb %>%
mutate(date = gdate)
return(tmilb)
}
calculate_timstuff <- function(game) {
game <- game %>%
mutate(VAA = calculate_VAA(vz0, ay, az, vy0, y0),
EAA = calculate_EAA(extension),
SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection),
ishandL = ifelse(phand == "L", 1, 0))
feature_vars <- c("ishandL", "start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate", "SADiff")
complete_rows <- complete.cases(game[, feature_vars])
game_complete <- game[complete_rows, ]
game_na <- game[!complete_rows,]
game_na$TimStuff <- NA
game_complete$TimStuff <- scale_TimStuff(
predict(model, as.matrix(cbind(game_complete$ishandL, game_complete$start_speed,
game_complete$IVB, game_complete$HB, game_complete$EAA,
game_complete$x0, game_complete$z0, game_complete$spin_rate,
game_complete$SADiff))),
-0.002620635, 0.006021368)
game_complete <- rbind(game_complete, game_na)
return(game_complete)
}
scale_TimStuff <- function(raw_score, model_mean, model_sd) {
scaled_score <- (raw_score - model_mean) / model_sd
result <- 100 - (scaled_score * 10)
return(result)
}
summary_table <- function(data) {
# Current year summary
current_summary <- data %>%
group_by(`Pitcher Name`, `Pitcher ID`, pitch_name) %>%
summarize(
Pitches = n(),
'Velo' = round(mean(start_speed, na.rm = TRUE), 1),
'Spin' = round(mean(spin_rate, na.rm = TRUE), 0),
'Ext' = round(mean(extension, na.rm = TRUE), 1),
'IVB' = round(mean(IVB, na.rm = TRUE), 1),
'HB' = round(mean(HB, na.rm = TRUE), 1),
'RelX' = round(mean(x0, na.rm = TRUE), 1),
'RelZ' = round(mean(z0, na.rm = TRUE), 1),
'TimStuff' = round(mean(TimStuff, na.rm = TRUE), 0),
.groups = "drop"
)
data_2024 <- rbind(MLB,AAA,FSL) %>%
group_by(`Pitcher Name`, `Pitcher ID`, pitch_name) %>%
summarize(
'Velo25' = round(mean(start_speed, na.rm = TRUE), 1),
'Spin25' = round(mean(spin_rate, na.rm = TRUE), 0),
'Ext25' = round(mean(extension, na.rm = TRUE), 1),
'IVB25' = round(mean(IVB, na.rm = TRUE), 1),
'HB25' = round(mean(HB, na.rm = TRUE), 1),
'RelX25' = round(mean(x0, na.rm = TRUE), 1),
'RelZ25' = round(mean(z0, na.rm = TRUE), 1),
.groups = "drop"
)
# Join and calculate differences
combined_data <- current_summary %>%
left_join(data_2024, by = c("Pitcher Name", "Pitcher ID", "pitch_name")) %>%
mutate(
'Velo_Diff' = round(Velo - Velo25, 1),
'Spin_Diff' = round(Spin - Spin25, 1),
'Ext_Diff' = round(Ext - Ext25, 1),
# IVB calculation modified to handle sign changes correctly
'IVB_Diff' = round(ifelse(sign(IVB) != sign(IVB25),
sign(IVB) * (abs(IVB) + abs(IVB25)),
ifelse(IVB < 0,
-1 * (abs(IVB) - abs(IVB25)),
abs(IVB) - abs(IVB25))), 1),
# HB shows increased movement in either direction as positive
'HB_Diff' = round(abs(HB) - abs(HB25), 1),
'RelX_Diff' = round(RelX - RelX25, 1),
'RelZ_Diff' = round(RelZ - RelZ25, 1)
) %>%
arrange(-Pitches)
return(combined_data)
}
# Load TimStuff model
model <- xgb.load('TimStuff2.ubj')
# UI Definition
ui <- fluidPage(
titlePanel("Spring Training Pitch Comparison Dashboard (Not Mobile Compatible)"),
sidebarLayout(
sidebarPanel(
width = 2,
dateInput("date", "Date:"),
selectizeInput("level", "Level:",
c("MLB")),
actionButton("submit", "Get Dashboard"),
downloadButton("download_summary", "Download Summary")
),
mainPanel(
dataTableOutput("schedule")
)
)
)
# Server Definition
server <- function(input, output, session) {
data <- reactiveVal()
games <- reactiveVal()
summary <- reactiveVal()
observeEvent(input$submit, {
season <- format(input$date, "%Y")
# Get schedule based on selected level
schedule_data <- switch(input$level,
"MLB" = mlb_schedule(season = season, level_ids = "1"),
"AAA" = mlb_schedule(season = season, level_ids = "11"),
"FSL" = mlb_schedule(season = season, level_ids = "14"),
"College (Statcast Parks Only)" = mlb_schedule(season = season, level_ids = "22"),
"Futures Game" = mlb_schedule(season = season, level_ids = "21"),
"AFL" = {
sbid <- mlb_schedule(2026, 17) %>%
filter(teams_away_team_name %in% c("Glendale Desert Dogs", "Mesa Solar Sox",
"Peoria Javelinas", "Salt River Rafters",
"Scottsdale Scorpions", "Surprise Saguaros")) %>%
filter(gameday_type == "E")
sbid
}
)
data(schedule_data)
schedule <- data() %>% filter(date == input$date)
# Initialize empty games dataframe
games_data <- data.frame()
# Process each game
for(n in 1:nrow(schedule)) {
tryCatch({
game1 <- pitcher_summary(schedule[n,6], schedule[n,1])
games_data <- rbind(game1, games_data)
}, error = function(e) {
message(paste("Error occurred for game:", schedule[n,6], "on", schedule[n,1]))
})
}
# Calculate TimStuff and create summary
games_data <- calculate_timstuff(games_data)
games(games_data)
summary_data <- summary_table(games_data)
summary(summary_data)
# Render comparison table
output$schedule <- renderDT({
datatable(summary_data,
options = list(
pageLength = 10,
lengthMenu = c(10, 25, 50, 100),
scrollX = TRUE, # Enable horizontal scrolling
fixedColumns = list(left = 4), # Freeze first 3 columns (Name, ID, Pitch)
columnDefs = list(
list(className = 'dt-center', targets = "_all"),
# Names and identifiers
list(width = '150px', targets = c(0, 1)), # Pitcher Name, Pitcher ID
list(width = '100px', targets = 2), # pitch_name
list(width = '70px', targets = 3), # Pitches
# Current year stats
list(width = '50px', targets = c(4:11)), # Velo, Spin, Ext, IVB, HB, RelX, RelZ, TimStuff
# 2024 stats
list(width = '10px', targets = c(12:18)), # Velo_2024, Spin_2024, Ext_2024, IVB_2024, HB_2024, RelX_2024, RelZ_2024
# Difference columns
list(width = '50px', targets = c(19:25)) # All _Diff columns
)
),
extensions = 'FixedColumns'
) %>%
formatStyle(
c('Velo_Diff', 'Spin_Diff', 'Ext_Diff', 'IVB_Diff',
'HB_Diff', 'RelX_Diff', 'RelZ_Diff'),
backgroundColor = styleInterval(
cuts = 0,
values = c('#ffcdd2', '#c8e6c9')
)
)
})
})
# Download handler for summary CSV
output$download_summary <- downloadHandler(
filename = function() {
paste("comparison_data_", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(summary(), file, row.names = FALSE)
}
)
}
shinyApp(ui, server)