Data Analysis, Data Visualization

Super Bowl 54 Matchup – Which Team Has the Edge?

SuperBowl 54

Super Bowl 54 is only 2 days away so we decided to analyze the upcoming game by pulling some data from We used R to scrape the data from the San Francisco 49ers and Kansas City Chiefs stats pages. The data were then cleaned and transformed slightly to make ingestion into Tableau Public simple.

You can switch between viewing Quarterback, Defensive and Offensive matchups by using the navigation buttons on the top. You can also switch between the regular and post-season. Patrick Mahomes seems to have the edge over Jimmy G in most categories during the regular season other than Completion %. Click the image to go to the Tableau dashboard where you can interact with the data yourself.


It appears that San Francisco may have a slight edge on defense, forcing and recovering more fumbles than Kansas City. What other insights can you find by using this dashboard?

Superbowl 54 Defenses

The R script is shown below. We used rvest to scrape the data as it makes the process simple, allowing us to grab all of the data we need with two function calls.


### Season Stats ###
kc_stats <- rvest::html("")
kc_stats <- rvest::html_table(kc_stats)

kc_stats_post <- rvest::html("")
kc_stats_post <- rvest::html_table(kc_stats_post)

sf_stats <- rvest::html("")
sf_stats <- rvest::html_table(sf_stats)

sf_stats_post <- rvest::html("")
sf_stats_post <- rvest::html_table(sf_stats_post)

save(kc_stats, kc_stats_post, sf_stats, sf_stats_post, file="team_stats.RData")

### QB
kc_stats_qb_reg <- data.frame(kc_stats[[2]][1,], season_type = 'regular')
kc_stats_qb_post <- data.frame(kc_stats_post[[2]][1,], season_type = 'post-season')

sf_stats_qb_reg <- data.frame(sf_stats[[2]][1,], season_type = 'regular')
sf_stats_qb_post <- data.frame(sf_stats_post[[2]][1,], season_type = 'post-season')

qbs <- rbind(kc_stats_qb_reg, kc_stats_qb_post, sf_stats_qb_reg, sf_stats_qb_post)
qbs$QB <- c("Patrick Mahomes", "Patrick Mahomes", "Jimmy Garoppolo", "Jimmy Garoppolo")
write.csv(qbs, file="qbs.csv", row.names = FALSE)

### Defense
kc_stats_def_reg <- data.frame(kc_stats[[8]][nrow(kc_stats[[8]]),], season_type = 'regular')
kc_stats_def_post <- data.frame(kc_stats_post[[8]][nrow(kc_stats_post[[8]]),], season_type = 'post-season')

sf_stats_def_reg <- data.frame(sf_stats[[8]][nrow(sf_stats[[8]]),], season_type = 'regular')
sf_stats_def_post <- data.frame(sf_stats_post[[8]][nrow(sf_stats_post[[8]]),], season_type = 'post-season')

defs <- rbind(kc_stats_def_reg, kc_stats_def_post, sf_stats_def_reg, sf_stats_def_post)
defs$TEAM <- c("KC DEF", "KC DEF", "SF DEF", "SF DEF")
write.csv(defs, file="defs.csv", row.names = FALSE)

### Offense
kc_stats_off_reg <- data.frame(kc_stats[[10]][nrow(kc_stats[[10]]),], season_type = 'regular')
kc_stats_off_post <- data.frame(kc_stats_post[[10]][nrow(kc_stats_post[[10]]),], season_type = 'post-season')

sf_stats_off_reg <- data.frame(sf_stats[[10]][nrow(sf_stats[[10]]),], season_type = 'regular')
sf_stats_off_post <- data.frame(sf_stats_post[[10]][nrow(sf_stats_post[[10]]),], season_type = 'post-season')

offs <- rbind(kc_stats_off_reg, kc_stats_off_post, sf_stats_off_reg, sf_stats_off_post)
offs$TEAM <- c("KC OFF", "KC OFF", "SF OFF", "SF OFF")
write.csv(offs, file="offs.csv", row.names = FALSE)