{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "cd16fdae", "metadata": {}, "outputs": [], "source": [ "from __future__ import annotations\n", "import os\n", "from datetime import date\n", "from pathlib import Path\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "from pybaseball import statcast\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.cluster import KMeans\n", "from sklearn.neighbors import NearestNeighbors\n", "\n", "import plotly.express as px\n", "import plotly.graph_objects as go" ] }, { "cell_type": "code", "execution_count": 2, "id": "4c2c0387", "metadata": {}, "outputs": [], "source": [ "pd.set_option(\"display.max_columns\", 200)\n", "CACHE_DIR = Path(\"data/cache\")\n", "CACHE_DIR.mkdir(parents=True, exist_ok=True)\n", "ARTIFACTS_DIR = Path(\"artifacts\")\n", "ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)" ] }, { "cell_type": "code", "execution_count": 3, "id": "11bb8296", "metadata": {}, "outputs": [], "source": [ "def default_window():\n", " \"\"\"Return a recent season window (approx opening day to today).\"\"\"\n", " today = date.today()\n", " start = date(today.year if today.month >= 3 else today.year - 1, 3, 1)\n", " return start.isoformat(), today.isoformat()\n", "\n", "\n", "def cache_path(start: str, end: str) -> Path:\n", " return CACHE_DIR / f\"statcast_{start}_{end}.parquet\"" ] }, { "cell_type": "code", "execution_count": 4, "id": "cc0b9aee", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('2025-03-01', '2025-10-30')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def load_statcast(start_date: str, end_date: str, force: bool = False) -> pd.DataFrame:\n", " \"\"\"Download Statcast data with simple caching to Parquet.\"\"\"\n", " cp = cache_path(start_date, end_date)\n", " if cp.exists() and not force:\n", " print(f\"Loading cached: {cp}\")\n", " return pd.read_parquet(cp)\n", " print(\"Downloading from Statcast (pybaseball)... window size affects duration.\")\n", " df = statcast(start_dt=start_date, end_dt=end_date)\n", " if \"pitch_type\" in df.columns:\n", " df = df[df[\"pitch_type\"].notna()]\n", " df.to_parquet(cp, index=False)\n", " print(f\"Cached to: {cp}\")\n", " return df\n", "\n", "\n", "start_date, end_date = default_window()\n", "start_date, end_date" ] }, { "cell_type": "code", "execution_count": 5, "id": "db1cf23c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading cached: data/cache/statcast_2025-03-01_2025-10-30.parquet\n" ] }, { "data": { "text/plain": [ "((752024, 118),\n", " pitch_type game_date release_speed release_pos_x release_pos_z \\\n", " 0 SL 2025-10-29 86.7 -2.63 5.58 \n", " 1 SL 2025-10-29 87.0 -2.55 5.6 \n", " 2 SL 2025-10-29 87.2 -2.58 5.59 \n", " \n", " player_name batter pitcher events description spin_dir \\\n", " 0 Hoffman, Jeff 606192 656546 strikeout swinging_strike \n", " 1 Hoffman, Jeff 606192 656546 None blocked_ball \n", " 2 Hoffman, Jeff 606192 656546 None ball \n", " \n", " spin_rate_deprecated break_angle_deprecated break_length_deprecated \\\n", " 0 \n", " 1 \n", " 2 \n", " \n", " zone des game_type stand p_throws \\\n", " 0 14 Teoscar Hernández strikes out swinging. W R R \n", " 1 14 Ball In Dirt W R R \n", " 2 14 Ball W R R \n", " \n", " home_team away_team type hit_location bb_type balls strikes game_year \\\n", " 0 LAD TOR S 2 None 2 2 2025 \n", " 1 LAD TOR B None 1 2 2025 \n", " 2 LAD TOR B None 0 2 2025 \n", " \n", " pfx_x pfx_z plate_x plate_z on_3b on_2b on_1b outs_when_up inning \\\n", " 0 0.25 0.13 0.63 1.1 669257 2 9 \n", " 1 0.22 0.07 1.22 0.09 669257 2 9 \n", " 2 0.13 0.2 0.82 1.34 669257 2 9 \n", " \n", " inning_topbot hc_x hc_y tfs_deprecated tfs_zulu_deprecated umpire \\\n", " 0 Bot \n", " 1 Bot \n", " 2 Bot \n", " \n", " sv_id vx0 vy0 vz0 ax ay az \\\n", " 0 7.310843 -126.234364 -4.940849 1.600881 19.561186 -30.09391 \n", " 1 8.567988 -126.485626 -7.272101 1.046798 20.184028 -30.388937 \n", " 2 7.933229 -126.959588 -4.65206 0.265795 19.239855 -29.350702 \n", " \n", " sz_top sz_bot hit_distance_sc launch_speed launch_angle \\\n", " 0 3.43 1.59 \n", " 1 3.35 1.59 \n", " 2 3.37 1.59 \n", " \n", " effective_speed release_spin_rate release_extension game_pk fielder_2 \\\n", " 0 87.8 2674 6.4 813022 672386 \n", " 1 87.6 2811 6.3 813022 672386 \n", " 2 88.4 2804 6.4 813022 672386 \n", " \n", " fielder_3 fielder_4 fielder_5 fielder_6 fielder_7 fielder_8 \\\n", " 0 665489 643396 676391 665926 664702 662139 \n", " 1 665489 643396 676391 665926 664702 662139 \n", " 2 665489 643396 676391 665926 664702 662139 \n", " \n", " fielder_9 release_pos_y estimated_ba_using_speedangle \\\n", " 0 680718 54.08 \n", " 1 680718 54.24 \n", " 2 680718 54.09 \n", " \n", " estimated_woba_using_speedangle woba_value woba_denom babip_value \\\n", " 0 0.0 0.0 1 0 \n", " 1 \n", " 2 \n", " \n", " iso_value launch_speed_angle at_bat_number pitch_number pitch_name \\\n", " 0 0 73 5 Slider \n", " 1 73 4 Slider \n", " 2 73 3 Slider \n", " \n", " home_score away_score bat_score fld_score post_away_score \\\n", " 0 1 6 1 6 6 \n", " 1 1 6 1 6 6 \n", " 2 1 6 1 6 6 \n", " \n", " post_home_score post_bat_score post_fld_score if_fielding_alignment \\\n", " 0 1 1 6 Standard \n", " 1 1 1 6 Standard \n", " 2 1 1 6 Standard \n", " \n", " of_fielding_alignment spin_axis delta_home_win_exp delta_run_exp \\\n", " 0 Standard 89 -0.001 -0.216 \n", " 1 Standard 25 0.0 0.033 \n", " 2 Standard 30 0.0 0.009 \n", " \n", " bat_speed swing_length estimated_slg_using_speedangle \\\n", " 0 66.1 8.5 \n", " 1 \n", " 2 \n", " \n", " delta_pitcher_run_exp hyper_speed home_score_diff bat_score_diff \\\n", " 0 0.216 -5 -5 \n", " 1 -0.033 -5 -5 \n", " 2 -0.009 -5 -5 \n", " \n", " home_win_exp bat_win_exp age_pit_legacy age_bat_legacy age_pit \\\n", " 0 0.001 0.001 32 32 32 \n", " 1 0.001 0.001 32 32 32 \n", " 2 0.001 0.001 32 32 32 \n", " \n", " age_bat n_thruorder_pitcher n_priorpa_thisgame_player_at_bat \\\n", " 0 33 1 3 \n", " 1 33 1 3 \n", " 2 33 1 3 \n", " \n", " pitcher_days_since_prev_game batter_days_since_prev_game \\\n", " 0 \n", " 1 \n", " 2 \n", " \n", " pitcher_days_until_next_game batter_days_until_next_game \\\n", " 0 \n", " 1 \n", " 2 \n", " \n", " api_break_z_with_gravity api_break_x_arm api_break_x_batter_in \\\n", " 0 2.84 -0.25 -0.25 \n", " 1 2.9 -0.22 -0.22 \n", " 2 2.73 -0.13 -0.13 \n", " \n", " arm_angle attack_angle attack_direction swing_path_tilt \\\n", " 0 21.292185 -28.318897 34.361479 \n", " 1 \n", " 2 \n", " \n", " intercept_ball_minus_batter_pos_x_inches \\\n", " 0 43.225687 \n", " 1 \n", " 2 \n", " \n", " intercept_ball_minus_batter_pos_y_inches \n", " 0 45.809537 \n", " 1 \n", " 2 )" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_raw = load_statcast(start_date, end_date, force=False)\n", "df_raw.shape, df_raw.head(3)" ] }, { "cell_type": "code", "execution_count": 6, "id": "55548f5d", "metadata": {}, "outputs": [], "source": [ "INCHES_PER_FOOT = 12.0\n", "\n", "def _safe_rate(num, den):\n", " return np.divide(\n", " num, den, out=np.full_like(num, np.nan, dtype=float), where=den > 0\n", " )\n", "\n", "\n", "def signed_arm_side(hb_in_raw: pd.Series, p_throws: pd.Series) -> pd.Series:\n", " \"\"\"\n", " Convert Statcast pfx_x (catcher-right positive) into 'arm-side positive' for both RHP and LHP.\n", " For RHP: arm-side = +pfx_x.\n", " For LHP: arm-side = -pfx_x (flip sign).\n", " \"\"\"\n", " handed = p_throws.fillna(\"R\").str.upper().str[0]\n", " sign = np.where(handed == \"R\", 1.0, -1.0)\n", " return -hb_in_raw * sign\n", "\n", "\n", "def engineer_pitch_features(df: pd.DataFrame) -> pd.DataFrame:\n", " \"\"\"\n", " Aggregates to (player_name, pitch_type, p_throws) with handedness-aware XY:\n", " X = hb_as_in (Arm-Side + / Glove-Side -), Y = ivb_in (Ride + / Drop -)\n", " Also computes CSW, Whiff, GB, Zone%.\n", " \"\"\"\n", " cols = [\n", " \"pitch_type\",\n", " \"player_name\",\n", " \"game_date\",\n", " \"events\",\n", " \"description\",\n", " \"p_throws\",\n", " \"stand\",\n", " \"release_pos_x\",\n", " \"release_pos_z\",\n", " \"pfx_x\",\n", " \"pfx_z\",\n", " \"release_speed\",\n", " \"release_spin_rate\",\n", " \"plate_x\",\n", " \"plate_z\",\n", " \"zone\",\n", " ]\n", " have = [c for c in cols if c in df.columns]\n", " df = df[have].copy()\n", "\n", " # outcomes\n", " df[\"is_called_strike\"] = (df[\"description\"] == \"called_strike\").astype(int)\n", " df[\"is_swing\"] = (\n", " df[\"description\"]\n", " .isin([\"swinging_strike\", \"swinging_strike_blocked\", \"foul\", \"hit_into_play\"])\n", " .astype(int)\n", " )\n", " df[\"is_whiff\"] = (\n", " df[\"description\"]\n", " .isin([\"swinging_strike\", \"swinging_strike_blocked\"])\n", " .astype(int)\n", " )\n", " df[\"is_ball\"] = (df[\"description\"] == \"ball\").astype(int)\n", " df[\"is_in_play\"] = (df[\"description\"] == \"hit_into_play\").astype(int)\n", " # quick GB proxy (refine later with launch angle)\n", " df[\"is_gb\"] = (\n", " df[\"events\"]\n", " .isin([\"groundout\", \"field_error\", \"single\", \"double\", \"triple\"])\n", " .astype(int)\n", " )\n", "\n", " # movement proxies in inches\n", " df[\"hb_in_raw\"] = df[\"pfx_x\"] * INCHES_PER_FOOT # + = break to catcher's right\n", " df[\"ivb_in\"] = df[\"pfx_z\"] * INCHES_PER_FOOT # + = ride, − = drop\n", " df[\"hb_as_in\"] = signed_arm_side(\n", " df[\"hb_in_raw\"], df.get(\"p_throws\")\n", " ) # + = arm-side, − = glove-side\n", "\n", " grp = df.groupby([\"player_name\", \"pitch_type\", \"p_throws\"], as_index=False)\n", " agg = grp.agg(\n", " n=(\"pitch_type\", \"size\"),\n", " velo=(\"release_speed\", \"mean\"),\n", " spin=(\"release_spin_rate\", \"mean\"),\n", " ivb_in=(\"ivb_in\", \"mean\"),\n", " hb_as_in=(\"hb_as_in\", \"mean\"),\n", " rel_height=(\"release_pos_z\", \"mean\"),\n", " rel_side=(\"release_pos_x\", \"mean\"),\n", " cs=(\"is_called_strike\", \"sum\"),\n", " swings=(\"is_swing\", \"sum\"),\n", " whiffs=(\"is_whiff\", \"sum\"),\n", " inplay=(\"is_in_play\", \"sum\"),\n", " gb=(\"is_gb\", \"sum\"),\n", " )\n", "\n", " agg[\"csw\"] = _safe_rate(agg[\"cs\"] + agg[\"whiffs\"], agg[\"n\"])\n", " agg[\"whiff_rate\"] = _safe_rate(agg[\"whiffs\"], agg[\"swings\"])\n", " agg[\"gb_rate\"] = _safe_rate(agg[\"gb\"], agg[\"inplay\"])\n", " # rough in-zone proxy\n", " agg[\"zone_pct\"] = _safe_rate(agg[\"cs\"] + agg[\"inplay\"], agg[\"n\"])\n", "\n", " keep = [\n", " \"player_name\",\n", " \"pitch_type\",\n", " \"p_throws\",\n", " \"n\",\n", " \"velo\",\n", " \"spin\",\n", " \"ivb_in\",\n", " \"hb_as_in\",\n", " \"rel_height\",\n", " \"rel_side\",\n", " \"csw\",\n", " \"whiff_rate\",\n", " \"gb_rate\",\n", " \"zone_pct\",\n", " ]\n", " out = agg[keep].dropna(subset=[\"velo\", \"ivb_in\", \"hb_as_in\"])\n", " return out" ] }, { "cell_type": "code", "execution_count": 7, "id": "bba4c7b2", "metadata": {}, "outputs": [], "source": [ "ARCH_FEATURES = [\n", " \"velo\",\n", " \"ivb_in\",\n", " \"hb_as_in\",\n", " \"rel_height\",\n", " \"rel_side\",\n", " \"spin\",\n", " \"csw\",\n", " \"whiff_rate\",\n", " \"gb_rate\",\n", " \"zone_pct\",\n", "]\n", "\n", "\n", "def fit_kmeans(df_feat: pd.DataFrame, k: int = 8, random_state: int = 42):\n", " df = df_feat.dropna(subset=ARCH_FEATURES).copy()\n", " X = df[ARCH_FEATURES].values\n", " scaler = StandardScaler()\n", " Xs = scaler.fit_transform(X)\n", " km = KMeans(n_clusters=k, n_init=20, random_state=random_state)\n", " labels = km.fit_predict(Xs)\n", " df[\"cluster\"] = labels\n", "\n", " nn = NearestNeighbors(n_neighbors=6, metric=\"euclidean\")\n", " nn.fit(Xs)\n", " return df, scaler, km, nn" ] }, { "cell_type": "code", "execution_count": 8, "id": "f3210171", "metadata": {}, "outputs": [], "source": [ "# --- Replacement: cluster tags using XY + functional flavor ---\n", "def _mag_label(v, q25, q75, small=\"Subtle\", mid=\"Moderate\", big=\"Heavy\"):\n", " if v >= q75:\n", " return big\n", " if v <= q25:\n", " return small\n", " return mid\n", "\n", "\n", "def _side_label(hb_as):\n", " return \"Arm-Side\" if hb_as >= 0 else \"Glove-Side\"\n", "\n", "\n", "def _vert_label(ivb):\n", " return \"Ride\" if ivb >= 0 else \"Drop\"\n", "\n", "\n", "def xy_cluster_tags(df_with_clusters: pd.DataFrame) -> dict[int, str]:\n", " \"\"\"\n", " Returns {cluster_id: label} like:\n", " Slider: Glove-Side • Moderate Sweep, Subtle Ride • Strike-Throwing\n", " Changeup: Arm-Side • Subtle Ride, Heavy Run • Grounder-First\n", " \"\"\"\n", " df = df_with_clusters.copy()\n", "\n", " q_abs_ivb25 = np.nanquantile(np.abs(df[\"ivb_in\"]), 0.25)\n", " q_abs_ivb75 = np.nanquantile(np.abs(df[\"ivb_in\"]), 0.75)\n", " q_abs_hb25 = np.nanquantile(np.abs(df[\"hb_as_in\"]), 0.25)\n", " q_abs_hb75 = np.nanquantile(np.abs(df[\"hb_as_in\"]), 0.75)\n", "\n", " q_wh75 = np.nanquantile(df[\"whiff_rate\"], 0.75)\n", " q_gb75 = np.nanquantile(df[\"gb_rate\"], 0.75)\n", " q_zn75 = np.nanquantile(df[\"zone_pct\"], 0.75)\n", "\n", " q_wh50 = np.nanquantile(df[\"whiff_rate\"], 0.50)\n", " q_gb50 = np.nanquantile(df[\"gb_rate\"], 0.50)\n", " q_zn50 = np.nanquantile(df[\"zone_pct\"], 0.50)\n", "\n", " tags = {}\n", " for c, sub in df.groupby(\"cluster\"):\n", " row = sub.mean(numeric_only=True)\n", " dom_pt = (\n", " sub[\"pitch_type\"].mode().iloc[0]\n", " if not sub[\"pitch_type\"].mode().empty\n", " else \"Pitch\"\n", " )\n", "\n", " side = _side_label(row[\"hb_as_in\"])\n", " vert = _vert_label(row[\"ivb_in\"])\n", "\n", " mag_side = _mag_label(\n", " abs(row[\"hb_as_in\"]), q_abs_hb25, q_abs_hb75\n", " ) # \"Subtle/Moderate/Heavy\"\n", " mag_vert = _mag_label(abs(row[\"ivb_in\"]), q_abs_ivb25, q_abs_ivb75)\n", "\n", " # Functional flavor\n", " flavor = []\n", " if row[\"whiff_rate\"] >= q_wh75:\n", " flavor.append(\"Whiff-First\")\n", " if row[\"gb_rate\"] >= q_gb75:\n", " flavor.append(\"Grounder-First\")\n", " if row[\"zone_pct\"] >= q_zn75:\n", " flavor.append(\"Strike-Throwing\")\n", " if not flavor:\n", " diffs = {\n", " \"Whiff-First\": row[\"whiff_rate\"] - q_wh50,\n", " \"Grounder-First\": row[\"gb_rate\"] - q_gb50,\n", " \"Strike-Throwing\": row[\"zone_pct\"] - q_zn50,\n", " }\n", " flavor.append(max(diffs, key=diffs.get))\n", "\n", " # Compose label: \": , \"\n", " # Examples:\n", " # \"Slider: Glove-Side • Heavy Sweep, Subtle Drop • Strike-Throwing\"\n", " # \"Changeup: Arm-Side • Moderate Run, Subtle Drop • Grounder-First\"\n", " side_noun = \"Sweep\" if side == \"Glove-Side\" else \"Run\"\n", " vert_noun = \"Ride\" if vert == \"Ride\" else \"Drop\"\n", " shape = f\"{side} • {mag_side} {side_noun}, {mag_vert} {vert_noun}\"\n", " tags[c] = f\"{dom_pt}: {shape} • \" + \" / \".join(flavor)\n", "\n", " return tags" ] }, { "cell_type": "code", "execution_count": 9, "id": "74c47e88", "metadata": {}, "outputs": [], "source": [ "def nearest_comps(\n", " row: pd.Series, df_fit: pd.DataFrame, scaler, nn, within_pitch_type=True, k=6\n", "):\n", " X_all = df_fit[ARCH_FEATURES].values\n", " xq = scaler.transform(row[ARCH_FEATURES].values.reshape(1, -1))\n", " dists, idxs = nn.kneighbors(xq, n_neighbors=k)\n", " comps = df_fit.iloc[idxs[0]].copy()\n", " if within_pitch_type:\n", " comps = comps[comps[\"pitch_type\"] == row[\"pitch_type\"]]\n", " return comps[\n", " [\n", " \"player_name\",\n", " \"pitch_type\",\n", " \"p_throws\",\n", " \"velo\",\n", " \"ivb_in\",\n", " \"hb_as_in\",\n", " \"whiff_rate\",\n", " \"gb_rate\",\n", " \"cluster_name\",\n", " ]\n", " ].head(k - 1)" ] }, { "cell_type": "code", "execution_count": 10, "id": "ebcbaea2", "metadata": {}, "outputs": [], "source": [ "def movement_scatter_xy(\n", " df: pd.DataFrame, color=\"pitch_type\", facet_by_handedness=False\n", "):\n", " df_plot = df.copy()\n", " if facet_by_handedness:\n", " fig = px.scatter(\n", " df_plot,\n", " x=\"hb_as_in\",\n", " y=\"ivb_in\",\n", " color=color,\n", " facet_col=\"p_throws\",\n", " hover_data=[\n", " \"player_name\",\n", " \"pitch_type\",\n", " \"p_throws\",\n", " \"velo\",\n", " \"whiff_rate\",\n", " \"gb_rate\",\n", " \"csw\",\n", " ],\n", " )\n", " else:\n", " fig = px.scatter(\n", " df_plot,\n", " x=\"hb_as_in\",\n", " y=\"ivb_in\",\n", " color=color,\n", " hover_data=[\n", " \"player_name\",\n", " \"pitch_type\",\n", " \"p_throws\",\n", " \"velo\",\n", " \"whiff_rate\",\n", " \"gb_rate\",\n", " \"csw\",\n", " ],\n", " )\n", "\n", " fig.update_layout(\n", " xaxis_title=\"Horizontal: Arm-Side (+) | Glove-Side (−)\",\n", " yaxis_title=\"Vertical: Ride (+) | Drop (−)\",\n", " legend_title_text=color,\n", " )\n", " # quadrant guides (for every subplot if faceted)\n", " for ax in fig.select_yaxes():\n", " fig.add_hline(\n", " y=0,\n", " line_dash=\"dot\",\n", " row=ax.anchor.split(\"y\")[-1] if facet_by_handedness else None,\n", " col=None,\n", " )\n", " for ax in fig.select_xaxes():\n", " fig.add_vline(\n", " x=0,\n", " line_dash=\"dot\",\n", " row=ax.anchor.split(\"x\")[-1] if facet_by_handedness else None,\n", " col=None,\n", " )\n", "\n", " # helpful annotations (single-plot case)\n", " if not facet_by_handedness:\n", " fig.add_annotation(\n", " x=1,\n", " y=0,\n", " xref=\"paper\",\n", " yref=\"paper\",\n", " xanchor=\"left\",\n", " yanchor=\"top\",\n", " text=\"AS (+)\",\n", " showarrow=False,\n", " font=dict(size=10),\n", " )\n", " fig.add_annotation(\n", " x=0,\n", " y=0,\n", " xref=\"paper\",\n", " yref=\"paper\",\n", " xanchor=\"right\",\n", " yanchor=\"top\",\n", " text=\"GS (−)\",\n", " showarrow=False,\n", " font=dict(size=10),\n", " )\n", " fig.add_annotation(\n", " x=0.5,\n", " y=1,\n", " xref=\"paper\",\n", " yref=\"paper\",\n", " xanchor=\"center\",\n", " yanchor=\"bottom\",\n", " text=\"Ride (+)\",\n", " showarrow=False,\n", " font=dict(size=10),\n", " )\n", " fig.add_annotation(\n", " x=0.5,\n", " y=0,\n", " xref=\"paper\",\n", " yref=\"paper\",\n", " xanchor=\"center\",\n", " yanchor=\"top\",\n", " text=\"Drop (−)\",\n", " showarrow=False,\n", " font=dict(size=10),\n", " )\n", " return fig\n", "\n", "\n", "def radar_quality(row: pd.Series):\n", " cats = [\"csw\", \"whiff_rate\", \"gb_rate\", \"zone_pct\"]\n", " vals = [row[c] for c in cats]\n", " fig = go.Figure(data=go.Scatterpolar(r=vals, theta=cats, fill=\"toself\"))\n", " fig.update_layout(\n", " polar=dict(radialaxis=dict(visible=True, range=[0, 1])), showlegend=False\n", " )\n", " return fig\n", "\n", "\n", "def xy_blurb(row: pd.Series) -> str:\n", " side = \"Arm-Side\" if row[\"hb_as_in\"] >= 0 else \"Glove-Side\"\n", " vert = \"Ride\" if row[\"ivb_in\"] >= 0 else \"Drop\"\n", " return (\n", " f\"{row['pitch_type']} ({row['p_throws']}): {side}, {vert}. \"\n", " f\"Velo {row['velo']:.1f} mph | AS {row['hb_as_in']:.1f}\\\" | \"\n", " f\"Ride {row['ivb_in']:.1f}\\\" | CSW {row['csw']:.2f} | \"\n", " f\"Whiff {row['whiff_rate']:.2f} | GB {row['gb_rate']:.2f} | Zone {row['zone_pct']:.2f}\"\n", " )" ] }, { "cell_type": "code", "execution_count": 11, "id": "a3d31501", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading cached: data/cache/statcast_2025-03-01_2025-10-30.parquet\n" ] } ], "source": [ "start_date, end_date = default_window()\n", "# For a fast first run, you can narrow the window, e.g.:\n", "# start_date, end_date = \"2024-04-01\", \"2024-04-07\"\n", "\n", "df_raw = load_statcast(start_date, end_date, force=False)\n", "df_feat = engineer_pitch_features(df_raw)\n", "\n", "df_fit, scaler, km, nn = fit_kmeans(df_feat, k=8)\n", "cluster_names = xy_cluster_tags(df_fit)\n", "df_fit[\"cluster_name\"] = df_fit[\"cluster\"].map(cluster_names)\n", "\n", "# Save artifacts\n", "df_feat.to_parquet(ARTIFACTS_DIR / \"pitch_features.parquet\", index=False)\n", "df_fit.to_parquet(ARTIFACTS_DIR / \"pitch_features_clusters.parquet\", index=False)" ] }, { "cell_type": "code", "execution_count": 48, "id": "a8b637de", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Williams, Devin'" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pitchers = sorted(df_fit[\"player_name\"].unique().tolist())\n", "SELECTED_PITCHER = pitchers[1041] if pitchers else None\n", "SELECTED_PITCHER" ] }, { "cell_type": "code", "execution_count": 49, "id": "cbcd0c40", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
player_namepitch_typep_throwsnvelospinivb_inhb_as_inrel_heightrel_sidecswwhiff_rategb_ratezone_pctclustercluster_name
4547Williams, DevinCHR63683.7240572734.992126-4.71849119.2267925.147233-2.5115090.3207550.3567250.2500000.2798741SL: Glove-Side • Moderate Sweep, Subtle Drop •...
4548Williams, DevinFCR689.2666672411.83333313.26-1.285.521667-2.380.5000000.6666670.0000000.3333331SL: Glove-Side • Moderate Sweep, Subtle Drop •...
4549Williams, DevinFFR58494.1239732365.51027415.68054812.8344525.391387-2.3465920.2893840.3333330.3859650.2482887FF: Arm-Side • Moderate Run, Moderate Ride • S...
\n", "
" ], "text/plain": [ " player_name pitch_type p_throws n velo spin \\\n", "4547 Williams, Devin CH R 636 83.724057 2734.992126 \n", "4548 Williams, Devin FC R 6 89.266667 2411.833333 \n", "4549 Williams, Devin FF R 584 94.123973 2365.510274 \n", "\n", " ivb_in hb_as_in rel_height rel_side csw whiff_rate \\\n", "4547 -4.718491 19.226792 5.147233 -2.511509 0.320755 0.356725 \n", "4548 13.26 -1.28 5.521667 -2.38 0.500000 0.666667 \n", "4549 15.680548 12.834452 5.391387 -2.346592 0.289384 0.333333 \n", "\n", " gb_rate zone_pct cluster \\\n", "4547 0.250000 0.279874 1 \n", "4548 0.000000 0.333333 1 \n", "4549 0.385965 0.248288 7 \n", "\n", " cluster_name \n", "4547 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "4548 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "4549 FF: Arm-Side • Moderate Run, Moderate Ride • S... " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "customdata": [ [ "Williams, Devin", "CH", "R", 83.72405660377359, 0.3567251461988304, 0.25, 0.32075471698113206 ] ], "hovertemplate": "pitch_type=%{customdata[1]}
hb_as_in=%{x}
ivb_in=%{y}
player_name=%{customdata[0]}
p_throws=%{customdata[2]}
velo=%{customdata[3]}
whiff_rate=%{customdata[4]}
gb_rate=%{customdata[5]}
csw=%{customdata[6]}", "legendgroup": "CH", "marker": { "color": "#636efa", "symbol": "circle" }, "mode": "markers", "name": "CH", "orientation": "v", "showlegend": true, "type": "scatter", "x": [ 19.22679245283019 ], "xaxis": "x", "y": [ -4.718490566037736 ], "yaxis": "y" }, { "customdata": [ [ "Williams, Devin", "FC", "R", 89.26666666666667, 0.6666666666666666, 0, 0.5 ] ], "hovertemplate": "pitch_type=%{customdata[1]}
hb_as_in=%{x}
ivb_in=%{y}
player_name=%{customdata[0]}
p_throws=%{customdata[2]}
velo=%{customdata[3]}
whiff_rate=%{customdata[4]}
gb_rate=%{customdata[5]}
csw=%{customdata[6]}", "legendgroup": "FC", "marker": { "color": "#EF553B", "symbol": "circle" }, "mode": "markers", "name": "FC", "orientation": "v", "showlegend": true, "type": "scatter", "x": [ -1.28 ], "xaxis": "x", "y": [ 13.26 ], "yaxis": "y" }, { "customdata": [ [ "Williams, Devin", "FF", "R", 94.12397260273973, 0.3333333333333333, 0.38596491228070173, 0.2893835616438356 ] ], "hovertemplate": "pitch_type=%{customdata[1]}
hb_as_in=%{x}
ivb_in=%{y}
player_name=%{customdata[0]}
p_throws=%{customdata[2]}
velo=%{customdata[3]}
whiff_rate=%{customdata[4]}
gb_rate=%{customdata[5]}
csw=%{customdata[6]}", "legendgroup": "FF", "marker": { "color": "#00cc96", "symbol": "circle" }, "mode": "markers", "name": "FF", "orientation": "v", "showlegend": true, "type": "scatter", "x": [ 12.83445205479452 ], "xaxis": "x", "y": [ 15.68054794520548 ], "yaxis": "y" } ], "layout": { "annotations": [ { "font": { "size": 10 }, "showarrow": false, "text": "AS (+)", "x": 1, "xanchor": "left", "xref": "paper", "y": 0, "yanchor": "top", "yref": "paper" }, { "font": { "size": 10 }, "showarrow": false, "text": "GS (−)", "x": 0, "xanchor": "right", "xref": "paper", "y": 0, "yanchor": "top", "yref": "paper" }, { "font": { "size": 10 }, "showarrow": false, "text": "Ride (+)", "x": 0.5, "xanchor": "center", "xref": "paper", "y": 1, "yanchor": "bottom", "yref": "paper" }, { "font": { "size": 10 }, "showarrow": false, "text": "Drop (−)", "x": 0.5, "xanchor": "center", "xref": "paper", "y": 0, "yanchor": "top", "yref": "paper" } ], "legend": { "title": { "text": "pitch_type" }, "tracegroupgap": 0 }, "margin": { "t": 60 }, "shapes": [ { "line": { "dash": "dot" }, "type": "line", "x0": 0, "x1": 1, "xref": "x domain", "y0": 0, "y1": 0, "yref": "y" }, { "line": { "dash": "dot" }, "type": "line", "x0": 0, "x1": 0, "xref": "x", "y0": 0, "y1": 1, "yref": "y domain" } ], "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmap" } ], "heatmapgl": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmapgl" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "sequentialminus": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } }, "xaxis": { "anchor": "y", "domain": [ 0, 1 ], "title": { "text": "Horizontal: Arm-Side (+) | Glove-Side (−)" } }, "yaxis": { "anchor": "x", "domain": [ 0, 1 ], "title": { "text": "Vertical: Ride (+) | Drop (−)" } } } }, "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "fill": "toself", "r": [ 0.32075471698113206, 0.3567251461988304, 0.25, 0.279874213836478 ], "theta": [ "csw", "whiff_rate", "gb_rate", "zone_pct" ], "type": "scatterpolar" } ], "layout": { "polar": { "radialaxis": { "range": [ 0, 1 ], "visible": true } }, "showlegend": false, "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmap" } ], "heatmapgl": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmapgl" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "sequentialminus": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } } } }, "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "CH (R): Arm-Side, Drop. Velo 83.7 mph | AS 19.2\" | Ride -4.7\" | CSW 0.32 | Whiff 0.36 | GB 0.25 | Zone 0.28\n" ] }, { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "fill": "toself", "r": [ 0.5, 0.6666666666666666, 0, 0.3333333333333333 ], "theta": [ "csw", "whiff_rate", "gb_rate", "zone_pct" ], "type": "scatterpolar" } ], "layout": { "polar": { "radialaxis": { "range": [ 0, 1 ], "visible": true } }, "showlegend": false, "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmap" } ], "heatmapgl": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmapgl" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "sequentialminus": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } } } }, "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "FC (R): Glove-Side, Ride. Velo 89.3 mph | AS -1.3\" | Ride 13.3\" | CSW 0.50 | Whiff 0.67 | GB 0.00 | Zone 0.33\n" ] }, { "data": { "application/vnd.plotly.v1+json": { "config": { "plotlyServerURL": "https://plot.ly" }, "data": [ { "fill": "toself", "r": [ 0.2893835616438356, 0.3333333333333333, 0.38596491228070173, 0.2482876712328767 ], "theta": [ "csw", "whiff_rate", "gb_rate", "zone_pct" ], "type": "scatterpolar" } ], "layout": { "polar": { "radialaxis": { "range": [ 0, 1 ], "visible": true } }, "showlegend": false, "template": { "data": { "bar": [ { "error_x": { "color": "#2a3f5f" }, "error_y": { "color": "#2a3f5f" }, "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "bar" } ], "barpolar": [ { "marker": { "line": { "color": "#E5ECF6", "width": 0.5 }, "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "barpolar" } ], "carpet": [ { "aaxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "baxis": { "endlinecolor": "#2a3f5f", "gridcolor": "white", "linecolor": "white", "minorgridcolor": "white", "startlinecolor": "#2a3f5f" }, "type": "carpet" } ], "choropleth": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "choropleth" } ], "contour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "contour" } ], "contourcarpet": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "contourcarpet" } ], "heatmap": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmap" } ], "heatmapgl": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "heatmapgl" } ], "histogram": [ { "marker": { "pattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 } }, "type": "histogram" } ], "histogram2d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2d" } ], "histogram2dcontour": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "histogram2dcontour" } ], "mesh3d": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "type": "mesh3d" } ], "parcoords": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "parcoords" } ], "pie": [ { "automargin": true, "type": "pie" } ], "scatter": [ { "fillpattern": { "fillmode": "overlay", "size": 10, "solidity": 0.2 }, "type": "scatter" } ], "scatter3d": [ { "line": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatter3d" } ], "scattercarpet": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattercarpet" } ], "scattergeo": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergeo" } ], "scattergl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattergl" } ], "scattermapbox": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scattermapbox" } ], "scatterpolar": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolar" } ], "scatterpolargl": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterpolargl" } ], "scatterternary": [ { "marker": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "type": "scatterternary" } ], "surface": [ { "colorbar": { "outlinewidth": 0, "ticks": "" }, "colorscale": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "type": "surface" } ], "table": [ { "cells": { "fill": { "color": "#EBF0F8" }, "line": { "color": "white" } }, "header": { "fill": { "color": "#C8D4E3" }, "line": { "color": "white" } }, "type": "table" } ] }, "layout": { "annotationdefaults": { "arrowcolor": "#2a3f5f", "arrowhead": 0, "arrowwidth": 1 }, "autotypenumbers": "strict", "coloraxis": { "colorbar": { "outlinewidth": 0, "ticks": "" } }, "colorscale": { "diverging": [ [ 0, "#8e0152" ], [ 0.1, "#c51b7d" ], [ 0.2, "#de77ae" ], [ 0.3, "#f1b6da" ], [ 0.4, "#fde0ef" ], [ 0.5, "#f7f7f7" ], [ 0.6, "#e6f5d0" ], [ 0.7, "#b8e186" ], [ 0.8, "#7fbc41" ], [ 0.9, "#4d9221" ], [ 1, "#276419" ] ], "sequential": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ], "sequentialminus": [ [ 0, "#0d0887" ], [ 0.1111111111111111, "#46039f" ], [ 0.2222222222222222, "#7201a8" ], [ 0.3333333333333333, "#9c179e" ], [ 0.4444444444444444, "#bd3786" ], [ 0.5555555555555556, "#d8576b" ], [ 0.6666666666666666, "#ed7953" ], [ 0.7777777777777778, "#fb9f3a" ], [ 0.8888888888888888, "#fdca26" ], [ 1, "#f0f921" ] ] }, "colorway": [ "#636efa", "#EF553B", "#00cc96", "#ab63fa", "#FFA15A", "#19d3f3", "#FF6692", "#B6E880", "#FF97FF", "#FECB52" ], "font": { "color": "#2a3f5f" }, "geo": { "bgcolor": "white", "lakecolor": "white", "landcolor": "#E5ECF6", "showlakes": true, "showland": true, "subunitcolor": "white" }, "hoverlabel": { "align": "left" }, "hovermode": "closest", "mapbox": { "style": "light" }, "paper_bgcolor": "white", "plot_bgcolor": "#E5ECF6", "polar": { "angularaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "radialaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "scene": { "xaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "yaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" }, "zaxis": { "backgroundcolor": "#E5ECF6", "gridcolor": "white", "gridwidth": 2, "linecolor": "white", "showbackground": true, "ticks": "", "zerolinecolor": "white" } }, "shapedefaults": { "line": { "color": "#2a3f5f" } }, "ternary": { "aaxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "baxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" }, "bgcolor": "#E5ECF6", "caxis": { "gridcolor": "white", "linecolor": "white", "ticks": "" } }, "title": { "x": 0.05 }, "xaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 }, "yaxis": { "automargin": true, "gridcolor": "white", "linecolor": "white", "ticks": "", "title": { "standoff": 15 }, "zerolinecolor": "white", "zerolinewidth": 2 } } } } }, "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "FF (R): Arm-Side, Ride. Velo 94.1 mph | AS 12.8\" | Ride 15.7\" | CSW 0.29 | Whiff 0.33 | GB 0.39 | Zone 0.25\n" ] } ], "source": [ "if SELECTED_PITCHER:\n", " df_p = df_fit[df_fit[\"player_name\"] == SELECTED_PITCHER].sort_values(\"pitch_type\")\n", " try:\n", " display(df_p)\n", " except NameError:\n", " print(df_p)\n", "\n", " try:\n", " display(movement_scatter_xy(df_p, color=\"pitch_type\"))\n", " except NameError:\n", " movement_scatter_xy(df_p, color=\"pitch_type\").show()\n", "\n", " for _, row in df_p.iterrows():\n", " try:\n", " display(radar_quality(row))\n", " except NameError:\n", " radar_quality(row).show()\n", " print(xy_blurb(row))\n", "else:\n", " print(\"No pitchers found. Try a different date window.\")" ] }, { "cell_type": "code", "execution_count": 50, "id": "b4349e0e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Nearest comps — CH (SL: Glove-Side • Moderate Sweep, Subtle Drop • Whiff-First):\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
player_namepitch_typep_throwsveloivb_inhb_as_inwhiff_rategb_ratecluster_name
4547Williams, DevinCHR83.724057-4.71849119.2267920.3567250.250000SL: Glove-Side • Moderate Sweep, Subtle Drop •...
4685Yoho, CraigCHR77.122892-2.94939817.9985540.2894740.181818CH: Arm-Side • Moderate Run, Moderate Ride • W...
3773Seabold, ConnorCHR81.337931-2.69586216.1213790.2592590.285714CH: Arm-Side • Moderate Run, Moderate Ride • W...
4530Whitlock, GarrettCHR84.3065130.17747117.0170110.3092110.361702CH: Arm-Side • Moderate Run, Moderate Ride • W...
1768Henderson, LoganCHR81.8672225.48866718.7006670.2528740.250000CH: Arm-Side • Moderate Run, Moderate Ride • W...
\n", "
" ], "text/plain": [ " player_name pitch_type p_throws velo ivb_in hb_as_in \\\n", "4547 Williams, Devin CH R 83.724057 -4.718491 19.226792 \n", "4685 Yoho, Craig CH R 77.122892 -2.949398 17.998554 \n", "3773 Seabold, Connor CH R 81.337931 -2.695862 16.121379 \n", "4530 Whitlock, Garrett CH R 84.306513 0.177471 17.017011 \n", "1768 Henderson, Logan CH R 81.867222 5.488667 18.700667 \n", "\n", " whiff_rate gb_rate cluster_name \n", "4547 0.356725 0.250000 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "4685 0.289474 0.181818 CH: Arm-Side • Moderate Run, Moderate Ride • W... \n", "3773 0.259259 0.285714 CH: Arm-Side • Moderate Run, Moderate Ride • W... \n", "4530 0.309211 0.361702 CH: Arm-Side • Moderate Run, Moderate Ride • W... \n", "1768 0.252874 0.250000 CH: Arm-Side • Moderate Run, Moderate Ride • W... " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Nearest comps — FC (SL: Glove-Side • Moderate Sweep, Subtle Drop • Whiff-First):\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
player_namepitch_typep_throwsveloivb_inhb_as_inwhiff_rategb_ratecluster_name
4548Williams, DevinFCR89.26666713.26-1.280.6666670.0SL: Glove-Side • Moderate Sweep, Subtle Drop •...
3116Overton, ConnorFCR88.6857148.640.3771430.5000000.0SL: Glove-Side • Moderate Sweep, Subtle Drop •...
471Boyer, LoganFCR90.7416674.76-7.160.8000000.0SL: Glove-Side • Moderate Sweep, Subtle Drop •...
2360Liranzo, JesúsFCR86.061.68-5.1840.5000000.0SL: Glove-Side • Moderate Sweep, Subtle Drop •...
\n", "
" ], "text/plain": [ " player_name pitch_type p_throws velo ivb_in hb_as_in \\\n", "4548 Williams, Devin FC R 89.266667 13.26 -1.28 \n", "3116 Overton, Connor FC R 88.685714 8.64 0.377143 \n", "471 Boyer, Logan FC R 90.741667 4.76 -7.16 \n", "2360 Liranzo, Jesús FC R 86.06 1.68 -5.184 \n", "\n", " whiff_rate gb_rate cluster_name \n", "4548 0.666667 0.0 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "3116 0.500000 0.0 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "471 0.800000 0.0 SL: Glove-Side • Moderate Sweep, Subtle Drop •... \n", "2360 0.500000 0.0 SL: Glove-Side • Moderate Sweep, Subtle Drop •... " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Nearest comps — FF (FF: Arm-Side • Moderate Run, Moderate Ride • Strike-Throwing):\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
player_namepitch_typep_throwsveloivb_inhb_as_inwhiff_rategb_ratecluster_name
4549Williams, DevinFFR94.12397315.68054812.8344520.3333330.385965FF: Arm-Side • Moderate Run, Moderate Ride • S...
4090Strzelecki, PeterFFR93.96060615.1163646.8836360.3157890.333333FF: Arm-Side • Moderate Run, Moderate Ride • S...
14Abreu, BryanFFR97.28225516.6294536.5170150.3445690.304348FF: Arm-Side • Moderate Run, Moderate Ride • S...
2620McCullers Jr., LanceFFR91.78840611.8069577.4904350.3750000.444444FF: Arm-Side • Moderate Run, Moderate Ride • S...
4506Wheeler, ZackFFR96.00818914.72913310.0023120.2701690.253731FF: Arm-Side • Moderate Run, Moderate Ride • S...
\n", "
" ], "text/plain": [ " player_name pitch_type p_throws velo ivb_in \\\n", "4549 Williams, Devin FF R 94.123973 15.680548 \n", "4090 Strzelecki, Peter FF R 93.960606 15.116364 \n", "14 Abreu, Bryan FF R 97.282255 16.629453 \n", "2620 McCullers Jr., Lance FF R 91.788406 11.806957 \n", "4506 Wheeler, Zack FF R 96.008189 14.729133 \n", "\n", " hb_as_in whiff_rate gb_rate \\\n", "4549 12.834452 0.333333 0.385965 \n", "4090 6.883636 0.315789 0.333333 \n", "14 6.517015 0.344569 0.304348 \n", "2620 7.490435 0.375000 0.444444 \n", "4506 10.002312 0.270169 0.253731 \n", "\n", " cluster_name \n", "4549 FF: Arm-Side • Moderate Run, Moderate Ride • S... \n", "4090 FF: Arm-Side • Moderate Run, Moderate Ride • S... \n", "14 FF: Arm-Side • Moderate Run, Moderate Ride • S... \n", "2620 FF: Arm-Side • Moderate Run, Moderate Ride • S... \n", "4506 FF: Arm-Side • Moderate Run, Moderate Ride • S... " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "if SELECTED_PITCHER:\n", " for _, row in df_p.iterrows():\n", " comps = nearest_comps(row, df_fit, scaler, nn, within_pitch_type=True, k=6)\n", " print(f\"\\nNearest comps — {row['pitch_type']} ({row['cluster_name']}):\")\n", " try:\n", " display(comps)\n", " except NameError:\n", " print(comps.to_string(index=False))" ] } ], "metadata": { "kernelspec": { "display_name": "base", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 5 }