# 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)