TimStats commited on
Commit
7cde333
·
verified ·
1 Parent(s): 2da4694

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +332 -43
app.R CHANGED
@@ -1,58 +1,347 @@
 
1
  library(shiny)
 
 
2
  library(bslib)
 
 
 
3
  library(dplyr)
 
 
4
  library(ggplot2)
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- df <- readr::read_csv("penguins.csv")
7
- # Find subset of columns that are suitable for scatter plot
8
- df_num <- df |> select(where(is.numeric), -Year)
9
-
10
- ui <- page_sidebar(
11
- theme = bs_theme(bootswatch = "minty"),
12
- title = "Penguins explorer",
13
- sidebar = sidebar(
14
- varSelectInput("xvar", "X variable", df_num, selected = "Bill Length (mm)"),
15
- varSelectInput("yvar", "Y variable", df_num, selected = "Bill Depth (mm)"),
16
- checkboxGroupInput("species", "Filter by species",
17
- choices = unique(df$Species), selected = unique(df$Species)
18
- ),
19
- hr(), # Add a horizontal rule
20
- checkboxInput("by_species", "Show species", TRUE),
21
- checkboxInput("show_margins", "Show marginal plots", TRUE),
22
- checkboxInput("smooth", "Add smoother"),
23
- ),
24
- plotOutput("scatter")
25
- )
26
 
27
- server <- function(input, output, session) {
28
- subsetted <- reactive({
29
- req(input$species)
30
- df |> filter(Species %in% input$species)
31
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- output$scatter <- renderPlot(
34
- {
35
- p <- ggplot(subsetted(), aes(!!input$xvar, !!input$yvar)) +
36
- theme_light() +
37
- list(
38
- theme(legend.position = "bottom"),
39
- if (input$by_species) aes(color = Species),
40
- geom_point(),
41
- if (input$smooth) geom_smooth()
42
- )
43
 
44
- if (input$show_margins) {
45
- margin_type <- if (input$by_species) "density" else "histogram"
46
- p <- p |> ggExtra::ggMarginal(
47
- type = margin_type, margins = "both",
48
- size = 8, groupColour = input$by_species, groupFill = input$by_species
49
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- p
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  },
54
- res = 100
 
 
 
 
 
 
 
 
 
 
 
 
55
  )
56
  }
57
 
58
  shinyApp(ui, server)
 
 
 
 
 
1
+ # Load required libraries
2
  library(shiny)
3
+ library(plotly)
4
+ # library(gridlayout)
5
  library(bslib)
6
+ library(DT)
7
+ library(rsconnect)
8
+ library(baseballr)
9
  library(dplyr)
10
+ library(tidyverse)
11
+ # library(rvest)
12
  library(ggplot2)
13
+ # library(janitor)
14
+ # library(ggthemes)
15
+ # library(ggpubr)
16
+ # library(jsonlite)
17
+ # library(utils)
18
+ # library(grid)
19
+ # library(gridExtra)
20
+ # library(png)
21
+ library(xgboost)
22
+ library(httr)
23
+ # library(jpeg)
24
 
25
+ #pdf(file = NULL)
26
+ Sys.setenv(TZ='EST')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ # Helper functions
29
+ format_output_data <- function(data) {
30
+ data <- data |>
31
+ select(`Pitcher Name`, `Pitcher ID`, `Batter Name`, `Batter ID`, pitch_name, pitchNum, des, start_speed, spin_rate, extension,
32
+ IVB, HB, x0, z0, SADiff, TimStuff) |>
33
+ mutate(
34
+ `Pitch` = pitch_name,
35
+ `Pitch Number` = pitchNum,
36
+ `Pitch Description` = des,
37
+ `Pitch Speed` = round(start_speed, 1),
38
+ `Spin Rate` = round(spin_rate, 0),
39
+ `Extension` = round(extension, 1),
40
+ `Induced Vertical Break` = round(IVB, 1),
41
+ `Horizontal Break` = round(HB, 1),
42
+ `Horizontal Release Point` = round(x0, 1),
43
+ `Vertical Release Point` = round(z0, 1),
44
+ `Spin Axis Difference` = round(SADiff, 0),
45
+ `TimStuff` = round(TimStuff, 0)
46
+ ) |>
47
+ select(`Pitcher Name`, `Pitcher ID`, `Batter Name`, `Batter ID`, `Pitch`, `Pitch Number`, `Pitch Description`, `Pitch Speed`,
48
+ `Spin Rate`, `Extension`, `Induced Vertical Break`, `Horizontal Break`, `Horizontal Release Point`,
49
+ `Vertical Release Point`,`Spin Axis Difference`, `TimStuff`)
50
+
51
+ return(data)
52
+ }
53
 
54
+ is_barrel <- function(df) {
55
+ df$barrel <- with(df, ifelse(hit_angle <= 50 & hit_speed >= 97 & hit_speed * 1.5 -
56
+ hit_angle >= 117 & hit_speed + hit_angle >= 123, 1, 0))
57
+ return(df)
58
+ }
 
 
 
 
 
59
 
60
+ VAA <- function(milbtotal){
61
+ milbtotal <- milbtotal %>%
62
+ mutate(VAA = -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
63
+ ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi))
64
+ }
65
+
66
+ calculate_EAA <- function(extension) {
67
+ extension / 6.3
68
+ }
69
+
70
+ calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
71
+ inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
72
+ inSA <- ifelse(inSA < 0, inSA + 360, inSA)
73
+ SADiff <- spinDirection - inSA
74
+ SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
75
+ SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
76
+ return(SADiff)
77
+ }
78
+
79
+ calculate_VAA <- function(vz0, ay, az, vy0, y0) {
80
+ -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
81
+ ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
82
+ }
83
+
84
+ pitcher_summary <- function(game_pk,date){
85
+ gdate <- as.Date.character(date)
86
+ gdate <- as.Date(gdate)
87
+ tmilb <- mlb_pbp(game_pk)
88
+ tmilb <- tmilb %>%
89
+ filter(type == "pitch")
90
+ tmilb <- tmilb %>%
91
+ select(matchup.batter.fullName,matchup.batter.id,matchup.pitcher.fullName,matchup.pitcher.id,
92
+ matchup.pitchHand.code, pitchNumber,result.event,details.description,details.type.description,
93
+ result.description,pitchData.startSpeed,pitchData.plateTime,pitchData.zone,
94
+ pitchData.breaks.spinRate,pitchData.extension, pitchData.coordinates.pX,
95
+ pitchData.coordinates.pZ,pitchData.coordinates.x0, pitchData.coordinates.y0,
96
+ pitchData.coordinates.z0,pitchData.coordinates.aX,pitchData.coordinates.aY,
97
+ pitchData.coordinates.aZ,pitchData.coordinates.vX0,pitchData.coordinates.vZ0,
98
+ pitchData.coordinates.vY0,pitchData.coordinates.pfxX,pitchData.coordinates.pfxZ,
99
+ pitchData.breaks.breakVerticalInduced,pitchData.breaks.breakHorizontal,
100
+ hitData.launchSpeed,hitData.launchAngle,hitData.totalDistance,details.isInPlay,
101
+ last.pitch.of.ab,pitchData.breaks.spinDirection)
102
+ colnames(tmilb) <- c("Batter Name","Batter ID","Pitcher Name","Pitcher ID","phand",
103
+ "pitchNum","result","description","pitch_name","des","start_speed",
104
+ "plateTime","zone","spin_rate","extension","px","pz","x0",
105
+ "y0","z0","ax","ay","az","vx0","vz0","vy0","pfxX","pfxZ",
106
+ "IVB","HB","hit_speed","hit_angle","hit_distance","inPlay",
107
+ "lastPitch","spinDirection")
108
+ tmilb <- is_barrel(tmilb)
109
+ tmilb <- tmilb %>%
110
+ mutate(is_strike_swinging = ifelse(description == "Swinging Strike" |
111
+ description == "Foul Tip",TRUE,FALSE))
112
+ tmilb <- tmilb %>%
113
+ mutate(date = gdate)
114
+ return(tmilb)
115
+ }
116
+
117
+ calculate_timstuff <- function(game) {
118
+ game <- game %>%
119
+ mutate(VAA = calculate_VAA(vz0, ay, az, vy0, y0),
120
+ EAA = calculate_EAA(extension),
121
+ SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection),
122
+ team_fielding_id = ifelse(description %in% c("Called Strike", "Swinging Strike", "Swinging Strike (Blocked)"), 1, 0),
123
+ 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),
124
+ is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
125
+ Pitch = pitch_name)
126
+
127
+ feature_vars <- c("start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate", "SADiff")
128
+ complete_rows <- complete.cases(game[, feature_vars])
129
+ game_complete <- game[complete_rows, ]
130
+ game_na <- game[!complete_rows,]
131
+ game_na$TimStuff <- NA
132
+
133
+ lhp_fb <- game_complete[game_complete$phand == 'L' & game_complete$Pitch %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball"), ]
134
+ lhp_off <- game_complete[game_complete$phand == 'L' & game_complete$Pitch %in% c("Changeup", "Splitter", "Screwball", "Forkball"), ]
135
+ lhp_break <- game_complete[game_complete$phand == 'L' & game_complete$Pitch %in% c("Slider", "Slurve", "Sweeper", "Curveball", "Knuckle Curve", "Slow Curve", "Knuckleball"), ]
136
+
137
+ rhp_fb <- game_complete[game_complete$phand == 'R' & game_complete$Pitch %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball"), ]
138
+ rhp_off <- game_complete[game_complete$phand == 'R' & game_complete$Pitch %in% c("Changeup", "Splitter", "Screwball", "Forkball"), ]
139
+ rhp_break <- game_complete[game_complete$phand == 'R' & game_complete$Pitch %in% c("Slider", "Slurve", "Sweeper", "Curveball", "Knuckle Curve", "Slow Curve", "Knuckleball"), ]
140
+
141
+ lhp_fb$TimStuff <- scale_TimStuff(predict(FB, as.matrix(cbind(lhp_fb$start_speed, lhp_fb$IVB, -lhp_fb$HB, lhp_fb$EAA, -lhp_fb$x0, lhp_fb$z0, lhp_fb$spin_rate, lhp_fb$SADiff))), -0.0011801, 0.007989927)
142
+ lhp_off$TimStuff <- scale_TimStuff(predict(Off, as.matrix(cbind(lhp_off$start_speed, lhp_off$IVB, -lhp_off$HB, lhp_off$EAA, -lhp_off$x0, lhp_off$z0, lhp_off$spin_rate, lhp_off$SADiff))), -0.002239657, 0.01043216)
143
+ lhp_break$TimStuff <- scale_TimStuff(predict(Break, as.matrix(cbind(lhp_break$start_speed, lhp_break$IVB, -lhp_break$HB, lhp_break$EAA, -lhp_break$x0, lhp_break$z0, lhp_break$spin_rate, lhp_break$SADiff))), -0.004822031, 0.007912765)
144
+
145
+ rhp_fb$TimStuff <- scale_TimStuff(predict(FB, as.matrix(cbind(rhp_fb$start_speed, rhp_fb$IVB, rhp_fb$HB, rhp_fb$EAA, rhp_fb$x0, rhp_fb$z0, rhp_fb$spin_rate, rhp_fb$SADiff))), -0.0011801, 0.007989927)
146
+ rhp_off$TimStuff <- scale_TimStuff(predict(Off, as.matrix(cbind(rhp_off$start_speed, rhp_off$IVB, rhp_off$HB, rhp_off$EAA, rhp_off$x0, rhp_off$z0, rhp_off$spin_rate, rhp_off$SADiff))), -0.002239657, 0.01043216)
147
+ rhp_break$TimStuff <- scale_TimStuff(predict(Break, as.matrix(cbind(rhp_break$start_speed, rhp_break$IVB, rhp_break$HB, rhp_break$EAA, rhp_break$x0, rhp_break$z0, rhp_break$spin_rate, rhp_break$SADiff))), -0.004822031, 0.007912765)
148
+
149
+ game_complete <- rbind(lhp_fb, lhp_off, lhp_break, rhp_fb, rhp_off, rhp_break,game_na)
150
+ return(game_complete)
151
+ }
152
+
153
+ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
154
+ scaled_score <- (raw_score - model_mean) / model_sd
155
+ result <- 50 - (scaled_score * 10)
156
+ return(result)
157
+ }
158
+
159
+ pitch_plot <- function(game){
160
+ ggplot()+
161
+ geom_point(game,mapping =
162
+ aes(x=px, y = pz,color = pitch_name),size = 3.5) +
163
+ geom_segment(aes(x=-0.71,xend = 0.71, y = 1.5,yend = 1.5))+
164
+ geom_segment(aes(x=-0.71,xend = 0.71, y = 3.6,yend = 3.6))+
165
+ geom_segment(aes(x= 0.71,xend = 0.71, y = 1.5,yend = 3.6))+
166
+ geom_segment(aes(x=-0.71,xend = -0.71, y = 1.5,yend =3.6))+
167
+ xlab(" ")+
168
+ ylab(" ") +
169
+ xlim(-3,3)+
170
+ ylim(0.2,4)+
171
+ coord_fixed(ratio = 1) +
172
+ ggthemes::theme_few()+
173
+ guides(color = guide_legend(title = "Pitch Type"))+
174
+ theme(axis.text.x=element_blank(),
175
+ axis.ticks.x=element_blank(),
176
+ axis.text.y=element_blank(),
177
+ axis.ticks.y=element_blank())
178
+ }
179
+
180
+ # Load models
181
+ FB <- xgb.load('FB.model')
182
+ Off <- xgb.load('Off.model')
183
+ Break <- xgb.load('Break.model')
184
+
185
+ summary_table <- function(data,pitchern){
186
+ # rows <- nrow(data)
187
+ data <- data %>%
188
+ group_by(data[,3],Pitch) %>%
189
+ summarize(
190
+ Pitches = n(),
191
+ #'Pitch%' = round(sum(Pitches)/sum(rows) * 100, digits = 1),
192
+ 'Avg. Velo' = round(mean(start_speed, na.rm = TRUE), digits = 1),
193
+ 'Spin Rate' = round(mean(spin_rate, na.rm = TRUE), digits = 0),
194
+ 'Extension' = round(mean(extension, na.rm = TRUE), digits = 1),
195
+ 'IVB' = round(mean(IVB, na.rm = TRUE), digits = 1),
196
+ 'HB' = round(mean(HB, na.rm = TRUE), digits = 1),
197
+ 'Horz. Release Point' = round(mean(x0,na.rm = TRUE),digits = 1),
198
+ 'Vert. Release Point' = round(mean(z0,na.rm = TRUE),digits = 1),
199
+ 'Spin Axis Difference (in degrees)' = round(mean(SADiff,na.rm = TRUE),digits = 0),
200
+ 'VAA' = round(mean(VAA, na.rm = TRUE), digits = 1),
201
+ 'CSW%' = round(sum(team_fielding_id, na.rm = TRUE) / sum(!is.na(team_fielding_id)) * 100, digits = 1),
202
+ 'Whiff%' = round(sum(is_strike_swinging, na.rm = TRUE) / sum(swing, na.rm = TRUE) * 100, digits = 1),
203
+ 'TimStuff' = round(mean(TimStuff, na.rm = TRUE), digits = 0)
204
+ ) %>%
205
+ arrange(-Pitches)
206
+
207
+ return(data)
208
+ }
209
+
210
+ # UI Definition
211
+ ui <- fluidPage(
212
+ tags$head(
213
+ tags$style(HTML("
214
+ @media (max-width: 768px) {
215
+ .sidebar { width: 100%; float: none; }
216
+ .main-content { margin-left: 0; }
217
+ .selectize-input { font-size: 14px; }
218
+ .form-group { margin-bottom: 10px; }
219
+ .action-button { width: 100%; }
220
  }
221
+ "))
222
+ ),
223
+ titlePanel("MLB/AAA/FSL Stuff Dashboard"),
224
+ sidebarLayout(
225
+ sidebarPanel(
226
+ width = 2,
227
+ dateInput("date", "Date:"),
228
+ selectizeInput("level", "Level:", c("MLB", "AAA", "FSL", "College (Statcast Parks Only)", "Futures Game")),
229
+ actionButton("submit", "Get Dashboard"),
230
+ downloadButton("download_summary", "Download Summary CSV"),
231
+ selectizeInput("pitcherf", "Pitcher Pitch Level Stuff", c("")),
232
+ actionButton("pls", "Get Pitch Level Stuff"),
233
+ downloadButton("download_pitch_level", "Download Pitch Level CSV")
234
+ ),
235
+ mainPanel(
236
+ dataTableOutput("schedule")
237
+ )
238
+ )
239
+ )
240
+
241
 
242
+ server <- function(input, output, session) {
243
+ data <- reactiveVal()
244
+ games <- reactiveVal()
245
+ summary <- reactiveVal()
246
+ pitch_level_data <- reactiveVal()
247
+
248
+ observeEvent(input$submit, {
249
+ season <- format(input$date, "%Y")
250
+ if (input$level == "MLB") {
251
+ data(mlb_schedule(season = season, level_ids = "1"))
252
+ } else if (input$level == "AAA") {
253
+ data(mlb_schedule(season = season, level_ids = "11"))
254
+ } else if (input$level == "FSL") {
255
+ data(mlb_schedule(season = season, level_ids = "14"))
256
+ } else if (input$level == "College (Statcast Parks Only)") {
257
+ data(mlb_schedule(season = season, level_ids = "22"))
258
+ } else if (input$level == "Futures Game"){
259
+ data(mlb_schedule(season = season, level_ids = "21"))
260
+ }
261
+
262
+ schedule <- data()
263
+ schedule <- schedule[schedule$date == input$date, ]
264
+ games <- data.frame(
265
+ "Batter Name" = character(), "Batter ID" = numeric(), "Pitcher Name" = character(), "Pitcher ID" = numeric(),
266
+ "result" = character(), "description" = character(), "pitch_name" = character(), "des" = character(),
267
+ "start_speed" = numeric(), "plateTime" = numeric(), "zone" = numeric(), "spin_rate" = numeric(),
268
+ "extension" = numeric(), "px" = numeric(), "pz" = numeric(), "x0" = numeric(), "y0" = numeric(), "z0" = numeric(),
269
+ "ax" = numeric(), "ay" = numeric(), "az" = numeric(), "vx0" = numeric(), "vz0" = numeric(), "vy0" = numeric(),
270
+ "pfxX" = numeric(), "pfxZ" = numeric(), "IVB" = numeric(), "HB" = numeric(), "hit_speed" = numeric(),
271
+ "hit_angle" = numeric(), "hit_distance" = numeric(), "inPlay" = logical(), "lastPitch" = logical(),
272
+ "spinDirection" = numeric(), "phand" = character(), "barrel" = numeric(), "is_strike_swinging" = numeric(),
273
+ "date" = as.Date(character())
274
+ )
275
+
276
+ for(n in 1:nrow(schedule)){
277
+ tryCatch({
278
+ game1 <- pitcher_summary(schedule[n,6], schedule[n,1])
279
+ games <- rbind(game1, games)
280
+ }, error = function(e) {
281
+ message(paste("Error occurred for game:", schedule[n,6], "on", schedule[n,1], "- Skipping this game."))
282
+ })
283
+ }
284
+
285
+ games <- calculate_timstuff(games)
286
+ games(games)
287
+ summary_data <- summary_table(games)
288
+ summary(summary_data)
289
+ output$schedule <- renderDT({
290
+ datatable(summary_data, options = list(
291
+ pageLength = 10,
292
+ lengthMenu = c(10, 25, 50, 100),
293
+ columnDefs = list(
294
+ list(className = 'dt-center', targets = "_all")
295
+ )
296
+ ))
297
+ })
298
+ updateSelectizeInput(session = getDefaultReactiveDomain(), "pitcherf", "Pitcher Pitch Level Stuff", games[, "Pitcher Name"])
299
+ })
300
+
301
+ observeEvent(input$pls, {
302
+ games_data <- isolate(games())
303
+ if (!is.null(games_data) && !is.null(input$pitcherf) && input$pitcherf != "") {
304
+ games_data <- games_data[games_data$`Pitcher Name` == input$pitcherf, ]
305
+ formatted_data <- format_output_data(games_data)
306
+ pitch_level_data(formatted_data)
307
+ output$schedule <- renderDT({
308
+ datatable(formatted_data, options = list(
309
+ pageLength = 10,
310
+ lengthMenu = c(10, 25, 50, 100),
311
+ columnDefs = list(
312
+ list(className = 'dt-center', targets = "_all")
313
+ )
314
+ ))
315
+ })
316
+ } else {
317
+ output$schedule <- renderDataTable(NULL)
318
+ pitch_level_data(NULL)
319
+ }
320
+ })
321
+
322
+ # Download handler for summary CSV
323
+ output$download_summary <- downloadHandler(
324
+ filename = function() {
325
+ paste("summary_data_", Sys.Date(), ".csv", sep = "")
326
  },
327
+ content = function(file) {
328
+ write.csv(summary(), file, row.names = FALSE)
329
+ }
330
+ )
331
+
332
+ # Download handler for pitch level CSV
333
+ output$download_pitch_level <- downloadHandler(
334
+ filename = function() {
335
+ paste("pitch_level_data_", Sys.Date(), ".csv", sep = "")
336
+ },
337
+ content = function(file) {
338
+ write.csv(pitch_level_data(), file, row.names = FALSE)
339
+ }
340
  )
341
  }
342
 
343
  shinyApp(ui, server)
344
+
345
+
346
+
347
+