Using Linear Regression to “Play Out” the Full 2020 MLB Season
By: Ryan Werth
Tags: Linear Regression, Baseball, Data Science, Pandas, Python

For nearly 60 years the Major League Baseball regular season has consisted of 162 games. In 2020, because of COVID, the season was shortened to just 60 games. Inspired by the book Moneyball, I wanted to see if I could make a linear regression model that maps a team’s statistics to their win total. I would then use this model to “play out” the remainder of the 2020 season and see how things would have shaken out in a COVID-free world.

This project served as an introduction to both web scraping and Linear Regression with Python for me. I scraped all of the data from Baseball-Reference.com, using both BeautifulSoup and Selenium. I used data from every 162 game season by every major league team since 1960. This gave me a dataset of 1150 full seasons.

All of the work was done using Python. The data cleaning and organization was done using Pandas. I used SKLearn for the modeling and statistical analysis. All of the plots were made using MatPlotLib. Using StreamLit I created a web application for users to play around with one of my models.

What is Linear Regression?

The first question to tackle is what exactly linear regression is. In simple terms, linear regression is a way to model a relationship between certain “features” and a “target”. A linear regression model will fit a linear equation to a group of features in order to provide a “prediction” of the target. An example is shown below.

Linear Regression Equation

The x’s are the features that I am fitting the model to, in this case, those will be baseball statistics. The y is the target value that I am trying to predict, for this model that will be total wins. Linear regression models will find the coefficients and intercept that get our calculated y closest to the actual target value. Ultimately I will get an equation that will map our statistics to a predicted win total.

What Features To Use?

The next question is which features I want to use. It would be fairly easy to predict a team's total wins using Run Differential. In fact, one of the most popular win prediction models, the Pythagorean Theorem of Baseball, is built using only Runs Scored and Runs Allowed. I opted to not use either Runs Scored or Runs Allowed because it’s really not that fun or interesting to find a relationship between run differential and total wins. “Teams that score more win more” is not a very compelling finding. So what features did I use?

I started by analyzing what I will refer to as “Pure Statistics”:

PA (Plate Appearances)

1B (Singles)

2B (Doubles)

3B (Triples)

HR (Home Runs)

BB (Walks)

SB (Stolen Bases)

SO (StrikeOuts)

These are the “building blocks” of baseball statistics. While they obviously will correlate with wins, none of them are direct predictors on their own. I also used the per-game stats rather than the season totals. Let’s look at how exactly they each do correlate with total wins.

I used the Pandas .corr() functionality to calculate the correlation between all of the “pure stats” and wins. Stats closer to 1.0 are more positively correlated with wins, while stats closer to -1.0 are more negatively correlated with wins. While this helps to show general trends, it can not be used to determine which stats are more important than any others. With that being said, it is still interesting to note that walks are heavily correlated with winning. If you read Moneyball this fact probably won’t surprise you. Other than that though, nothing really jumps off the page from this information.

Building a Model

The first thing I did when I started modeling was to split my data into train, validate, and test sets. This gives me the ability to train a model, then validate and later test it on data the model has not seen before. The first model I created was a simple Least Squares Model. I used Sci-Kit Learn to create and fit the model on my training set. Right off the bat (no pun intended), this model showed an R² of 0.82 on the validation data. This means that my model could explain about 80% of the observed variance in total wins. Scaling the data and looking at the model’s coefficients can lend us some more insights into what the model is doing. Stats with an _A signify it is the stat against the team.

Coefficients for the first Linear Regression Model

The above table shows the model’s coefficients for each stat. This can give us an idea of the “weight” or “importance” of each stat. It is interesting to note that Plate Appearance and Plate Appearance Against are contributing inversely to what one would expect (PA has a negative coefficient while PA_A is positive). This is probably because there is a lot of colinearity between Plate Appearances and the other stats. All of the info that Plate Appearances can add to the model is contained within the other features. So for the next model, I removed the Plate Appearances.

After removing Plate Appearances the new model had a similar R² on validation data, meaning that it indeed did not add anything that the other stats didn't already tell the model. This is the model I used for the web application I created. The next step I took was to analyze the residuals. The residuals are the difference between my model’s predicted win totals and the actual win totals when the model is run on the validation data.

This residual plot shows me something interesting, the residuals have a negative trend when plotted with respect to actual games won. This means that the model over-predicts for the worse teams and under-predicts for the better teams. Let’s try to fix that.

Feature Engineering

In order to try and resolve this over-predicting/under-predicting predicament, I turned to feature engineering. Feature engineering is the process of “manipulating” the input features to take on a new form that may have a more linear relationship with the target. The first thing I tried was creating polynomial combinations of the features. This means every feature is multiplied with every other feature (including itself) to create many more features. In this case, it turned my14 features into 120. Ido this transformation because the input features for a linear model may interact in unexpected or often nonlinear ways. This transform could help uncover those.

After this transformation, the new model had an R² that was consistently higher on training data than it was on validation data. This means that my new model is over-fitting this newly transformed data. The model is now starting to describe the random error and is not generalizing enough for the unseen validation data.

I then tried to use a regularization technique known as Ridge Regression on my overfit model. Ridge Regression models add a degree of bias to the regression estimates, in order to compensate for multicollinearity. This model did solve the issue of overfitting but did not really perform any better than my model did before feature engineering.

Domain Knowledge

When exploring feature engineering, it occurred to me that baseball is already chock-full of these “engineered features”. Batting-Average, On-Base Percentage, and Slugging Percentage are all just different ways to manipulate the pure stats. So I decided to build a model around these features.

Once again my new model’s R² was in the low .8’s, but a big improvement can be seen in the residual plot.

These new residual plots are very close to normally distributed. This means the model is over-predicting just as much as it is under-predicting. That’s a good sign and one reason why I ended up picking this model. The other reason is that the 2020 season had a lot of 7-inning games, meaning that using a per-game stat like any of the“pure stats” would not have led to underpredictions. With these average stats, it doesn't matter if the games were 7-innings or 9-innings because they are calculated on a per At-Bat or per Plate Appearance basis.

Analyzing The Model

Once I was satisfied with the model, I wanted to analyze the errors using a test set that the model had not seen in either the training or validation process. On this set, the model had a Mean Absolute Error of 3.99 wins. This means that on average the model would predict the win total within 4 wins of the actual number. And with a standard deviation of 2.9 games, about 85% of the predictions land within 7 games of the actual values.

Applying The Model

The last step of this process was to apply the 2020 statistics to my final model. I used the 2020 statistics through the 60 games played to predict how many wins each team would have over a 102 game period. My final findings are pretty thoroughly summed up in the plot below.

The blue dashed line is the “extrapolation line”. If a team continued their 60 game win percentage over 162 games they would fall right on that line. The teams above the line “underperformed” their stats in 2020 and would be expected to progress as the season continued. Teams below the line were the opposite, they “overperformed” their stats and would be expected to regress over the next 102 games. So what insights can I glean from these results?

The Playoff Expansion Was Made For a Team Like the 2020 Yankees

When the 2020 season was shortened from 162 games to 60 games, the playoffs were also expanded from 10 teams to 16 teams. There was likely a myriad of reasons for this expansion, but one of them was because 60 games are not enough to determine the 10 best teams. A 10 game rough streak hurts a lot more in a 60 games season then it does in a 162 game season. The model has the Yankees winning almost 95 games and making the playoffs if the season were to have continued. However, through 60 games the Yankees had only won 33 of them and would have been left out of the playoffs if the league had not expanded the format.

The Miami Marlins Got Very Very Lucky

The Miami Marlins were the luckiest team in baseball according to the model. After 60 games they had 31 wins and even made the playoffs (thanks to the expansion). However, if the full 162 games were played out, not only does the model have them missing the playoffs, but they would have finished the season 10 games under .500. If not for COVID, Miami would have had their 10th straight losing season and 17th straight season without a playoff birth.

The Dodgers Got Robbed

How can the season’s winningest team get robbed? The 2020 Dodgers were on pace to win 116 games. That would have tied the all-time record set by the 2001 Seattle Mariners. The model actually has them winning almost 120 games over the full season. The Dodgers were in the midst of a historic season, and unfortunately for them (and baseball fans in general) it just so happened to occur during a shortened MLB season.