openWAR and the Defensive Spectrum

One thing that all baseball fans can agree on is that there exists some kind of defensive spectrum in baseball. Bill James wrote about this long ago, but the observation stems from three facts inherent to the game:

  1. All players must play offense and defense
  2. Batting is the same for all players
  3. Defense is not the same for all players

Astute readers will note that #1 is softened in the American League due to the designated hitter, but I would argue that because of this they don’t play real baseball in the AL anyway. #NLforLife

As defensive skills can vary profounding among positions, but all players must hit, it stands to reason that offensive skills will vary among positions as well. In general, there is a negative correlation between offensive and defensive skills, as players who specialize in the latter (pitchers, catchers, and shortstops) tend to be much weaker hitters and those who specialize in the former (DHs and first basemen).

In this post, we’ll see how this idea manifests itself, and how openWAR attempts to correct for it.

Loading the MLBAM Data

First, we’ll load the MLBAM data from the 2013 season from the openWAR package.

require(openWAR)
data(MLBAM2013)

Note that the data set is very large – it contains 185,273 rows and takes up 124 MB on my machine.

print(object.size(MLBAM2013), units = "Mb")

Now, in order to compute openWAR, we need to fit 26 different models on this data set, and a few of these computations can take a while. Thankfully, we have done these computations for you and stored the results in the package. The following data set contains the plate appearance-level results of the openWAR computations.

data(openWARPlays.2013)

Note that this data set has the same number of rows as the previous one. This is not a coincidence – they line up.

dim(MLBAM2013)
## [1] 185273     62
dim(openWARPlays.2013)
## [1] 185273     37

Let’s stitch these together.

ds = data.frame("delta.bat" = openWARPlays.2013$delta.bat, "batterPos" = MLBAM2013$batterPos, "description" = MLBAM2013$description)

The variable delta.bat contains the changes in the value of the expected run matrix that we associate with each of these plate appearances. These values have already been adjusted for ballpark, the platoon advantage, and whatever the baserunners did in excess of what the batting event. Thus, these values are our best estimate of what is directly attributable to the batter.

For example, here are the 3 most valuable batting events of 2013:

head(ds[order(ds$delta.bat, decreasing = TRUE), ], 3)
##        delta.bat batterPos
## 33710      3.409        3B
## 46595      3.409        1B
## 120965     3.401         C
##                                                                                                                                 description
## 33710  Nolan Arenado hits a grand slam (2) to center field.   Troy Tulowitzki scores.    Michael Cuddyer scores.    Wilin Rosario scores.
## 46595       Jordan Pacheco hits a grand slam (1) to left field.   Carlos Gonzalez scores.    Wilin Rosario scores.    DJ LeMahieu scores.
## 120965     Alex Avila hits a grand slam (7) to right field.   Prince Fielder scores.    Victor Martinez scores.    Jhonny Peralta scores.

They are all grand slams, as you might expect.

Offensive

So does the offensive output in 2013 reflect the defensive spectrum?

require(mosaic)
bwplot(delta.bat ~ batterPos, data=ds)

 

delta.bat-position

The differences may seem small, but note that these are on a plate appearance basis! Let’s compute the averages.

sort(mean(delta.bat ~ batterPos, data=ds), decreasing=TRUE)
##            1B            RF            CF            LF            DH            3B            2B             C 
##  0.0248487410  0.0171247406  0.0096762392  0.0088564800  0.0086178898  0.0040126957  0.0004839509 -0.0041235143 
##            SS            PH             P            UN 
## -0.0101546655 -0.0221117437 -0.1289599268 -0.1390434737 

It may be more instructive to put these on a 600 plate appearance scale.

600 * sort(mean(delta.bat ~ batterPos, data=ds), decreasing=TRUE)
##          1B          RF          CF          LF          DH          3B          2B           C          SS 
##  14.9092446  10.2748444   5.8057435   5.3138880   5.1707339   2.4076174   0.2903705  -2.4741086  -6.0927993 
##          PH           P          UN 
## -13.2670462 -77.3759561 -83.4260842 

Immediately we see the outlines of the defensive spectrum. First basemen, rightfielders, leftfielders, and DHs tend to contribute more, while catchers, shortstops, and pitchers contribute less. The small deviations from the commonly accepted spectrum are likely to even out over time, since we are only dealing with one season of data here. It’s a good things pitchers don’t hit 600 times in a season!

When we compute openWAR, we have to correct for this, and we do it using a regression (or equivalently, an ANOVA) model.

mod = lm(delta.bat ~ 0 + batterPos, data=ds)
coef(mod)
## batterPos1B batterPos2B batterPos3B  batterPosC batterPosCF batterPosDH
##    0.024849    0.000484    0.004013   -0.004124    0.009676    0.008618
## batterPosLF  batterPosP batterPosPH batterPosRF batterPosSS batterPosUN
##    0.008856   -0.128960   -0.022112    0.017125   -0.010155   -0.139043

The coefficients of this model gives us the offsets we should apply to each position, and they are equal in this case to the averages we computed earlier.

Why did we have to do this? Because otherwise, starting pitchers in the NL would be charged with roughly

100 * coef(mod)["batterPosP"]
## batterPosP
##      -12.9

runs for their batting contribution. This is accurate in the sense that your average pitcher will cost you 13 runs every 100 plate appearances, on average, relative to your average hitter. But what sense does that comparison make, since you don’t have the option of substituting an average hitter for your pitcher? The pitcher has to hit for himself (again, in the real form of baseball that is played in the National League), and so it only makes sense to compare pitchers to other pitchers when computing an aggregate measure of player performance like WAR.

So what is assigned to each player for each plate appearance is actually the residuals from this model, which is the contribution net of their defensive position.

bwplot(residuals(mod) ~ batterPos, data=ds)

resids-position

 

Again, these don’t look that different, but now the averages by position are all zero.

round(mean(residuals(mod) ~ batterPos, data=ds), digits = 14)
## 1B 2B 3B  C CF DH LF  P PH RF SS UN 
##  0  0  0  0  0  0  0  0  0  0  0  0 

Effects

What are the effects of these adjustments? Well, for one, it means that moving your catcher to first base changes the valuation of his hitting performance. That is, even if Joe Mauer does the exact same thing at the plate while playing first base that he did while catching, we’d value that contribution at nearly two wins less over the course of 650 plate appearances.

650 * (coef(mod)["batterPos1B"] - coef(mod)["batterPosC"])
## batterPos1B
##       18.83

Is that fair? Post to comments!

Follow

Get every new post delivered to your Inbox.

Join 28 other followers