TimStats commited on
Commit
7a75a3f
·
verified ·
1 Parent(s): 1db0d6c

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +550 -40
app.R CHANGED
@@ -1,58 +1,568 @@
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
  library(shiny)
2
+ library(DT)
3
+ library(rsconnect)
4
+ library(baseballr)
5
  library(dplyr)
6
+ library(tidyverse)
7
+ library(rvest)
8
  library(ggplot2)
9
+ library(janitor)
10
+ library(ggthemes)
11
+ library(jsonlite)
12
+ library(utils)
13
+ library(data.table)
14
+ library(xgboost)
15
+ library(httr)
16
+ library(bslib)
17
+ library(rtabulator)
18
+
19
+ download_private_csv <- function(repo_id, filename) {
20
+ url <- paste0("https://huggingface.co/datasets/", repo_id, "/resolve/main/", filename)
21
+ response <- GET(url, add_headers(Authorization = paste("Bearer", Sys.getenv("GETCSV"))))
22
+
23
+ if (status_code(response) == 200) {
24
+ content <- content(response, "text")
25
+ con <- textConnection(content)
26
+
27
+ # Try different read options
28
+ data <- read.csv(con,
29
+ header = TRUE,
30
+ check.names = FALSE, # This prevents R from modifying column names
31
+ fileEncoding = "UTF-8",
32
+ stringsAsFactors = FALSE)
33
+ close(con)
34
+ return(data)
35
+ } else {
36
+ stop("Failed to download dataset")
37
+ }
38
+ }
39
+
40
+ MLB <- download_private_csv("TimStats/StatcastDataAll", "MLB.csv")
41
+ AAA <- download_private_csv("TimStats/StatcastDataAll", "AAA.csv")
42
+ FSL <- download_private_csv("TimStats/StatcastDataAll", "FSL.csv")
43
+
44
+ is_barrel <- function(df) {
45
+ df$hit_speedr <- round(df$hit_speed)
46
+ df <- df |>
47
+ mutate(barrel = ifelse((hit_speedr >= 124) &
48
+ (hit_angle >= 0 & hit_angle <= 50),1,0)) |>
49
+ mutate(barrel = ifelse((hit_speedr == 123) &
50
+ (hit_angle >= 1 & hit_angle <= 50),1,barrel)) |>
51
+ mutate(barrel = ifelse((hit_speedr == 122) &
52
+ (hit_angle >= 2 & hit_angle <= 50),1,barrel)) |>
53
+ mutate(barrel = ifelse((hit_speedr == 121) &
54
+ (hit_angle >= 3 & hit_angle <= 50),1,barrel)) |>
55
+ mutate(barrel = ifelse((hit_speedr == 120) &
56
+ (hit_angle >= 4 & hit_angle <= 50),1,barrel)) |>
57
+ mutate(barrel = ifelse((hit_speedr == 119) &
58
+ (hit_angle >= 5 & hit_angle <= 50),1,barrel)) |>
59
+ mutate(barrel = ifelse((hit_speedr == 118) &
60
+ (hit_angle >= 6 & hit_angle <= 50),1,barrel)) |>
61
+ mutate(barrel = ifelse((hit_speedr == 117) &
62
+ (hit_angle >= 7 & hit_angle <= 50),1,barrel)) |>
63
+ mutate(barrel = ifelse((hit_speedr == 116) &
64
+ (hit_angle >= 8 & hit_angle <= 50),1,barrel)) |>
65
+ mutate(barrel = ifelse((hit_speedr == 115) &
66
+ (hit_angle >= 9 & hit_angle <= 50),1,barrel)) |>
67
+ mutate(barrel = ifelse((hit_speedr == 114) &
68
+ (hit_angle >= 10 & hit_angle <= 50),1,barrel)) |>
69
+ mutate(barrel = ifelse((hit_speedr == 113) &
70
+ (hit_angle >= 11 & hit_angle <= 50),1,barrel)) |>
71
+ mutate(barrel = ifelse((hit_speedr == 112) &
72
+ (hit_angle >= 12 & hit_angle <= 50),1,barrel)) |>
73
+ mutate(barrel = ifelse((hit_speedr == 111) &
74
+ (hit_angle >= 13 & hit_angle <= 50),1,barrel)) |>
75
+ mutate(barrel = ifelse((hit_speedr == 110) &
76
+ (hit_angle >= 14 & hit_angle <= 48),1,barrel)) |>
77
+ mutate(barrel = ifelse((hit_speedr == 109) &
78
+ (hit_angle >= 15 & hit_angle <= 46),1,barrel)) |>
79
+ mutate(barrel = ifelse((hit_speedr == 108) &
80
+ (hit_angle >= 16 & hit_angle <= 45),1,barrel)) |>
81
+ mutate(barrel = ifelse((hit_speedr == 107) &
82
+ (hit_angle >= 17 & hit_angle <= 43),1,barrel)) |>
83
+ mutate(barrel = ifelse((hit_speedr == 106) &
84
+ (hit_angle >= 18 & hit_angle <= 42),1,barrel)) |>
85
+ mutate(barrel = ifelse((hit_speedr == 105) &
86
+ (hit_angle >= 19 & hit_angle <= 40),1,barrel)) |>
87
+ mutate(barrel = ifelse((hit_speedr == 104) &
88
+ (hit_angle >= 20 & hit_angle <= 39),1,barrel)) |>
89
+ mutate(barrel = ifelse((hit_speedr == 103) &
90
+ (hit_angle >= 21 & hit_angle <= 37),1,barrel)) |>
91
+ mutate(barrel = ifelse((hit_speedr == 102) &
92
+ (hit_angle >= 22 & hit_angle <= 36),1,barrel)) |>
93
+ mutate(barrel = ifelse((hit_speedr == 101) &
94
+ (hit_angle >= 23 & hit_angle <= 34),1,barrel)) |>
95
+ mutate(barrel = ifelse((hit_speedr == 100) &
96
+ (hit_angle >= 24 & hit_angle <= 33),1,barrel)) |>
97
+ mutate(barrel = ifelse((hit_speedr == 99) &
98
+ (hit_angle >= 25 & hit_angle <= 31),1,barrel)) |>
99
+ mutate(barrel = ifelse((hit_speedr == 98) &
100
+ (hit_angle >= 26 & hit_angle <= 30),1,barrel)) |>
101
+ select(-hit_speedr)
102
+ return(df)
103
+ }
104
+
105
+ VAA <- function(milbtotal){
106
+ milbtotal <- milbtotal |>
107
+ mutate(vaa = -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
108
+ ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi))
109
+ }
110
+
111
+ calculate_EAA <- function(extension) {
112
+ extension / 6.3
113
+ }
114
+
115
+ calculate_SADiff <- function(pfxX, pfxZ, spinDirection) {
116
+ inSA <- atan2(pfxZ, pfxX) * 180/pi + 90
117
+ inSA <- ifelse(inSA < 0, inSA + 360, inSA)
118
+ SADiff <- spinDirection - inSA
119
+ SADiff <- ifelse(SADiff > 180, SADiff - 360, SADiff)
120
+ SADiff <- ifelse(SADiff < -180, SADiff + 360, SADiff)
121
+ return(SADiff)
122
+ }
123
+
124
+ calculate_VAA <- function(vz0, ay, az, vy0, y0) {
125
+ -atan((vz0+(az*(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))-vy0)/
126
+ ay))/(-sqrt((vy0*vy0)-(2*ay*(y0-(17/12))))))*(180/pi)
127
+ }
128
+
129
+ calculate_timstuff <- function(game) {
130
+ # game <- calculate_primary(game)
131
+ game <- game %>%
132
+ mutate(
133
+ #VAA = calculate_VAA(vz0, ay, az, vy0, y0),
134
+ EAA = calculate_EAA(extension),
135
+ SADiff = calculate_SADiff(pfxX, pfxZ, spinDirection),
136
+ #team_fielding_id = ifelse(description %in% c("Called Strike", "Swinging Strike", "Swinging Strike (Blocked)"), 1, 0),
137
+ # 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),
138
+ #is_strike_swinging = ifelse(is_strike_swinging, 1, 0),
139
+ #Pitch = pitch_name,
140
+ ishandL = ifelse(phand == "L",1,0))
141
+ # game <- calculate_primary(game)
142
+
143
+ feature_vars <- c("ishandL","start_speed", "IVB", "HB", "EAA", "x0", "z0", "spin_rate","SADiff")
144
+ complete_rows <- complete.cases(game[, feature_vars])
145
+ game_complete <- game[complete_rows, ]
146
+ game_na <- game[!complete_rows,]
147
+ game_na$TimStuff <- NA
148
+
149
+ rhp <- game_complete
150
+
151
+ # rhp <- game_complete[game_complete$ishandL == 0]
152
+ #
153
+ # 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)
154
+
155
+ 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)
156
+
157
+ game_complete <- rbind(rhp,game_na)
158
+ return(game_complete)
159
+ }
160
+
161
+ scale_TimStuff <- function(raw_score, model_mean, model_sd) {
162
+ scaled_score <- (raw_score - model_mean) / model_sd
163
+ result <- 100 - (scaled_score * 10)
164
+ return(result)
165
+ }
166
+
167
+ model <- xgb.load('TimStuff2.model')
168
+ #######
169
 
170
+ Swing <- function(milbtotal){
171
+ milbtotal <- milbtotal %>%
172
+ mutate(swing = ifelse(description == "Foul" |
173
+ description == "Foul Pitchout" |
174
+ description == "In play, no out" |
175
+ description == "In play, out(s)" |
176
+ description == "In play, run(s)" |
177
+ description == "Swinging Strike" |
178
+ description == "swinging Strike (Blocked)" |
179
+ description == "Foul Tip",1,0))
180
+ }
181
+
182
+ addChecks <- function(df){
183
+ df <- is_barrel(df)
184
+ df %>% mutate(
185
+ InPlayCheck = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out') ~ TRUE, TRUE ~ FALSE),
186
+ SwingCheck = case_when(description %in% c('Foul','Foul Bunt','Foul Pitchout','Foul Tip',
187
+ 'In play, run(s)','In play, out(s)','In play, no out',
188
+ 'Swinging Strike','Swinging Strike (Blocked)',
189
+ 'Missed Bunt') ~ TRUE, TRUE ~ FALSE),
190
+ ConCheck = case_when(description %in% c('In play, run(s)','In play, out(s)','In play, no out',
191
+ 'Foul','Foul Bunt','Foul Pitchout') ~ TRUE, TRUE ~ FALSE),
192
+ WhiffCheck = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)',
193
+ 'Missed Bunt','Foul Tip') ~ TRUE, TRUE ~ FALSE),
194
+ CalledStrikeCheck = case_when(description %in% c('Called Strike') ~ TRUE, TRUE ~ FALSE),
195
+ CSWCheck = case_when(description %in% c('Swinging Strike','Swinging Strike (Blocked)',
196
+ 'Missed Bunt','Foul Tip','Called Strike') ~ TRUE, TRUE ~ FALSE),
197
+ StrikeCheck = case_when(description %in% c('Called Strike','Foul','Foul Bunt','Foul Pitchout',
198
+ 'Foul Tip','In play, no out','In play, out(s)',
199
+ 'In play, run(s)','Missed Bunt','Pitchout',
200
+ 'Swinging Stike','Swinging Strike (Blocked)') ~ TRUE, TRUE ~ FALSE),
201
+ BallCheck = case_when(description %in% c('Ball','Ball In Dirt','Hit By Pitch') ~ TRUE, TRUE ~ FALSE),
202
+ SweetSpotCheck = case_when(between(hit_angle,10,30) ~ TRUE, TRUE ~ FALSE),
203
+ HardHitCheck = case_when(hit_speed >= 95 ~ TRUE, TRUE ~ FALSE),
204
+ ZoneCheck = ifelse(zone <= 9, TRUE, FALSE),
205
+ Single = case_when(result == "Single" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
206
+ Double = case_when(result == "Double" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
207
+ Triple = case_when(result == "Triple" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
208
+ `Home Run` = case_when(result == "Home Run" & InPlayCheck == TRUE ~ TRUE, TRUE ~ FALSE),
209
+ WalkCheck = case_when(balls >= 4 & result == "Walk" ~ TRUE, TRUE ~ FALSE),
210
+ HBPCheck = case_when(description == "Hit By Pitch" & result == "Hit By Pitch" ~ TRUE, TRUE ~ FALSE),
211
+ StrikeoutCheck = case_when(strikes >= 3 & result %in% c("Strikeout",'Stikeout Double Play') ~ TRUE, TRUE ~ FALSE),
212
+ SacrificeCheck = case_when(InPlayCheck == TRUE & result %in% c('Sac Fly','Sac Bunt',
213
+ 'Sac Fly Double Play','Sac Bunt Double Play') ~ TRUE, TRUE ~ FALSE),
214
+ IBBCheck = case_when(pitchNum == 1 & result == "Intent Walk" ~ TRUE, TRUE ~ FALSE),
215
+ ABCheck = StrikeoutCheck + InPlayCheck - SacrificeCheck,
216
+ PACheck = ABCheck + WalkCheck + HBPCheck,
217
+ TopZoneCheck = if_else(zone < 4,TRUE,FALSE),
218
+ BotZoneCheck = if_else(zone > 6 & zone < 10,TRUE,FALSE),
219
+ CompSwingCheck = if_else(bat_speed >= 60 & hit_speed >= 90,TRUE,FALSE),
220
+ CompSwingCheck = ifelse(bat_speed >= quantile(bat_speed,.1,na.rm = TRUE),1,0)
221
+ )
222
+ }
223
+ #' test <- addChecks(MLB) %>% group_by(`Pitcher Name`,season,pitch_name) %>%
224
+ #' summarise(
225
+ #' Pitches = n(),
226
+ #' 'Avg Velo' = mean(start_speed,na.rm = TRUE),
227
+ #' 'Top Velo' = max(start_speed,na.rm = TRUE),
228
+ #' #'TimStuff+' = mean(TimStuff,na.rm = TRUE),
229
+ #' 'Max EV' = max(hit_speed,na.rm = TRUE),
230
+ #' 'Avg EV' = mean(hit_speed,na.rm = TRUE),
231
+ #' 'EV90' = quantile(hit_speed,0.9,na.rm = TRUE),
232
+ #' 'Avg LA' = mean(hit_angle,na.rm = TRUE),
233
+ #' 'stdevLA' = sd(hit_angle,na.rm = TRUE),
234
+ #' 'HardHit%' = mean(HardHitCheck[InPlayCheck == TRUE],na.rm = TRUE),
235
+ #' 'Barrel%' = mean(barrel[InPlayCheck == TRUE],na.rm = TRUE),
236
+ #' 'Sweet Spot%' = mean(SweetSpotCheck[InPlayCheck == TRUE],na.rm = TRUE),
237
+ #' 'xwOBA' = mean(expected_woba,na.rm = TRUE),
238
+ #' 'xwOBACON' = mean(expected_woba[InPlayCheck == TRUE],na.rm = TRUE),
239
+ #' 'Contact%' = mean(ConCheck[SwingCheck == TRUE],na.rm = TRUE),
240
+ #' 'ZCon%' = mean(ConCheck[ZoneCheck == TRUE] & SwingCheck == TRUE,na.rm = TRUE),
241
+ #' 'ZSwing%' = mean(SwingCheck[ZoneCheck == TRUE],na.rm = TRUE),
242
+ #' 'OCon%' = mean(ConCheck[ZoneCheck == FALSE] & SwingCheck == TRUE,na.rm = TRUE),
243
+ #' 'Chase%' = mean(SwingCheck[ZoneCheck == FALSE],na.rm = TRUE),
244
+ #' 'SwStr%' = mean(WhiffCheck,na.rm = TRUE),
245
+ #' 'Whiff%' = mean(WhiffCheck[SwingCheck == TRUE],na.rm = TRUE),
246
+ #' 'Zone%' = mean(ZoneCheck,na.rm = TRUE),
247
+ #' 'Strike%' = mean(StrikeCheck,na.rm = TRUE),
248
+ #' 'Swing%' = mean(SwingCheck,na.rm = TRUE),
249
+ #' 'Spin Rate' = mean(spin_rate,na.rm = TRUE),
250
+ #' 'Extension' = mean(extension,na.rm = TRUE),
251
+ #' 'IVB' = mean(IVB,na.rm = TRUE),
252
+ #' 'HB' = mean(HB,na.rm = TRUE),
253
+ #' # 'VAA' = mean(vaa,na.rm = TRUE),
254
+ #' # 't3VAA' = mean(vaa[TopZoneCheck == TRUE],na.rm = TRUE),
255
+ #' # 'b3VAA' = mean(vaa[BotZoneCheck == TRUE],na.rm = TRUE),
256
+ #' 'CSW%' = mean(CSWCheck,na.rm = TRUE),
257
+ #' 'Arm Angle' = mean(arm_angle,na.rm = TRUE),
258
+ #' 'Bat Speed' = mean(bat_speed[CompSwingCheck == TRUE],na.rm = TRUE)
259
+ #' )
260
+ MLB_processed <- MLB %>%
261
+ calculate_timstuff() %>%
262
+ addChecks() %>%
263
+ VAA() %>%
264
+ mutate('Pitch Name' = pitch_name) %>%
265
+ mutate('Season' = season) %>%
266
+ mutate('Level' = level) %>%
267
+ mutate('Pitch Type' = case_when(
268
+ pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
269
+ pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
270
+ "Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
271
+ pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
272
+ TRUE ~ NA_character_ # Corrected default case
273
+ )) %>%
274
+ mutate('Batter Side' = bside) %>%
275
+ mutate('Pitcher Hand' = phand) %>%
276
+ mutate('Stadium' = venue_name) %>%
277
+ mutate('Batter Home/Away' = ifelse(`Batter Team` == teams_home_team_name,"Home","Away")) %>%
278
+ mutate('Pitcher Home/Away' = ifelse(`Pitcher Team` == teams_home_team_name,"Home","Away"))
279
+
280
+ AAA_processed <- AAA %>%
281
+ calculate_timstuff() %>%
282
+ addChecks()%>%
283
+ VAA()%>%
284
+ mutate('Pitch Name' = pitch_name) %>%
285
+ mutate('Season' = season) %>%
286
+ mutate('Level' = level) %>%
287
+ mutate('Pitch Type' = case_when(
288
+ pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
289
+ pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
290
+ "Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
291
+ pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
292
+ TRUE ~ NA_character_ # Corrected default case
293
+ ))
294
+
295
+ FSL_processed <- FSL %>%
296
+ calculate_timstuff() %>%
297
+ addChecks()%>%
298
+ VAA()%>%
299
+ mutate('Pitch Name' = pitch_name) %>%
300
+ mutate('Season' = season) %>%
301
+ mutate('Level' = level) %>%
302
+ mutate('Pitch Type' = case_when(
303
+ pitch_name %in% c("Four-Seam Fastball", "Sinker", "Cutter", "Fastball") ~ "Fastball",
304
+ pitch_name %in% c("Slider", "Sweeper", "Slurve", "Curveball", "Screwball",
305
+ "Knuckle Curve", "Slow Curve", "Eephus") ~ "Breaking",
306
+ pitch_name %in% c("Changeup", "Splitter", "Forkball") ~ "Offspeed",
307
+ TRUE ~ NA_character_ # Corrected default case
308
+ ))
309
 
310
+ # Available stats for the UI
311
+ available_stats <- c("Pitches", "Avg Velo", "Top Velo", "TimStuff", "Max EV", "Avg EV",
312
+ "EV90", "Avg LA", "stdevLA", "HardHit%", "Barrel%", "Sweet Spot%",
313
+ "xwOBA", "xwOBACON", "Contact%", "ZCon%", "ZSwing%", "OCon%",
314
+ "Chase%", "SwStr%", "Whiff%", "Zone%", "Strike%", "Swing%",
315
+ "Spin Rate", "Extension", "IVB", "HB", "CSW%", "Arm Angle", "Bat Speed")
316
+
317
+ # Then your UI and server code
318
  ui <- page_sidebar(
319
+ title = "MLB/AAA/FSL Statcast Data",
320
+ theme = bs_theme(preset = "united"),
321
  sidebar = sidebar(
322
+ width = 300,
323
+ p("X: ", a("(@TimStats)", href = "https://twitter.com/timstats")),
324
+ p("Data Updated To:", Sys.Date() - 1),
325
+
326
+ card(
327
+ class = "mb-3",
328
+ selectInput("level", "Level:",
329
+ c("MLB", "AAA", "FSL"),
330
+ multiple = TRUE
331
+ ),
332
+ selectInput("group", "Group By:",
333
+ c("Pitcher Name", "Pitcher ID", "Pitch Name","Batter Name",
334
+ "Batter ID","Season","Level","Pitch Type","Stadium",
335
+ "Batter Home/Away","Pitcher Home/Away","Pitcher Team",
336
+ "Batter Team"),
337
+ multiple = TRUE
338
+ ),
339
+ selectInput("stats", "Select Stats:",
340
+ choices = available_stats,
341
+ multiple = TRUE
342
+ ),
343
+ dateRangeInput("dates", "Date Range:",
344
+ start = "2024-02-23",
345
+ end = "2024-09-30"
346
+ )
347
  ),
348
+ actionButton("add_filter", "Add Filter", class = "btn-primary mb-3"),
349
+ uiOutput("filter_container"),
350
+ hr(),
351
+ actionButton("update_table", "Update Table", class = "btn-success")
352
  ),
353
+ mainPanel(
354
+ tabulatorOutput("table", height = "800px", width = "1600px")
355
+ )
356
  )
357
 
358
  server <- function(input, output, session) {
359
+ filters <- reactiveVal(list())
360
+ counter <- reactiveVal(0)
361
+ table_trigger <- reactiveVal(0)
362
+ last_click <- reactiveVal(0)
363
+
364
+ available_columns <- reactive({
365
+ stat_cols <- setNames(
366
+ input$stats,
367
+ input$stats
368
+ )
369
+ c(stat_cols)
370
  })
371
+
372
+ removeClicks <- reactiveVal(0)
373
+
374
+ # observeEvent(input$update_table, {
375
+ # table_trigger(table_trigger() + 1)
376
+ # })
377
+
378
+ processed_data <- eventReactive(input$update_table, {
379
+ req(input$level, input$stats, input$group)
380
+
381
+ # Combine preprocessed data from selected levels
382
+ combined_data <- bind_rows(
383
+ if ("MLB" %in% input$level) MLB_processed,
384
+ if ("AAA" %in% input$level) AAA_processed,
385
+ if ("FSL" %in% input$level) FSL_processed
386
+ )
387
+
388
+ # First group and calculate all stats
389
+ grouped_data <- combined_data %>%
390
+ group_by(across(all_of(input$group))) %>%
391
+ summarise(
392
+ Pitches = n(),
393
+ 'Avg Velo' = mean(start_speed, na.rm = TRUE),
394
+ 'Top Velo' = max(start_speed, na.rm = TRUE),
395
+ 'TimStuff' = mean(TimStuff, na.rm = TRUE),
396
+ 'Max EV' = max(hit_speed, na.rm = TRUE),
397
+ 'Avg EV' = mean(hit_speed, na.rm = TRUE),
398
+ 'EV90' = quantile(hit_speed, 0.9, na.rm = TRUE),
399
+ 'Avg LA' = mean(hit_angle, na.rm = TRUE),
400
+ 'stdevLA' = sd(hit_angle, na.rm = TRUE),
401
+ 'HardHit%' = mean(HardHitCheck[InPlayCheck == TRUE], na.rm = TRUE) * 100,
402
+ 'Barrel%' = mean(barrel[InPlayCheck == TRUE], na.rm = TRUE) * 100,
403
+ 'Sweet Spot%' = mean(SweetSpotCheck[InPlayCheck == TRUE], na.rm = TRUE) * 100,
404
+ 'xwOBA' = mean(expected_woba, na.rm = TRUE),
405
+ 'xwOBACON' = mean(expected_woba[InPlayCheck == TRUE], na.rm = TRUE),
406
+ 'Contact%' = mean(ConCheck[SwingCheck == TRUE], na.rm = TRUE) * 100,
407
+ 'ZCon%' = mean(ConCheck[ZoneCheck == TRUE & SwingCheck == TRUE], na.rm = TRUE) * 100,
408
+ 'ZSwing%' = mean(SwingCheck[ZoneCheck == TRUE], na.rm = TRUE) * 100,
409
+ 'OCon%' = mean(ConCheck[ZoneCheck == FALSE & SwingCheck == TRUE], na.rm = TRUE) * 100,
410
+ 'Chase%' = mean(SwingCheck[ZoneCheck == FALSE], na.rm = TRUE) * 100,
411
+ 'SwStr%' = mean(WhiffCheck, na.rm = TRUE) * 100,
412
+ 'Whiff%' = mean(WhiffCheck[SwingCheck == TRUE], na.rm = TRUE) * 100,
413
+ 'Zone%' = mean(ZoneCheck, na.rm = TRUE) * 100,
414
+ 'Strike%' = mean(StrikeCheck, na.rm = TRUE) * 100,
415
+ 'Swing%' = mean(SwingCheck, na.rm = TRUE) * 100,
416
+ 'Spin Rate' = mean(spin_rate, na.rm = TRUE),
417
+ 'Extension' = mean(extension, na.rm = TRUE),
418
+ 'IVB' = mean(IVB, na.rm = TRUE),
419
+ 'HB' = mean(HB, na.rm = TRUE),
420
+ 'VAA' = mean(vaa, na.rm = TRUE),
421
+ 't3VAA' = mean(vaa[TopZoneCheck == TRUE], na.rm = TRUE),
422
+ 'b3VAA' = mean(vaa[BotZoneCheck == TRUE], na.rm = TRUE),
423
+ 'CSW%' = mean(CSWCheck, na.rm = TRUE) * 100,
424
+ 'Arm Angle' = mean(arm_angle, na.rm = TRUE),
425
+ 'Bat Speed' = mean(bat_speed[CompSwingCheck == TRUE], na.rm = TRUE),
426
+ .groups = 'drop'
427
+ ) %>%
428
+ mutate(
429
+ across(c('xwOBA', 'xwOBACON'), ~round(., 3)), # wOBA metrics to 3 decimals
430
+ across(where(is.numeric) & !c('xwOBA', 'xwOBACON'), ~round(., 1)) # everything else to 1 decimal
431
+ )
432
+
433
+ # Then apply filters to the summarized data
434
+ filtered_data <- grouped_data
435
+ current_filters <- filters()
436
+
437
+ for (filter in current_filters) {
438
+ column <- input[[filter$column_id]]
439
+ operator <- input[[filter$operator_id]]
440
+ value <- input[[filter$value_id]]
441
+
442
+ if (!is.null(column) && !is.null(operator) && !is.null(value) && value != "") {
443
+ filtered_data <- switch(operator,
444
+ "eq" = filtered_data %>% filter(!!sym(column) == value),
445
+ "like" = filtered_data %>% filter(grepl(value, !!sym(column), ignore.case = TRUE)),
446
+ "gt" = filtered_data %>% filter(!!sym(column) > as.numeric(value)),
447
+ "lt" = filtered_data %>% filter(!!sym(column) < as.numeric(value)),
448
+ "gte" = filtered_data %>% filter(!!sym(column) >= as.numeric(value)),
449
+ "lte" = filtered_data %>% filter(!!sym(column) <= as.numeric(value)),
450
+ filtered_data
451
+ )
452
+ }
453
+ }
454
+
455
+ # Finally select only the requested columns
456
+ filtered_data %>%
457
+ select(all_of(c(input$group, input$stats)))
458
+ })
459
+
460
+ # Tabulator rendering with formatting
461
+ output$table <- renderTabulator({
462
+ req(processed_data())
463
+ column_defs <- c(
464
+ # Group columns
465
+ map(input$group, function(col) {
466
+ list(field = col, title = col, width = 175) # reduced from 250
467
+ }),
468
+ # Stat columns
469
+ map(input$stats, function(col) {
470
  list(
471
+ field = col,
472
+ title = col,
473
+ width = 110, # reduced from 150
474
+ formatter = if(col %in% c("xwOBA", "xwOBACON")) "number" else "number",
475
+ formatterParams = if(col %in% c("xwOBA", "xwOBACON")) {
476
+ list(precision = 3)
477
+ } else {
478
+ list(precision = 1)
479
+ }
480
  )
481
+ })
482
+ )
483
+ tabulator(processed_data(),
484
+ options = list(
485
+ selectable = TRUE,
486
+ layout = "fitColumns",
487
+ columns = column_defs
488
+ )
489
+ )
490
+ })
491
+ # Add observers for filter management and other reactive elements
492
+ observeEvent(input$add_filter, {
493
+ isolate({
494
+ current_counter <- counter()
495
+ filter_id <- paste0("filter_", current_counter)
496
+
497
+ new_filter <- list(
498
+ id = filter_id,
499
+ column_id = paste0("column_", filter_id),
500
+ operator_id = paste0("operator_", filter_id),
501
+ value_id = paste0("value_", filter_id)
502
+ )
503
+
504
+ current_filters <- filters()
505
+ filters(c(current_filters, list(new_filter)))
506
+ counter(current_counter + 1)
507
+ })
508
+ })
509
+
510
+ observe({
511
+ current_filters <- filters()
512
+
513
+ lapply(current_filters, function(filter) {
514
+ observeEvent(input[[paste0("remove_", filter$id)]], {
515
+ isolate({
516
+ removeClicks(removeClicks() + 1)
517
+ new_filters <- current_filters[sapply(current_filters, function(f) f$id != filter$id)]
518
+ filters(new_filters)
519
+ })
520
+ }, ignoreInit = TRUE, ignoreNULL = TRUE)
521
+ })
522
+ })
523
+
524
+ # Filter container UI
525
+ output$filter_container <- renderUI({
526
+ removeClicks()
527
+ current_filters <- filters()
528
+
529
+ lapply(current_filters, function(filter) {
530
+ div(
531
+ class = "mb-1",
532
+ div(
533
+ style = "display: flex; gap: 10px; align-items: center;",
534
+ selectInput(filter$column_id, "Column",
535
+ choices = available_columns(),
536
+ width = "200px",
537
+ selected = input[[filter$column_id]]
538
+ ),
539
+ selectInput(filter$operator_id, "Stat",
540
+ choices = c(
541
+ "=" = "eq",
542
+ "contains" = "like",
543
+ ">" = "gt",
544
+ "<" = "lt",
545
+ ">=" = "gte",
546
+ "<=" = "lte"
547
+ ),
548
+ width = "100px",
549
+ selected = input[[filter$operator_id]]
550
+ ),
551
+ textInput(filter$value_id, "Value",
552
+ value = input[[filter$value_id]],
553
+ width = "150px"
554
+ ),
555
+ actionButton(
556
+ paste0("remove_", filter$id),
557
+ icon("trash"),
558
+ class = "btn-danger btn-sm",
559
+ style = "margin-top: 22px;"
560
+ )
561
  )
562
+ )
563
+ })
564
+ })
 
 
 
565
  }
566
 
567
+ # Run the application
568
+ shinyApp(ui = ui, server = server)