Dome Team Performance in the NFL

Daniel Donohue
Posted on Nov 5, 2015

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).

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

LineThere 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

DensityThe 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

DomeBarFacetThis 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.

OutdoorBarFacetNote 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

YardTempSmoothThe 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.

About Author

Daniel Donohue

Daniel Donohue

Daniel Donohue (A.B. Mathematics, M.S. Mathematics) spent the last three years as a Ph.D. student in mathematics studying topics in algebraic geometry, but decided a few short months ago that he needed a change in venue and career....
View all posts by Daniel Donohue >

Related Articles

Leave a Comment

No comments found.

View Posts by Categories


Our Recent Popular Posts


View Posts by Tags

#python #trainwithnycdsa 2019 airbnb Alex Baransky alumni Alumni Interview Alumni Reviews Alumni Spotlight alumni story Alumnus API Application artist aws beautiful soup Best Bootcamp Best Data Science 2019 Best Data Science Bootcamp Best Data Science Bootcamp 2020 Best Ranked Big Data Book Launch Book-Signing bootcamp Bootcamp Alumni Bootcamp Prep Bundles California Cancer Research capstone Career Career Day citibike clustering Coding Course Demo Course Report D3.js data Data Analyst data science Data Science Academy Data Science Bootcamp Data science jobs Data Science Reviews Data Scientist Data Scientist Jobs data visualization Deep Learning Demo Day Discount dplyr employer networking feature engineering Finance Financial Data Science Flask gbm Get Hired ggplot2 googleVis Hadoop higgs boson Hiring hiring partner events Hiring Partners Industry Experts Instructor Blog Instructor Interview Job Job Placement Jobs Jon Krohn JP Morgan Chase Kaggle Kickstarter lasso regression Lead Data Scienctist Lead Data Scientist leaflet linear regression Logistic Regression machine learning Maps matplotlib Medical Research Meet the team meetup Networking neural network Neural networks New Courses nlp NYC NYC Data Science nyc data science academy NYC Open Data NYCDSA NYCDSA Alumni Online Online Bootcamp Open Data painter pandas Part-time Portfolio Development prediction Prework Programming PwC python python machine learning python scrapy python web scraping python webscraping Python Workshop R R language R Programming R Shiny r studio R Visualization R Workshop R-bloggers random forest Ranking recommendation recommendation system regression Remote remote data science bootcamp Scrapy scrapy visualization seaborn Selenium sentiment analysis Shiny Shiny Dashboard Spark Special Special Summer Sports statistics streaming Student Interview Student Showcase SVM Switchup Tableau team TensorFlow Testimonial tf-idf Top Data Science Bootcamp twitter visualization web scraping Weekend Course What to expect word cloud word2vec XGBoost yelp