This repository contains a reproducible pipeline and interactive Streamlit app that recommends next pitches to throw to a batter based on historical data, derived batter archetypes, pitcher arsenals, and a run-value model trained on play-level outcomes. The project combines data preparation, feature engineering, machine learning, and a lightweight UI to explore pitch sequencing from a sabermetric perspective.
- Goal: recommend the next pitch (and ranked candidate list) for a pitcher vs. batter matchup in a given count and game situation, using a trained run-value model and batter-pitchtype matchup stats.
- Data sources: Statcast play-by-play (raw/processed), derived batter archetypes, inferred pitcher arsenals, and a run-expectancy lookup table.
- Unique aspects:
- Stepwise, name-driven Streamlit UI that accepts names only for batter and pitcher and manages an at-bat flow (choose pitch, record outcome, repeat).
- Uses run-expectancy to convert model predicted run-values into softmax probabilities to prioritize lower expected runs.
- Combines a model-based scoring approach with heuristic fallbacks and batter-specific pitchtype stats for robustness.
- Create a Python 3.13 virtual environment and install dependencies:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt- Run the Streamlit app locally:
streamlit run app.py- Open http://localhost:8501 in your browser.
If you deploy to Streamlit Cloud, ensure the "main module" path in the deployment settings is set to app/app.py (this repo provides a tiny wrapper at that location that imports the canonical app.py).
Files & directories:
data/raw/— raw data dumps (example:statcast_2025.csv, and name mapping CSVs for batters/pitchers)data/processed/— parquet files with cleaned features and aggregated batter-pitchtype stats (cleaned.parquet,features.parquet,runvalue.parquet)pitcher_assets/pitcher_arsenals.csv— inferred pitcher arsenals built from Statcast
Steps performed:
- Acquire raw Statcast plays and name mapping files. Place raw CSVs in
data/raw/. - Run
src/preprocess.pyto clean raw play-by-play rows and filter to the seasons and events of interest.- Tasks performed: parse timestamps, normalize pitch types, drop clearly invalid rows (missing pitch_type + outcome), and standardize batter/pitcher name fields.
- Run
src/feature_engineering.pyto compute per-play features and aggregate batter vs. pitch_type summary stats.- Feature tasks: extract plate_x/plate_z approximations, map human-friendly location tokens to plate coordinates for the model when a visual target is used, compute last-pitch deltas, and bucket zones.
- Handle missing values and outliers:
- Numeric missing values are filled with domain-appropriate defaults (e.g., 0 for rate stats when absent, or median imputation for continuous measures) inside the feature pipeline.
- Extreme outliers are winsorized or clipped where appropriate (for example, release speeds outside realistic ranges are clipped to min/max bounds).
- Save processed data to Parquet under
data/processed/for efficient re-use during model training.
Filtering decisions and heuristics:
- Only include pitchers/batters with sufficient counts for stable estimates (e.g., minimum plate appearances or pitch-type exposures). See the processing scripts for exact thresholds used.
- Limit seasons or games as needed for a stable modeling window (e.g., last 1–3 seasons depending on dataset size).
Primary features used by the run-value model and UI:
- Count and base-state features: balls, strikes, outs_when_up, on_1b, on_2b, on_3b
- Pitch characteristics: pitch_type (categorical), release_speed, release_spin_rate
- Plate and zone features: plate_x, plate_z, zone
- Temporal features: last_pitch_type, last_pitch_result, last_pitch_speed_delta
- Batter matchup aggregates: batter_pitchtype_woba, batter_pitchtype_whiff_rate, batter_pitchtype_run_value — derived from aggregating
features.parquetby batter and pitch type
Transformations and preprocessing:
- Categorical variables are encoded via one-hot/target-encoding inside the model pipeline.
- Numeric variables are scaled when required by the model (the training pipeline contains scalers where necessary).
- Feature interactions (e.g., count x pitch_type) are created to help the model capture contextual pitch selection effects.
Rationale for chosen features:
- The chosen features capture both the physical context (release speed, zone) and matchup context (batter-specific pitchtype rates) that drive run expectancy.
- Using batter-specific aggregated pitchtype run-values allows the model to account for individual tendencies beyond league averages.
Files of interest:
src/model_train.py— training script that builds the modeling pipeline and saves the artifactartifacts/runvalue_model.pkl— trained scikit-learn pipeline and model (used for scoring in the Streamlit app)
Workflow summary:
- Load processed features (
data/processed/features.parquet) and therunvaluelabels (data/processed/runvalue.parquet). - Split data into train/validation/test sets by game or time window to avoid leakage (time-based split recommended).
- Build a scikit-learn pipeline that handles categorical encoding, imputation, scaling, and the estimator.
- Example model choices: GradientBoostingRegressor or RandomForestRegressor for run-value prediction. In some experiments a multi-output approach was used for predicting multiple outcome components.
- Tune hyperparameters using grid search or randomized search with cross-validation on the train set.
- Evaluate on the validation set and select the best model.
- Re-fit on train+validation and test once on the holdout test set. Save the final pipeline using
joblib.dumporpickletoartifacts/runvalue_model.pkl.
Notes:
- When saving models, be mindful of library versions (scikit-learn). If your deployment environment uses a different scikit-learn version you may see compatibility warnings when unpickling.
Metrics and methods used:
- For run-value regression: mean squared error (MSE), mean absolute error (MAE), and explained variance.
- For derived decision-support quality: evaluate whether the model's ranking of pitch candidates correlates with lower realized run outcomes in holdout data.
- Cross-validation: time-aware cross-validation (sliding window) is used to avoid leakage across games/seasons.
- Diagnostics: residual plots, feature importances, and partial dependence plots were used to understand model behavior.
How the model is used in practice:
- The Streamlit app (
app.py) loadsartifacts/runvalue_model.pklat runtime (if present) and uses it to score every pitch in the pitcher's arsenal for the current state. The app also builds a smallbatter_stats_mapfrom batter vs. pitch_type aggregates so predictions are batter-aware. - The raw model predictions are converted into candidate probabilities by computing expected_after_RE = current_RE + predicted_run_value, then applying a softmax over the negative expected_after_RE to prefer lower expected runs.
UI flow:
- Choose batter (names-only) and pitcher (names-only), set the count and base occupancy, click "Start at-bat".
- The app computes candidate pitches (model-based when possible, heuristic otherwise) and renders them with percent probabilities and small strike-zone visuals.
- Select a pitch, record the outcome, and the session updates the count and recomputes candidates. This lets you step through an at-bat interactively.
Deployment notes:
- On Streamlit Cloud: set the repository and branch, and set the main module to
app/app.py(the wrapper imports the canonicalapp.py). If you prefer, configure the host to useapp.pydirectly and delete the wrapper. - If you see scikit-learn InconsistentVersionWarning when loading the model, consider re-saving the model with the environment's scikit-learn or pinning scikit-learn in
requirements.txtto the training version.
app.py— main Streamlit UI and session management (stepwise at-bat flow, candidate rendering, model integration)app/app.py— minimal wrapper imported by some hosts (imports rootapp.py) for compatibility with hosting entrypoint settingssrc/attack_recommender.py— heuristic candidate generator and batter-pitchtype stat helperssrc/predict.py— model wrapper and scoring utilities (loadsartifacts/runvalue_model.pkl)src/model_train.py— training script for building the pipeline and saving modelssrc/feature_engineering.py— functions that compute play-level features and batter-pitchtype aggregatessrc/preprocess.py— cleaning and normalization of raw Statcast play-by-playsrc/build_pitcher_arsenals.py— infer pitcher arsenals from Statcast and writepitcher_assets/pitcher_arsenals.csvdata/raw/— raw CSVs (statcast, mapping files)data/processed/— parquet outputs used for training (cleaned.parquet,features.parquet,runvalue.parquet)artifacts/runvalue_model.pkl— trained model artifact used in productionpitcher_assets/pitcher_arsenals.csv— inferred arsenals for pitchers
- Ensure raw data is available in
data/raw/and the mapping CSVs for names are present. - Run preprocessing and feature engineering (order matters):
python src/preprocess.py --input data/raw/statcast_2025.csv --output data/processed/cleaned.parquet
python src/feature_engineering.py --input data/processed/cleaned.parquet --output data/processed/features.parquet- Train the model:
python src/model_train.py --features data/processed/features.parquet --labels data/processed/runvalue.parquet --out artifacts/runvalue_model.pkl- Start the app and confirm it loads the saved model and artifacts.
If any step fails because of package version issues (e.g., scikit-learn), create a virtualenv with pinned versions or re-run training in the deployment environment to produce a compatible artifact.
- Add more batter-level features, including Statcast swing/contact metrics (swinging_strike_rate, chase_rate, etc.)
- Model ensemble or stacking to improve run-value predictions
- Re-train model periodically or with online updates to capture season drift
- Create a richer visualization dashboard showing sequence-level outcomes and aggregated evaluation (e.g., which pitch sequences lower run expectancy vs. holdout)
- Add user authentication and session persistence to save at-bats for later analysis
If you have questions or want to reproduce the pipeline, open an issue or contact the maintainer (repository owner). Thanks to the Statcast data providers and the open-source Python ecosystem for enabling reproducible analytics.
This repository includes a FastAPI serverless endpoint at api/recommend.py that wraps the recommender logic.
Quick steps to deploy:
- Install the Vercel CLI and log in.
- Push this repository to GitHub (or connect your repo in the Vercel dashboard).
- Vercel will detect the Python serverless functions under
api/and install packages fromrequirements.txt.
Endpoint:
POST /.vercel.app/api/recommend (or https://<your-deployment>.vercel.app/api/recommend)
Example JSON body:
{ "batter": "455117", "pitcher": "123456", "count": "1-1", "situation": {"risp": true, "outs": 2, "late_inning": true} }
The endpoint returns a JSON recommendation (recommended_sequence, confidence, strategy_notes, ...).
Notes:
- Vercel serverless functions are short-lived. This API is stateless and loads data on startup.
- The Streamlit app in
app/app.pyis not suitable for Vercel serverless; consider using the/api/recommendendpoint from a lightweight static frontend or another deployment for Streamlit.