
Simulating a College Football Season with cfbseedR
Saiem Gilani
Source: vignettes/cfbseedR-season-simulation.Rmd
cfbseedR-season-simulation.RmdcfbseedR is the SportsDataverse’s season simulator for college football, a college adaptation of nflseedR. Give it a schedule and it computes conference standings with each conference’s official tiebreakers, crowns the conference champions, seeds the 12-team College Football Playoff and plays the bracket. Leave some results blank and it simulates them, thousands of times over.
cfbfastR supplies the schedule. This article rebuilds the 2025 regular season’s standings and playoff field, then rewinds to week 10 and asks what the rest of the season looked like from there.
Load and install the packages
cfbseedR is on CRAN.
if (!requireNamespace('pak', quietly = TRUE)){
install.packages('pak')
}
pak::pak(c("dplyr", "cfbseedR", "gt", "scales"))
library(dplyr)
library(cfbseedR)
# cfbfastR is deliberately NOT in the pak() list above. pkgdown and
# R CMD check render this vignette against the package being built, and
# installing from CRAN here would overwrite that dev build with the last
# release -- so any function added since it would vanish mid-render.
library(cfbfastR)load_cfb_schedules() calls the CollegeFootballData API, so
it needs a free API key. Get one at https://collegefootballdata.com/key and register it
before running the rest (see ?register_cfbd):
Sys.setenv(CFBD_API_KEY = "YOUR-API-KEY-HERE")From a cfbfastR schedule to cfbseedR games
cfb_games_from_schedule() maps a
load_cfb_schedules() frame onto the games table cfbseedR
works with. It turns the final score into result (the home
team’s margin) and marks each game "REG",
"CONF_CHAMP" (the schedule notes a championship game) or
"POST" (bowls and the playoff).
season <- 2025
sched <- load_cfb_schedules(season)
games <- cfb_games_from_schedule(sched) |>
# cfbseedR plays the playoff bracket itself, from the seeds it computes
filter(game_type != "POST")
count(games, game_type)## # A tibble: 2 × 2
## game_type n
## <chr> <int>
## 1 CONF_CHAMP 9
## 2 REG 879
The teams table lists the FBS programs and their conference that season. FCS opponents stay out of it: cfbseedR still counts their games toward the FBS teams’ records, but gives them no standings row, so they can’t be seeded.
teams <- bind_rows(
select(sched, team = home_team, conference = home_conference, division = home_division),
select(sched, team = away_team, conference = away_conference, division = away_division)
) |>
filter(division == "fbs") |>
distinct(team, conference)
count(teams, conference, sort = TRUE)## # A tibble: 11 × 2
## conference n
## <chr> <int>
## 1 Big Ten 18
## 2 ACC 17
## 3 Big 12 16
## 4 SEC 16
## 5 American Athletic 14
## 6 Sun Belt 14
## 7 Mid-American 13
## 8 Conference USA 12
## 9 Mountain West 12
## 10 FBS Independents 2
## 11 Pac-12 2
Standings and the playoff field
With every result filled in, cfb_standings() ranks each
conference with its own tiebreaker procedure, and
cfb_playoff_seeds() builds the field.
autobid = "2025" applies that season’s rule: the five
highest-ranked conference champions are in.
standings <- cfb_standings(games, teams)
seeds <- cfb_playoff_seeds(standings, playoff_seeds = 12L, autobid = "2025")
seeds |>
filter(!is.na(seed)) |>
arrange(seed) |>
select(seed, team, conference, conf_champ, wins, losses)## # A tibble: 12 × 6
## seed team conference conf_champ wins losses
## <int> <chr> <chr> <lgl> <int> <int>
## 1 1 Indiana Big Ten TRUE 13 0
## 2 2 Texas Tech Big 12 TRUE 12 1
## 3 3 James Madison Sun Belt TRUE 12 1
## 4 4 Georgia SEC TRUE 12 1
## 5 5 Ohio State Big Ten FALSE 12 1
## 6 6 Oregon Big Ten FALSE 11 1
## 7 7 Ole Miss SEC FALSE 11 1
## 8 8 Texas A&M SEC FALSE 11 1
## 9 9 BYU Big 12 FALSE 11 2
## 10 10 North Texas American Athletic FALSE 11 2
## 11 11 Tulane American Athletic TRUE 11 2
## 12 12 Oklahoma SEC FALSE 10 2
Without a rankings table, seeding orders teams by win
percentage, then strength of victory, strength of schedule and point
differential. The real field is seeded by the selection committee’s
ranking, so pass the committee’s final top 25 (team,
rank) as rankings to reproduce it.
Simulating the rest of the season from week 10
Now rewind. Blank out every result between two FBS teams from week 10
on, and let cfb_simulations() play those games. Games
against FCS opponents keep their real results, because FCS teams carry
no rating.
cutoff <- 10
fbs <- teams$team
games_wk10 <- games |>
mutate(result = ifelse(week >= cutoff & home_team %in% fbs & away_team %in% fbs,
NA, result))
sum(is.na(games_wk10$result))## [1] 298
The default results generator is an Elo model carried over from nflseedR. Left alone it starts every team at a random rating, so seed it with CFBD’s own Elo: each team’s post-game rating from its last game before the cutoff.
played <- filter(sched, season_type == "regular", week < cutoff)
elo <- bind_rows(
select(played, start_date, team = home_team, elo = home_postgame_elo),
select(played, start_date, team = away_team, elo = away_postgame_elo)
) |>
filter(team %in% fbs, !is.na(elo)) |>
arrange(start_date) |>
group_by(team) |>
summarise(elo = last(elo))
elo <- setNames(elo$elo, elo$team)
head(sort(elo, decreasing = TRUE), 10)## Notre Dame Ohio State Indiana Oregon Georgia Penn State Alabama
## 2193 2098 2091 2055 2034 1990 1978
## Ole Miss Texas Iowa
## 1963 1925 1907
cfb_simulations() passes elo on to the
results generator. Each simulation plays out weeks 10 through the
conference championship games, recomputes the standings and the 12
seeds, and plays the bracket. Conference championship games are played
as scheduled, between the two teams that actually made them.
set.seed(2025)
sim <- cfb_simulations(
games_wk10, teams,
simulations = 500,
playoff_seeds = 12L,
autobid = "2025",
elo = elo,
verbosity = "NONE"
)sim$overall has one row per team: average wins and the
share of simulations in which it won its conference, made the playoff,
earned the top seed and won the national title.
sim$overall |>
arrange(desc(playoff)) |>
select(team, conference, wins, conf_champ, playoff, seed1, won_natty) |>
head(16)## # A tibble: 16 × 7
## team conference wins conf_champ playoff seed1 won_natty
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Indiana Big Ten 12.0 0.508 0.95 0.256 0.23
## 2 Ohio State Big Ten 11.9 0.492 0.92 0.268 0.278
## 3 Georgia SEC 11.0 0.62 0.738 0.06 0.142
## 4 North Texas American Athletic 11.0 0.584 0.722 0.016 0.006
## 5 James Madison Sun Belt 10.8 0.83 0.67 0.034 0.004
## 6 Texas Tech Big 12 10.7 0.696 0.644 0.066 0.03
## 7 Alabama SEC 10.7 0.38 0.63 0.048 0.068
## 8 Texas A&M SEC 10.7 0 0.626 0.116 0.02
## 9 Ole Miss SEC 10.5 0 0.596 0 0.06
## 10 Memphis American Athletic 9.92 0 0.566 0 0
## 11 Oregon Big Ten 10.1 0 0.542 0 0.118
## 12 BYU Big 12 10.4 0.304 0.482 0.08 0.002
## 13 Georgia Tech ACC 10.1 0 0.452 0.036 0
## 14 Boise State Mountain West 10.1 0.734 0.45 0 0
## 15 Miami ACC 10.0 0 0.43 0 0.02
## 16 Louisville ACC 9.51 0 0.372 0 0.002
summary() turns the same numbers into a table grouped by
conference:
summary(sim)| Simulating the 2025 college football season | |||||
| summary of 500 simulations using cfbseedR | |||||
| AVG. WINS |
Win CONF |
Make CFP |
No.1 Seed |
Win NATTY |
|
|---|---|---|---|---|---|
| ACC | |||||
| Georgia Tech | 10.1 | 0% | 45% | 4% | 0% |
| Miami | 10.0 | 0% | 43% | 0% | 2% |
| Virginia | 9.9 | 43% | 33% | 1% | |
| Louisville | 9.5 | 0% | 37% | 0% | |
| Pittsburgh | 7.8 | 0% | 0% | 0% | |
| SMU | 7.6 | 0% | 0% | 0% | 0% |
| Duke | 7.5 | 57% | 1% | 0% | 0% |
| Wake Forest | 7.2 | 0% | 1% | 0% | 0% |
| Clemson | 6.4 | 0% | 0% | 0% | 0% |
| California | 6.3 | 0% | 0% | 0% | 0% |
| NC State | 5.5 | 0% | 0% | 0% | 0% |
| Florida State | 5.5 | 0% | 0% | 0% | 0% |
| Syracuse | 4.2 | 0% | 0% | 0% | 0% |
| North Carolina | 4.0 | 0% | 0% | 0% | 0% |
| Virginia Tech | 3.9 | 0% | 0% | 0% | 0% |
| Stanford | 3.8 | 0% | 0% | 0% | 0% |
| Boston College | 1.7 | 0% | 0% | 0% | 0% |
| American Athletic | |||||
| North Texas | 11.0 | 58% | 72% | 2% | |
| Tulane | 10.1 | 42% | 35% | 0% | |
| Memphis | 9.9 | 0% | 57% | 0% | 0% |
| South Florida | 9.2 | 0% | 5% | 0% | 0% |
| Navy | 8.5 | 0% | 8% | 0% | 0% |
| East Carolina | 7.4 | 0% | 0% | 0% | 0% |
| Army | 6.5 | 0% | 0% | 0% | 0% |
| Temple | 5.8 | 0% | 0% | 0% | 0% |
| UTSA | 5.5 | 0% | 0% | 0% | 0% |
| UAB | 4.7 | 0% | 0% | 0% | 0% |
| Rice | 4.6 | 0% | 0% | 0% | 0% |
| Florida Atlantic | 4.1 | 0% | 0% | 0% | 0% |
| Tulsa | 3.0 | 0% | 0% | 0% | 0% |
| Charlotte | 1.2 | 0% | 0% | 0% | 0% |
| Big 12 | |||||
| Texas Tech | 10.7 | 70% | 64% | 7% | 3% |
| BYU | 10.4 | 30% | 48% | 8% | |
| Utah | 9.1 | 0% | 15% | 0% | |
| Houston | 8.8 | 0% | 4% | 0% | 0% |
| Cincinnati | 8.8 | 0% | 9% | 0% | 0% |
| TCU | 8.4 | 0% | 8% | 0% | 0% |
| Arizona State | 7.6 | 0% | 0% | 0% | 0% |
| Iowa State | 7.4 | 0% | 0% | 0% | 0% |
| UCF | 6.7 | 0% | 0% | 0% | 0% |
| Kansas State | 6.5 | 0% | 0% | 0% | 0% |
| Kansas | 6.1 | 0% | 0% | 0% | 0% |
| Arizona | 5.9 | 0% | 0% | 0% | 0% |
| Baylor | 5.7 | 0% | 0% | 0% | 0% |
| Colorado | 4.8 | 0% | 0% | 0% | 0% |
| West Virginia | 2.8 | 0% | 0% | 0% | 0% |
| Oklahoma State | 1.2 | 0% | 0% | 0% | 0% |
| Big Ten | |||||
| Indiana | 12.0 | 51% | 95% | 26% | 23% |
| Ohio State | 11.9 | 49% | 92% | 27% | 28% |
| Oregon | 10.1 | 0% | 54% | 0% | 12% |
| Michigan | 8.8 | 0% | 2% | 0% | 0% |
| Washington | 8.7 | 0% | 0% | 0% | 0% |
| Iowa | 8.5 | 0% | 4% | 0% | |
| USC | 8.5 | 0% | 4% | 0% | |
| Nebraska | 7.4 | 0% | 0% | 0% | 0% |
| Illinois | 7.2 | 0% | 0% | 0% | 0% |
| Minnesota | 6.8 | 0% | 0% | 0% | 0% |
| Penn State | 6.5 | 0% | 0% | 0% | 0% |
| Northwestern | 6.3 | 0% | 0% | 0% | 0% |
| Maryland | 6.1 | 0% | 0% | 0% | 0% |
| Rutgers | 4.8 | 0% | 0% | 0% | 0% |
| Michigan State | 3.7 | 0% | 0% | 0% | 0% |
| UCLA | 3.4 | 0% | 0% | 0% | 0% |
| Wisconsin | 3.0 | 0% | 0% | 0% | 0% |
| Purdue | 2.2 | 0% | 0% | 0% | 0% |
| Conference USA | |||||
| Jacksonville State | 8.7 | 76% | 7% | 0% | 0% |
| Kennesaw State | 7.9 | 24% | 3% | 0% | 0% |
| Western Kentucky | 7.8 | 0% | 1% | 0% | 0% |
| Delaware | 7.2 | 0% | 0% | 0% | 0% |
| Missouri State | 6.6 | 0% | 0% | 0% | 0% |
| Louisiana Tech | 6.5 | 0% | 0% | 0% | 0% |
| Liberty | 5.9 | 0% | 0% | 0% | 0% |
| Florida International | 5.1 | 0% | 0% | 0% | 0% |
| New Mexico State | 4.7 | 0% | 0% | 0% | 0% |
| UTEP | 3.7 | 0% | 0% | 0% | 0% |
| Middle Tennessee | 2.8 | 0% | 0% | 0% | 0% |
| Sam Houston | 1.5 | 0% | 0% | 0% | 0% |
| FBS Independents | |||||
| Notre Dame | 9.9 | 0% | 0% | 0% | 0% |
| UConn | 7.9 | 0% | 0% | 0% | 0% |
| Mid-American | |||||
| Ohio | 8.2 | 0% | 0% | 0% | 0% |
| Miami (OH) | 7.9 | 64% | 2% | 0% | 0% |
| Toledo | 7.0 | 0% | 0% | 0% | 0% |
| Central Michigan | 6.8 | 0% | 0% | 0% | 0% |
| Western Michigan | 6.5 | 36% | 0% | 0% | 0% |
| Bowling Green | 5.8 | 0% | 0% | 0% | 0% |
| Buffalo | 5.5 | 0% | 0% | 0% | 0% |
| Akron | 4.5 | 0% | 0% | 0% | 0% |
| Northern Illinois | 4.5 | 0% | 0% | 0% | 0% |
| Ball State | 4.1 | 0% | 0% | 0% | 0% |
| Kent State | 4.1 | 0% | 0% | 0% | 0% |
| Eastern Michigan | 3.4 | 0% | 0% | 0% | 0% |
| Massachusetts | 0.6 | 0% | 0% | 0% | 0% |
| Mountain West | |||||
| UNLV | 10.1 | 27% | 35% | 0% | 0% |
| Boise State | 10.1 | 73% | 45% | 0% | 0% |
| San Diego State | 9.2 | 0% | 19% | 0% | 0% |
| Hawai'i | 7.4 | 0% | 0% | 0% | |
| New Mexico | 7.1 | 0% | 0% | 0% | 0% |
| Fresno State | 6.5 | 0% | 0% | 0% | 0% |
| Wyoming | 6.1 | 0% | 0% | 0% | 0% |
| Utah State | 5.6 | 0% | 0% | 0% | 0% |
| San José State | 4.8 | 0% | 0% | 0% | 0% |
| Air Force | 3.9 | 0% | 0% | 0% | 0% |
| Colorado State | 3.0 | 0% | 0% | 0% | 0% |
| Nevada | 2.0 | 0% | 0% | 0% | 0% |
| Pac-12 | |||||
| Washington State | 6.2 | 72% | 0% | 0% | 0% |
| Oregon State | 2.8 | 28% | 0% | 0% | 0% |
| SEC | |||||
| Georgia | 11.0 | 62% | 74% | 6% | 14% |
| Texas A&M | 10.7 | 0% | 63% | 12% | 2% |
| Alabama | 10.7 | 38% | 63% | 5% | 7% |
| Ole Miss | 10.5 | 0% | 60% | 0% | 6% |
| Vanderbilt | 9.1 | 0% | 9% | 0% | 0% |
| Tennessee | 8.9 | 0% | 0% | 0% | |
| Texas | 8.5 | 0% | 4% | 0% | 0% |
| Missouri | 8.3 | 0% | 0% | 0% | |
| Oklahoma | 7.8 | 0% | 2% | 0% | 0% |
| LSU | 7.1 | 0% | 0% | 0% | 0% |
| Auburn | 6.3 | 0% | 0% | 0% | 0% |
| Florida | 4.8 | 0% | 0% | 0% | 0% |
| Mississippi State | 4.8 | 0% | 0% | 0% | 0% |
| South Carolina | 4.7 | 0% | 0% | 0% | 0% |
| Kentucky | 4.1 | 0% | 0% | 0% | 0% |
| Arkansas | 3.5 | 0% | 0% | 0% | 0% |
| Sun Belt | |||||
| James Madison | 10.8 | 83% | 67% | 3% | |
| Troy | 9.0 | 17% | 6% | 0% | 0% |
| Old Dominion | 7.9 | 0% | 0% | 0% | 0% |
| Marshall | 7.7 | 0% | 0% | 0% | 0% |
| Southern Miss | 7.6 | 0% | 0% | 0% | |
| Texas State | 6.5 | 0% | 0% | 0% | 0% |
| App State | 5.5 | 0% | 0% | 0% | 0% |
| Coastal Carolina | 5.4 | 0% | 0% | 0% | 0% |
| Arkansas State | 5.4 | 0% | 0% | 0% | 0% |
| Georgia Southern | 4.6 | 0% | 0% | 0% | 0% |
| South Alabama | 4.4 | 0% | 0% | 0% | 0% |
| Louisiana | 4.1 | 0% | 0% | 0% | 0% |
| UL Monroe | 3.4 | 0% | 0% | 0% | 0% |
| Georgia State | 1.6 | 0% | 0% | 0% | 0% |
To spread a larger run across cores, set a parallel plan first:
simulations are split into chunks and dispatched with
furrr.
future::plan("multisession")
sim <- cfb_simulations(games_wk10, teams, simulations = 10000, chunks = 8,
elo = elo, autobid = "2025")
future::plan("sequential")cfbseedR’s own articles cover the tiebreaker ladder and CFP rules and custom results generators.
Related SportsDataverse packages
- cfbfastR - college football
- hoopR - men’s basketball
- wehoop - women’s basketball
- baseballr - baseball
- fastRhockey - hockey
- oddsapiR - betting odds
- cfbseedR - college football season and playoff simulation (CRAN)
- cfb4th - college football fourth-down decisions (r-universe)
- sdvplotR - team logos, colours and tables for every SportsDataverse league (r-universe)
- sportyR - playing surfaces, including the college football field (CRAN)
- cfbplotR - college football logos and colours (r-universe)
- sportsdataverse-py - the Python package
- sportsdataverse-R - the R meta-package