Skip to contents

cfbseedR 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.

Citation

To cite the cfbfastR R package in publications, use:

BibTeX Citation

@misc{cfbfastr,
  author = {Saiem Gilani and Akshay Easwaran and Jared Lee and Eric Hess},
  title = {cfbfastR: Access College Football Play by Play Data},
  url = {https://cfbfastR.sportsdataverse.org/},
  year = {2021}
}