Spaces:
Sleeping
Sleeping
Download plumber.R from cdcavin/queens-api: direct link, hf CLI and curl.
- Browser
- Download file 6.71 kB
-
https://huggingface.co/spaces/cdcavin/queens-api/resolve/main/plumber.R
- Command line
-
hf download hf://spaces/cdcavin/queens-api/plumber.R
-
curl -L -o plumber.R https://huggingface.co/spaces/cdcavin/queens-api/resolve/main/plumber.R
6.71 kB
| #QUEENS PICTH DESIGN APP API | |
| library(plumber) | |
| library(mgcv) | |
| library(data.table) | |
| library(jsonlite) | |
| cat("Loading model artifacts...\n") | |
| fit <- readRDS("fit_l6_champ.rds") | |
| vaa_mod <- readRDS("vaa_anchor_model.rds") | |
| haa_mod <- readRDS("haa_anchor_model.rds") | |
| scaling <- readRDS("shape_scaling_factors.rds") | |
| mu_l6 <- scaling$mu_l6 | |
| sig_l6 <- scaling$sig_l6 | |
| train_means <- fread("train_means.csv") | |
| model_levels <- c("FF","CU","ST","SI","FC","SL","CH","FS","KC","SV","KN","EP","FA","FO","CS","SC") | |
| cat(sprintf("Model ready. %d pitch types loaded.\n", length(model_levels))) | |
| cat("Loading pitcher profiles...\n") | |
| pitcher_profiles <- tryCatch({ | |
| dt <- fread("pitcher_profiles.csv") | |
| cat(sprintf("Pitcher profiles loaded: %d rows, %d pitchers.\n", | |
| nrow(dt), dt[, uniqueN(player_name)])) | |
| dt | |
| }, error = function(e) { | |
| warning(sprintf("Failed to load pitcher_profiles.csv: %s", conditionMessage(e))) | |
| NULL | |
| }) | |
| build_features <- function(d) { | |
| d <- as.data.table(d) | |
| d[, p_throws := fifelse(p_throws %in% c("Left","LHP"), "L", | |
| fifelse(p_throws %in% c("Right","RHP"), "R", p_throws))] | |
| d[, pfx_x_sym := fifelse(p_throws == "L", pfx_x * -1, pfx_x)] | |
| d[, plate_x_sym := fifelse(p_throws == "L", plate_x * -1, plate_x)] | |
| d[, rel_x_sym := fifelse(p_throws == "L", release_pos_x * -1, release_pos_x)] | |
| if (!"release_extension" %in% names(d)) d[, release_extension := 6.0] | |
| d[is.na(release_extension), release_extension := 6.0] | |
| d[, flight_time := (1.4167 - 50) / vy0] | |
| d[, vaa := atan((vz0 + az * flight_time) / -(vy0 + ay * flight_time)) * (180 / pi)] | |
| d[, haa := atan((vx0 + ax * flight_time) / -(vy0 + ay * flight_time)) * (180 / pi)] | |
| d[, haa_sym := fifelse(p_throws == "L", haa * -1, haa)] | |
| d[, vaa_resid := vaa - predict(vaa_mod, newdata = d)] | |
| d[, haa_resid_sym := haa_sym - predict(haa_mod, newdata = d)] | |
| d <- merge(d, train_means, by = "pitch_type", all.x = FALSE) | |
| d[, ivb_diff := pfx_z - base_ivb] | |
| d[, hb_diff_sym := pfx_x_sym - base_hb] | |
| d[, pitch_type := factor(as.character(pitch_type), levels = model_levels)] | |
| d[, p_throws := factor(as.character(p_throws), levels = c("L", "R"))] | |
| d[!is.na(pitch_type)] | |
| } | |
| score_pitches <- function(d) { | |
| d <- build_features(d) | |
| if (nrow(d) == 0) return(list(error = "No valid pitches after feature engineering")) | |
| raw <- predict(fit, newdata = d) | |
| shape_plus <- (((raw * -1) - mu_l6) / sig_l6) * 10 + 100 | |
| term_mat <- predict(fit, newdata = d, type = "terms") | |
| col_names <- colnames(term_mat) | |
| list( | |
| n = nrow(d), | |
| shape_plus = round(as.numeric(shape_plus), 1), | |
| terms = lapply(seq_len(nrow(d)), function(i) { | |
| row <- as.list(round(term_mat[i, ], 3)) | |
| names(row) <- col_names | |
| row | |
| }) | |
| ) | |
| } | |
| #* @apiTitle Queens Shape+ API | |
| #* @apiDescription Scores pitch shapes using the Shape+ v2.0 GAM model | |
| #* @filter cors | |
| function(req, res) { | |
| res$setHeader("Access-Control-Allow-Origin", "*") | |
| res$setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") | |
| res$setHeader("Access-Control-Allow-Headers", "Content-Type") | |
| if (req$REQUEST_METHOD == "OPTIONS") { | |
| res$status <- 200 | |
| return(list()) | |
| } | |
| plumber::forward() | |
| } | |
| #* @post /score | |
| #* @serializer json | |
| function(req) { | |
| tryCatch({ | |
| body <- fromJSON(req$postBody, simplifyDataFrame = TRUE) | |
| score_pitches(as.data.table(body)) | |
| }, error = function(e) { | |
| list(error = conditionMessage(e)) | |
| }) | |
| } | |
| #* Find comparable pitchers by movement profile | |
| #* Expects JSON body: pitch_type, ivb, hb_sym, velo, arm_angle, p_throws | |
| #* Returns top 5 closest pitchers with their full arsenals | |
| #* @post /find_comps | |
| #* @serializer unboxedJSON | |
| function(req) { | |
| tryCatch({ | |
| # ββ Guard: profiles must have loaded at startup ββββββββββββββββββββββββββ | |
| if (is.null(pitcher_profiles)) { | |
| return(list(error = "pitcher_profiles.csv failed to load at startup")) | |
| } | |
| # ββ Parse request body βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| body <- fromJSON(req$postBody, simplifyVector = TRUE) | |
| input_pitch_type <- as.character(body$pitch_type) | |
| input_p_throws <- as.character(body$p_throws) | |
| input_ivb <- as.numeric(body$ivb) | |
| input_hb <- as.numeric(body$hb_sym) | |
| input_velo <- as.numeric(body$velo) | |
| input_arm <- as.numeric(body$arm_angle) | |
| # ββ Filter to matching anchor pitch type (any handedness β hb_sym is already symmetrized) | |
| anchor_rows <- pitcher_profiles[pitch_type == input_pitch_type] | |
| if (nrow(anchor_rows) == 0) { | |
| return(list(error = "no matches")) | |
| } | |
| # ββ Compute weighted distance for each matching row ββββββββββββββββββββββ | |
| # Arm angle tightly weighted (/8) so a submarine pitcher never matches an over-the-top guy. | |
| # If input arm_angle is NA/null, skip that term to avoid NaN distances. | |
| arm_term <- if (!is.na(input_arm)) ((anchor_rows$arm_angle - input_arm) / 8)^2 else 0 | |
| anchor_rows[, dist := sqrt( | |
| ((ivb - input_ivb) / 3)^2 + | |
| ((hb_sym - input_hb) / 3)^2 + | |
| ((velo - input_velo) / 5)^2 + | |
| arm_term | |
| )] | |
| # ββ Top 5 closest pitchers βββββββββββββββββββββββββββββββββββββββββββββββ | |
| top5 <- head(anchor_rows[order(dist)], 5) | |
| # ββ For each top pitcher, pull their full arsenal ββββββββββββββββββββββββ | |
| result <- lapply(seq_len(nrow(top5)), function(i) { | |
| row <- top5[i] | |
| pitcher_nm <- row$player_name | |
| anchor_dist <- round(row$dist, 3) | |
| arsenal_rows <- pitcher_profiles[player_name == pitcher_nm] | |
| arsenal <- lapply(seq_len(nrow(arsenal_rows)), function(j) { | |
| r <- arsenal_rows[j] | |
| list( | |
| pitch_type = r$pitch_type, | |
| velo = r$velo, | |
| ivb = r$ivb, | |
| hb_sym = r$hb_sym, | |
| rel_z = r$rel_z, | |
| arm_angle = r$arm_angle, | |
| n = r$n | |
| ) | |
| }) | |
| list( | |
| player_name = pitcher_nm, | |
| p_throws = row$p_throws, | |
| anchor_dist = anchor_dist, | |
| arsenal = arsenal | |
| ) | |
| }) | |
| result | |
| }, error = function(e) { | |
| list(error = conditionMessage(e)) | |
| }) | |
| } | |
| #* Health check | |
| #* @get /health | |
| function() { | |
| list(status = "ok", model = "fit_l6_champ", time = as.character(Sys.time())) | |
| } | |