Pitch Predict — Part 1. Using Machine Learning to Predict the…
By: Josh Mancuso
Tags: Baseball, Data Science, Pandas, Machine Learning, Python

Using Machine Learning to Predict the Next Pitch

This is part 1 of 3 in a series of posts covering the work from a recent Data Science project at Lambda School. The project github repo can be found here. To gain some experience with Plotly Dash, I also created a dashboard app, here. Part 2 of this series can be found here, and part 3 here.

Introduction

A general description of the project and our goals

The task of hitting a baseball at the major league level is extremely difficult. Consider the benchmark for a great hitter is a batting avg of .300 — that’s only a 30% success rate. If the batter knew what pitch the pitcher was likely to throw next, his chances of success could improve significantly. Are pitchers more predictable than batters (or humans in general) realize? For a collaborative Data Science group project, based on data which encapsulates a pitcher’s prior tendencies, batter’s prior tendencies, game state, the last n-chronological pitches, and pitcher-batter prior matchup history, we set out to use machine learning models to attempt to answer that question.

This project was intended to form the foundation for further study, and potentially, an application which combines the pitch prediction functionality with real-time information about the current game, pitcher, and batter presented within an attractive user interface. Design decisions were made with this eventuality in mind, and many components were designed to be generalized and scaled with elastic resources.

Data Gathering

An explanation of where our data is from, its high level characteristics, how we store and access it, and how it is subdivided

Pitch data was collected from Statcast via the pybaseball package. Pybaseball’s statcast class allows us to query pitches by date range. After identifying the start and end dates of the 2017, 2018, and 2019* (until 8/31/19) regular seasons, we were able to load the data into Pandas Dataframes containing every recorded pitch from each regular season. These Dataframes were then subject to rudimentary data cleaning. After cleaning, the dataframes were compressed and stored in github as pickle files.

import pandas as pd

from pybaseball import statcast drop_cols = ['spin_dir','spin_rate_deprecated', 'break_angle_deprecated','break_length_deprecated',

'game_type', 'tfs_deprecated', 'tfs_zulu_deprecated', 'umpire'] #2017

df = statcast('2017-04-02', '2017-10-02')

df = df.drop(columns=drop_cols) df.to_pickle(path='pitches_2017.pkl', compression='zip')

With future project developments in mind, the data, in addition to being compressed and stored in the main project GitHub repository as pickle files, was also stored in an S3 bucket as pickle files and csv files. These csv files were then used to create a Redshift table, containing all regular season pitches from 2010–2019. This storage architecture was designed for future iterations of this project in order to scale the number of pitchers for whom models would be trained and for the creation of a real-time pitch prediction web application. For the scope of the current project, model training and predictions were generated in Jupyter Lab notebooks on AWS Sagemaker ml.m4.4xlarge instances. The project dashboard was created using Plotly Dash and hosted on Heroku.

Data Pre-Processing / Feature Engineering

Explanation of engineered features and methods

As is often the case in the field of data science, the majority of the time spent on this project was devoted to data cleaning, processing, and feature engineering. At the outset, we took a sort of high level overview of the project, and tried to layout a blueprint of all the necessary steps and how we wanted to approach transforming the raw pitch data from Statcast into the format we wanted for our input vector into machine learning models.

Clean up bad/faulty data

Without going too far into the details, we encountered a few glitches in the data here and there during the process of data wrangling and feature engineering, so we added some code upfront to try to take care of most of it. Basically this included things like weeding out a small number of games played overseas without the Statcast camera tracking system installed, replacing unknown pitch type with NaN values, and fixing the number of balls from 4 to 3 in a few cases, and a few other minor instances of similar faulty data.

Engineered Features for all pitch data:

While the raw Statcast data itself includes a large number of features, we tried to get creative and came up with some pretty cool additional features in an attempt to augment the performance of our predictive models. Since every pitcher has a unique repertoire of pitches and individual tendencies, trying to build a predictive model with data from all pitchers lumped together would be futile. For this reason, we knew that each pitcher we wanted to predict would have to be filtered from the complete dataset of all pitches. Before reaching that step though, there were plenty of features we engineered that would be applicable to all pitchers.

Game State Features

The Statcast data includes the current status of the count, (via a feature for balls and feature for strikes), but we also wanted to use the 12 possible counts as a categorical variable. We combined the balls and strikes into a string representation, where 0–0 count was encoded as ‘00’, 3–2 as ‘32’, etc. Additionally, we further mapped the count into 3 categories representing whether the count favored the pitcher, the batter, or if the count was neutral. It stands to reason that pitchers approach these situations differently, and their choice of pitch should be heavily variable based on the favorability of the count.

Next we created a feature that represented the score differential, or how many runs ahead or behind the pitchers team was. Next, using the data for baserunners, we converted the baserunner features from using the baserunner id to a binary 1/0 whether or not a runner was on each base. Also we added a feature for whether ANY player was on any base vs bases empty, and also a feature whether or not the bases were loaded. A pitcher may have a subtle difference in his tendencies when throwing from the stretch with runners on base as opposed to from a full windup when the bases are empty. A runner on first base has the potential to steal a base and advance, so perhaps some pitchers are less likely to throw as many breaking balls in the dirt that could get away from the catcher and allow the runner to advance into scoring position. Perhaps a runner on 2nd base with a clear view of the catchers pitch signals and potential to steal the sign and tip off the player at bat with a signal of their own could influence a pitchers choice of pitches. Maybe some of these tendencies could contribute to a machine learning model in making predictions.

Strikezone and Batter Swung / Chased Features

Using the location data from the Statcast camera system, of both the boundaries of the strikezone for that particular hitter based on his height/stance, as well as the dimension of the size of home plate, we created a feature that classified each pitch as in or out of the strikezone (regardless of whether the umpire called it a ball or a strike). Also, using the ‘description’ feature from Statcast, we extracted the descriptions that indicated that a batter swung at the pitch, and created a binary feature representing whether or not the batter swung. By combining these engineered features, we created a feature to represent whether or not the hitter ‘chased’ a pitch that was outside of the strikezone. We thought this would be useful later when creating our scouting report on each batter, since if a batter has a tendency to chase at certain pitch types outside the strike zone, that information may be known by the pitcher in advance of the matchup and he may be more likely than otherwise to throw certain pitch types vs that player.

def make_strikezone_swung_and_chase_features(df):

df = df.copy()



#create swung column

def swung(x):

swung = ['foul','hit_into_play','swinging_strike',

'hit_into_play_no_out', 'hit_into_play_score',

'foul_tip','swinging_strike_blocked',

'foul_bunt','missed_bunt']

return 1 if x in swung else 0 df['batter_swung'] = df['description'].apply(swung)



#initialize in_strikezone and chased features:

df['in_strikezone'] = 1

df['chased'] = 0



df['ball_high'] = df['plate_z'] > df['sz_top']

df['ball_low'] = df['plate_z'] < df['sz_bot']

df['ball_left'] = df['plate_x'].apply(lambda x: x < -0.73)

df['ball_right'] = df['plate_x'].apply(lambda x: x > 0.73)

df['in_strikezone'] = df['ball_high'] + df['ball_low'] + df['ball_left'] + df['ball_right']



df['in_strikezone'] = df['in_strikezone'].apply(lambda x: 0 if x > 0 else 1)



df['chased'] = df['batter_swung'] - df['in_strikezone']

df['chased'] = df['chased'].apply(lambda x: 1 if x == 1 else 0)

return df

Pitch Category and Null Value Imputation

Unfortunately a very small percentage (~0.4%) of the pitches in the Statcast database, perhaps due to random glitches in the camera system or some other reason, had missing values for the pitch type classification of that pitch. In order to preserve continuity of pitches on a game by game basis, rather than simply dropping all these rows from the dataframe, we decided to impute these values. Our imputation strategy was just to use the overall distributions of pitch type for a given pitcher, and make a random guess, using those distributions as the weights for the random guess. Next, we mapped different pitch types into a more general pitch type category: fastballs (4-seam, 2-seam, cutter, sinker), breaking balls (curveballs, sliders, knuckle curves, screwball), and offspeed pitches (change-up, knuckle ball, and eephus pitch).

Up Next:

Coming next, part 2 will continue with feature engineering, including the creation of batter scouting report (tendencies and success vs different types of pitches), a pitcher scouting report, game flow/trailing pitch features, and pitch tendencies from pitcher/batter prior matchup history. Finally, part 3 will cover model selection, training, and analysis of the model predictions.