On this page
A rank says where a team sits, but not how close the teams around it are. This recipe puts one team in the middle of an 11-row table, the five teams ranked just above it and the five just below, so the gap to its neighbours shows next to the number.
It uses three sdvplotR helpers: gt_sdv_logos() for the
logos and names, gt_color_ranks() for the rank column and
gt_highlight_cells() for the focal row.
The data
cfbfastR’s load_espn_cfb_team_summaries() reads the
SportsDataverse season summary release: one row per FBS team, with each
metric next to a _rank column.
summaries <- as.data.frame(cfbfastR::load_espn_cfb_team_summaries(2025))
dim(summaries)
#> [1] 136 788
head(grep("_rank$", names(summaries), value = TRUE))
#> [1] "playsgame_off_rank" "TEPA_off_rank"
#> [3] "EPAgame_off_rank" "EPAplay_off_rank"
#> [5] "EPAdrive_off_rank" "early_down_EPA_off_rank"Not every metric has a rank, so check the three this recipe uses. Rank 1 is the best team in each one: the highest net and offensive EPA per play, and the fewest EPA per play allowed on defense.
The window
Sort by the rank column and take the 11 rows centred on the team. Near the top or bottom of the table there are not five teams on both sides, so the window slides instead of shrinking: it stays 11 rows and the focal team moves off centre.
neighbours <- function(data, team, metric, k = 5) {
data <- data[order(data[[paste0(metric, "_rank")]]), ]
i <- match(team, data$pos_team)
first <- min(max(i - k, 1), nrow(data) - 2 * k)
rows <- data[first:(first + 2 * k), ]
data.frame(
rank = rows[[paste0(metric, "_rank")]],
team = rows$pos_team,
value = rows[[metric]]
)
}
focal <- "Illinois"
neighbours(summaries, focal, "net_adj_epa")
#> rank team value
#> 1 30 Louisville 0.1487155
#> 2 31 South Florida 0.1463640
#> 3 32 Virginia 0.1397002
#> 4 33 TCU 0.1341972
#> 5 34 Iowa State 0.1333091
#> 6 35 Illinois 0.1278535
#> 7 36 East Carolina 0.1276368
#> 8 37 Tennessee 0.1206984
#> 9 38 Toledo 0.1150806
#> 10 39 Pittsburgh 0.1124272
#> 11 40 Old Dominion 0.1124005The table
The rank colors use the whole league as their domain (1
to the number of teams), not the 11 rows on show, so a mid-table window
reads as mid-table. The focal team’s name and value are filled with
gt_highlight_cells(), which takes a logical mask the same
shape as the columns it fills.
neighbour_table <- function(data, focal_team, metric, label) {
win <- neighbours(data, focal_team, metric)
is_focal <- win$team == focal_team
gt(win) |>
tab_header(title = label) |>
gt_sdv_logos(team, league = "cfb", height = 20, include_name = TRUE) |>
fmt_number(value, decimals = 3) |>
cols_label(rank = "Rank", team = "Team", value = "Value") |>
gt_theme_sdv_team(team = focal_team, league = "cfb", density = "compact") |>
gt_color_ranks(rank, domain = c(1, nrow(data))) |>
gt_highlight_cells(c(team, value), cbind(is_focal, is_focal), bold = TRUE)
}
neighbour_table(summaries, focal, "net_adj_epa", metrics[["net_adj_epa"]])| Net adj. EPA/play | ||
| Rank | Team | Value |
|---|---|---|
| 30 |
Louisville |
0.149 |
| 31 |
South Florida |
0.146 |
| 32 |
Virginia |
0.140 |
| 33 |
TCU |
0.134 |
| 34 |
Iowa State |
0.133 |
| 35 |
Illinois |
0.128 |
| 36 |
East Carolina |
0.128 |
| 37 |
Tennessee |
0.121 |
| 38 |
Toledo |
0.115 |
| 39 |
Pittsburgh |
0.112 |
| 40 |
Old Dominion |
0.112 |
Three metrics side by side
The same function, once per metric, laid out with
gt_grid().
tables <- lapply(names(metrics), function(m) {
neighbour_table(summaries, focal, m, metrics[[m]])
})
gt_grid(
tables,
ncol = 3,
gap = 12,
title = paste(focal, "and its neighbours"),
subtitle = "2025 FBS season, five teams either side by rank",
caption = "Data: cfbfastR::load_espn_cfb_team_summaries()"
)| Net adj. EPA/play | ||
| Rank | Team | Value |
|---|---|---|
| 30 |
Louisville |
0.149 |
| 31 |
South Florida |
0.146 |
| 32 |
Virginia |
0.140 |
| 33 |
TCU |
0.134 |
| 34 |
Iowa State |
0.133 |
| 35 |
Illinois |
0.128 |
| 36 |
East Carolina |
0.128 |
| 37 |
Tennessee |
0.121 |
| 38 |
Toledo |
0.115 |
| 39 |
Pittsburgh |
0.112 |
| 40 |
Old Dominion |
0.112 |
| Offense EPA/play | ||
| Rank | Team | Value |
|---|---|---|
| 29 |
Eastern Michigan |
0.120 |
| 30 |
SMU |
0.119 |
| 31 |
UAB |
0.116 |
| 32 |
Army |
0.113 |
| 33 |
Ohio |
0.111 |
| 34 |
Illinois |
0.111 |
| 35 |
Penn State |
0.109 |
| 36 |
Temple |
0.106 |
| 37 |
Texas |
0.104 |
| 38 |
NC State |
0.100 |
| 39 |
Utah State |
0.093 |
| Defense EPA/play | ||
| Rank | Team | Value |
|---|---|---|
| 94 |
Marshall |
0.108 |
| 95 |
North Texas |
0.110 |
| 96 |
Louisiana |
0.111 |
| 97 |
Mississippi State |
0.112 |
| 98 |
NC State |
0.114 |
| 99 |
Illinois |
0.117 |
| 100 |
San José State |
0.118 |
| 101 |
Kent State |
0.118 |
| 102 |
App State |
0.119 |
| 103 |
Texas State |
0.119 |
| 104 |
Tennessee |
0.121 |
Checking the window
A team more than five places from either end is row 6 of 11. Check it for every team on all three metrics, then look at the rows the top and bottom teams land on.
row_of <- function(team, metric) {
match(team, neighbours(summaries, team, metric)$team)
}
n <- nrow(summaries)
for (m in names(metrics)) {
ranks <- summaries[[paste0(m, "_rank")]]
rows <- vapply(summaries$pos_team, row_of, integer(1), metric = m)
stopifnot(all(rows[ranks > 5 & ranks <= n - 5] == 6))
}
ranks <- summaries$net_adj_epa_rank
edges <- summaries$pos_team[order(ranks)][c(1:6, (n - 5):n)]
data.frame(
team = edges,
rank = ranks[match(edges, summaries$pos_team)],
row = unname(vapply(edges, row_of, integer(1), metric = "net_adj_epa"))
)
#> team rank row
#> 1 Indiana 1 1
#> 2 Ohio State 2 2
#> 3 Notre Dame 3 3
#> 4 Miami 4 4
#> 5 Texas Tech 5 5
#> 6 Oregon 6 6
#> 7 Charlotte 131 6
#> 8 Buffalo 132 7
#> 9 Akron 133 8
#> 10 Ball State 134 9
#> 11 Sam Houston 135 10
#> 12 Massachusetts 136 11Related SportsDataverse packages
- sdvplot - the Python plotting package
- sdvplot-js - the JavaScript / TypeScript plotting packages
- cfbfastR - college football
- hoopR - men’s basketball
- wehoop - women’s basketball
- baseballr - baseball
- fastRhockey - hockey
- oddsapiR - betting odds
- sportyR - playing surfaces
- sportsdataverse-py - the Python package
- sportsdataverse-R - the R meta-package

Louisville
South Florida
Virginia
TCU
Iowa State
Illinois
East Carolina
Tennessee
Toledo
Pittsburgh
Old Dominion
Eastern Michigan
SMU
UAB
Army
Ohio
Penn State
Temple
Texas
NC State
Utah State
Marshall
North Texas
Louisiana
Mississippi State
San José State
Kent State
App State
Texas State