Skip to contents

2021 NFL Standings & Team Ratings

Easy Moderate Difficult Most Difficult

Table created by: Kyle Cuilla @kc_analytics with {reactablefmtr} • Data: Pro-Football-Reference.com


Code

# Function to pull data from PFR
get_pfr_standings <- function(year) {

  url <- paste0("https://www.pro-football-reference.com/years/",year,"/")
  AFC_table <- url %>%
    xml2::read_html() %>%
    html_nodes(xpath = '//*[@id="AFC"]') %>%
    html_table()
  AFC_table <- AFC_table[[1]]
  NFC_table <- url %>%
    xml2::read_html() %>%
    html_nodes(xpath = '//*[@id="NFC"]') %>%
    html_table()
  NFC_table <- NFC_table[[1]]
  NFL_table <- rbind(AFC_table, NFC_table)
  NFL_table_clean <- NFL_table %>%
    mutate(Division = ifelse(str_detect(Tm, "FC"), Tm, NA)) %>%
    fill(Division, .direction = "down") %>%
    filter(str_detect(Tm, "FC", negate = TRUE)) %>%
    mutate(Playoffs = ifelse(str_detect(Tm, "[*+]"), "Yes", "No")) %>%
    mutate(Tm = gsub("[*+]", "", Tm)) %>%
    rename(Record = `W-L%`) %>%
    unite(Record, W, L, T, sep = "-") %>%
    mutate(Team = word(Tm, -1)) %>%
    mutate(
      Team = case_when(
        Team == "Team" ~ "Football Team",
        TRUE ~ Team
      )
    ) %>%
    mutate_at(c("SRS", "OSRS", "DSRS", "PF", "PA", "MoV", "SoS"),
              as.numeric) %>%
    mutate(SOS = ntile(SoS, 4)) %>%
    select(
      Division,
      Team,
      Record,
      Playoffs,
      SOS,
      PF,
      PA,
      MoV,
      OSRS,
      DSRS,
      SRS,
    )
}

# Join to team logos from {nflfastR}
data <- get_pfr_standings(2021) %>%
  left_join(distinct(select(teams_colors_logos, team_nick, team_logo_espn)),
            by = c('Team' = 'team_nick')) %>%
  select(Division, team_logo_espn, everything())

# Create table
data %>%
  mutate(
    mov_cols = case_when(MoV >= 0 ~ "darkgreen",
                         TRUE ~ "red"),
    playoff_cols = case_when(Playoffs == "Yes" ~ "darkgreen",
                             TRUE ~ "red")
  ) %>%
  reactable(
    .,
    theme = fivethirtyeight(centered = TRUE, header_font_size = 11),
    pagination = FALSE,
    showSortIcon = FALSE,
    highlight = TRUE,
    compact = TRUE,
    defaultSorted = "SRS",
    defaultSortOrder = "desc",
    columnGroups = list(
      colGroup(name = "Team Rating (SRS)",
               columns = c("SRS", "OSRS", "DSRS")),
      colGroup(name = "Team Scoring & Margin of Victory",
               columns = c("PF", "PA", "MoV"))
    ),
    rowStyle = group_border_sort("Division"),
    columns = list(
      Division = colDef(
        name = "Div.",
        maxWidth = 85,
        style = group_merge_sort("Division")
      ),
      team_logo_espn = colDef(
        name = "",
        maxWidth = 75,
        sortable = FALSE,
        style = background_img(height = "200%", width = "125%")
      ),
      Team = colDef(
        minWidth = 100,
        align = "left",
        cell = merge_column(., "Record", merged_position = "below"),
        style = list(borderRight = "1px solid #777")
      ),
      Record = colDef(show = FALSE),
      Playoffs = colDef(
        maxWidth = 70,
        align = "center",
        cell = pill_buttons(., color_ref = "playoff_cols", opacity = 0.7),
        style = list(borderRight = "1px solid #777")
      ),
      playoff_cols = colDef(show = FALSE),
      SOS = colDef(
        maxWidth = 47,
        align = "center",
        cell = icon_sets(., icon_set = "ski rating", icon_position = "over"),
        style = list(borderRight = "1px solid #777")
      ),
      PF = colDef(
        maxWidth = 180,
        cell = data_bars(., text_size = 13, box_shadow = TRUE)),
      PA = colDef(
        maxWidth = 180,
        cell = data_bars(., text_size = 13, box_shadow = TRUE)),
      MoV = colDef(
        maxWidth = 80,
        cell = pill_buttons(., number_fmt = function(value) sprintf("%+0.1f", value), colors = "transparent", opacity = 0, bold_text = TRUE, text_color_ref = "mov_cols"
        ),
        style = list(borderRight = "1px solid #777")
      ),
      mov_cols = colDef(show = FALSE),
      OSRS = colDef(
        maxWidth = 55,
        cell = color_tiles(., bias = 1.3, box_shadow = TRUE)
      ),
      DSRS = colDef(
        maxWidth = 55,
        cell = color_tiles(., bias = 0.6, box_shadow = TRUE)
      ),
      SRS = colDef(
        maxWidth = 55,
        cell = color_tiles(., bias = 0.7, box_shadow = TRUE)
      )
    )
  ) %>%
  add_title("2021 NFL Standings & Team Ratings", margin = margin(0, 0, 10, 0)) %>%
  add_icon_legend(icon_set = "ski rating") %>%
  add_source("Table created by: Kyle Cuilla @kc_analytics with {reactablefmtr} •  Data: Pro-Football-Reference.com", font_size = 12)