Spaces:
Sleeping
Sleeping
| # Load required libraries | |
| library(shiny) | |
| library(plotly) | |
| library(bslib) | |
| library(DT) | |
| library(rsconnect) | |
| library(baseballr) | |
| library(dplyr) | |
| library(ggplot2) | |
| library(xgboost) | |
| library(httr) | |
| #pdf(file = NULL) | |
| Sys.setenv(TZ='EST') | |
| # Helper functions | |
| format_output_data <- function(data) { | |
| data <- data |> | |
| select(`Pitcher Name`, `Pitcher ID`, `Batter Name`, `Batter ID`, pitch_name, pitchNum, des, start_speed, spin_rate, extension, | |
| IVB, HB, x0, z0, SADiff, TimStuff) |> | |
| mutate( | |
| `Pitch` = pitch_name, | |
| `Pitch Number` = pitchNum, | |
| `Pitch Description` = des, | |
| `Pitch Speed` = round(start_speed, 1), | |
| `Spin Rate` = round(spin_rate, 0), | |
| `Extension` = round(extension, 1), | |
| `Induced Vertical Break` = round(IVB, 1), | |
| `Horizontal Break` = round(HB, 1), | |
| `Horizontal Release Point` = round(x0, 1), | |
| `Vertical Release Point` = round(z0, 1), | |
| `Spin Axis Difference` = round(SADiff, 0), | |
| `TimStuff` = round(TimStuff, 0) | |
| ) |> | |
| select(`Pitcher Name`, `Pitcher ID`, `Batter Name`, `Batter ID`, `Pitch`, `Pitch Number`, `Pitch Description`, `Pitch Speed`, | |
| `Spin Rate`, `Extension`, `Induced Vertical Break`, `Horizontal Break`, `Horizontal Release Point`, | |
| `Vertical Release Point`,`Spin Axis Difference`, `TimStuff`) | |
| return(data) | |
| } | |
| is_barrel <- function(df) { | |
| df$barrel <- with(df, ifelse(hit_angle <= 50 & hit_speed >= 97 & hit_speed * 1.5 - | |
| hit_angle >= 117 & hit_speed + hit_angle >= 123, 1, 0)) | |
| return(df) | |
| } | |
| VAA <- function(milbtotal){ | |
| milbtotal <- milbtotal %>% | |
| mutate(VAA = -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/ | |
| ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)) | |
| } | |
| 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") | |
| tmilb <- tmilb %>% | |
| select(matchup.batter.fullName,matchup.batter.id,matchup.pitcher.fullName,matchup.pitcher.id, | |
| matchup.pitchHand.code, pitchNumber,result.event,details.description,details.type.description, | |
| result.description,pitchData.startSpeed,pitchData.plateTime,pitchData.zone, | |
| pitchData.breaks.spinRate,pitchData.extension, pitchData.coordinates.pX, | |
| pitchData.coordinates.pZ,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, | |
| hitData.launchSpeed,hitData.launchAngle,hitData.totalDistance,details.isInPlay, | |
| last.pitch.of.ab,pitchData.breaks.spinDirection) | |
| colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID","phand", | |
| "pitchNum","result","description","pitch_name","des","start_speed", | |
| "plateTime","zone","spin_rate","extension","px","pz","x0", | |
| "y0","z0","ax","ay","az","vx0","vz0","vy0","pfxX","pfxZ", | |
| "IVB","HB","hit_speed","hit_angle","hit_distance","inPlay", | |
| "lastPitch","spinDirection") | |
| tmilb <- is_barrel(tmilb) | |
| tmilb <- tmilb %>% | |
| mutate(is_strike_swinging = ifelse(description == "Swinging Strike" | | |
| description == "Foul Tip",TRUE,FALSE)) | |
| tmilb <- tmilb %>% | |
| mutate(date = gdate) | |
| return(tmilb) | |
| } | |
| calculate_timstuff <- function(game) { | |
| # game <- calculate_primary(game) | |
| game <- game %>% | |
| mutate(VAA = calculate_VAA(vz0, ay, az, vy0, y0), | |
| EAA = calculate_EAA(extension), | |
| SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection), | |
| team_fielding_id = ifelse(description %in% c("Called Strike", "Swinging Strike", "Swinging Strike (Blocked)"), 1, 0), | |
| swing = ifelse(description %in% c("Foul", "Foul Pitchout", "In play, no out", "In play, out(s)", "In play, run(s)", "Swinging Strike", "Swinging Strike (Blocked)", "Foul Tip"), 1, 0), | |
| is_strike_swinging = ifelse(is_strike_swinging, 1, 0), | |
| Pitch = pitch_name, | |
| ishandL = ifelse(phand == "L",1,0)) | |
| # game <- calculate_primary(game) | |
| 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 | |
| rhp <- game_complete | |
| # rhp <- game_complete[game_complete$ishandL == 0] | |
| # | |
| # lhp$TimStuff <- scale_TimStuff(predict(model, as.matrix(cbind(lhp$ishandL,lhp$start_speed, lhp$IVB, lhp$HB, lhp$EAA, lhp$x0, lhp$z0, lhp$spin_rate, lhp$SADiff,lhp$primary_speed,lhp$primary_IVB,lhp$primary_HB))), -0.00249975, 0.007566558) | |
| rhp$TimStuff <- scale_TimStuff(predict(model, as.matrix(cbind(rhp$ishandL,rhp$start_speed, rhp$IVB, rhp$HB, rhp$EAA, rhp$x0, rhp$z0, rhp$spin_rate, rhp$SADiff))), -0.002620635, 0.006021368) | |
| game_complete <- rbind(rhp,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) | |
| } | |
| pitch_plot <- function(game){ | |
| ggplot()+ | |
| geom_point(game,mapping = | |
| aes(x=px, y = pz,color = pitch_name),size = 3.5) + | |
| geom_segment(aes(x=-0.71,xend = 0.71, y = 1.5,yend = 1.5))+ | |
| geom_segment(aes(x=-0.71,xend = 0.71, y = 3.6,yend = 3.6))+ | |
| geom_segment(aes(x= 0.71,xend = 0.71, y = 1.5,yend = 3.6))+ | |
| geom_segment(aes(x=-0.71,xend = -0.71, y = 1.5,yend =3.6))+ | |
| xlab(" ")+ | |
| ylab(" ") + | |
| xlim(-3,3)+ | |
| ylim(0.2,4)+ | |
| coord_fixed(ratio = 1) + | |
| ggthemes::theme_few()+ | |
| guides(color = guide_legend(title = "Pitch Type"))+ | |
| theme(axis.text.x=element_blank(), | |
| axis.ticks.x=element_blank(), | |
| axis.text.y=element_blank(), | |
| axis.ticks.y=element_blank()) | |
| } | |
| # Load models | |
| model <- xgb.load('TimStuff2.ubj') | |
| summary_table <- function(data,pitchern){ | |
| # rows <- nrow(data) | |
| data <- data %>% | |
| group_by(data[,3],Pitch) %>% | |
| summarize( | |
| Pitches = n(), | |
| #'Pitch%' = round(sum(Pitches)/sum(rows) * 100, digits = 1), | |
| 'Avg. Velo' = round(mean(start_speed, na.rm = TRUE), digits = 1), | |
| 'Spin Rate' = round(mean(spin_rate, na.rm = TRUE), digits = 0), | |
| 'Extension' = round(mean(extension, na.rm = TRUE), digits = 1), | |
| 'IVB' = round(mean(IVB, na.rm = TRUE), digits = 1), | |
| 'HB' = round(mean(HB, na.rm = TRUE), digits = 1), | |
| 'Horz. Release Point' = round(mean(x0,na.rm = TRUE),digits = 1), | |
| 'Vert. Release Point' = round(mean(z0,na.rm = TRUE),digits = 1), | |
| 'Spin Axis Difference (in degrees)' = round(mean(SADiff,na.rm = TRUE),digits = 0), | |
| 'VAA' = round(mean(VAA, na.rm = TRUE), digits = 1), | |
| 'CSW%' = round(sum(team_fielding_id, na.rm = TRUE) / sum(!is.na(team_fielding_id)) * 100, digits = 1), | |
| 'Whiff%' = round(sum(is_strike_swinging, na.rm = TRUE) / sum(swing, na.rm = TRUE) * 100, digits = 1), | |
| 'TimStuff+' = round(mean(TimStuff, na.rm = TRUE), digits = 0) | |
| ) %>% | |
| arrange(-Pitches) | |
| return(data) | |
| } | |
| # UI Definition | |
| ui <- fluidPage( | |
| tags$head( | |
| tags$style(HTML(" | |
| @media (max-width: 768px) { | |
| .sidebar { width: 100%; float: none; } | |
| .main-content { margin-left: 0; } | |
| .selectize-input { font-size: 14px; } | |
| .form-group { margin-bottom: 10px; } | |
| .action-button { width: 100%; } | |
| } | |
| ")) | |
| ), | |
| titlePanel("MLB/AAA/FSL Stuff Dashboard (Load Times may take a while as app gathers Statcast Data)"), | |
| sidebarLayout( | |
| sidebarPanel( | |
| width = 2, | |
| dateRangeInput("date", "Date:"), | |
| selectizeInput("level", "Level:", c("MLB", "AAA", "FSL", "College (Statcast Parks Only)", "Draft League","AFL","Appy League")), | |
| actionButton("submit", "Get Dashboard"), | |
| downloadButton("download_summary", "Download Summary"), | |
| selectizeInput("pitcherf", "Pitcher Pitch Level Stuff", c("")), | |
| actionButton("pls", "Get Pitch Level Stuff"), | |
| downloadButton("download_pitch_level", "Download Pitch Level") | |
| ), | |
| mainPanel( | |
| dataTableOutput("schedule") | |
| ) | |
| ) | |
| ) | |
| server <- function(input, output, session) { | |
| data <- reactiveVal() | |
| games <- reactiveVal() | |
| summary <- reactiveVal() | |
| pitch_level_data <- reactiveVal() | |
| observeEvent(input$submit, { | |
| season <- format(input$date[1], "%Y") | |
| if (input$level == "MLB") { | |
| data(mlb_schedule(season = season, level_ids = "1")) | |
| } else if (input$level == "AAA") { | |
| data(mlb_schedule(season = season, level_ids = "11")) | |
| } else if (input$level == "FSL") { | |
| data(mlb_schedule(season = season, level_ids = "14")) | |
| } else if (input$level == "College (Statcast Parks Only)") { | |
| data(mlb_schedule(season = season, level_ids = "22")) | |
| } else if (input$level == "Draft League"){ | |
| draft <- mlb_schedule(season = season, level_ids = "22") %>% | |
| filter(teams_away_team_name == "Frederick Keys" | | |
| teams_away_team_name == "State College Spikes" | | |
| teams_away_team_name == "Trenton Thunder" | | |
| teams_away_team_name == "Williamsport Crosscutters" | | |
| teams_away_team_name == "West Virginia Black Bears" | | |
| teams_away_team_name == "Mahoning Valley Scrappers") | |
| data(draft) | |
| } | |
| else if (input$level == "Appy League"){ | |
| draft <- mlb_schedule(season = season, level_ids = "22") %>% | |
| filter(teams_away_team_name == "Bluefield Ridge Runners" | | |
| teams_away_team_name == "Burlington Sock Puppets" | | |
| teams_away_team_name == "Danville Otterbots" | | |
| teams_away_team_name == "Elizabethton River Riders" | | |
| teams_away_team_name == "Greeneville Flyboys" | | |
| teams_away_team_name == "Pulaski River Turtles" | | |
| teams_away_team_name == "Johnson City Doughboys" | | |
| teams_away_team_name == "Kingsport Axmen") | |
| data(draft) | |
| } | |
| else if (input$level == "AFL") { | |
| sbid <- mlb_schedule(2024,17) | |
| sbid <- sbid %>% | |
| filter(teams_away_team_name == "Glendale Desert Dogs" | | |
| teams_away_team_name == "Mesa Solar Sox" | | |
| teams_away_team_name == "Peoria Javelinas" | | |
| teams_away_team_name == "Salt River Rafters" | | |
| teams_away_team_name == "Scottsdale Scorpions" | | |
| teams_away_team_name == "Surprise Saguaros") %>% | |
| filter(gameday_type == "E") | |
| data(sbid) | |
| } | |
| schedule <- data() | |
| schedule <- schedule[schedule$date >= input$date[1] & schedule$date <= input$date[2], ] | |
| games <- data.frame( | |
| "Batter Name" = character(), "Batter ID" = numeric(), "Pitcher Name" = character(), "Pitcher ID" = numeric(), | |
| "result" = character(), "description" = character(), "pitch_name" = character(), "des" = character(), | |
| "start_speed" = numeric(), "plateTime" = numeric(), "zone" = numeric(), "spin_rate" = numeric(), | |
| "extension" = numeric(), "px" = numeric(), "pz" = numeric(), "x0" = numeric(), "y0" = numeric(), "z0" = numeric(), | |
| "ax" = numeric(), "ay" = numeric(), "az" = numeric(), "vx0" = numeric(), "vz0" = numeric(), "vy0" = numeric(), | |
| "pfxX" = numeric(), "pfxZ" = numeric(), "IVB" = numeric(), "HB" = numeric(), "hit_speed" = numeric(), | |
| "hit_angle" = numeric(), "hit_distance" = numeric(), "inPlay" = logical(), "lastPitch" = logical(), | |
| "spinDirection" = numeric(), "phand" = character(), "barrel" = numeric(), "is_strike_swinging" = numeric(), | |
| "date" = as.Date(character()) | |
| ) | |
| for(n in 1:nrow(schedule)){ | |
| tryCatch({ | |
| game1 <- pitcher_summary(schedule[n,6], schedule[n,1]) | |
| games <- rbind(game1, games) | |
| }, error = function(e) { | |
| message(paste("Error occurred for game:", schedule[n,6], "on", schedule[n,1], "- Skipping this game.")) | |
| }) | |
| } | |
| games <- calculate_timstuff(games) | |
| games(games) | |
| summary_data <- summary_table(games) | |
| summary(summary_data) | |
| output$schedule <- renderDT({ | |
| datatable(summary_data, options = list( | |
| pageLength = 10, | |
| lengthMenu = c(10, 25, 50, 100), | |
| columnDefs = list( | |
| list(className = 'dt-center', targets = "_all") | |
| ) | |
| )) | |
| }) | |
| updateSelectizeInput(session = getDefaultReactiveDomain(), "pitcherf", "Pitcher Pitch Level Stuff", games[, "Pitcher Name"]) | |
| }) | |
| observeEvent(input$pls, { | |
| games_data <- isolate(games()) | |
| if (!is.null(games_data) && !is.null(input$pitcherf) && input$pitcherf != "") { | |
| games_data <- games_data[games_data$`Pitcher Name` == input$pitcherf, ] | |
| formatted_data <- format_output_data(games_data) | |
| pitch_level_data(formatted_data) | |
| output$schedule <- renderDT({ | |
| datatable(formatted_data, options = list( | |
| pageLength = 10, | |
| lengthMenu = c(10, 25, 50, 100), | |
| columnDefs = list( | |
| list(className = 'dt-center', targets = "_all") | |
| ) | |
| )) | |
| }) | |
| } else { | |
| output$schedule <- renderDataTable(NULL) | |
| pitch_level_data(NULL) | |
| } | |
| }) | |
| # Download handler for summary CSV | |
| output$download_summary <- downloadHandler( | |
| filename = function() { | |
| paste("summary_data_", Sys.Date(), ".csv", sep = "") | |
| }, | |
| content = function(file) { | |
| write.csv(summary(), file, row.names = FALSE) | |
| } | |
| ) | |
| # Download handler for pitch level CSV | |
| output$download_pitch_level <- downloadHandler( | |
| filename = function() { | |
| paste("pitch_level_data_", Sys.Date(), ".csv", sep = "") | |
| }, | |
| content = function(file) { | |
| write.csv(pitch_level_data(), file, row.names = FALSE) | |
| } | |
| ) | |
| } | |
| shinyApp(ui, server) |