I’ve been a San Francisco Giants fan for as long as I can remember. And recently I had the opportunity to see my first in-person game of the season as the Giants took on the Miami Marlins at home on June 18, 2018. The week prior we had somehow lost three of four games to the Marlins in Miami, with Miami coming back in two of those games to score the winning runs in the 7th inning or later. The game I saw, sadly, was no different. We headed into the top of the 9th inning with a 4-2 lead and saw our “closer” (Hunter Strickland) give up three hits and three earned runs in 0.1 innings. Beautiful.

We lost that game 5-4, which, honestly, was not very surprising to me. After he got booed off the field by his home crowd, Strickland proceeded to punch a wall, break his hand, and land on the DL for 6-8 weeks. Generally this news would upset me, but I’ve never understood the appeal of Hunter Strickland, who should perhaps be more aptly labeled our “opener,” because he sure has a hard time closing games out. Maybe his absense will be a boon for us. And let’s not forget that as I type this, Miami is sitting comfortably in last place in the NL East, with a .398 win percentage (as of June 30, 2018). They are not a great team, yet we’re letting them beat us.

If you’re a Giants fan, I’m sure you share in my frustration here. And it’s no State secret that our bullpen has been lacking the last few seasons. Combine that with subpar run production, and you’ve got a recipe for disaster (think 2017). So I decided to take a look at how the Giants have started out through the first 83 games of this season.

Approach

My approach is simply to compare some key Giants pitching stats for this season to those from 2010-2017. Note, I’m not looking at individual players here, but the organization’s team performance. The Giants were World Series Champions in 2010, 2012, and 2014, but they had some definite off-years over that time frame as well (again, 2017 is the most obvious disaster). But this mix of ultimate success and pathetic failure should provide a nice landscape for comparison to see how we’ve started out 2018.

The data I’m using are the season-specific pitching data aggregated by team from baseball-reference.com. The pitching data provide some key indicators I was looking for (e.g., blown saves, earned runs against, and hits against), as well as some sense of our offensive production and overall success (e.g., our runs per game and total wins vs. losses). I’m using R to clean up the data and generate plots (using ggplot). The code I used has been provided at the bottom of the post and the data can be downloaded here. By plotting time series of some of these indicators I’m hoping to be able to ascertain whether the Giants have started off 2018 in their dominate 2010, 2012, and 2014 fashion, their lowly 2017 fashion, or somewhere in between.

Results

In each of the following plots, a line chart is provided for each season from 2010-2018. A point is provided on each plot to show where the Giants were that season after the 83rd game, which makes it a little easier to compare previous seasons to our current status in 2018. The seasons have also been labeled with an indicator of playoff success:

  • WSC: World Series Champions (2010, 2012, and 2014)
  • NLDS: Lost in National League Division Series (2016)
  • DNP: Did not make playoffs (2011, 2013, 2015, and 2017)

We’ll first look at a couple overarching indicators of success, which incorporate not only our pitching but also our offensive production: Totals wins and cumulative run differential (total opponent runs subtracted from total Giants runs over the course of the season). In the wins plot, it is encouraging to see that we are clearly outpacing our 2017 season in terms of total wins through 83 games. Additionally, we have about the same number of wins through 83 games as our World Series Champion 2010 team, but that is no guarantee of even making the playoffs. Our division has strengthened over the past couple of years, and that has made it much more difficult on the Giants to even grab hold of a Wilcard spot, let alone win the NL West. In 2011, we had more wins through 83 games, but didn’t even make the playoffs. In 2015, we had only slightly less wins through 83 games, and we again failed to make the playoffs. But, let’s hope that we can keep this upward trend going over the latter half of the season and earn ourselves a rightful place at the top of the NL West.

Cumulative wins by season and game number

The cumulative run differential plot is very interesting. In 2013 and 2017 we were outscored by more than 50 runs over the course of the season (almost 150 runs in 2017!), and it’s no surprise that we did not make the playoffs. In 2011 we more or less broke even, and again missed the playoffs. Our World Series Champion seasons, and even our 2016 season when we lost in the NLDS, all had run differentials of +50 runs or more by the end of the season. This is perhaps not surprising. What is surprising is that our run differential in 2015 was about +75—on par with our 2012 and 2016 seasons—and yet we didn’t even make the playoffs! Finally, it’s worth noting that in the 2010s we have yet to make the playoffs when we have a negative run differential by the end of the season. So far for 2018 our differential is about -25, which is not encouraging. We have seen a slight uptick in our run production recently, and we can hope that continues on.

Cumulative run differential by season and game number

Next, let’s look a bit closer at our pitching, in particular. One metric to consider is the team’s cumulative earned run average (ERA) per nine innings over the course of the year. In this case, we want to see really low ERA values. For our World Series years, the cumulative ERA by the end of the season was no more than four earned runs per nine innings. However, even in 2011 and 2015 the Giants had cumulative ERAs less than 4, but the team didn’t make the playoffs. This suggests that our run production was really poor in those years—which the run differential plot actually refutes—and/or the rest of the division played really well in those seasons. As it turns out, we came in 2nd to the Diamondbacks’ 94 wins in 2011 in the NL West, and the 2nd place teams in the NL East and NL Central divisions both had more wins than our 86. In 2015 our 84 wins came in 2nd to the Dodgers in the NL West, and the NL Central division was stacked, with three teams earning at least 97 wins that season (Cubs, Pirates, and Cardinals). Looking at 2018, our cumulative ERA peaked (so far) around five earned runs per nine innings (at about the 50th game), but it has since been in decline. This is great news, and with Bumgarner back in the rotation I am hopeful we can keep driving that cumulative ERA down.

Cumulative earned run average per nine innings by season and game number

Another key aspect of our pitching is the ability to win close games, i.e., earn saves. According to the MLB Glossary:

A save is awarded to the relief pitcher who finishes a game for the winning team, under certain circumstances…A relief pitcher recording a save must preserve his team’s lead while doing one of the following:

  • Enter the game with a lead of no more than three runs and pitch at least one inning.
  • Enter the game with the tying run in the on-deck circle, at the plate or on the bases.
  • Pitch at least three innings.

The saves plot below shows that the Giants have earned 25 saves through 83 games this season. This is up from 2013, 2015, and 2017, all seasons in which the Giants did not make the playoffs. However, 25 saves is quite a bit lower than the 29 or more saves recorded by the 83rd game of our three World Series seasons and even 2016 (34 saves by the 83rd game). Given our run production so far this season, I think we’ll need to continue racking up saves at a good clip if we want to make the playoffs. With Mark Melancon (our real closer) back in the lineup after his injury, I’m again hopeful that we can close out games in dominant fashion going forward.

Cumulative saves by season and game number

Finally, we’ll take a look at what prompted me to write this post: Blown saves. According to the MLB Glossary:

A blown save occurs when a relief pitcher enters a game in a save situation, but allows the tying run to score.

Blown saves do not necessarily result in a loss: A pitcher can blow a save, and his team can subsequently score more runs to eventually win the game. In this case, I wanted to look at specifically how many blown saves we have had so far this season that have resulted in a loss. This describes what I witnessed at the hands of Hunter Strickland on June 18th.

Looking at the plot below, we have already blown four saves for loss this season. This is on par with last season, 2013, and even our World Series winning season in 2012. Actually, through 83 games in 2014 (another of our World Series Champion seasons), we had already had five blown saves for loss. That provides a silver lining, and this is another instance where I’m hoping that Melancon will be able to curb our blown saves through the rest of this season.

Cumulative blown saves resulting in a loss by season and game number

Conclusion

We’ve looked at quite a few different metrics here to try and gauge how the Giants have done through their first 83 games this season. None of these metrics is perfect, and we didn’t even touch much on our offensive production nor defense. But, from what we have looked at, I’m encouraged:

  • We currently sit 3rd in the NL West behind the Diamondbacks and Dodgers (boo!), only 4.5 games out of 1st place.
  • We are 8-2 in our last 10 games. If we can keep this up going into the All-Star Break, we should put ourselves in good position for the 2nd half of the season.
  • Our team’s cumulative ERA has been dropping rapidly since about the 50th game this season. With Bumgarner and Melancon back, I’m expecting a continuing decline going forward.
  • Our blown saves for loss through 83 games are on par with our World Series-winning 2012 and 2014 seasons, which is to say that our blown saves has not blown us out of the water yet (if history has anything to say). With Melancon, we should be able to keep those blown saves down for the rest of the season.

My Projection: This will be the first year since 2012 that we unseed the Dodgers atop the NL West. If we pulled together a decent first 83 games with substantial injuries to Bumgarner, Melancon, and Evan Longoria, I think we’ll pull off a good second half of the season now that we’ve got Bumgarner and Melancon back (Longoria is expected to return sometime in August).

Appendix: R Code

library(tidyverse)

data <- read.csv('Giants_2010s_PitchingData.csv')

# Only select necessary columns
data_useful <- data %>%
  select(Year, Rk, Rslt, IP, H, R,
         ER, UER, BB, SO, HR, HBP,
         Pitchers.Used..Rest.GameScore.Dec.)

# Make Year a factor and add season result notation
data_useful$Year <- factor(data_useful$Year,
                           levels=c("2010", "2011", "2012", "2013",
                                    "2014", "2015", "2016", "2017", "2018"))
levels(data_useful$Year)[levels(data_useful$Year)=="2010"] <- "2010\nWSC"
levels(data_useful$Year)[levels(data_useful$Year)=="2011"] <- "2011\nDNP"
levels(data_useful$Year)[levels(data_useful$Year)=="2012"] <- "2012\nWSC"
levels(data_useful$Year)[levels(data_useful$Year)=="2013"] <- "2013\nDNP"
levels(data_useful$Year)[levels(data_useful$Year)=="2014"] <- "2014\nWSC"
levels(data_useful$Year)[levels(data_useful$Year)=="2015"] <- "2015\nDNP"
levels(data_useful$Year)[levels(data_useful$Year)=="2016"] <- "2016\nNLDS"
levels(data_useful$Year)[levels(data_useful$Year)=="2017"] <- "2017\nDNP"

# Get win/loss and number of runs for each team
# and then remove the Rslt and R columns
data_useful <- data_useful %>% 
  mutate(Win_or_Loss = substr(Rslt,0,1),
         SF_Runs = as.integer(gsub("[A-Z]", "", gsub("-[0-9]*", "", Rslt))),
         Opp_Runs = R) %>% 
  select(-c(Rslt,R))

# Extract data on saves and blown saves
# and then remove the Pitchers.Used..Rest.GameScore.Dec. column
data_useful <- data_useful %>% 
  mutate(Saves = ifelse(grepl("Sv", Pitchers.Used..Rest.GameScore.Dec.), 1, 0),
         Blown_Saves_Won = ifelse(grepl("BW", Pitchers.Used..Rest.GameScore.Dec.), 1, 0),
         Blown_Saves_Lost = ifelse(grepl("BL", Pitchers.Used..Rest.GameScore.Dec.), 1, 0)) %>% 
  select(-c(Pitchers.Used..Rest.GameScore.Dec.))

# plots
# Get last game number for 2018
last_game <- max(data_useful[data_useful$Year=="2018", "Rk"])

cumulative_era_plot <- data_useful %>%
  group_by(Year) %>%
  mutate(Cumulative_Annual_IP = cumsum(IP),
         Cumulative_Annual_Runs_Against = cumsum(Opp_Runs),
         Running_ERA = Cumulative_Annual_Runs_Against / Cumulative_Annual_IP) %>% 
  ggplot(aes(x = Rk, y = Running_ERA * 9, color = Running_ERA)) +
  geom_line(size = 1.2) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Cumulative ERA") +
  scale_color_gradient(low = "black", high = "red") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size= 14))

run_differential_plot <- data_useful %>%
  group_by(Year) %>%
  mutate(Cumulative_Annual_Run_Differential = cumsum(SF_Runs - Opp_Runs)) %>% 
  ggplot(aes(x = Rk, y = Cumulative_Annual_Run_Differential, color = Cumulative_Annual_Run_Differential)) +
  geom_line(size = 1.2) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Cumulative Run Differential (Giants - Opponents)") +
  scale_color_gradient(low = "red", high = "black") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14))

blown_saves_lost_data <- data_useful %>%
  group_by(Year) %>%
  mutate(Blown_Saves_Lost = cumsum(Blown_Saves_Lost))
blown_saves_lost_intercepts <- blown_saves_lost_data %>% 
  filter(Rk==last_game) %>%
  select(Blown_Saves_Lost)
blown_saves_lost_plot <- blown_saves_lost_data %>%
  ggplot(aes(x = Rk, y = Blown_Saves_Lost, color = Blown_Saves_Lost)) +
  geom_step(size = 1.2,) +
  geom_point(aes(x = last_game, y = Blown_Saves_Lost),
             shape = 21,
             size = 3,
             stroke = 3,
             colour = "black",
             fill = "white",
             data = blown_saves_lost_intercepts) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Blown Saves resulting in Loss") +
  scale_color_gradient(low = "black", high = "red") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14)) +
  scale_y_continuous(breaks = c(0, 2, 4, 6, 8, 10))

hits_against_data <- data_useful %>%
  group_by(Year) %>%
  mutate(Hits_Against = cumsum(H))
hits_against_intercepts <- hits_against_data %>% 
  filter(Rk==last_game) %>%
  select(Hits_Against)
hits_against_plot <- hits_against_data %>% 
  ggplot(aes(x = Rk, y = Hits_Against, color = Hits_Against)) +
  geom_line(size = 1.2) +
  geom_point(aes(x = last_game, y = Hits_Against),
             shape = 21,
             size = 3,
             stroke = 3,
             colour = "black",
             fill = "white",
             data = hits_against_intercepts) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Opponent Hits") +
  scale_color_gradient(low = "black", high = "red") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14))

unearned_runs_against_data <- data_useful %>%
  group_by(Year) %>%
  mutate(Unearned_Runs_Against = cumsum(UER))
unearned_runs_against_intercepts <- unearned_runs_against_data %>% 
  filter(Rk==last_game) %>%
  select(Unearned_Runs_Against)
unearned_runs_against_plot <- unearned_runs_against_data %>% 
  ggplot(aes(x = Rk, y = Unearned_Runs_Against, color = Unearned_Runs_Against)) +
  geom_line(size = 1.2) +
  geom_point(aes(x = last_game, y = Unearned_Runs_Against),
             shape = 21,
             size = 3,
             stroke = 3,
             colour = "black",
             fill = "white",
             data = unearned_runs_against_intercepts) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Opponent Unearned Runs") +
  scale_color_gradient(low = "black", high = "red") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14))

wins_data <- data_useful %>%
  group_by(Year) %>%
  mutate(Wins = cumsum(ifelse(Win_or_Loss=="W",1,0)))
wins_intercepts <- wins_data %>% 
  filter(Rk==last_game) %>%
  select(Wins)
wins_plot <- wins_data %>% 
  ggplot(aes(x = Rk, y = Wins)) +
  geom_line(size = 1.2) +
  geom_point(aes(x = last_game, y = Wins),
             shape = 21,
             size = 3,
             stroke = 3,
             colour = "black",
             fill = "white",
             data = wins_intercepts) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "SF Wins") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14))

saves_data <- data_useful %>%
  group_by(Year) %>%
  mutate(Saves = cumsum(Saves))
saves_intercepts <- saves_data %>% 
  filter(Rk==last_game) %>%
  select(Saves)
saves_plot <- saves_data %>% 
  ggplot(aes(x = Rk, y = Saves)) +
  geom_line(size = 1.2) +
  geom_point(aes(x = last_game, y = Saves),
             shape = 21,
             size = 3,
             stroke = 3,
             colour = "black",
             fill = "white",
             data = saves_intercepts) +
  facet_grid(. ~ Year) +
  labs(x = "Game Number", y = "Saves") +
  guides(color=FALSE) +
  theme(axis.text = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 14))