DaveM2430 commited on
Commit
a6d046d
Β·
verified Β·
1 Parent(s): ea5e64c

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +13 -132
app.R CHANGED
@@ -4,8 +4,6 @@ library(arrow)
4
  library(DT)
5
  library(ggplot2)
6
  library(shinyjs)
7
- library(tidymodels)
8
- library(xgboost)
9
 
10
  # ── Load data ──────────────────────────────────────────────────────────────────
11
  load_data <- function(level) {
@@ -13,11 +11,10 @@ load_data <- function(level) {
13
  arrow::read_parquet(path)
14
  }
15
 
16
- # ── Models ─────────────────────────────────────────────────────────────────────
17
- model <- readRDS("/app/Stuff+2.rds")
18
- league_stats <- readRDS("/app/NEW_LeagueStats2.rds")
19
- xgb_fit <- readRDS("/app/location_plus_model.rds")
20
- league_stats_pitcher <- readRDS("/app/location_plus_league_stats_pitcher.rds")
21
 
22
  # ── Pitch colors ───────────────────────────────────────────────────────────────
23
  pitch_colors <- c(
@@ -47,113 +44,8 @@ clean_pitch_type <- function(pt) {
47
  )
48
  }
49
 
50
- map_pitch_type <- function(pt) {
51
- case_when(
52
- pt %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ "Fastball",
53
- pt %in% c("Sinker","TwoSeamFastBall","OneSeamFastball") ~ "Sinker",
54
- pt == "Cutter" ~ "Cutter",
55
- pt %in% c("Curveball","CurveBall") ~ "Curveball",
56
- pt == "Slider" ~ "Slider",
57
- pt == "Sweeper" ~ "Sweeper",
58
- pt %in% c("ChangeUp","Changeup") ~ "Changeup",
59
- pt == "Splitter" ~ "Splitter",
60
- TRUE ~ NA_character_
61
- )
62
- }
63
-
64
- # ── Stuff+ ─────────────────────────────────────────────────────────────────────
65
- compute_stuff_plus <- function(data) {
66
- fb_avgs <- data %>%
67
- filter(TaggedPitchType %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam",
68
- "Sinker","TwoSeamFastBall","OneSeamFastball","Cutter")) %>%
69
- mutate(
70
- PitcherId = as.character(PitcherId),
71
- priority = case_when(
72
- TaggedPitchType %in% c("Fastball","FourSeamFastBall","Four-Seam","FourSeam") ~ 1,
73
- TaggedPitchType %in% c("Sinker","TwoSeamFastBall","OneSeamFastball") ~ 2,
74
- TaggedPitchType == "Cutter" ~ 3
75
- )
76
- ) %>%
77
- group_by(PitcherId, priority) %>%
78
- summarize(count=n(), fb_velo=mean(RelSpeed,na.rm=TRUE),
79
- fb_ivb=mean(InducedVertBreak,na.rm=TRUE),
80
- fb_hb=mean(HorzBreak,na.rm=TRUE), .groups="drop") %>%
81
- arrange(PitcherId, priority, desc(count)) %>%
82
- slice_head(by=PitcherId, n=1) %>%
83
- select(PitcherId, PrimaryFB=priority, fb_velo, fb_ivb, fb_hb) %>%
84
- mutate(PrimaryFB=case_when(
85
- PrimaryFB==1 ~ "Fastball", PrimaryFB==2 ~ "Sinker", PrimaryFB==3 ~ "Cutter"
86
- ))
87
-
88
- if (nrow(fb_avgs)==0) return(tibble(Pitcher=character(), `Stuff+`=numeric()))
89
-
90
- scored <- data %>%
91
- mutate(PitcherId=as.character(PitcherId),
92
- PitchType=map_pitch_type(TaggedPitchType)) %>%
93
- filter(!is.na(PitchType)) %>%
94
- left_join(fb_avgs, by="PitcherId") %>%
95
- mutate(
96
- FF_diff = ifelse(PitchType==PrimaryFB, 0, RelSpeed-fb_velo),
97
- IVB_diff = ifelse(PitchType==PrimaryFB, 0, InducedVertBreak-fb_ivb),
98
- HB_diff = ifelse(PitchType==PrimaryFB, 0, HorzBreak-fb_hb),
99
- SameSide = as.factor(ifelse(BatterSide==PitcherThrows, 1, 0)),
100
- PitchType= as.factor(PitchType)
101
- ) %>%
102
- filter(!is.na(InducedVertBreak), !is.na(RelSide), !is.na(HorzBreak),
103
- !is.na(SpinRate), !is.na(Extension), !is.na(RelHeight),
104
- !is.na(RelSpeed), !is.na(SameSide))
105
-
106
- if (nrow(scored)==0) return(tibble(Pitcher=character(), `Stuff+`=numeric()))
107
-
108
- scored %>%
109
- bind_cols(suppressWarnings(predict(model, scored, type="prob"))) %>%
110
- rename(expected_whiff=.pred_1) %>%
111
- group_by(Pitcher, PitchType) %>%
112
- summarise(expected_whiff_rate=mean(expected_whiff), .groups="drop") %>%
113
- left_join(league_stats, by="PitchType") %>%
114
- group_by(Pitcher) %>%
115
- summarise(
116
- `Stuff+` = round(weighted.mean(
117
- ((expected_whiff_rate - league_avg) / league_sd) * 10 + 100,
118
- w=rep(1,n()), na.rm=TRUE), 0),
119
- .groups="drop"
120
- )
121
- }
122
-
123
- # ── Location+ ──────────────────────────────────────────────────────────────────
124
- compute_location_plus <- function(data) {
125
- loc_data <- data %>%
126
- mutate(
127
- Balls = as.integer(Balls),
128
- Strikes = as.integer(Strikes),
129
- count_state = paste0(Balls,"-",Strikes),
130
- count_state = ifelse(!count_state %in% c("0-0","0-1","0-2","1-0","1-1","1-2",
131
- "2-0","2-1","2-2","3-0","3-1","3-2"),
132
- NA, count_state),
133
- count_state = as.factor(count_state),
134
- PitchType = map_pitch_type(TaggedPitchType)
135
- ) %>%
136
- filter(!is.na(PitchType), !is.na(count_state))
137
-
138
- if (nrow(loc_data)==0) return(tibble(Pitcher=character(), `Location+`=numeric()))
139
-
140
- predictions <- predict(xgb_fit, new_data=loc_data) %>% rename(xRV288=.pred)
141
-
142
- bind_cols(predictions, loc_data) %>%
143
- group_by(Pitcher, PitchType) %>%
144
- summarise(avg_xRV288=mean(xRV288,na.rm=TRUE), .groups="drop") %>%
145
- left_join(league_stats_pitcher, by="PitchType") %>%
146
- group_by(Pitcher) %>%
147
- summarise(
148
- `Location+` = round(weighted.mean(
149
- (-(avg_xRV288 - league_avg) / league_sd) * 10 + 100,
150
- w=rep(1,n()), na.rm=TRUE), 0),
151
- .groups="drop"
152
- )
153
- }
154
-
155
  # ── Pitcher stats ──────────────────────────────────────────────────────────────
156
- build_pitcher_stats <- function(df, min_pitches, stuff_scores, loc_scores) {
157
 
158
  team_lookup <- df %>%
159
  select(Pitcher, PitcherTeam) %>%
@@ -207,9 +99,8 @@ build_pitcher_stats <- function(df, min_pitches, stuff_scores, loc_scores) {
207
  na.rm=TRUE),
208
  .groups = "drop"
209
  ) %>%
210
- left_join(team_lookup, by="Pitcher") %>%
211
- left_join(stuff_scores, by="Pitcher") %>%
212
- left_join(loc_scores, by="Pitcher") %>%
213
  filter(Pitches >= min_pitches) %>%
214
  mutate(
215
  Name = sapply(Pitcher, format_name),
@@ -307,7 +198,8 @@ ui <- fluidPage(
307
  .modal-subtitle { font-size:12px; color:#888; margin:0 0 16px 0; }
308
  .section-label { font-size:12px; font-weight:700; color:#002147;
309
  text-transform:uppercase; letter-spacing:0.4px;
310
- margin:16px 0 8px 0; border-top:1px solid #eee; padding-top:14px; }
 
311
 
312
  #splash { position:fixed; top:0; left:0; width:100%; height:100%;
313
  background:#002147; display:flex; flex-direction:column;
@@ -428,21 +320,12 @@ ui <- fluidPage(
428
  # ── Server ─────────────────────────────────────────────────────────────────────
429
  server <- function(input, output, session) {
430
 
431
- # load data + compute models once per level change
432
  raw_data <- reactive({
433
  req(input$level)
434
  tryCatch({
435
- df <- load_data(input$level)
436
  list(
437
- df = df,
438
- stuff_scores = tryCatch(
439
- compute_stuff_plus(df),
440
- error=function(e) tibble(Pitcher=character(), `Stuff+`=numeric())
441
- ),
442
- loc_scores = tryCatch(
443
- compute_location_plus(df),
444
- error=function(e) tibble(Pitcher=character(), `Location+`=numeric())
445
- )
446
  )
447
  }, error = function(e) {
448
  showNotification(paste("Failed to load:", e$message), type="error")
@@ -456,8 +339,7 @@ server <- function(input, output, session) {
456
  build_pitcher_stats(
457
  raw_data()$df,
458
  input$min_pitches,
459
- raw_data()$stuff_scores,
460
- raw_data()$loc_scores
461
  )
462
  })
463
 
@@ -513,7 +395,6 @@ server <- function(input, output, session) {
513
  )
514
  })
515
 
516
- # modal
517
  current_pitcher <- reactiveVal(NULL)
518
 
519
  observeEvent(input$selected_pitcher, {
@@ -548,7 +429,7 @@ server <- function(input, output, session) {
548
 
549
  output$movement_plot <- renderPlot({
550
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
551
- pdata <- pitcher_plot_data()
552
  parts <- strsplit(current_pitcher(), ", ")[[1]]
553
  display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
554
 
 
4
  library(DT)
5
  library(ggplot2)
6
  library(shinyjs)
 
 
7
 
8
  # ── Load data ──────────────────────────────────────────────────────────────────
9
  load_data <- function(level) {
 
11
  arrow::read_parquet(path)
12
  }
13
 
14
+ load_scores <- function(level) {
15
+ path <- paste0("/app/", level, "_scores.parquet")
16
+ arrow::read_parquet(path)
17
+ }
 
18
 
19
  # ── Pitch colors ───────────────────────────────────────────────────────────────
20
  pitch_colors <- c(
 
44
  )
45
  }
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  # ── Pitcher stats ──────────────────────────────────────────────────────────────
48
+ build_pitcher_stats <- function(df, min_pitches, scores) {
49
 
50
  team_lookup <- df %>%
51
  select(Pitcher, PitcherTeam) %>%
 
99
  na.rm=TRUE),
100
  .groups = "drop"
101
  ) %>%
102
+ left_join(team_lookup, by="Pitcher") %>%
103
+ left_join(scores %>% select(Pitcher, `Stuff+`, `Location+`), by="Pitcher") %>%
 
104
  filter(Pitches >= min_pitches) %>%
105
  mutate(
106
  Name = sapply(Pitcher, format_name),
 
198
  .modal-subtitle { font-size:12px; color:#888; margin:0 0 16px 0; }
199
  .section-label { font-size:12px; font-weight:700; color:#002147;
200
  text-transform:uppercase; letter-spacing:0.4px;
201
+ margin:16px 0 8px 0; border-top:1px solid #eee;
202
+ padding-top:14px; }
203
 
204
  #splash { position:fixed; top:0; left:0; width:100%; height:100%;
205
  background:#002147; display:flex; flex-direction:column;
 
320
  # ── Server ─────────────────────────────────────────────────────────────────────
321
  server <- function(input, output, session) {
322
 
 
323
  raw_data <- reactive({
324
  req(input$level)
325
  tryCatch({
 
326
  list(
327
+ df = load_data(input$level),
328
+ scores = load_scores(input$level)
 
 
 
 
 
 
 
329
  )
330
  }, error = function(e) {
331
  showNotification(paste("Failed to load:", e$message), type="error")
 
339
  build_pitcher_stats(
340
  raw_data()$df,
341
  input$min_pitches,
342
+ raw_data()$scores
 
343
  )
344
  })
345
 
 
395
  )
396
  })
397
 
 
398
  current_pitcher <- reactiveVal(NULL)
399
 
400
  observeEvent(input$selected_pitcher, {
 
429
 
430
  output$movement_plot <- renderPlot({
431
  req(pitcher_plot_data(), nrow(pitcher_plot_data()) > 0)
432
+ pdata <- pitcher_plot_data()
433
  parts <- strsplit(current_pitcher(), ", ")[[1]]
434
  display_name <- if (length(parts)==2) paste(trimws(parts[2]),trimws(parts[1])) else current_pitcher()
435