Practice · Baseball · Simulated for practice
Eight invented pitchers and about 4,800 pitches: type, velocity, horizontal break, induced vertical break, location, swing and whiff. The raw material for pitch comps, arsenal reports and a whiff model.
pitcherstringpitch_typestringrelease_speednumber · mphhb_innumber · inivb_innumber · inplate_xnumber · ftplate_znumber · ftswinginteger · 0/1whiffinteger · 0/1Simulated for practice. Simulated for practice by AthlyticZ (agents/sim_data.py, seed 20261109). License CC BY 4.0. Generated from a documented model with realistic distributions; every name is invented.
Edit the code and press Run to recompute it in your browser.
Edit me: Change the sample size from 18 to 120 per pitch and run again, or add a column to the summary.
Data: Pitch movement and whiffs (pitch_movement.csv). Simulated for practice by AthlyticZ (agents/sim_data.py, seed 20261109). License CC BY 4.0.
The code runs in your browser on webR or Pyodide; nothing you type leaves the page.
import pandas as pd
pitches = pd.read_csv("data/pitch_movement.csv")
pitches.groupby(["pitcher", "pitch_type"]).size().unstack(fill_value=0)
Try each before opening the hint; open the worked solution only after your own attempt.
Build each pitcher's arsenal: usage, average velocity, horizontal break and induced vertical break for every pitch type.
Group by pitcher and pitch type, aggregate, then turn counts into shares within each pitcher with a grouped transform.
import pandas as pd
p = pd.read_csv("data/pitch_movement.csv")
arsenal = (p.groupby(["pitcher", "pitch_type"], as_index=False)
.agg(n=("hb_in", "size"), velo=("release_speed", "mean"), hb=("hb_in", "mean"), ivb=("ivb_in", "mean"))
.assign(usage=lambda d: d.n / d.groupby("pitcher").n.transform("sum"))
.round(2))
print(arsenal.head(10).to_string(index=False))
Output, from running this code on the dataset when the page was built
pitcher pitch_type n velo hb ivb usage
Caden Okonkwo-Reyes CH 112 84.48 14.54 7.35 0.17
Caden Okonkwo-Reyes FF 270 94.94 9.45 14.39 0.40
Caden Okonkwo-Reyes SI 123 91.98 15.87 9.86 0.18
Caden Okonkwo-Reyes SL 165 86.98 -5.92 1.74 0.25
Felix Dunmarrow CH 100 84.96 13.59 5.84 0.17
Felix Dunmarrow CU 74 78.57 -9.14 -12.68 0.13
Felix Dunmarrow FF 254 94.55 6.47 16.90 0.43
Felix Dunmarrow SL 158 84.94 -5.39 -0.29 0.27
Isaac Vantongeren CH 131 84.65 13.71 5.95 0.21
Isaac Vantongeren FF 325 92.92 4.38 14.10 0.51Which two pitchers throw the most similar slider? Standardize velocity and the two breaks across all sliders, and find the closest pair of pitcher averages.
Average each pitcher's sliders, z-score the three columns, then compute all pairwise distances with scipy.spatial.distance.pdist and take the smallest.
import numpy as np
import pandas as pd
from scipy.spatial.distance import pdist, squareform
p = pd.read_csv("data/pitch_movement.csv").query("pitch_type == 'SL'")
avg = p.groupby("pitcher")[["release_speed", "hb_in", "ivb_in"]].mean()
z = (avg - avg.mean()) / avg.std()
d = pd.DataFrame(squareform(pdist(z)), index=avg.index, columns=avg.index)
np.fill_diagonal(d.values, np.inf)
a, b = np.unravel_index(np.argmin(d.values), d.shape)
print(f"most similar sliders: {d.index[a]} and {d.columns[b]}, distance {d.values[a, b]:.2f}")
print(avg.loc[[d.index[a], d.columns[b]]].round(1).to_string())
Output, from running this code on the dataset when the page was built
most similar sliders: Caden Okonkwo-Reyes and Luis Vantongeren, distance 0.91
release_speed hb_in ivb_in
pitcher
Caden Okonkwo-Reyes 87.0 -5.9 1.7
Luis Vantongeren 86.8 -4.8 2.2Model whiffs: on swings only, fit a logistic regression of whiff on pitch type, velocity, both breaks and location. Hold out 30 percent and compare the log loss with a model that knows only the overall whiff rate.
One-hot the pitch type, describe location with an in-zone flag and how far below two feet the pitch was, and use scikit-learn's train_test_split and log_loss.
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
from sklearn.model_selection import train_test_split
s = pd.read_csv("data/pitch_movement.csv").query("swing == 1")
X = pd.get_dummies(s.pitch_type, prefix="pt").assign(
velo=s.release_speed, hb=s.hb_in, ivb=s.ivb_in,
in_zone=(s.plate_x.abs() <= 0.83) & s.plate_z.between(1.5, 3.5), below=(2.0 - s.plate_z).clip(lower=0)).astype(float)
Xtr, Xte, ytr, yte = train_test_split(X, s.whiff, test_size=0.3, random_state=11, stratify=s.whiff)
m = LogisticRegression(max_iter=2000).fit((Xtr - Xtr.mean()) / Xtr.std(), ytr)
p_model = m.predict_proba((Xte - Xtr.mean()) / Xtr.std())[:, 1]
p_base = np.full(len(yte), ytr.mean())
print(f"held-out log loss: model {log_loss(yte, p_model):.4f}, overall rate {log_loss(yte, p_base):.4f}")
Output, from running this code on the dataset when the page was built
held-out log loss: model 0.6002, overall rate 0.6031
Ask an agent to turn your medium answer into a function that returns the three closest comps for any pitcher and pitch type, with tests. Then ask it to explain, from your hard answer's coefficients, which pitch type and location raise whiff odds most; check the sign of each claim against the fitted model before you accept it.
The Armchair Scout. Every dataset in the practice library names its session the same way.
A short welcome note, a few session picks over two weeks, then only the updates you signed up for. Reply stop any time.