SummaryCard / app.R
TimStats's picture
Update app.R
444a13f verified
Raw
History Blame
20.8 kB
# Load required libraries
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)
library(jpeg)
pdf(file = NULL)
Sys.setenv(TZ='EST')
# Helper functions
download_and_process_image <- function(url) {
tryCatch({
response <- GET(url)
content_type <- http_type(response)
if (content_type %in% c("image/png", "image/jpeg")) {
temp_file <- tempfile(fileext = ifelse(content_type == "image/png", ".png", ".jpg"))
writeBin(content(response, "raw"), temp_file)
if (content_type == "image/png") {
img <- readPNG(temp_file)
} else {
img <- readJPEG(temp_file)
}
return(list(img = img, type = content_type))
} else {
warning(paste("Unsupported image type:", content_type))
return(NULL)
}
}, error = function(e) {
warning(paste("Error processing image:", e$message))
return(NULL)
})
}
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())
}
# Load models
FB <- xgb.load('FB.model')
Off <- xgb.load('Off.model')
Break <- xgb.load('Break.model')
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)
}
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)
}
scale_TimStuff <- function(raw_score, model_mean, model_sd) {
scaled_score <- (raw_score - model_mean) / model_sd
result <- 50 - (scaled_score * 10)
return(result)
}
calculate_primary <- function(data){
data <- data %>%
# Group by pitch_name, Pitcher Name, Pitcher Id, and date
group_by(pitch_name, `Pitcher Name`, `Pitcher ID`, date) %>%
# Count occurrences and calculate average start_speed, IVB, and HB for each group
mutate(
pitch_count = n(),
avg_start_speed = mean(start_speed, na.rm = TRUE),
avg_IVB = mean(IVB, na.rm = TRUE),
avg_HB = mean(HB, na.rm = TRUE)
) %>%
ungroup() %>%
# Group by Pitcher Name, Pitcher Id, and date
group_by(`Pitcher Name`, `Pitcher ID`, date) %>%
# Add a column to identify the highest occurrence
mutate(
is_highest_occurrence = case_when(
pitch_count == max(pitch_count) ~ 1,
TRUE ~ 0
)
) %>%
# If there's a tie, use avg_start_speed as a tiebreaker
mutate(
is_highest_occurrence = case_when(
is_highest_occurrence == 1 & pitch_count == max(pitch_count[is_highest_occurrence == 1]) &
avg_start_speed == max(avg_start_speed[is_highest_occurrence == 1]) ~ 1,
TRUE ~ 0
)
) %>%
# Calculate primary pitch metrics
mutate(
primary_speed = avg_start_speed[is_highest_occurrence == 1][1],
primary_IVB = avg_IVB[is_highest_occurrence == 1][1],
primary_HB = avg_HB[is_highest_occurrence == 1][1]
) %>%
# Ungroup to remove grouping structure
ungroup()
}
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","primary_speed","primary_IVB","primary_HB")
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,rhp$primary_speed,rhp$primary_IVB,rhp$primary_HB))), -0.00249975, 0.007566558)
game_complete <- rbind(rhp,game_na)
return(game_complete)
}
summary_table <- function(game) {
rows <- nrow(game)
game <- calculate_primary(game)
game <- calculate_TimStuff(game)
sumtable <- game %>%
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() %>%
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),
'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)
}
# Initialize schedule data
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 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("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(" ")),
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, {
tryCatch({
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 <- pitcher_summary(pname[,6],input$date)
}
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 <- pitcher_summary(pname[,6],input$date)
}
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 <- pitcher_summary(pname[,6],input$date)
}
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 <- pitcher_summary(pname[,6],input$date)
}
if(nrow(pname) == 0) {
showNotification("No pitchers found for the selected game.", type = "warning")
} else {
updateSelectizeInput(session = getDefaultReactiveDomain(),"pitcher","Pitcher:",choices = pname[,3])
}
}, error = function(e) {
showNotification(paste("Error finding pitchers:", e$message), type = "error")
})
})
combinedPlot <- reactiveVal()
observeEvent(input$update1, {
tryCatch({
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 <- pitcher_summary(id[,6],input$date)
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 <- pitcher_summary(id[,6],input$date)
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 <- pitcher_summary(id[,6],input$date)
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 <- pitcher_summary(id[,6],input$date)
game <- game %>%
filter(`Pitcher Name` == input$pitcher)
id <- as.character(game[1,4])
}
if(nrow(game) == 0) {
showNotification("No data available for the selected pitcher.", type = "warning")
return()
}
break_plot <- break_plot(game)
pitch_plot <- pitch_plot(game)
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")
text_grobs <- which(sapply(table_plot$grobs, function(g) {
g$name == "core-fg-1" && inherits(g$children[[1]], "text")
}))
for (i in text_grobs) {
text <- table_plot$grobs[[i]]$children[[1]]$label
table_plot$grobs[[i]]$children[[1]]$label <- sub("^\\d+\\s*", "", text)
}
grid.newpage()
grid.draw(table_plot)
mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
img_result <- download_and_process_image(mlb_url)
if (is.null(img_result)) {
img_result <- download_and_process_image(milb_url)
}
if (!is.null(img_result)) {
img_grob <- rasterGrob(img_result$img, interpolate = TRUE)
} else {
img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
}
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 (Data: MLB)", sep = " "), gp = gpar(fontsize = 20, font = 2))
)
combinedPlot(combined)
output$combinedPlot <- renderPlot({
grid.draw(combinedPlot())
})
}, error = function(e) {
showNotification(paste("Error generating card:", e$message), type = "error")
})
})
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)