SummaryCard / app.R
TimStats's picture
Update app.R
e268041 verified
Raw
History Blame
21.8 kB
library(shiny)
library(plotly)
library(gridlayout)
library(bslib)
library(DT)
library(rsconnect)
library(baseballr)
library(dplyr)
library(tidyverse)
library(rvest)
library(ggplot2)
library(janitor)
library(ggthemes)
library(ggpubr)
library(jsonlite)
library(utils)
library(grid)
library(gridExtra)
library(png)
library(xgboost)
library(httr)
pdf(file = NULL)
Sys.setenv(TZ='EST')
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))
}
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,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,matchup.pitchHand.code)
colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID",
"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","phand")
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)
}
break_plot <- function(game){
ggplot()+
geom_point(game,mapping =
aes(x=HB, y = IVB,color = pitch_name),size = 4) +
geom_vline(xintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
geom_hline(yintercept = 0, color = "lightblue",linewidth = 1,linetype = 4)+
xlab("Horizontal Break from Pitcher's Perspective (in) ")+
ylab("Induced Verical Break (in)") +
xlim(-25,25) +
ylim(-25,25) +
ggthemes::theme_igray()+
theme(panel.grid.minor = element_line(color = "gray",
size = 0.25,
linetype = 1))+
guides(color = guide_legend(title = "Pitch Type"))
}
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())
}
# Assume FB.model, Off.model, and Break.model are already loaded in the environment
# Function to calculate VAA (Vertical Approach Angle)
calculate_VAA <- function(vz0, ay, 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)
}
# Function to calculate EAA (Effective Approach Angle)
calculate_EAA <- function(extension) {
extension / 6.3
}
# Function to calculate SADiff (Spin Axis Differential)
calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
# Calculate initial inSA
inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
# Adjust inSA if it's negative
inSA <- ifelse(inSA < 0, inSA + 360, inSA)
# Calculate SADiff
SADiff <- spinDirection - inSA
# Adjust SADiff to be within -180 to 180 range
SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
return(SADiff)
}
# Function to scale TimStuff
scale_TimStuff <- function(raw_score, model_mean, model_sd) {
scaled_score <- (raw_score - model_mean) / model_sd
result <- 50 - (scaled_score * 10)
# Add logging
# cat("Raw score:", raw_score, "\n")
# cat("Model mean:", model_mean, "\n")
# cat("Model SD:", model_sd, "\n")
# cat("Scaled score:", scaled_score, "\n")
# cat("Final result:", result, "\n")
# Ensure the result is within a reasonable range
# result <- max(min(result, 100), 0)
return(result)
}
FB <- xgb.load('FB.model')
Off <- xgb.load('Off.model')
Break <- xgb.load('Break.model')
# Modify the summary_table function
summary_table <- function(game) {
rows <- nrow(game)
sumtable <- game |>
mutate(
VAA = calculate_VAA(vz0, ay, vy0, y0),
EAA = calculate_EAA(extension),
SADiff = calculate_SADiff(pfxX, pfxZ,spinDirection)
) |>
mutate(team_fielding_id = ifelse(description == "Called Strike" |
description == "Swinging Strike" |
description == "Swinging Strike (Blocked)", 1, 0)) |>
mutate(swing = ifelse(description == "Foul" |
description == "Foul Pitchout" |
description == "In play, no out" |
description == "In play, out(s)" |
description == "In play, run(s)" |
description == "Swinging Strike" |
description == "swinging Strike (Blocked)" |
description == "Foul Tip", 1, 0)) |>
mutate(is_strike_swinging = ifelse(is_strike_swinging == TRUE, 1, 0)) |>
mutate(Pitch = pitch_name) |>
rowwise() |>
mutate(TimStuff = if (phand == 'L') {
case_when(
Pitch %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~
ifelse(any(is.na(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(FB, matrix(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.0011801, 0.007989927)),
Pitch %in% c("Changeup", "Splitter", "Screwball", "Forkball") ~
ifelse(any(is.na(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(Off, matrix(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.002239657, 0.01043216)),
Pitch %in% c("Slider", "Slurve", "Sweeper", "Curveball", "Knuckle Curve", "Slow Curve", "Knuckleball") ~
ifelse(any(is.na(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(Break, matrix(c(start_speed, IVB, -HB, EAA, -x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.004822031, 0.007912765)),
TRUE ~ NA_real_
)
} else {
case_when(
Pitch %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~
ifelse(any(is.na(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(FB, matrix(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.0011801, 0.007989927)),
Pitch %in% c("Changeup", "Splitter", "Screwball", "Forkball") ~
ifelse(any(is.na(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(Off, matrix(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.002239657, 0.01043216)),
Pitch %in% c("Slider", "Slurve", "Sweeper", "Curveball", "Knuckle Curve", "Slow Curve", "Knuckleball") ~
ifelse(any(is.na(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff))),
NA_real_,
scale_TimStuff(predict(Break, matrix(c(start_speed, IVB, HB, EAA, x0, z0, spin_rate, SADiff), nrow = 1, ncol = 8)),
-0.004822031, 0.007912765)),
TRUE ~ NA_real_
)
}) |>
group_by(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),
'VAA' = round(mean(VAA, na.rm = TRUE), digits = 1),
# 'EAA' = round(mean(EAA, na.rm = TRUE), digits = 2),
# 'x0' = round(mean(x0, na.rm = TRUE), digits = 2),
# 'z0' = round(mean(z0, na.rm = TRUE), digits = 2),
# 'SADiff' = round(mean(SADiff, na.rm = TRUE), digits = 2),
'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(sumtable)
}
# The rest of your code remains the same
mlbid <- mlb_schedule(season = 2024, level_ids = "1")
mlbteamH <- mlbid |>
select(teams_home_team_name)
mlbteamH <- distinct(mlbteamH)
mlbteamA <- mlbid |>
select(teams_away_team_name)
mlbteamA <- distinct(mlbteamA)
aaaid <- mlb_schedule(season = 2024, level_ids = "11")
aaateamH <- aaaid |>
select(teams_home_team_name)
aaateamH <- distinct(aaateamH)
aaateamA <- aaaid |>
select(teams_away_team_name)
aaateamA <- distinct(aaateamA)
fslid <- mlb_schedule(season = 2024, level_ids = "14")
fslid <- fslid |>
filter(teams_home_team_name == "Daytona Tortugas" |
teams_home_team_name == "Jupiter Hammerheads" |
teams_home_team_name == "Palm Beach Cardinals" |
teams_home_team_name == "St. Lucie Mets" |
teams_home_team_name == "Bradenton Marauders" |
teams_home_team_name == "Clearwater Threshers" |
teams_home_team_name == "Dunedin Blue Jays" |
teams_home_team_name == "Fort Myers Mighty Mussels" |
teams_home_team_name == "Lakeland Flying Tigers" |
teams_home_team_name == "Tampa Tarpons")
fslteamH <- fslid |>
select(teams_home_team_name)
fslteamH <- distinct(fslteamH)
fslteamA <- fslid |>
select(teams_away_team_name)
fslteamA <- distinct(fslteamA)
sbid <- mlb_schedule(season = 2024, level_ids = "22")
sbteamH <- sbid |>
select(teams_home_team_name)
sbteamH <- distinct(sbteamH)
sbteamA <- sbid |>
select(teams_away_team_name)
sbteamA <- distinct(sbteamA)
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("2024 MLB/AAA/FSL Summary Cards"),
sidebarLayout(
sidebarPanel(
width = 2,
dateInput("date", "Date:"),
selectizeInput("level", "Level:", c("MLB", "AAA", "FSL", "College (Statcast Parks Only)")),
selectizeInput("homeT", "Home Team:", mlbteamH[,1]),
selectizeInput("awayT", "Away Team:", mlbteamA[,1]),
selectizeInput("gamenum", "Game Number (For Doubleheaders):", c("1", "2")),
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
selectizeInput("pitcher", "Pitcher Name:", c(" ")),
# selectizeInput("league", "MLB or MiLB Photo", c("MLB", "MiLB")),
textInput("title", "Card Title"),
actionButton("update1", "Make Card", icon("plus"),
style = "color: #FFFFFF; background-color: #0077B6; width: 100%;"),
downloadButton("downloadPlot", "Download Card")
),
mainPanel(
plotOutput("combinedPlot", height = "900px",width = "1350px")
)
)
)
server <- function(input, output, session) {
observeEvent(input$level, {
if(input$level == "AAA"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = aaateamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = aaateamA[,1])
}
if(input$level == "FSL"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = fslteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = fslteamA[,1])
}
if(input$level == "MLB"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = mlbteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = mlbteamA[,1])
}
if(input$level == "College (Statcast Parks Only)"){
updateSelectizeInput(session = getDefaultReactiveDomain(),"homeT","Home Team:",choices = sbteamH[,1])
updateSelectizeInput(session = getDefaultReactiveDomain(),"awayT","Away Team:",choices = sbteamA[,1])
}
})
observeEvent(input$update, {
if(input$level == "AAA"){
pname <- aaaid |>
filter(date == input$date) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
pname <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
if(input$level == "FSL"){
pname <- fslid |>
filter(date == input$date) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
pname <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
if(input$level == "MLB"){
pname <- mlbid |>
filter(date == as.character.Date(input$date)) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
pname <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
if(input$level == "College (Statcast Parks Only)"){
pname <- sbid |>
filter(date == input$date) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
pname <- tryCatch({pitcher_summary(pname[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
})
combinedPlot <- reactiveVal()
observeEvent(input$update1, {
if(input$level == "MLB"){
id <- mlbid |>
filter(date == as.character.Date(input$date)) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "FSL"){
id <- fslid |>
filter(date == as.character.Date(input$date)) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "AAA"){
id <- aaaid |>
filter(date == as.character.Date(input$date)) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(input$level == "College (Statcast Parks Only)"){
id <- sbid |>
filter(date == as.character.Date(input$date)) |>
filter(teams_home_team_name == input$homeT) |>
filter(teams_away_team_name == input$awayT) |>
filter(game_number == input$gamenum)
game <- tryCatch({pitcher_summary(id[,6],input$date)},error=function(e){cat("ERROR :",conditionMessage(e), "\n")})
game <- game |>
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
break_plot <- break_plot(game)
pitch_plot <- pitch_plot(game)
# Create table plot
# Update the tableGrob in the observeEvent(input$update1, {...}) block
table_plot <- tableGrob(summary_table(game), theme = ttheme_default(
core = list(fg_params = list(cex = 1.7)),
colhead = list(fg_params = list(cex = 1.2)),
rowhead = list(fg_params = list(cex = 2.5))))
table_plot$widths[[1]] <- unit(0, "cm")
# Find the text grobs in the first column
text_grobs <- which(sapply(table_plot$grobs, function(g) {
g$name == "core-fg-1" && inherits(g$children[[1]], "text")
}))
# Remove the numbers from these text grobs
for (i in text_grobs) {
text <- table_plot$grobs[[i]]$children[[1]]$label
table_plot$grobs[[i]]$children[[1]]$label <- sub("^\\d+\\s*", "", text)
}
# Draw the modified table
grid.newpage()
grid.draw(table_plot)
# Download and process image
# if(input$league == "MLB"){
# y <- paste("https://midfield.mlbstatic.com/v1/people/","/mlb/300?circle=false",sep = id)
# } else {
# y <- paste("https://midfield.mlbstatic.com/v1/people/","/milb/300?circle=false",sep = id)
# }
#if (input$league == "MLB") {
y <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
# } else {
# y <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
# }
# Initialize the URL to NULL
y <- NULL
# Try the MLB URL first
mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
response <- httr::GET(mlb_url)
if (httr::status_code(response) == 200) {
y <- mlb_url # Use MLB URL if it returns a 200 status code
} else if (httr::status_code(response) == 404) {
# If MLB URL fails with a 404, try the MiLB URL
milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
response <- httr::GET(milb_url)
if (httr::status_code(response) == 200) {
y <- milb_url # Use MiLB URL if it returns a 200 status code
} else {
stop("Both MLB and MiLB URLs failed with 404 error")
}
} else {
stop("MLB URL failed with an unexpected error")
}
photo <- tempfile()
download.file(y, photo, mode = 'wb')
img <- readPNG(photo)
img_grob <- rasterGrob(img, interpolate = TRUE)
# Combine all elements into one plot
combined <- grid.arrange(
arrangeGrob(table_plot, img_grob, ncol = 2, widths = c(2/3, 1/3)),
arrangeGrob(break_plot, pitch_plot, ncol = 2),
nrow = 2,
top = textGrob(paste(input$title, "Made by @TimStats", sep = " "), gp = gpar(fontsize = 20, font = 2))
)
combinedPlot(combined)
output$combinedPlot <- renderPlot({
grid.draw(combinedPlot())
})
})
output$downloadPlot <- downloadHandler(
filename = function() {
paste("baseball_card_", Sys.Date(), ".png", sep = "")
},
content = function(file) {
ggsave(file, plot = combinedPlot(), width = 18, height = 12, dpi = 300)
}
)
}
shinyApp(ui, server)