Spaces:
Runtime error
Runtime error
| # 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) |