Introduction
Our project examines NCAA Division 1 football teams who made head
coaching changes in the last ten seasons. Our goal is to measure the
impact that first year head coaches have during their first season with
a new team. We plan to do this by comparing various measures of success
such as win percentage, touchdown differential, turnover differential,
and offensive/defensive rank to the season before under the previous
head coach. Our project is inspired by the recent success Duke’s
football team has seen after the hiring of head coach Mike Elko after
numerous sub-par seasons.
Data Gathering and Cleaning
We started by gathering data from https://www.sports-reference.com/cfb/ which has college
football data dating back to 2013 on both teams and coaches. We
downloaded the csv’s of every college football season available as well
as all of the head coach data that was available. After cleaning our
datasets to eliminate potential conflicts with joining the data, we were
able to combine our datasets of different seasons into two main datasets
with all of the team data and all of the coach data. Then, we were able
to join the two new datasets (coach_data
and
cfb_data
) into one big dataset (big_data
) with
all of the team data and coach’s record from each season dating back to
2013.
#Load packages
library(tidyverse)
library(readr)
library(dplyr)
library(knitr)
library(stringr)
library(data.table)
library(broom)
library(gghighlight)
library(gapminder)
library(patchwork)
#Load CSVs and Initial Data Cleaning
coach13 <- read_csv("Data/2013_Coaches.csv")
coach14 <- read_csv("Data/2014_Coaches.csv")
coach15 <- read_csv("Data/2015_Coaches.csv")
coach16 <- read_csv("Data/2016_Coaches.csv") %>%
select(-Year) %>%
mutate(Year = 2016)
coach17 <- read_csv("Data/2017_Coaches.csv")
coach18 <- read_csv("Data/2018_Coaches.csv")
coach19 <- read_csv("Data/2019_Coaches.csv")
coach20 <- read_csv("Data/2020_Coaches.csv")
coach21 <- read_csv("Data/2021_Coaches.csv")
cfb13 <- read_csv("Data/cfb13.csv") %>%
mutate(Year = 2013) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb14 <- read_csv("Data/cfb14.csv") %>%
mutate(Year = 2014) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb15 <- read_csv("Data/cfb15.csv") %>%
mutate(Year = 2015) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb16 <- read_csv("Data/cfb16.csv") %>%
mutate(Year = 2016) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb17 <- read_csv("Data/cfb17.csv") %>%
mutate(Year = 2017) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb18 <- read_csv("Data/cfb18.csv") %>%
mutate(Year = 2018) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb19 <- read_csv("Data/cfb19.csv") %>%
mutate(Year = 2019) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb20 <- read_csv("Data/cfb20.csv") %>%
mutate(Year = 2020) %>%
mutate(Team = case_when(
Team == "Miami (FL) (ACC)" ~ "Miami_FL (ACC)",
Team == "Miami (OH) (MAC)" ~ "Miami_OH (MAC)",
TRUE ~ Team
)) %>% separate(col = "Team", into = "Team", sep = " [(]")
cfb21 <- read_csv("Data/cfb21.csv") %>%
mutate(Year = 2021) %>%
select(-"...1") %>%
separate(col = "Team", into = "Team", sep = " [(]")
#Combine datasets to make one big dataset
names(cfb21) <- make.names(names(cfb21), unique=TRUE)
cfb_data <- bind_rows(cfb13, cfb14, cfb15, cfb16, cfb17, cfb18, cfb19, cfb20,
cfb21)
coach_data <- bind_rows(coach13, coach14, coach15, coach16, coach17, coach18,
coach19, coach20, coach21)
names(coach_data)[3] <- "Team"
cfb_data <- cfb_data %>%
select(-c("...41", "Kickoff.Return.Def.Rank", "Opp.Kickoff.Returns",
"Kickoff.Touchbacks", "Opponent.Kickoff.Return.Yards",
"Opp.Kickoff.Return.Touchdowns.Allowed",
"Avg.Yards.per.Kickoff.Return.Allowed", "Win.Loss", "NA.",
"Interceptions.Thrown.y"))
coach_data <- coach_data %>%
filter(W + L > 7)
cfb_data[cfb_data == "App State"] <- "Appalachian State"
cfb_data[cfb_data == "Appalachian St."] <- "Appalachian State"
cfb_data[cfb_data == "Arizona St."] <- "Arizona State"
cfb_data[cfb_data == "Arkansas St."] <- "Arkansas State"
cfb_data[cfb_data == "Army West Point"] <- "Army"
cfb_data[cfb_data == "Ball St."] <- "Ball State"
cfb_data[cfb_data == "Boise St."] <- "Ball State"
cfb_data[cfb_data == "Central Mich."] <- "Central Michigan"
cfb_data[cfb_data == "Coastal Caro."] <- "Coastal Carolina"
cfb_data[cfb_data == "Colorado St."] <- "Colorado State"
cfb_data[cfb_data == "UConn"] <- "Connecticut"
cfb_data[cfb_data == "Eastern Mich."] <- "Eastern Michigan"
cfb_data[cfb_data == "Fla. Atlantic"] <- "Florida Atlantic"
cfb_data[cfb_data == "FIU"] <- "Florida International"
cfb_data[cfb_data == "Florida St."] <- "Florida State"
cfb_data[cfb_data == "Fresno St."] <- "Fresno State"
cfb_data[cfb_data == "Ga. Southern"] <- "Georgia Southern"
cfb_data[cfb_data == "Georgia St."] <- "Georgia State"
cfb_data[cfb_data == "Iowa St."] <- "Iowa State"
cfb_data[cfb_data == "Kansas St."] <- "Kansas State"
cfb_data[cfb_data == "Kent St."] <- "Kent State"
cfb_data[cfb_data == "La.-Monroe"] <- "Louisiana-Monroe"
cfb_data[cfb_data == "ULM"] <- "Louisiana-Monroe"
cfb_data[cfb_data == "Michigan St."] <- "Michigan State"
cfb_data[cfb_data == "Middle Tenn."] <- "Middle Tennessee State"
cfb_data[cfb_data == "Mississippi St."] <- "Mississippi State"
cfb_data[cfb_data == "New Mexico St."] <- "New Mexico State"
cfb_data[cfb_data == "NIU"] <- "Norhtern Illinois"
cfb_data[cfb_data == "Northern Ill."] <- "Norhtern Illinois"
cfb_data[cfb_data == "Ohio St."] <- "Ohio State"
cfb_data[cfb_data == "Oklahoma St."] <- "Oklahoma State"
cfb_data[cfb_data == "Oregon St."] <- "Oregon State"
cfb_data[cfb_data == "Penn St."] <- "Penn State"
cfb_data[cfb_data == "San Diego St."] <- "San Diego State"
cfb_data[cfb_data == "San Jose St."] <- "San Jose State"
cfb_data[cfb_data == "South Fla."] <- "South Florida"
cfb_data[cfb_data == "Southern Miss."] <- "Southern Mississippi"
cfb_data[cfb_data == "Southern California"] <- "USC"
cfb_data[cfb_data == "Western Mich."] <- "Western Michigan"
cfb_data[cfb_data == "Western Ky."] <- "Western Kentucky"
cfb_data[cfb_data == "Washington St."] <- "Washington State"
cfb_data[cfb_data == "Utah St."] <- "Utah State"
cfb_data[cfb_data == "Texas St."] <- "Texas State"
coach_data[coach_data == "Bowling Green State"] <- "Bowling Green"
coach_data[coach_data == "Brigham Young"] <- "BYU"
coach_data[coach_data == "North Carolina State"] <- "NC State"
coach_data[coach_data == "Pitt"] <- "Pittsburgh"
coach_data[coach_data == "Miami (FL)"] <- "Miami_FL"
coach_data[coach_data == "Miami (OH)"] <- "Miami_OH"
big_data <- inner_join(cfb_data, coach_data, by = c("Team", "Year")) %>%
select(c(Team, Coach, W, L, Pct, Year, Conf, Total.Points, Points.Allowed,
Off.Rank, Off.Yards.Play, Turnover.Margin, Def.Rank, Touchdowns,
Penalties, Penalty.Yards.Per.Game, Yards.Play.Allowed,
Off.TDs.Allowed, Scoring.Off.Rank, Avg.Points.per.Game.Allowed,
Touchdowns.Allowed))
Modeling and Data Analysis
To examine the impact of first year coaches, we first need to
identify seasons in which the head coach is in his first year. To do
this, we created a for loop that checks if the previous observation is
of the same team and a different coach. Because the observations are
ordered by school name and then year, we know that two observations of
the same school indicate back-to-back seasons. If the previous row was
the same team and a different head coach, then we assign the indicator
variable new_coach
to be 1. If the coach is the same as the
previous year or if the observation above is a different team (there is
no older data for the team being examined), then new_coach
is assigned a value of 0. After we identified rows that were
back-to-back seasons, we created lag variables for certain statistics
from the season before including touchdowns, turnover differential, win
percentage, touchdowns allowed, and offensive and defensive rank. Then,
each observation that also had data from the previous season contained
the statistics of the first season under the new head coach as well as
the statistics from the final season of the old head coach. For the
statistics we were considering, we created delta
variables
that measure the change in the statistic from previous season. For
example, delta_pct
is the winning percentage of the first
season under the new head coach minus the winning percentage of the last
season under the old coach. We then created linear regression models to
test if new_coach
is a significant predictor on any of the
delta
variables.
#Determining new coaches
new_coach = c()
big_data <- big_data %>%
arrange(Team, Year)
for (i in 2:nrow(big_data)){
if ((big_data$Team[i] == big_data$Team[i-1]) &
(big_data$Coach[i] != big_data$Coach[i-1])){
new_coach = append(new_coach, 1)
}
else{
new_coach = append(new_coach, 0)
}
}
#Making new_coach a factor variable
big_data <- big_data[-1, ] %>%
cbind(new_coach)
big_data <- big_data %>%
mutate(new_coach = as.factor(new_coach))
old_data <- big_data %>% select(c("Turnover.Margin", "Touchdowns",
"Scoring.Off.Rank"))
new_data <- paste("lag", old_data, sep = ".")
#Create lag variables from last year stats, delta variables
big_data <-
big_data %>%
group_by(Team) %>%
mutate(lastyr_TD = lag(Touchdowns, n = 1, default = NA),
lastyr_TOmargin = lag(Turnover.Margin, n = 1, default = NA),
lastyr_pct = lag(Pct, n = 1, default = NA),
lastyr_TD.allowed = lag(Off.TDs.Allowed, n = 1, default = NA),
lastyr_Offrank = lag(Off.Rank, n = 1, default = NA),
lastyr_Defrank = lag(Def.Rank, n = 1, default = NA),
delta_TOmargin = Turnover.Margin - lastyr_TOmargin,
delta_TD = Touchdowns - lastyr_TD,
delta_pct = Pct - lastyr_pct,
delta_TD.allowed = Off.TDs.Allowed - lastyr_TD.allowed,
delta_Offrank = Off.Rank - lastyr_Offrank,
delta_Defrank = Def.Rank - lastyr_Defrank) %>%
filter(Year != 2013)
#Linear models to test significance of new_coach on different stats
lm.1 <- lm(delta_pct ~ new_coach, data = big_data)
lm.2 <- lm(delta_TD.allowed ~ new_coach, data = big_data)
lm.3 <- lm(delta_TD ~ new_coach, data = big_data)
lm.4 <- lm(delta_TOmargin ~ new_coach, data = big_data)
lm.5 <- lm(delta_Offrank ~ new_coach, data = big_data)
lm.6 <- lm(delta_Defrank ~ new_coach, data = big_data)
tidy(lm.1) %>%
kable(digits = 3)
(Intercept) |
0.007 |
0.008 |
0.928 |
0.354 |
new_coach1 |
-0.031 |
0.018 |
-1.741 |
0.082 |
tidy(lm.2) %>%
kable(digits = 3)
(Intercept) |
-0.208 |
0.422 |
-0.492 |
0.623 |
new_coach1 |
0.553 |
0.970 |
0.570 |
0.569 |
tidy(lm.3) %>%
kable(digits = 3)
(Intercept) |
0.462 |
0.559 |
0.827 |
0.409 |
new_coach1 |
-3.281 |
1.284 |
-2.555 |
0.011 |
tidy(lm.4) %>%
kable(digits = 3)
(Intercept) |
0.205 |
0.348 |
0.590 |
0.556 |
new_coach1 |
-1.147 |
0.799 |
-1.435 |
0.152 |
tidy(lm.5) %>%
kable(digits = 3)
(Intercept) |
-0.871 |
1.349 |
-0.646 |
0.519 |
new_coach1 |
4.731 |
3.099 |
1.527 |
0.127 |
tidy(lm.6) %>%
kable(digits = 3)
(Intercept) |
-0.047 |
1.35 |
-0.034 |
0.973 |
new_coach1 |
3.701 |
3.10 |
1.194 |
0.233 |
Our model outputs above do not show much about
new_coach
, but it does show that new_coach
is
significant in predicting the change in touchdowns scored using an \(\alpha\)-level of 0.05. The coefficient is
negative, which means that teams with new coaches are statistically more
likely to see a regression in touchdowns from the year before than teams
with returning coaches. However, there are many outside lurking
variables as well as numerous different types of head coaching changes
as we mention in the discussion. For this reason, it is very hard to
group every new coaching observation together and even harder to make
generalized predictions about teams using only whether or not they have
a new head coach. Still, it makes sense that teams with a new head coach
would score less on average than teams without a new head coach.
Visualizations
annotations <- data.frame(
xpos = c(-Inf,-Inf,Inf,Inf),
ypos = c(-Inf, Inf,-Inf,Inf),
annotateText = c("Good Offense, Good Defense",
"Good Offense, Bad Defense",
"Bad Offense, Good Defense",
"Bad Offense, Bad Defense"),
hjustvar = c(0,0,1,1) ,
vjustvar = c(0,1,0,1))
big_data %>%
ggplot(aes(x=Off.Rank, y=Def.Rank)) +
geom_point(color = "red") +
gghighlight::gghighlight(new_coach == 1) +
scale_x_continuous(breaks = seq(0, 150, 10)) +
scale_y_continuous(breaks = seq(0, 150, 10)) +
geom_vline(xintercept = 65, linetype ="dashed") +
geom_hline(yintercept = 65, linetype ="dashed") +
labs(title = "Offensive and Defensive Rank",
subtitle = "Highlighted by New Head Coach",
x = "Offensive Rank", y = "Defensive Rank") +
geom_text(data=annotations,
aes(x=xpos,y=ypos,hjust=hjustvar,vjust=vjustvar,label=annotateText))

big_data %>%
group_by(new_coach) %>%
ggplot(aes(x = new_coach, y = delta_TD)) +
geom_boxplot() +
labs(title = "Touchdown Differencial from Previous Year",
subtitle = "Grouped by New Head Coach (1) or Returning (0)",
x = "New Coach", y = "Touchdown Difference")

big_data %>%
group_by(new_coach) %>%
ggplot(aes(x = new_coach, y = delta_pct)) +
geom_boxplot() +
labs(title = "Win Percentage Difference from Previous Year",
subtitle = "Grouped by New Head Coach (1) or Returning (0)",
x = "New Coach", y = "Win Pct Difference")

big_data %>%
group_by(new_coach) %>%
ggplot(aes(x = new_coach, y = delta_TOmargin)) +
geom_boxplot() +
labs(title = "Turnover Differencial from Previous Year",
subtitle = "Grouped by New Head Coach (1) or Returning (0)",
x = "New Coach", y = "Turnover Difference")

test_data <- big_data %>%
mutate(include = case_when(
new_coach == 1 ~ 1,
lead(new_coach, 1) == 1 ~ 1,
T ~ 0
)) %>%
filter(include == 1) %>%
mutate(group_no = 0)
for(i in seq(1, nrow(test_data), 2)){
test_data[i, 32] = i
test_data[i+1, 32] = i
i = i+2
}
We can see from the boxplots that there is a small difference when
comparing the change in win percentage, touchdown differential, and
turnover differential from year to year for new head coaches
vs. returning head coaches. As we discussed before, this is to be
expected when comparing all new head coaches to all previous head
coaches. In the next part, we will compare new head coaches only to head
coaches in their final year (before they either left or got fired).
Further Modeling
lm.1t <- lm(delta_pct ~ new_coach, data = test_data)
lm.2t <- lm(delta_TD.allowed ~ new_coach, data = test_data)
lm.3t <- lm(delta_TD ~ new_coach, data = test_data)
lm.4t <- lm(delta_TOmargin ~ new_coach, data = test_data)
lm.5t <- lm(delta_Offrank ~ new_coach, data = test_data)
lm.6t <- lm(delta_Defrank ~ new_coach, data = test_data)
tidy(lm.1t) %>%
kable(digits = 3)
(Intercept) |
-0.046 |
0.018 |
-2.598 |
0.010 |
new_coach1 |
0.022 |
0.024 |
0.935 |
0.351 |
tidy(lm.2t) %>%
kable(digits = 3)
(Intercept) |
154.435 |
7.618 |
20.272 |
0.000 |
new_coach1 |
0.121 |
10.241 |
0.012 |
0.991 |
tidy(lm.3t) %>%
kable(digits = 3)
(Intercept) |
-1.375 |
1.364 |
-1.008 |
0.314 |
new_coach1 |
-1.444 |
1.828 |
-0.790 |
0.430 |
tidy(lm.4t) %>%
kable(digits = 3)
(Intercept) |
-0.787 |
0.838 |
-0.939 |
0.349 |
new_coach1 |
-0.155 |
1.123 |
-0.138 |
0.890 |
tidy(lm.5t) %>%
kable(digits = 3)
(Intercept) |
4.132 |
3.411 |
1.211 |
0.227 |
new_coach1 |
-0.273 |
4.571 |
-0.060 |
0.952 |
tidy(lm.6t) %>%
kable(digits = 3)
(Intercept) |
2.037 |
3.382 |
0.602 |
0.547 |
new_coach1 |
1.618 |
4.532 |
0.357 |
0.721 |
When we test for significance using only observations where there was
a new coach and the season before, there is even less of a relationship
between new_coach
and the various delta values. This could
potentially be a good sign as this shows that while new coaches
generally do slightly worse than the coach from the previous season,
they do not perform as bad compared to looking at all returning coaches
and not just ones who left or got fired. This issue of coaches leaving
vs. getting fired is something we will discuss later, but this is
another example of the various technicalities regarding new coaches that
we have discovered as
Discussion
Both the above visualizations and regressions show that there is a
very weak, or even non-existent, relationship between a team having a
new coach and performance versus the previous year. Again, this is not
surprising given the two conflicting ways a new coach is needed in a
program: the old coach was bad enough to get fired, or was good enough
that he was poached by another program. We discuss this more in
limitations, but the difficulties of transforming a team in just one
year should be obvious, so it is again unsurprising that we don’t see
particularly noteworthy changes year-to-year with new coaches versus off
seasons without a coaching transition.
Limitations and Future Research
While this project allowed us a glimpse into the impact that first
year head coaches have on a college football program, there were many
limitations to our research that we would try to fix. First of all,
college football hires happen in many different ways and at various
points of the year. While some new head coaches take over in the middle
of the season or even before one bowl game, most coaches have an entire
off-season to get used to things in their new position. Because of these
discrepancies, we only considered first year coaches who coached at
least six games, meaning any coach who took over late in the season
didn’t count in our model and their first “official” season in our model
was their first season in which they coached eight games.
Next, we realized there are different reasons coaches depart teams
and it is likely unfair to treat them all the same. There is a very big
distinction between a coach’s last season being because he got fired
vs. because he took a better job based on his success. On the other
hand, vacated jobs can either be filled by coaches on the rise or a
coach who is a replacement for a recent hire elsewhere. For example, if
a successful mid-major coach leaves for a power five school, that coach
usually takes over for a struggling power five team with expectations to
do better than recent years. However, the program they’re leaving
usually has to settle for a worse coach and has lower expectations than
the power five team who hired the new coach. Because of this, we tried
aggregating our results to just power 5 or group of 5 teams with the
belief that these programs more rarely have successful coaches poached
by other programs, but there are still many different teams with
different histories and definitions of success which makes it very hard
to see any noticeable results. While it was difficult to classify these
distinctions with the data we have, this is something that would be
interesting to look at in the future.
Lastly, we realized only analyze a coach’s first year on the job
likely does not reflect the whole picture, as rebuilds are generally a
process that takes multiple years. Given that head coaching changes
often accompany other large organizational changes, a new coach’s first
year is unlikely to be a perfect reflection of their abilities. Many
coaches who would prove to be very successful in a program inherent
dysfunctional systems and poor recruits (after all, that is more than
likely why the previous coach didn’t return), which are not things that
can can be fixed in just one year. This also relates to the problem
regarding mid-season hires and when a coach’s first “official” season
is. Studying the impact of hiring a coach mid-season vs. after the
season is over could be another potential area to look at if we were to
keep exploring.
Future projects would also explore how the background of new hires
may determine success. It is a common idea that defensive coordinators
often perform worse as head coaches than other head coaches or
offensive-minded coaches, so it would be interesting to see if this
claim holds any merit.