A 2018 Bullpen Update

Bullpens are notoriously difficult to predict. Baseball is volatile by nature, and relievers even more so. Despite this, many teams invested heavily in relievers over the offseason. Here’s what bullpens are doing so far, one quarter through the season.

First, we’ll compare how each team’s bullpen performed last year with how one would expect them to perform based on batted ball data. If a team lies above the line, then their actual results were not as bad as one would expect, and they were lucky. The farther below the line, the more unlucky a team was. For instance, in 2017, the Tigers’ bullpen was very bad but also lucky (they should have been worse), while the Dodgers’ bullpen was not only very good but also unlucky (they should have performed even better).


This is the same chart, but with 2018 data instead:


The Diamondbacks’ bullpen has been great, but they’ve been a little lucky. As good as the Brewers’ bullpen has been (led by Josh Hader), one would expect them to be even better. As we would expect, one can see on these two graphs that wOBA and xwOBA are strongly correlated.

First, we’ll start with actual results. The Diamondbacks’ bullpen is the most improved by wOBA allowed, improving by nearly 0.05 points, while the Indians’ bullpen is worse by a large margin, allowing a wOBA 0.06 points higher than in 2017 so far. In terms of actual results, the Rockies’ bullpen has been exactly the same despite their large investments over the offseason.


Team 2017 wOBA 2018 wOBA wOBA Diff
ARI 0.298 0.249 -0.049
MIL 0.314 0.270 -0.044
SD 0.318 0.278 -0.040
HOU 0.308 0.270 -0.038
NYM 0.332 0.298 -0.034
DET 0.351 0.321 -0.030
PHI 0.315 0.288 -0.027
CHC 0.301 0.274 -0.027
ATL 0.316 0.294 -0.022
TEX 0.334 0.315 -0.019
CIN 0.323 0.315 -0.008
PIT 0.314 0.311 -0.003
TOR 0.308 0.306 -0.002
WSH 0.315 0.313 -0.002
BAL 0.318 0.316 -0.002
COL 0.314 0.314 0.000
SF 0.318 0.322 0.004
STL 0.304 0.311 0.007
OAK 0.321 0.332 0.011
MIA 0.321 0.334 0.013
TB 0.291 0.304 0.013
SEA 0.305 0.318 0.013
BOS 0.285 0.299 0.014
NYY 0.273 0.288 0.015
MIN 0.318 0.336 0.018
LAA 0.295 0.322 0.027
CWS 0.317 0.353 0.036
LAD 0.283 0.319 0.036
KC 0.316 0.353 0.037
CLE 0.276 0.340 0.064

However, actual results don’t tell the story. We can also look at the results each bullpen should expect given the data on batted balls allowed.


Here, we can see that the Rockies’ bullpen has actually been worse than last year in terms of expected results. There is very little correlation between either actual or expected bullpen results from year to year.

Team 2017 xwOBA 2018 xwOBA xwOBA Diff
DET 0.345 0.325 -0.020
SD 0.310 0.294 -0.016
MIL 0.305 0.293 -0.012
ATL 0.322 0.314 -0.008
OAK 0.326 0.321 -0.005
ARI 0.308 0.303 -0.005
BAL 0.322 0.318 -0.004
MIN 0.328 0.325 -0.003
PHI 0.309 0.309 0.000
CHC 0.307 0.307 0.000
NYM 0.318 0.320 0.002
PIT 0.314 0.322 0.008
HOU 0.295 0.307 0.012
SF 0.315 0.329 0.014
BOS 0.299 0.313 0.014
TOR 0.299 0.315 0.016
WSH 0.311 0.329 0.018
MIA 0.320 0.338 0.018
CIN 0.309 0.329 0.020
SEA 0.308 0.329 0.021
TEX 0.319 0.342 0.023
CWS 0.321 0.347 0.026
NYY 0.288 0.314 0.026
COL 0.299 0.330 0.031
TB 0.291 0.326 0.035
STL 0.305 0.340 0.035
LAA 0.301 0.339 0.038
LAD 0.279 0.318 0.039
KC 0.307 0.365 0.058
CLE 0.283 0.346 0.063

Finally, here’s a table that shows how each team’s bullpen “luck” has changed year to year, where “luck” is defined as xwOBA-wOBA. The one that stands out the most is the Diamondbacks. Luck is also unsurprisingly inconsistent year-to-year.


Team 2017 Luck 2018 Luck Luck Diff
MIN 0.010 -0.011 -0.021
OAK 0.005 -0.011 -0.016
CWS 0.004 -0.006 -0.010
BAL 0.004 0.002 -0.002
CLE 0.007 0.006 -0.001
BOS 0.014 0.014 0.000
LAD -0.004 -0.001 0.003
MIA -0.001 0.004 0.005
SEA 0.003 0.011 0.008
DET -0.006 0.004 0.010
SF -0.003 0.007 0.010
PIT 0.000 0.011 0.011
LAA 0.006 0.017 0.011
NYY 0.015 0.026 0.011
ATL 0.006 0.020 0.014
TOR -0.009 0.009 0.018
WSH -0.004 0.016 0.020
KC -0.009 0.012 0.021
TB 0.000 0.022 0.022
SD -0.008 0.016 0.024
CHC 0.006 0.033 0.027
PHI -0.006 0.021 0.027
CIN -0.014 0.014 0.028
STL 0.001 0.029 0.028
COL -0.015 0.016 0.031
MIL -0.009 0.023 0.032
NYM -0.014 0.022 0.036
TEX -0.015 0.027 0.042
ARI 0.010 0.054 0.044
HOU -0.013 0.037 0.050

On the whole, xwOBA is up this year (by a mean of 0.0148), but wOBA is down very slightly (by a mean of -0.0013). This gap will probably decrease as luck evens out and the weather continues to get better over the course of the season.

Can Noah Syndergaard Make It Through the Next Year?

Probably not.

The Mets are not healthy. Their five best starters would combine to make one of the better starting rotations in recent history. Unfortunately, it is seeming increasingly unlikely that all five will pitch at the same time again. Steven Matz finished 2016 with a surgery to remove a bone spur in his elbow. He hasn’t pitched yet this season. Matt Harvey had season-ending surgery to alleviate thoracic outlet syndrome after a disappointing start to the season. Jacob deGrom missed the last part of 2016 for ulnar nerve surgery. Depth option Seth Lugo is out with a partial UCL tear.

Noah Syndergaard is the only one of the five to not have had Tommy John surgery. Despite pitching through a bone spur last season, he has been remarkably healthy. However, he left his opening day start this season with a blister. And now this:

As others have noted, lat strains can be fairly serious—Matz missed two months with the same injury in 2015. Syndergaard is probably out until at least the All-Star break, a big blow to the Mets. The big story here of course is that the Mets started Syndergaard even after he refused a suggested MRI. However, I believe a further, more serious injury awaits Syndergaard.

Syndergaard didn’t always throw a slider:

Syndergaard Pitch Usage

Indeed, he started throwing it toward the end of 2015 (his rookie season), and relied heavily on it in 2016.

There’s been a lot of work on this topic. Fangraphs’ Eno Sarris termed the pitch the Mets are throwing the “Dan Warthen Slider” in 2015. Sarris notes in his piece:

Critics might point to arm injuries on the Mets as proof that the pitch is hard on the arm, but Warthen laughs that off. “It’s easy on the arm when done correctly, it’s not one of those pitches that you try to make break,” he said. And these pitchers all throw hard, and there is a relationship between just throwing hard and arm injury. It’s impossible to split those effects apart.

Obviously there’s more contributing factors to injury than throwing this one specific slider. The Mets’ five aces throw very hard. Perhaps more importantly, they all throw breaking pitchers (the Warthen slider) very hard.

Tommy John and Sliders

This is a graph of pitchers who threw at least 250 sliders between 2015 and 2017. Perceived velocity is on the y-axis, and is correlated with release extension on the x-axis (the farther a pitcher’s arm gets from the mound, the faster the ball will appear to a hitter).

The only pitcher with a slider that has averaged above 90 MPH in effective speed that hasn’t had Tommy John surgery yet is…Noah Syndergaard. Jon Gray and Jake Arrieta are both also near the threshold.

Arrieta Pitch Usage

Arrieta throws a mix of a cutter and slider. When he was traded to the Cubs from the Orioles in July 2013, his month to month slider usage began increasing almost immediately. In 2014, his first full season with the Cubs, he threw the pitch 29% of the time. In 2015, he threw it 29.5% of the time. However, his usage has decreased since then, and now sits at 16.1% so far this year (potentially due to his lost command of it). He threw his slider more than 20% of the time for about two years, and then decreased his usage again. It’s not too surprising that his arm has held up, especially considering his conditioning.

Plot 49

DeGrom got Tommy John surgery at 22, Harvey 24, Wheeler 25, and Matz 19. Syndergaard is 24 now. Out of the two who had Tommy John in the Majors, Harvey pitched for 1.5 years at the Major League level before needing surgery and Wheeler had about the same amount of time as well. Syndergaard has been pitching for about half a year longer than either of them, but it’s concerning how much the timelines line up. Syndergaard is just a little past the mean age of Tommy John surgery in the last 10 years (23.28).

Jon Gray is an interesting case due to his frequent comparisons to Syndergaard. His arm seems healthy now, but he’s also only pitched for a year and a half so far. I wouldn’t be surprised if he ends up in the same position as Syndergaard soon, though, especially since it appears that he’s started throwing the slider even harder in limited starts this season.

Maybe Syndergaard’s injury is a blessing in disguise. There’s only one other pitcher I could find in the past seven years that’s undergone Tommy John surgery after a lat strain or tear. However, many have gotten lat strains after Tommy John (including Syndergaard’s teammate Matz). It’s certainly good that he’s not trying to pitch through it. If he does rush back or not take the injury seriously, though, it could put even more strain on his likely endangered elbow. Due to Syndergaard’s attitude about this situation so far, his desire to throw as hard as possible, and the Mets’ reliance on and mismanagement of him, I doubt he makes it through 2018 with his elbow intact.

The Spin Room

Spin is one of the most-discussed relatively new Statcast metrics. It is especially relevant when looking at four-seam fastballs, as that pitch tends to be the most straightforward and therefore easiest to predict movement-wise. We know spin has a positive effect on fastballs, but what is the extent of that effect? Are there any potential cons to throwing a ball with such a high spin rate?

Here are the four-seam spin kings from 2015-20171:

Rank Name Pitches Spin (RPM)
1 Andrew Bailey 582 2669
2 Carl Edwards Jr. 521 2656
3 Rafael Betancourt 379 2562
4 Jose Leclerc 151 2560
5 Matt Bush 707 2558
6 Tyson Ross 610 2539
7 Justin Verlander 3532 2534
8 Max Scherzer 4005 2526
9 Yimi Garcia 605 2524
10 Yu Darvish 742 2522
11 Garett Richards 984 2513
12 Edubray Ramos 381 2508
13 David Robertson 136 2507
14 Aroldis Chapman 1513 2507
15 Yohan Pino 120 2502

A rather large number of these pitchers are dealing or have recently dealt with arm injuries. Garrett Richards only made six starts last year and is out until at least June, and Tyson Ross hasn’t pitched since April 4th of 2016. Yu Darvish missed all of 2015 and half of 2016. Andrew Bailey missed all of 2014 and pitched 8.2 major league innings in 2015 before putting in a 43.2 inning season with mixed results in 2016. He’s currently on the 10-day DL for right shoulder inflammation. The Cubs are carefully limiting their usage of Edwards. Yimi Garcia pitched 56.2 innings for the Dodgers last year, but only made it through 8.1 this season before having to undergo Tommy John surgery.

Whether this high frequency of injuries is specific to high-spin fastball pitchers or not is unclear, but it certainly seems worth investigating. Of course, many of these pitchers also hit the high 90s, which may have more to do with it.

Another thing that’s important to notice is that high fastball spin does not necessarily entail high quality results. Rafael Betancourt threw 39.1 innings for Colorado in 2015 but allowed an ERA of 6.18 and a FIP of 3.34. He hasn’t pitched in the majors since (presumably he retired after that year, his age 40 season). Yohan Pino hasn’t pitched in the Majors since 2015, spending 2016 in the KBO. Edubray Ramos is off to a slow start with the Phillies this season, though he did complete 40 relatively high quality innings last season.

On the flip side, some of the games best and most promising pitchers show up on this list. Max Scherzer and Justin Verlander are both seasoned aces and Cy-Young winners. Jose Leclerc is a promising young prospect for the Rangers who struggles with command (though he’s only allowed one walk in nine innings so far this year). It’s impressive that he’s so high on the leaderboard despite being only 23. (Interestingly, four of the top 15 four-seam spin rates belong to Rangers.) Carl Edwards pitched in the tenth inning of game seven of the World Series and looks to be the Cubs’ potential closer of the future. Aroldis Chapman is as durable and dominant as ever. Matt Bush continues to move up the ranks in the Rangers’ bullpen, allowing just one hit in his last three appearances (all in the ninth inning). David Robertson remains a quality closer despite a slight down year last year.

High spin rate is obviously not the only key to pitching success, especially when just looking at one pitch. However, high four-seam spin rate does seem to be correlated to some amount of potential, if not direct success. And some of the game’s most dominant pitchers are near the top of this list. It will be interesting to see how the baseball community treats spin data as it continues to become more accessible.

  1. Minimum of 100 qualifying pitches. Statcast spin data appears to only go back to 2015. Data from Baseball Savant. [return]

Starters Who Can't Finish What They Started

Every pitcher performs differently in different scenarios. Some starters can’t finish innings. More specifically, some starters are pretty good at getting two outs, but much worse at getting the third. In looking at potential ways to identify starters who may work better as relievers, I came across Baseball Reference’s number of outs splits. To find good candidates for this analysis, I looked at Fangraphs’ splits leaderboard for worst ERA with two outs. I took the top (or bottom) three pitchers with more than 30 IP: Tyler Duffey, Anibal Sanchez, and Michael Pineda.

For a quick refresher, tOPS+ refers to how well a player performed in a particular split compared to as a whole with 100 as an average. sOPS+ is how well a player performed in a split compared to how the league performed in that same split on average.

We’ll look at Duffey and Sanchez first before getting to the more interesting case of Pineda:

Tyler Duffey

Outs ERA tOPS+ sOPS+
0 4.74 100 128
1 5.15 66 94
2 9.90 132 184

Anibal Sanchez

Outs ERA tOPS+ sOPS+
0 4.22 114 132
1 3.96 55 72
2 9.82 125 160

Neither Duffey nor Sanchez were great last year, but they were both awful with two outs. Interestingly, both were better than league average with one out, but worse with no outs, and even worse with two.

(As a side note, last year Sanchez’ fellow Tigers Jordan Zimmerman and Mike Pelfrey had top 20 ERAs with two outs as well.)

Michael Pineda is notoriously frustrating to watch. Despite flashes of brilliance, he has never quite been able to put it all together for a sustained period of time. Part of that appears to be his struggles with two outs:

Outs ERA tOPS+ sOPS+
0 2.47 75 80
1 3.53 71 80
2 8.84 148 172

All three of these pitchers allowed a better than league average OPS with one out. Michael Pineda was better than league average with both zero and one out(s). However, he’s worse than Anibal Sanchez at his worst with two outs!

In a very small sample size (~25 plate appearances each), Pineda does not seem to have alleviated his struggles with two outs in 2017, despite a 23 K/BB ratio and a total sOPS+ of 73:

Outs tOPS+ sOPS+
0 126 99
1 28 7
2 140 109

Interestingly, while Pineda got lit up on his first start, allowing four runs in 3.2 innings, in his second he took a perfect game into the seventh and only allowed three runs across his last two starts.

Anibal Sanchez has appeared only as a reliever thus far this year and has allowed 15 runs in 9 innings. Tyler Duffey has also only been a reliever for the Twins, though he hasn’t allowed any runs in 8.2 innings, with only one walk and seven strikeouts. Across a very small sample size, he has allowed an sOPS+ of 28, -20, and 29 with 0, 1, and 2 outs respectively. Duffey has performed best in high leverage situations, while Sanchez has only faced one hitter in such a situation (he allowed a home run).

Kris Bryant Loves the Reds

Kris Bryant was very good last year. The Reds were…not great last year. In fact, the Reds’ bullpen was historically bad. Early in the season they set a record for consecutive games allowing at least one run, and later in the season they set a record for home runs allowed.

Kris Bryant, of course, was the National League MVP.

This is what the MVP did against normal pitching in 699 plate appearances:

.292 29 .385 .554 149

This is what Kris Bryant did to Reds pitching in 88 plate appearances:

.364 10 .443 .831 165

tOPS+ is a measure of how well a player performed in this split as opposed to their average. Imagine a normal Major League Baseball player. Now imagine Kris Bryant. The difference between these two players is less than the difference between Kris Bryant being pitched to by the Reds and normal Kris Bryant. Against-Reds Kris Bryant is to normal Kris Bryant as normal Kris Bryant is to a league-average hitter.

sOPS+ shows how well a player performed in a split compared to the league average in that split. Bryant’s sOPS+ against the Reds was 211. That speaks for itself.

In fact, more than 25% of Bryant’s home runs were against the Reds. A more modest 20.7% of his extra-base hits were against them.

This was Reds pitching against everyone:

4.91 .263 258 1.95 116

This was Reds Pitching against the Cubs:

6.99 .285 42 1.39 125

So the Reds were worse against the Cubs than against the league as a whole.

The advanced batted ball data backs up Bryant’s Reds dominance:

Reds? Angle Velocity Distance Barrel %
No 19.5 89.6 235.1 16.4%
Yes 21.9 91.3 250.1 22.9%

Against the Reds, Bryant’s batted ball profile had a more optimal launch angle and greater velocity, distance, and barrel frequency.1

The code for this analysis is located here.2 Much of the data for this post was collected from Baseball Reference.

Tracing Starter Performance by Inning

In an attempt to sharpen my data visualization skills, I decided to look at some of the top starters from the 2016 season by inning. The pitchers were selected by a combination of innings pitched, complete games, and personal preference. Each pitcher pitched at least 150 innings last season. One thing to keep in mind with all of these is that as the game goes on the sample size decreases. Starters rarely made it through the ninth inning this season, so later game results should be taken with a grain of salt. This type of analysis is best for getting a broad idea of a pitcher.1

The x-axis for each of these graphs is innings. The y-axis varies based on the metric being measured.


Every pitcher except Scherzer and Verlander (both past Tigers, coincidentally) featured 2-3 pitches that had very similar velocity at the top of the charts. All the pitchers featured a curveball of some sort that averaged below 80 mph except Kluber whose averaged 84+ mph.


Kyle Hendricks’ spin rate varies wildly from inning to inning. His pitches are grouped closer together, which plays into his strategy of deceiving hitters with consistency until it’s too late. Hendricks also has one of the lower average spin rates, with only Kluber’s chart having the same axis. In contrast, David Price throws three pitches that all had an average spin rate of over 2000 throughout the entire game. Max Scherzer’s slider, on the other hand, spun around 1/4th as much on average.


Curveballs tend to have the greatest average break, followed by changeups, sliders, and sinkers, with cutters and two- and four-seam fastballs at the bottom. Curveball break tends to go down in the ninth inning. Even Rick Porcello’s break takes a very slight downturn then, despite it having the highest break by far.

In terms of more general observations, check out Johnny Cueto’s remarkably inconsistent curveball. The pitch varies the most in all three metrics inning-to-inning, and he didn’t throw it past the seventh inning. Of course, he only threw it 11 times all season, and only twice in a day once. He never threw it with a runner on base, and preferred to throw it with nobody out. The pitch had pretty good results: five balls, three called strikes, one swinging strike, a groundout and a popout.

  1. The code for this analysis is here. [return]

Carl Edwards Had a Bad Day in a Great Year

This post was featured on the Fangraphs community blog here

This week I played around with the baseballr package, which provides easy access to FanGraphs, Baseball Reference, and Statcast data in R1.

I’ve been particularly intrigued recently by Carl Edwards Jr., a Cubs reliever who got called up last season. He had always seemed to be surprisingly good, but I wasn’t aware quite how good he was until I calculated wOBA allowed by pitchers in 2016 and found that he had the third lowest in the league, behind only Kenley Jansen and Zach Britton, and in front of Clayton Kershaw, Aroldis Chapman, and Andrew Miller. Ranking Edwards among four of the game’s top closers and the game’s best starter seemed strange. Here are the six pitchers with the lowest wOBA-against last season:

Zach Britton 67.0 0.54 0.836 0.231 1.80 0.188
Kenley Jansen 68.2 1.83 0.670 0.244 1.34 0.188
Carl Edwards 36.0 3.75 0.806 0.162 2.79 0.201
Clayton Kershaw 149.2 1.68 0.722 0.256 1.78 0.202
Aroldis Chapman 58.0 1.55 0.862 0.268 1.42 0.206
Andrew Miller 74.1 1.45 0.686 0.258 1.68 0.209

Edwards stands out negatively in several respects here. He pitched the least innings out of that group by far, and was almost certainly put in the least stressful situations. His ERA is almost two points higher than the next highest’s, and his FIP is nearly a point higher than Britton’s, the second highest mark. His BABIP is also remarkably low, due in part to luck and in part to the Cubs’ historically good defense. So why is his wOBA so remarkable?

Looking through Edwards’ game log, two bad appearances stand out:

  1. August 13th, where he allowed five runs on one hit and four walks while recording just two outs.
  2. September 17th, where he allowed three runs on three hits (two of them home runs) in an inning’s work.

If we remove the August 13th outing, Edwards’ ERA drops to 2.55, almost an entire point. If we remove the September 17th outing as well, it drops to 1.83. Removing the first performance, his FIP drops to 2.56. Removing the other brings it down to 1.96, which is still higher than the other five pitchers (possibly due to his BABIP) but much closer. Bad pitching performances are part of a pitcher’s year, and shouldn’t be entirely disregarded. However, it seems likely that something was off (mechanically, physically, or mentally) on August 13th.

We’ll get back to these games later.

As the Cubs consistently carried three catchers last season, I thought it would be interesting to compare Edwards’ performance across all three:

cej_by_catcher <- cej %>%
  group_by(pitch_type, catcher) %>%
  summarize(n = n(),
            avg_mph = mean(as.numeric(as.character(start_speed)), na.rm = TRUE),
            avg_spin = mean(as.numeric(as.character(spin_rate)), na.rm = TRUE),
            avg_hit_distance = mean(hit_distance_sc, na.rm = TRUE),
            avg_hit_speed = mean(hit_speed, na.rm = TRUE),
            avg_hit_angle = mean(hit_angle, na.rm = TRUE),
            avg_barrel = mean(barrel, na.rm = TRUE),
            avg_ext = mean(release_extension, na.rm = TRUE)) %>%
  mutate(pitch_pct = n/sum(n)) %>%
  filter(pitch_type != "CH", pitch_type != "IN") # These each occur once and may be mistakes

The baseballr package also allows us to look at Statcast data from Baseball Savant:

cej <- scrape_statcast_savant_pitcher(start_date = "2016-04-06", end_date = "2016-10-15", pitcherid = 605218)
type catcher count mph hit_dist hit_spd pct
CU Ross 27 81.16 119 73.6 16.67%
CU Montero 53 81.54 258.8 94.86 32.72%
CU Federowicz 3 81.25 NA NA 1.85%
CU Contreras 79 81.02 208.8 87.74 48.77%
FF Ross 99 95.72 186.8 84.31 21.57%
FF Montero 147 95.44 211.1 83.77 32.03%
FF Federowicz 9 95.77 183 95.5 1.96%
FF Contreras 204 95.44 199.4 85.94 44.44%

From the table, we can see that Edwards throws two main pitches—a four-seam fastball and a curveball. He pitched most often to Willson Contreras, then Miguel Montero, then David Ross (and once to Tim Federowicz). Edwards threw his fastball a notch faster to Ross than other catchers, which could be due to the small sample size. He also threw his fastball more to Ross than other catchers: despite throwing 16.67% of his curveballs to Ross, he threw 21.57% of his fastballs to him. There are several reasons this might be the case:

  1. Edwards focused on his fastball earlier in the season before gaining more confidence in his curve.
  2. Ross saw that Edwards’ fastball was producing better results and called it more often.
  3. Contreras was more confident in his agility and therefore ability to block a curveball than Ross was.
  4. Random sampling and a small sample size.

Let’s take a look at the results these pitches got. By plotting hit velocity and hit distance, we can compare results across catchers:


When Ross was catching, Edwards tended to generate softer contact that went shorter distances. We can use the Statcast data to see why that is.


This chart, plotting spin against pitch velocity, shows something interesting: Edwards’ pitches had the highest spin when throwing to Ross. Curveballs with higher spin tend to induce more ground balls2, which is advantageous for Edwards thanks in part to the defense behind him. High spin on his four-seamer is essential to Edwards’ style, and that was maximized when Ross was catching. Of course, this isn’t necessarily related to the catcher. It could be the case that Ross just happened to catch Edwards on his better days. For greater parity, we can look to see what happens to Contreras’ numbers if we take away the two worst games in Edwards’ season.


Already we see that removing the two bad outings brings Contreras much closer to Ross and Montero in terms of average hit speed and hit distance. Now we can look at how removing the bad outing affects velocity and spin.


There isn’t any effect on the spin of Edwards’ curveball while Contreras is catching, but the spin on his fastball gets much closer to Ross than Montero. Let’s compare Edwards’ Statcast data between the August 13th outing and his overall averages (the bolded rows are from the 13th):

Type MPH Spin Ext
FF 95.55 1876 6.85
FF 94.60 1602 6.73
CU 81.20 1605 6.31
CU 81.49 1480 6.19

If we add in the other poor outing:

Type MPH Spin Ext
FF 95.54 1873 6.85
FF 95.08 1749 6.74
CU 81.23 1610 6.31
CU 81.12 1502 6.26

Adding in the second bad outing makes the numbers more similar, lending credence to the idea that the first outing was an outlier. On August 13th the spin on his fastball, his extension, and his velocity were all down a tick.

Carl Edwards had a quietly great year out of the bullpen for the Cubs. He was among the league leaders in wOBA-against, which is initially surprising based on his peripheral numbers. Upon removing an outing where the spin rate and velocity on his signature pitch took a steep downturn, Edwards’ peripheral numbers match up more closely with the type of performance you’d expect from someone in that elite group of pitchers. Carl Edwards had a bad day—here’s to more good ones.

  1. As part of this analysis, I defined a function playername_lookup that gets a player’s name from their MLBAM id (this function is based on the playerid_lookup function from the baseballr package). It is provided here for reference. [return]
  2. http://m.mlb.com/news/article/160896926/statcast-spin-rate-compared-to-velocity/ [return]

Events and Dynamic UI

This week I focused on improving the usability of the Shiny app. I used Shiny’s renderUI function to dynamically generate the options for the game select dropdown. I also set up a baseball API here, and added another API endpoint to get the list of games that happened on any given day.

One of the nicer things I added to the Shiny app was the use of the tryCatch function to hide confusing error messages from the user, like this:

     splitGames <- unlist(strsplit(input$game, " @ "))
     away <- teams[which(grepl(splitGames[1], names(teams)))]
     home <- teams[which(grepl(splitGames[2], names(teams)))]

     data <- getEvents(year, month, day, away, home)
     sliderInput("event", "Event:",
                 min = 1, max = length(data$probabilities),
                 value = length(data$probabilities), sep = 1)
   }, error = function(e) {
     stop("That is not a valid event")

In reality, much of my week was spent fighting with R’s several methods of dealing with strings and arrays. Most of the changes this week were in the front end user-facing part of the app, so I’m excited to work more on the backend next week (maybe finally improving the win probability calculation?). As a side note, the app does generally work for spring training games (as long as Gameday is working at the moment).

An App!

This week I made a Shiny interface to the win probability and description services. It can be viewed here. To do this, I had to set up hosting for both Shiny and my Elixir backend service. I also switched from embedding/posting the plots on Plotly to embedding them in the new Shiny application, which should help reduce costs. Learning more about Shiny made implementing the interface relatively simple. The generation of the graphs is now abstracted into a function that grabs JSON data from the API server I set up, which makes it much easier to generate graphs for any desired game.

I would like to make several improvements to the graph next week. Including scores would be helpful. Perhaps most interesting would be implementing automatic “swing-point” detection and adding concise labels for the greatest win probability changes in a game. Finally, it would be nice if there were a way to view which teams played on which days, as right now it’s difficult to find a game that you want to graph.

Descriptions and Probabilities

This week I mostly focused on experimenting with the Retrosheet and MLB Gameday API formats. Using the baseball umbrella application combined with R and Plotly, I generated a win expectancy graph for a random game.

In any given situation, the win expectancy is equivalent to the percentage of games that teams in that exact situation went on to win. To calculate the win probability of a situation, the application converts the situation into a hash using the number of outs, inning, number and position of base runners, and the current score, then looks it up in a table of historical win probabilities. This table was calculated using Greg Stoll’s scripts and Retrosheet data. The code for the win expectancy service can be found here, and the general downloader’s source is here. What follows is an example of a manually-generated win expectancy graph of the game that occurred on May 10th of last season between the Padres and the Cubs.

First, we utilize the two functions probabilities_from_game/2 and descriptions_from_game/2 to get vectors of win expectancies and descriptions of every event that occurred in the game:

iex(1)> BaseballSlurper.Server.probabilities_from_game("2016-05-10", ["sdn", "chn"])
[[0.5652355266525733, 0.5826495329514373, 0.593064127007075],
 [0.5669081828973761, 0.5470361185179661, 0.534091190836288],
 [0.5569277494191046, 0.5742743999613658, 0.5803200220916497],...]

iex(2)> BaseballSlurper.Server.descriptions_from_game("2016-05-10", ["sdn", "chn"])
[["Jon Jay grounds out, second baseman Ben Zobrist to first baseman Anthony Rizzo.",
  "Wil Myers flies out to center fielder Dexter Fowler.",
  "Matt Kemp lines out to center fielder Dexter Fowler."],
 ["Dexter Fowler strikes out swinging.",
  "Jason Heyward lines out to center fielder Jon Jay.",
  "Kris Bryant lines out to right fielder Matt Kemp."],...]

We can then plug these two vectors into R to get an interactive win expectancy graph with Plotly:

Sys.setenv("plotly_username" = "benfb")
Sys.setenv("plotly_api_key" = API_KEY)
probabilities <- c(PROBABILITY_VECTOR)
x <- seq_along(y)
data <- data.frame(x, y, event)

kb <- list(
  xref = 'paper',
  yref = 'y',
  x = seq(0, 1, by=(1/length(x)))[23],
  y = probabilities[23],
  xanchor = 'right',
  yanchor = 'middle',
  text = 'K Bryant Double',
  font = list(family = 'Arial',
              size = 16,
              color = 'rgba(67,67,67,1)'),
  showarrow = FALSE)

ad <- list(
  xref = 'paper',
  yref = 'y',
  x = seq(0, 1, by=(1/length(x)))[72],
  y = probabilities[72],
  xanchor = 'right',
  yanchor = 'middle',
  text = 'A Dickerson Home Run',
  font = list(family = 'Arial',
              size = 16,
              color = 'rgba(67,67,67,1)'),
  showarrow = FALSE)

p <- plot_ly(
  x = seq_along(y),
  y = probabilities,
  type = 'scatter',
  mode = 'lines',
  text = event) %>%
  title = 'SDN @ CHN 2016-05-10',
  xaxis = list(title = 'Game Event'),
  yaxis = list(title = 'Home Team Win Probability')) %>%
layout(annotations = list(kb, ad))

This R code yields the following chart:

For comparison, the Fangraph’s win expectancy graph is here, and you can find the full video of the game on MLB’s YouTube channel1.

The next step will be to automate this process and look into self-hosting the graphs instead of going through Plotly’s rate-limited API.

  1. The date in the title of this video is actually incorrect. It’s actually the game from May 10th. [return]