Spaces:
Sleeping
Sleeping
Update app.R
Browse files
app.R
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# Load required libraries
|
| 2 |
library(shiny)
|
| 3 |
library(plotly)
|
| 4 |
library(gridlayout)
|
|
@@ -21,6 +20,7 @@ library(png)
|
|
| 21 |
library(xgboost)
|
| 22 |
library(httr)
|
| 23 |
library(jpeg)
|
|
|
|
| 24 |
|
| 25 |
pdf(file = NULL)
|
| 26 |
Sys.setenv(TZ='EST')
|
|
@@ -98,48 +98,53 @@ pitcher_summary <- function(game_pk,date){
|
|
| 98 |
}
|
| 99 |
|
| 100 |
break_plot <- function(game){
|
| 101 |
-
ggplot()+
|
| 102 |
-
geom_point(
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
pitch_plot <- function(game){
|
| 118 |
-
ggplot()+
|
| 119 |
-
geom_point(
|
| 120 |
-
|
| 121 |
-
geom_segment(aes(x=-0.71,xend = 0.71, y =
|
| 122 |
-
geom_segment(aes(x=
|
| 123 |
-
geom_segment(aes(x= 0.71,xend = 0.71, y = 1.5,yend = 3.6))+
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
xlim(-3,3)+
|
| 128 |
-
ylim(0.2,4)+
|
| 129 |
coord_fixed(ratio = 1) +
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
# Load models
|
| 139 |
-
FB <- xgb.load('FB.model')
|
| 140 |
-
Off <- xgb.load('Off.model')
|
| 141 |
-
Break <- xgb.load('Break.model')
|
| 142 |
model <- xgb.load('TimStuff2.model')
|
|
|
|
| 143 |
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
|
| 144 |
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
|
| 145 |
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
|
|
@@ -163,32 +168,24 @@ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
|
|
| 163 |
result <- 100 - (scaled_score * 10)
|
| 164 |
return(result)
|
| 165 |
}
|
|
|
|
| 166 |
calculate_primary <- function(data){
|
| 167 |
data <- data %>%
|
| 168 |
-
# Group by pitch_name, Pitcher Name, Pitcher Id, and date
|
| 169 |
group_by(pitch_name, `Pitcher Name`, `Pitcher ID`, date) %>%
|
| 170 |
-
|
| 171 |
-
# Count occurrences and calculate average start_speed, IVB, and HB for each group
|
| 172 |
mutate(
|
| 173 |
pitch_count = n(),
|
| 174 |
avg_start_speed = mean(start_speed, na.rm = TRUE),
|
| 175 |
avg_IVB = mean(IVB, na.rm = TRUE),
|
| 176 |
avg_HB = mean(HB, na.rm = TRUE)
|
| 177 |
) %>%
|
| 178 |
-
|
| 179 |
ungroup() %>%
|
| 180 |
-
# Group by Pitcher Name, Pitcher Id, and date
|
| 181 |
group_by(`Pitcher Name`, `Pitcher ID`, date) %>%
|
| 182 |
-
|
| 183 |
-
# Add a column to identify the highest occurrence
|
| 184 |
mutate(
|
| 185 |
is_highest_occurrence = case_when(
|
| 186 |
pitch_count == max(pitch_count) ~ 1,
|
| 187 |
TRUE ~ 0
|
| 188 |
)
|
| 189 |
) %>%
|
| 190 |
-
|
| 191 |
-
# If there's a tie, use avg_start_speed as a tiebreaker
|
| 192 |
mutate(
|
| 193 |
is_highest_occurrence = case_when(
|
| 194 |
is_highest_occurrence == 1 & pitch_count == max(pitch_count[is_highest_occurrence == 1]) &
|
|
@@ -196,15 +193,11 @@ calculate_primary <- function(data){
|
|
| 196 |
TRUE ~ 0
|
| 197 |
)
|
| 198 |
) %>%
|
| 199 |
-
|
| 200 |
-
# Calculate primary pitch metrics
|
| 201 |
mutate(
|
| 202 |
primary_speed = avg_start_speed[is_highest_occurrence == 1][1],
|
| 203 |
primary_IVB = avg_IVB[is_highest_occurrence == 1][1],
|
| 204 |
primary_HB = avg_HB[is_highest_occurrence == 1][1]
|
| 205 |
) %>%
|
| 206 |
-
|
| 207 |
-
# Ungroup to remove grouping structure
|
| 208 |
ungroup()
|
| 209 |
}
|
| 210 |
|
|
@@ -219,7 +212,6 @@ calculate_timstuff <- function(game) {
|
|
| 219 |
is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
|
| 220 |
Pitch = pitch_name,
|
| 221 |
ishandL = ifelse(phand == "L",1,0))
|
| 222 |
-
# game <- calculate_primary(game)
|
| 223 |
|
| 224 |
feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff","primary_speed","primary_IVB","primary_HB")
|
| 225 |
complete_rows <- complete.cases(game[, feature_vars])
|
|
@@ -229,15 +221,12 @@ calculate_timstuff <- function(game) {
|
|
| 229 |
|
| 230 |
rhp <- game_complete
|
| 231 |
|
| 232 |
-
# rhp <- game_complete[game_complete$ishandL == 0]
|
| 233 |
-
#
|
| 234 |
-
# 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)
|
| 235 |
-
|
| 236 |
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)
|
| 237 |
|
| 238 |
game_complete <- rbind(rhp,game_na)
|
| 239 |
return(game_complete)
|
| 240 |
}
|
|
|
|
| 241 |
summary_table <- function(game) {
|
| 242 |
rows <- nrow(game)
|
| 243 |
game <- calculate_primary(game)
|
|
@@ -273,7 +262,30 @@ summary_table <- function(game) {
|
|
| 273 |
) %>%
|
| 274 |
arrange(-Pitches)
|
| 275 |
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
# Initialize schedule data
|
|
@@ -322,59 +334,57 @@ sbteamA <- distinct(sbteamA)
|
|
| 322 |
|
| 323 |
# UI Definition
|
| 324 |
ui <- fluidPage(
|
| 325 |
-
|
| 326 |
-
tags$style(HTML("
|
| 327 |
-
@media (max-width: 768px) {
|
| 328 |
-
.sidebar { width: 100%; float: none; }
|
| 329 |
-
.main-content { margin-left: 0; }
|
| 330 |
-
.selectize-input { font-size: 14px; }
|
| 331 |
-
.form-group { margin-bottom: 10px; }
|
| 332 |
-
.action-button { width: 100%; }
|
| 333 |
-
}
|
| 334 |
-
"))
|
| 335 |
-
),
|
| 336 |
titlePanel("2024 MLB/AAA/FSL Summary Cards"),
|
| 337 |
sidebarLayout(
|
| 338 |
sidebarPanel(
|
| 339 |
-
width =
|
| 340 |
-
dateInput("date", "Date:"),
|
| 341 |
-
selectizeInput("level", "Level:",
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
|
| 346 |
-
|
| 347 |
-
selectizeInput("pitcher", "Pitcher Name:",
|
| 348 |
textInput("title", "Card Title"),
|
| 349 |
actionButton("update1", "Make Card", icon("plus"),
|
| 350 |
-
|
| 351 |
-
downloadButton("downloadPlot", "Download Card")
|
| 352 |
),
|
| 353 |
mainPanel(
|
| 354 |
-
plotOutput("combinedPlot", height = "900px",width = "
|
| 355 |
)
|
| 356 |
)
|
| 357 |
)
|
|
|
|
| 358 |
server <- function(input, output, session) {
|
| 359 |
observeEvent(input$level, {
|
| 360 |
if(input$level == "AAA"){
|
| 361 |
-
updateSelectizeInput(session
|
| 362 |
-
updateSelectizeInput(session
|
| 363 |
}
|
| 364 |
if(input$level == "FSL"){
|
| 365 |
-
updateSelectizeInput(session
|
| 366 |
-
updateSelectizeInput(session
|
| 367 |
}
|
| 368 |
if(input$level == "MLB"){
|
| 369 |
-
updateSelectizeInput(session
|
| 370 |
-
updateSelectizeInput(session
|
| 371 |
}
|
| 372 |
if(input$level == "College (Statcast Parks Only)"){
|
| 373 |
-
updateSelectizeInput(session
|
| 374 |
-
updateSelectizeInput(session
|
| 375 |
}
|
| 376 |
})
|
| 377 |
|
|
|
|
|
|
|
| 378 |
observeEvent(input$update, {
|
| 379 |
tryCatch({
|
| 380 |
if(input$level == "AAA"){
|
|
@@ -383,7 +393,7 @@ server <- function(input, output, session) {
|
|
| 383 |
filter(teams_home_team_name == input$homeT) %>%
|
| 384 |
filter(teams_away_team_name == input$awayT) %>%
|
| 385 |
filter(game_number == input$gamenum)
|
| 386 |
-
pname <- pitcher_summary(pname[,6],input$date)
|
| 387 |
}
|
| 388 |
if(input$level == "FSL"){
|
| 389 |
pname <- fslid %>%
|
|
@@ -391,7 +401,7 @@ server <- function(input, output, session) {
|
|
| 391 |
filter(teams_home_team_name == input$homeT) %>%
|
| 392 |
filter(teams_away_team_name == input$awayT) %>%
|
| 393 |
filter(game_number == input$gamenum)
|
| 394 |
-
pname <- pitcher_summary(pname[,6],input$date)
|
| 395 |
}
|
| 396 |
if(input$level == "MLB"){
|
| 397 |
pname <- mlbid %>%
|
|
@@ -399,7 +409,7 @@ server <- function(input, output, session) {
|
|
| 399 |
filter(teams_home_team_name == input$homeT) %>%
|
| 400 |
filter(teams_away_team_name == input$awayT) %>%
|
| 401 |
filter(game_number == input$gamenum)
|
| 402 |
-
pname <- pitcher_summary(pname[,6],input$date)
|
| 403 |
}
|
| 404 |
if(input$level == "College (Statcast Parks Only)"){
|
| 405 |
pname <- sbid %>%
|
|
@@ -407,92 +417,73 @@ server <- function(input, output, session) {
|
|
| 407 |
filter(teams_home_team_name == input$homeT) %>%
|
| 408 |
filter(teams_away_team_name == input$awayT) %>%
|
| 409 |
filter(game_number == input$gamenum)
|
| 410 |
-
pname <- pitcher_summary(pname[,6],input$date)
|
| 411 |
}
|
| 412 |
|
| 413 |
if(nrow(pname) == 0) {
|
| 414 |
showNotification("No pitchers found for the selected game.", type = "warning")
|
| 415 |
} else {
|
| 416 |
-
updateSelectizeInput(session
|
|
|
|
| 417 |
}
|
| 418 |
}, error = function(e) {
|
| 419 |
showNotification(paste("Error finding pitchers:", e$message), type = "error")
|
| 420 |
})
|
| 421 |
})
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
combinedPlot <- reactiveVal()
|
| 424 |
observeEvent(input$update1, {
|
|
|
|
| 425 |
tryCatch({
|
| 426 |
-
|
| 427 |
-
id <- mlbid %>%
|
| 428 |
-
filter(date == as.character.Date(input$date)) %>%
|
| 429 |
-
filter(teams_home_team_name == input$homeT) %>%
|
| 430 |
-
filter(teams_away_team_name == input$awayT) %>%
|
| 431 |
-
filter(game_number == input$gamenum)
|
| 432 |
-
game <- pitcher_summary(id[,6],input$date)
|
| 433 |
-
game <- game %>%
|
| 434 |
-
filter(`Pitcher Name` == input$pitcher)
|
| 435 |
-
id <- as.character(game[1,4])
|
| 436 |
-
}
|
| 437 |
-
if(input$level == "FSL"){
|
| 438 |
-
id <- fslid %>%
|
| 439 |
-
filter(date == as.character.Date(input$date)) %>%
|
| 440 |
-
filter(teams_home_team_name == input$homeT) %>%
|
| 441 |
-
filter(teams_away_team_name == input$awayT) %>%
|
| 442 |
-
filter(game_number == input$gamenum)
|
| 443 |
-
game <- pitcher_summary(id[,6],input$date)
|
| 444 |
-
game <- game %>%
|
| 445 |
-
filter(`Pitcher Name` == input$pitcher)
|
| 446 |
-
id <- as.character(game[1,4])
|
| 447 |
-
}
|
| 448 |
-
if(input$level == "AAA"){
|
| 449 |
-
id <- aaaid %>%
|
| 450 |
-
filter(date == as.character.Date(input$date)) %>%
|
| 451 |
-
filter(teams_home_team_name == input$homeT) %>%
|
| 452 |
-
filter(teams_away_team_name == input$awayT) %>%
|
| 453 |
-
filter(game_number == input$gamenum)
|
| 454 |
-
game <- pitcher_summary(id[,6],input$date)
|
| 455 |
-
game <- game %>%
|
| 456 |
-
filter(`Pitcher Name` == input$pitcher)
|
| 457 |
-
id <- as.character(game[1,4])
|
| 458 |
-
}
|
| 459 |
-
if(input$level == "College (Statcast Parks Only)"){
|
| 460 |
-
id <- sbid %>%
|
| 461 |
-
filter(date == as.character.Date(input$date)) %>%
|
| 462 |
-
filter(teams_home_team_name == input$homeT) %>%
|
| 463 |
-
filter(teams_away_team_name == input$awayT) %>%
|
| 464 |
-
filter(game_number == input$gamenum)
|
| 465 |
-
game <- pitcher_summary(id[,6],input$date)
|
| 466 |
-
game <- game %>%
|
| 467 |
-
filter(`Pitcher Name` == input$pitcher)
|
| 468 |
-
id <- as.character(game[1,4])
|
| 469 |
-
}
|
| 470 |
|
| 471 |
if(nrow(game) == 0) {
|
| 472 |
showNotification("No data available for the selected pitcher.", type = "warning")
|
| 473 |
return()
|
| 474 |
}
|
| 475 |
|
| 476 |
-
break_plot <- break_plot(game)
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
rowhead = list(fg_params = list(cex = 2.5))))
|
| 483 |
-
table_plot$widths[[1]] <- unit(0, "cm")
|
| 484 |
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
| 488 |
|
| 489 |
-
for
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
}
|
| 493 |
|
| 494 |
-
|
| 495 |
-
grid.draw(table_plot)
|
| 496 |
|
| 497 |
mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
|
| 498 |
milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
|
|
@@ -508,13 +499,76 @@ server <- function(input, output, session) {
|
|
| 508 |
img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
|
| 509 |
}
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
)
|
| 517 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
combinedPlot(combined)
|
| 519 |
|
| 520 |
output$combinedPlot <- renderPlot({
|
|
|
|
|
|
|
| 1 |
library(shiny)
|
| 2 |
library(plotly)
|
| 3 |
library(gridlayout)
|
|
|
|
| 20 |
library(xgboost)
|
| 21 |
library(httr)
|
| 22 |
library(jpeg)
|
| 23 |
+
library(zoo) # For rolling mean calculation
|
| 24 |
|
| 25 |
pdf(file = NULL)
|
| 26 |
Sys.setenv(TZ='EST')
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
break_plot <- function(game){
|
| 101 |
+
ggplot(game, aes(x = HB, y = IVB, color = pitch_name)) +
|
| 102 |
+
geom_point(size = 2) +
|
| 103 |
+
geom_vline(xintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
|
| 104 |
+
geom_hline(yintercept = 0, color = "lightblue", linewidth = 1, linetype = 4) +
|
| 105 |
+
labs(x = "Horizontal Break (in)", y = "Induced Vertical Break (in)",
|
| 106 |
+
title = "Pitch Movement") +
|
| 107 |
+
xlim(-25, 25) +
|
| 108 |
+
ylim(-25, 25) +
|
| 109 |
+
# scale_x_continuous(breaks = seq(-20, 20, by = 20)) +
|
| 110 |
+
# scale_y_continuous(breaks = seq(-20, 20, by = 20)) +
|
| 111 |
+
theme_minimal() +
|
| 112 |
+
theme(
|
| 113 |
+
legend.position = "bottom",
|
| 114 |
+
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 115 |
+
panel.grid.minor = element_line(color = "gray", size = 0.25, linetype = 1),
|
| 116 |
+
aspect.ratio = 1 # This ensures the plot is square
|
| 117 |
+
) +
|
| 118 |
+
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 119 |
}
|
| 120 |
|
| 121 |
pitch_plot <- function(game){
|
| 122 |
+
ggplot(game, aes(x = px, y = pz, color = pitch_name)) +
|
| 123 |
+
geom_point(size = 3.5) +
|
| 124 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 1.5, yend = 1.5)) +
|
| 125 |
+
geom_segment(aes(x = -0.71, xend = 0.71, y = 3.6, yend = 3.6)) +
|
| 126 |
+
geom_segment(aes(x = 0.71, xend = 0.71, y = 1.5, yend = 3.6)) +
|
| 127 |
+
geom_segment(aes(x = -0.71, xend = -0.71, y = 1.5, yend = 3.6)) +
|
| 128 |
+
labs(x = NULL, y = NULL, title = "Pitch Location") +
|
| 129 |
+
xlim(-3, 3) +
|
| 130 |
+
ylim(0.2, 4) +
|
|
|
|
|
|
|
| 131 |
coord_fixed(ratio = 1) +
|
| 132 |
+
theme_minimal() +
|
| 133 |
+
theme(
|
| 134 |
+
legend.position = "bottom",
|
| 135 |
+
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 136 |
+
axis.text = element_blank(),
|
| 137 |
+
axis.ticks = element_blank()
|
| 138 |
+
) +
|
| 139 |
+
guides(color = guide_legend(title = "Pitch Type", nrow = 1))
|
| 140 |
}
|
| 141 |
|
| 142 |
# Load models
|
| 143 |
+
# FB <- xgb.load('FB.model')
|
| 144 |
+
# Off <- xgb.load('Off.model')
|
| 145 |
+
# Break <- xgb.load('Break.model')
|
| 146 |
model <- xgb.load('TimStuff2.model')
|
| 147 |
+
|
| 148 |
calculate_VAA <- function(vz0, ay, az, vy0, y0) {
|
| 149 |
-atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
|
| 150 |
ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
|
|
|
|
| 168 |
result <- 100 - (scaled_score * 10)
|
| 169 |
return(result)
|
| 170 |
}
|
| 171 |
+
|
| 172 |
calculate_primary <- function(data){
|
| 173 |
data <- data %>%
|
|
|
|
| 174 |
group_by(pitch_name, `Pitcher Name`, `Pitcher ID`, date) %>%
|
|
|
|
|
|
|
| 175 |
mutate(
|
| 176 |
pitch_count = n(),
|
| 177 |
avg_start_speed = mean(start_speed, na.rm = TRUE),
|
| 178 |
avg_IVB = mean(IVB, na.rm = TRUE),
|
| 179 |
avg_HB = mean(HB, na.rm = TRUE)
|
| 180 |
) %>%
|
|
|
|
| 181 |
ungroup() %>%
|
|
|
|
| 182 |
group_by(`Pitcher Name`, `Pitcher ID`, date) %>%
|
|
|
|
|
|
|
| 183 |
mutate(
|
| 184 |
is_highest_occurrence = case_when(
|
| 185 |
pitch_count == max(pitch_count) ~ 1,
|
| 186 |
TRUE ~ 0
|
| 187 |
)
|
| 188 |
) %>%
|
|
|
|
|
|
|
| 189 |
mutate(
|
| 190 |
is_highest_occurrence = case_when(
|
| 191 |
is_highest_occurrence == 1 & pitch_count == max(pitch_count[is_highest_occurrence == 1]) &
|
|
|
|
| 193 |
TRUE ~ 0
|
| 194 |
)
|
| 195 |
) %>%
|
|
|
|
|
|
|
| 196 |
mutate(
|
| 197 |
primary_speed = avg_start_speed[is_highest_occurrence == 1][1],
|
| 198 |
primary_IVB = avg_IVB[is_highest_occurrence == 1][1],
|
| 199 |
primary_HB = avg_HB[is_highest_occurrence == 1][1]
|
| 200 |
) %>%
|
|
|
|
|
|
|
| 201 |
ungroup()
|
| 202 |
}
|
| 203 |
|
|
|
|
| 212 |
is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
|
| 213 |
Pitch = pitch_name,
|
| 214 |
ishandL = ifelse(phand == "L",1,0))
|
|
|
|
| 215 |
|
| 216 |
feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff","primary_speed","primary_IVB","primary_HB")
|
| 217 |
complete_rows <- complete.cases(game[, feature_vars])
|
|
|
|
| 221 |
|
| 222 |
rhp <- game_complete
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
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)
|
| 225 |
|
| 226 |
game_complete <- rbind(rhp,game_na)
|
| 227 |
return(game_complete)
|
| 228 |
}
|
| 229 |
+
|
| 230 |
summary_table <- function(game) {
|
| 231 |
rows <- nrow(game)
|
| 232 |
game <- calculate_primary(game)
|
|
|
|
| 262 |
) %>%
|
| 263 |
arrange(-Pitches)
|
| 264 |
|
| 265 |
+
result <- sumtable %>%
|
| 266 |
+
select(Pitch, Pitches, `Pitch%`, `Avg. Velo`, `Spin Rate`, Extension,IVB, HB, VAA, `CSW%`, `Whiff%`, `TimStuff+`) %>%
|
| 267 |
+
rename(
|
| 268 |
+
"Type" = Pitch,
|
| 269 |
+
"#" = Pitches,
|
| 270 |
+
"Velo" = `Avg. Velo`,
|
| 271 |
+
"Spin" = `Spin Rate`,
|
| 272 |
+
"Ext" = Extension,
|
| 273 |
+
"Use%" = `Pitch%`
|
| 274 |
+
) %>%
|
| 275 |
+
mutate(
|
| 276 |
+
"Use%" = paste0(`Use%`, "%"),
|
| 277 |
+
"Spin" = format(round(Spin), big.mark = ","),
|
| 278 |
+
Velo = round(Velo, 1),
|
| 279 |
+
Ext = round(Ext, 1),
|
| 280 |
+
IVB = round(IVB, 1),
|
| 281 |
+
HB = round(HB, 1),
|
| 282 |
+
VAA = round(VAA, 1),
|
| 283 |
+
`CSW%` = paste0(`CSW%`, "%"),
|
| 284 |
+
`Whiff%` = paste0(`Whiff%`, "%")
|
| 285 |
+
) %>%
|
| 286 |
+
arrange(desc(`#`))
|
| 287 |
+
|
| 288 |
+
return(result)
|
| 289 |
}
|
| 290 |
|
| 291 |
# Initialize schedule data
|
|
|
|
| 334 |
|
| 335 |
# UI Definition
|
| 336 |
ui <- fluidPage(
|
| 337 |
+
theme = bs_theme(version = 5, bootswatch = "flatly"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
titlePanel("2024 MLB/AAA/FSL Summary Cards"),
|
| 339 |
sidebarLayout(
|
| 340 |
sidebarPanel(
|
| 341 |
+
width = 3,
|
| 342 |
+
dateInput("date", "Date:", value = Sys.Date()),
|
| 343 |
+
selectizeInput("level", "Level:",
|
| 344 |
+
c("MLB", "AAA", "FSL", "College (Statcast Parks Only)"),
|
| 345 |
+
options = list(
|
| 346 |
+
placeholder = 'Select a level',
|
| 347 |
+
onInitialize = I('function() { this.setValue(""); }')
|
| 348 |
+
)),
|
| 349 |
+
selectizeInput("homeT", "Home Team:", NULL),
|
| 350 |
+
selectizeInput("awayT", "Away Team:", NULL),
|
| 351 |
+
selectizeInput("gamenum", "Game Number:", c("1", "2")),
|
| 352 |
actionButton("update", "Find Pitcher", icon("magnifying-glass"),
|
| 353 |
+
class = "btn-primary btn-block"),
|
| 354 |
+
selectizeInput("pitcher", "Pitcher Name:", NULL),
|
| 355 |
textInput("title", "Card Title"),
|
| 356 |
actionButton("update1", "Make Card", icon("plus"),
|
| 357 |
+
class = "btn-success btn-block"),
|
| 358 |
+
downloadButton("downloadPlot", "Download Card", class = "btn-info btn-block")
|
| 359 |
),
|
| 360 |
mainPanel(
|
| 361 |
+
plotOutput("combinedPlot", height = "900px", width = "100%")
|
| 362 |
)
|
| 363 |
)
|
| 364 |
)
|
| 365 |
+
|
| 366 |
server <- function(input, output, session) {
|
| 367 |
observeEvent(input$level, {
|
| 368 |
if(input$level == "AAA"){
|
| 369 |
+
updateSelectizeInput(session, "homeT", "Home Team:", choices = aaateamH[,1])
|
| 370 |
+
updateSelectizeInput(session, "awayT", "Away Team:", choices = aaateamA[,1])
|
| 371 |
}
|
| 372 |
if(input$level == "FSL"){
|
| 373 |
+
updateSelectizeInput(session, "homeT", "Home Team:", choices = fslteamH[,1])
|
| 374 |
+
updateSelectizeInput(session, "awayT", "Away Team:", choices = fslteamA[,1])
|
| 375 |
}
|
| 376 |
if(input$level == "MLB"){
|
| 377 |
+
updateSelectizeInput(session, "homeT", "Home Team:", choices = mlbteamH[,1])
|
| 378 |
+
updateSelectizeInput(session, "awayT", "Away Team:", choices = mlbteamA[,1])
|
| 379 |
}
|
| 380 |
if(input$level == "College (Statcast Parks Only)"){
|
| 381 |
+
updateSelectizeInput(session, "homeT", "Home Team:", choices = sbteamH[,1])
|
| 382 |
+
updateSelectizeInput(session, "awayT", "Away Team:", choices = sbteamA[,1])
|
| 383 |
}
|
| 384 |
})
|
| 385 |
|
| 386 |
+
game_data <- reactiveVal()
|
| 387 |
+
|
| 388 |
observeEvent(input$update, {
|
| 389 |
tryCatch({
|
| 390 |
if(input$level == "AAA"){
|
|
|
|
| 393 |
filter(teams_home_team_name == input$homeT) %>%
|
| 394 |
filter(teams_away_team_name == input$awayT) %>%
|
| 395 |
filter(game_number == input$gamenum)
|
| 396 |
+
pname <- pitcher_summary(pname[,6], input$date)
|
| 397 |
}
|
| 398 |
if(input$level == "FSL"){
|
| 399 |
pname <- fslid %>%
|
|
|
|
| 401 |
filter(teams_home_team_name == input$homeT) %>%
|
| 402 |
filter(teams_away_team_name == input$awayT) %>%
|
| 403 |
filter(game_number == input$gamenum)
|
| 404 |
+
pname <- pitcher_summary(pname[,6], input$date)
|
| 405 |
}
|
| 406 |
if(input$level == "MLB"){
|
| 407 |
pname <- mlbid %>%
|
|
|
|
| 409 |
filter(teams_home_team_name == input$homeT) %>%
|
| 410 |
filter(teams_away_team_name == input$awayT) %>%
|
| 411 |
filter(game_number == input$gamenum)
|
| 412 |
+
pname <- pitcher_summary(pname[,6], input$date)
|
| 413 |
}
|
| 414 |
if(input$level == "College (Statcast Parks Only)"){
|
| 415 |
pname <- sbid %>%
|
|
|
|
| 417 |
filter(teams_home_team_name == input$homeT) %>%
|
| 418 |
filter(teams_away_team_name == input$awayT) %>%
|
| 419 |
filter(game_number == input$gamenum)
|
| 420 |
+
pname <- pitcher_summary(pname[,6], input$date)
|
| 421 |
}
|
| 422 |
|
| 423 |
if(nrow(pname) == 0) {
|
| 424 |
showNotification("No pitchers found for the selected game.", type = "warning")
|
| 425 |
} else {
|
| 426 |
+
updateSelectizeInput(session, "pitcher", "Pitcher:", choices = unique(pname$`Pitcher Name`))
|
| 427 |
+
game_data(pname)
|
| 428 |
}
|
| 429 |
}, error = function(e) {
|
| 430 |
showNotification(paste("Error finding pitchers:", e$message), type = "error")
|
| 431 |
})
|
| 432 |
})
|
| 433 |
|
| 434 |
+
rolling_timstuff <- reactive({
|
| 435 |
+
req(input$update1, game_data())
|
| 436 |
+
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
|
| 437 |
+
game <- calculate_timstuff(game)
|
| 438 |
+
game %>%
|
| 439 |
+
arrange(date) %>%
|
| 440 |
+
group_by(pitch_name) %>%
|
| 441 |
+
mutate(rolling_timstuff = rollmean(TimStuff, k = 5, fill = NA, align = "right"),
|
| 442 |
+
pitch_number = row_number()) %>%
|
| 443 |
+
ungroup() # Make sure to ungroup after the grouping operations
|
| 444 |
+
})
|
| 445 |
+
|
| 446 |
combinedPlot <- reactiveVal()
|
| 447 |
observeEvent(input$update1, {
|
| 448 |
+
req(game_data())
|
| 449 |
tryCatch({
|
| 450 |
+
game <- game_data() %>% filter(`Pitcher Name` == input$pitcher)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
|
| 452 |
if(nrow(game) == 0) {
|
| 453 |
showNotification("No data available for the selected pitcher.", type = "warning")
|
| 454 |
return()
|
| 455 |
}
|
| 456 |
|
| 457 |
+
break_plot <- break_plot(game) +
|
| 458 |
+
theme(legend.position = "none")
|
| 459 |
+
|
| 460 |
+
pitch_plot <- pitch_plot(game) +
|
| 461 |
+
theme(legend.position = "none")
|
| 462 |
|
| 463 |
+
# Create a formatted table
|
| 464 |
+
table_data <- summary_table(game)
|
| 465 |
+
num_rows <- nrow(table_data)
|
|
|
|
|
|
|
| 466 |
|
| 467 |
+
table_plot <- tableGrob(table_data, rows = NULL, theme = ttheme_minimal(
|
| 468 |
+
core = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white")),
|
| 469 |
+
colhead = list(fg_params = list(hjust = 0.5, x = 0.5, fontface = "bold"), bg_params = list(fill = "#f0f0f0")),
|
| 470 |
+
rowhead = list(fg_params = list(hjust = 0.5, x = 0.5), bg_params = list(fill = "white"))
|
| 471 |
+
))
|
| 472 |
|
| 473 |
+
# Set a fixed total height for the table, adjusting row heights based on number of pitches
|
| 474 |
+
total_height <- unit(1, "npc")
|
| 475 |
+
row_height <- total_height / (num_rows + 1) # +1 for header row
|
| 476 |
+
table_plot$heights <- unit(rep(row_height, num_rows + 1), "npc")
|
| 477 |
+
|
| 478 |
+
# Adjust column widths
|
| 479 |
+
table_plot$widths <- unit(c(0.1, 0.06, 0.06, 0.08, 0.1, 0.06, 0.08, 0.08, 0.08, 0.1, 0.1, 0.1), "npc")
|
| 480 |
+
|
| 481 |
+
# Add alternating row colors
|
| 482 |
+
for(i in seq(2, nrow(table_plot), 2)) {
|
| 483 |
+
table_plot$grobs[[i]]$gp$fill <- "#f9f9f9"
|
| 484 |
}
|
| 485 |
|
| 486 |
+
id <- as.character(game$`Pitcher ID`[1])
|
|
|
|
| 487 |
|
| 488 |
mlb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/mlb/300?circle=false")
|
| 489 |
milb_url <- paste0("https://midfield.mlbstatic.com/v1/people/", id, "/milb/300?circle=false")
|
|
|
|
| 499 |
img_grob <- textGrob("Image not available", gp = gpar(col = "red", fontsize = 20))
|
| 500 |
}
|
| 501 |
|
| 502 |
+
# Create the rolling TimStuff+ graph
|
| 503 |
+
rolling_data <- rolling_timstuff()
|
| 504 |
+
timstuff_plot <- ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
|
| 505 |
+
geom_line(size = 1) +
|
| 506 |
+
geom_point(size = 1) +
|
| 507 |
+
theme_minimal() +
|
| 508 |
+
labs(title = "5-Pitch Rolling TimStuff+",
|
| 509 |
+
x = "Pitch Number", y = "TimStuff+") +
|
| 510 |
+
ylim(70, 130) +
|
| 511 |
+
scale_x_continuous(breaks = seq(5, 55, by = 5), limits = c(5,NA)) +
|
| 512 |
+
theme(
|
| 513 |
+
plot.title = element_text(hjust = 0.5, face = "bold"),
|
| 514 |
+
legend.position = "none",
|
| 515 |
+
panel.grid.major.x = element_line(color = "gray", size = 0.5)
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
# Create title and data source text
|
| 519 |
+
title_text <- textGrob(
|
| 520 |
+
paste(input$pitcher, format(input$date, "%m/%d/%y"), "Summary Card by @TimStats"),
|
| 521 |
+
gp = gpar(fontsize = 16, fontface = "bold")
|
| 522 |
+
)
|
| 523 |
+
data_source_text <- textGrob(
|
| 524 |
+
"Data: MLB",
|
| 525 |
+
gp = gpar(fontsize = 8),
|
| 526 |
+
x = unit(1, "npc") - unit(2, "mm"),
|
| 527 |
+
y = unit(2, "mm"),
|
| 528 |
+
just = c("right", "bottom")
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
# Create a horizontal legend
|
| 532 |
+
legend <- get_legend(
|
| 533 |
+
ggplot(rolling_data, aes(x = pitch_number, y = rolling_timstuff, color = pitch_name)) +
|
| 534 |
+
geom_point(size = 5) +
|
| 535 |
+
theme(legend.position = "bottom",
|
| 536 |
+
legend.title = element_blank(),
|
| 537 |
+
text = element_text(size = 12.5),
|
| 538 |
+
#legend.key.size = unit(.5,"cm"),
|
| 539 |
+
legend.box = "horizontal" ) +
|
| 540 |
+
guides(color = guide_legend(nrow = 1))
|
| 541 |
)
|
| 542 |
|
| 543 |
+
# Combine all plots
|
| 544 |
+
combined <- grid.arrange(
|
| 545 |
+
arrangeGrob(
|
| 546 |
+
arrangeGrob(
|
| 547 |
+
img_grob,
|
| 548 |
+
title_text,
|
| 549 |
+
ncol = 1,
|
| 550 |
+
heights = c(4, 1)
|
| 551 |
+
),
|
| 552 |
+
pitch_plot,
|
| 553 |
+
ncol = 2,
|
| 554 |
+
widths = c(1, 1)
|
| 555 |
+
),
|
| 556 |
+
arrangeGrob(
|
| 557 |
+
break_plot,
|
| 558 |
+
timstuff_plot,
|
| 559 |
+
ncol = 2,
|
| 560 |
+
widths = c(1, 1)
|
| 561 |
+
),
|
| 562 |
+
#arrangeGrob(
|
| 563 |
+
legend,
|
| 564 |
+
#),
|
| 565 |
+
#arrangeGrob(
|
| 566 |
+
table_plot,
|
| 567 |
+
#),
|
| 568 |
+
data_source_text,
|
| 569 |
+
nrow = 5,
|
| 570 |
+
heights = c(1.2, 1.2, 0.05, 1.1, 0.05) # Adjusted these values
|
| 571 |
+
)
|
| 572 |
combinedPlot(combined)
|
| 573 |
|
| 574 |
output$combinedPlot <- renderPlot({
|