Data Driven Research around NFL Dome Team Performance
Contributed by Daniel Donohue. Daniel was a student of the NYC Data Science Academy 12-week full-time data science bootcamp program from Sep. 23 to Dec. 18, 2015. This post was based on his first class project (due at the end of the 2nd week of the program).
Daniel Michael Donohue
4 November 2015
Introduction
The National Football League is unique among the four major American sports, in that its players’ images as modern-day gladiators often have the appropriate backdrop of driving wind, rain, snow, and cold. There are a handful of teams, however, that play their eight regular season home games inside domes, and it is not unreasonable to think that this might present some difficulty when moving outdoors late in the season.
Indeed, this is commonly discussed (perhaps anecdotally) as the NFL season gets late: “Sure, they do well in the comfort of their dome, but can they take their success on the road in poor weather conditions?” The goal of this first project was to investigate just that. Namely, can we see visually whether an NFL team that plays their home games in a climate-controlled environment inherently suffers some disadvantage when playing outdoors, especially in inclement weather?
The Dataset
To address this question, we used a dataset obtained with license from Armchair Analysis that is comprised of twenty-four csv files covering every aspect of every play, team, game, and player from 2000-2014. This is a fascinating dataset, and there’s certainly a lot of insight to be gleaned from it. We only used two of the files for this project, though: one containing general game information (score, weather conditions, etc.); and one containing very specific game statistics.
Average Margin
The most obvious metric of a team’s performance is the final margin of the score, so this is where we began. The first visualization of this benchmark that we made was a simple graph of average margin versus NFL season, one for dome teams playing in open-air stadiums, and one for the rest of the NFL teams when on the road. Before this, we need to prepare the dataset for graphing.
# Load the required packages, the datasets, and create character vectors of
# dome and outdoor teams.
library(dplyr)
library(reshape2)
library(ggplot2)
game <- read.csv("nfl_00-14/csv/game.csv", stringsAsFactors = FALSE)
team <- read.csv("nfl_00-14/csv/team.csv", stringsAsFactors = FALSE)
dome.teams <- c("ATL", "MIN", "NO", "STL", "DET", "IND", "ARI", "HOU")
outdoor.teams <- unique(filter(game, !(h %in% dome.teams))$h)
# Add a column to the game dataframe for final margin from the perspective of
# the visting team. A negative final margin indicates that the visiting
# team lost.
game <- mutate(game, v.margin=ptsv - ptsh)
# Create an object containing instances of dome teams playing in open-air
# stadiums, and outdoor teams playing away. Note that the Dallas Cowboys
# moved from an open-air stadium to a dome in the 2009 season.
dome.at.outdoor <- filter(game,
(v %in% dome.teams | (v == "DAL" & seas > 2008)) &
(h %in% outdoor.teams))
outdoor.away <- filter(game,
(v %in% outdoor.teams | (v == "DAL" & seas <= 2008)))
Next, we group the dataframes dome.at.outdoor and outdoor.away by season and summarize by the average on the v.margin (visiting margin) column:
dome.seas.margin <- group_by(dome.at.outdoor, seas) %>%
summarise(avg.away.margin=mean(v.margin))
outdoor.seas.margin <- group_by(outdoor.away, seas) %>%
summarise(avg.away.margin=mean(v.margin))
# Melt these into a single dataframe for ggplot.
away.margin <- melt(list(dome.seas.margin, outdoor.seas.margin),
id.var=c("seas", "seas"))
Finally, we are ready to create the first plot.
avg.visiting.margin <- ggplot(data=away.margin, aes(x=seas, y=value,
colour=factor(L1), group=factor(L1))) +
geom_line() +
scale_color_manual(name="Away Margin in the NFL",
breaks=c(1, 2),
labels=c("Dome Teams \n Playing Outdoors",
"Outdoor Stadium Teams' \n Away Margin"),
values=c('blue', 'orange')) +
theme_bw() +
xlab("Season") +
ylab("Average Margin") +
ggtitle("Average Margin of Victory in the NFL, 2000-2014")
avg.visiting.margin
There is pretty wild fluctuation for dome teams, yet most years they do underperform when compared to the rest of the league. It is also interesting to note that NFL teams lose on the road by a little under a field goal—at least some evidence that home-field advantage exists in the league.
Next, we want to see what the distribution of away margins are for dome teams and the rest of the NFL. Again, we first need to prepare a dataframe for visualization.
# Melt the dome and game dataframes into a single dataframe.
away.all <- melt(list(dome.at.outdoor, outdoor.away), id.vars=c("gid", "gid"),
measure.vars=c("v.margin", "v.margin"))
# Calculate the average margins because I'm going to overlay these on the
# density plots.
mean.away.all <- group_by(away.all, L1) %>%
summarise(mean.val=mean(value)) %>%
select(L1, mean.val)
And we obtain this:
visiting.density <- ggplot(data=away.all, aes(x=value,
fill=factor(L1))) +
geom_density(alpha=.2) +
scale_fill_manual(name="",
breaks=c(1, 2),
labels=c("Dome Teams \nPlaying Outdoors",
"Outdoor Stadium \nTeams Away"),
values=c("blue", "orange")) +
geom_vline(data=mean.away.all, aes(xintercept=mean.val,
colour=factor(L1)), linetype="dashed", size=.75, alpha=.5) +
scale_colour_manual(breaks=c(1, 2), values=c("blue", "orange")) +
theme_bw() +
xlab("Final Margin") +
ylab("Density") +
geom_text(data=mean.away.all, aes(x=4.1, y=.0375, label="-1.94 pts/game"),
color='orange', size=5) +
geom_text(data=mean.away.all, aes(x=-10, y=.0375, label="-4.04 pts/game"),
color='blue', size=5) +
ggtitle("Density Plots of Away Margin in the NFL, 2000-2014")
visiting.density
The distribution for outdoor teams appears fairly normal. The distribution for dome teams has somewhat of a negative skew, which indicates that they tend to be on the receiving end of more blowouts. This seems to be in line with the initial hypothesis that dome teams indeed perform worse on the road than the rest of the league, but is this difference in means statistically significant? To get an idea, we can perform a two-sample t-test, with the alternative hypothesis that the true value of the average dome team away margin is less than that of the rest of the NFL.
t.test(dome.at.outdoor$v.margin, outdoor.away$v.margin, alternative = "less")
##
## Welch Two Sample t-test
##
## data: dome.at.outdoor$v.margin and outdoor.away$v.margin
## t = -3.68, df = 1247.1, p-value = 0.0001216
## alternative hypothesis: true difference in means is less than 0
## 95 percent confidence interval:
## -Inf -1.16323
## sample estimates:
## mean of x mean of y
## -4.047736 -1.943072
The p-value is extremely small. We are therefore led to reject the null hypothesis that the means are the same across the two groups, in favor of the alternative hypothesis that the mean visiting margin of dome teams playing outdoors is significantly less than the mean visiting margin for the rest of the NFL.
Inclement Weather
As stated above, the argument that dome teams perform worse in open-air stadiums is largely based on the assumption that they aren’t as prepared to deal with inclement weather as their opponents. We therefore seek to compare various game statistics (passing yards, rushing yards, first downs, etc.) across different weather types. Since there are twenty-four unique game conditions in the original dataframe, we first group similar weather conditions using the following function to make the condition column less unwieldy.
weather = function(x) {
if(x %in% c("Chance Rain", "Light Rain", "Rain", "Thunderstorms")) {
return("Rain")
}
else if(x %in% c("Clear", "Fair", "Partly Sunny", "Mostly Sunny", "Sunny")) {
return("Clear")
}
else if(x %in% c("Closed Roof", "Dome")) {
return("Dome")
}
else if(x %in% c("Cloudy", "Partly Cloudy", "Mostly Cloudy")) {
return("Cloudy")
}
else if(x %in% c("Foggy", "Hazy")) {
return("Fog")
}
else {
return("Snow")
}
}
Next, the specific game statistics are in the team dataframe, while the game conditions are in the game dataframe. Luckily, both dataframes have a unique “game ID,” so we can join the two dataframes on that value.
dome.weather <- inner_join(dome.at.outdoor, team, "gid") %>%
# Filter out the outdoor teams that are hosting the dome teams.
filter(tname %in% dome.teams | (tname == "DAL" & seas > 2008)) %>%
# Select weather condition, visiting margin, points scores, rushing first downs,
# passing first downs, first downs obtained through penalties, rushing yardage,
# passing yardage, penalties committed, red zone attempts, red zone conversions,
# short third-down attempts, short third-down conversions, long third-down attempts, and
# long third-down conversions.
select(cond, v.margin, pts, rfd, pfd, ifd, ry, py, pen,
rza, rzc, s3a, s3c, l3a, l3c) %>%
# Add columns for total first downs, red zone efficiency, and adjusted third-down
# efficiency (which lends greater weight to long third-down conversions).
mutate(fd=rfd + pfd + ifd, rze=rzc / rza,
a3e= (s3c + 1.5 * l3c) / (s3a + l3a)) %>%
# Drop irrelevant columns.
select(-c(rfd, pfd, ifd, rza, rzc, s3a, s3c, l3a, l3c)) %>%
filter(cond != "") %>% # A few rows didn't have a condition recorded.
# Replace NaNs with 0 for teams that had no red zone attempts, and group
# similar weather conditions using the above-defined function.
mutate(rze=ifelse(is.nan(rze), 0, rze), cond=sapply(cond, weather)) %>%
group_by(cond) %>%
summarise_each(funs(mean))
Then, we melt the dataframe and plot it with ggplot2.
dome.weather <- melt(dome.weather, id.vars="cond")
levels(dome.weather$variable) <- c("Margin", "Points", "Rushing Yards",
"Passing Yards", "Penalty Yards", "First Downs", "Red Zone Efficiency",
"Adjusted Third Down Rate")
weather.stats <- ggplot(data=dome.weather, aes(x=factor(cond), y=value,
fill=factor(cond))) +
geom_bar(stat="identity", position="dodge") +
facet_wrap(~variable, nrow=3, scales="free") +
scale_fill_brewer(name="Condition", palette="RdYlBu") +
xlab("") +
ylab("") +
theme_bw() +
ggtitle("Dome Team Statistics in Different Conditions")
weather.stats
This visualization seems to be pretty telling. Dome teams achieve lower totals in nearly every category in the snow and rain than they do in other conditions, losing by more than ten points on average in the snow. It is curious to observe that dome teams lose by more than nine points when they play in other teams’ domes.
For completeness, we can create the same visualization for outdoor teams playing in various weather conditions.
Note the scale on the y-axes. Contrasted with dome teams, we do not see as substantial a drop off in statistics in inclement weather; in fact, we see that, for instance, outdoor teams rush the ball better in snow and rain.
Temperature
Finally, we make a simple smoothed plot of total yardage (rushing yardage plus passing yardage) versus temperature (for temperatures below freezing), since this is the last adverse weather condition we have not considered yet. Again, we first need to join the dome and outdoor dataframes with the team dataframe to extract the yardage totals.
dome.temp <- inner_join(dome.at.outdoor, team, "gid") %>%
mutate(tot.yds = ry + py) %>%
select(temp, tot.yds) %>%
filter(temp < 32)
outdoor.temp <- inner_join(outdoor.away, team, "gid") %>%
mutate(tot.yds = ry + py) %>%
select(temp, tot.yds) %>%
filter(temp < 32)
# Melt for ggplot2.
yds.temp <- melt(list(dome.temp, outdoor.temp),
id.var = c("temp", "temp"))
# Plot.
temp.smooth <- ggplot(data=yds.temp, aes(x=temp, y=value,
color=factor(L1), group=factor(L1))) +
geom_smooth(na.rm=TRUE, alpha=.1) +
scale_color_manual(name="", breaks=c(1, 2),
labels=c("Dome Teams",
"Outdoor Stadium Teams"),
values=c('blue', 'orange')) +
theme_bw() +
xlab("Temperature") +
ylab("Total Yards") +
ggtitle("Yardage in Low Temperatures") +
xlim(10, 32)
temp.smooth
The locally weighted smoothing function for dome teams playing outdoors is increasing on most of the interval [10, 32], which means that they gain less yards as the temperature drops. Conversely, outdoor stadium teams seem to gain more yards as temperatures approach the extremely low ranges. On the other hand, dome teams have higher yardage totals in temperatures approaching freezing.
Conclusion
The few visualizations that we constructed seem to suggest that dome teams do indeed perform worse in inclement weather and low temperatures. The next question to address is, then: Why? Is it simply because dome teams have been bad during 2000-2014, irrespective of the weather? Or perhaps are the rosters of dome teams constructed in a way that makes them excel in a dome setting, but leaves them vulnerable in hostile weather conditions?
A more rigorous analysis might take into account overall record, type of personnel, and might examine the dome team’s three units (offense, defense, and special teams) separately to see if there is a significant performance drop in any one area.
The skills I demoed here can be learned through taking Data Science with Machine Learning bootcamp with NYC Data Science Academy.