Skip to contents

cfbseedR simulates and evaluates college football seasons: standings with a documented tiebreaker cascade, conference champions, College Football Playoff (CFP) seeding, and week-by-week season simulation with a pluggable results generator.

Everything in this article runs offline using the toy season bundled with the package (9 teams: two 4-team conferences, “Alpha” and “Beta”, plus one independent).

library(cfbseedR)

games <- read.csv(system.file("extdata", "toy_games.csv", package = "cfbseedR"))
teams <- read.csv(system.file("extdata", "toy_teams.csv", package = "cfbseedR"))

head(games)
#>    sim week game_type home_team away_team result neutral
#> 1 2024    1       REG        A1        A2      7       0
#> 2 2024    1       REG        A3        A4      3       0
#> 3 2024    1       REG        B1        B2      4       0
#> 4 2024    1       REG        B3        B4      6       0
#> 5 2024    2       REG        A3        A1      3       0
#> 6 2024    2       REG        A2        A4     14       0
teams
#>   team       conference
#> 1   A1            Alpha
#> 2   A2            Alpha
#> 3   A3            Alpha
#> 4   A4            Alpha
#> 5   B1             Beta
#> 6   B2             Beta
#> 7   B3             Beta
#> 8   B4             Beta
#> 9   I1 FBS Independents

Standings from a games frame

cfb_standings() needs a games frame (sim/season, game_type, week, home_team, away_team, result) and a teams frame (team, conference). It returns one row per team with overall and conference records, conference ranks (via the tiebreaker cascade), and conference champions.

standings <- cfb_standings(games, teams, verbosity = "NONE")
standings[, c("team", "conference", "wins", "losses", "conf_pct",
              "conf_rank", "conf_champ")]
#> # A tibble: 9 × 7
#>   team  conference        wins losses conf_pct conf_rank conf_champ
#>   <chr> <chr>            <int>  <int>    <dbl>     <int> <lgl>     
#> 1 A1    Alpha                3      2    0.667         1 TRUE      
#> 2 A2    Alpha                2      2    0.667         2 FALSE     
#> 3 A3    Alpha                2      1    0.667         3 FALSE     
#> 4 A4    Alpha                0      4    0             4 FALSE     
#> 5 B1    Beta                 5      0    1             1 TRUE      
#> 6 B2    Beta                 2      2    0.667         2 FALSE     
#> 7 B3    Beta                 1      2    0.333         3 FALSE     
#> 8 B4    Beta                 0      4    0             4 FALSE     
#> 9 I1    FBS Independents     2      0    0            NA FALSE

Two college-football-specific semantics to be aware of:

  • Conference championship games (game_type == "CONF_CHAMP") count toward the overall record and decide the conference champion, but not toward the conference record/rank.
  • sov / sos are conference-scoped: strength of victory and strength of schedule are computed over regular-season conference games only (sov over conference victories, sos over conference opponents); independents get 0.0.

If you have real data, cfb_games_from_schedule() maps a cfbfastR::load_cfb_schedules() frame into this schema:

# Not run: requires network access via cfbfastR
sched <- cfbfastR::load_cfb_schedules(2024)
games_2024 <- cfb_games_from_schedule(sched)

Playoff seeds with a rankings frame

cfb_playoff_seeds() implements CFP straight seeding (2025 rule): the field is the best-ranked teams with the 5 highest-ranked conference champions guaranteed inclusion, seeded strictly in ranking order. Pass a committee-style rankings frame (team, rank); without one, a documented fallback ordering (win pct, SOV, SOS, point differential) is used.

rankings <- data.frame(team = c("B1", "I1", "A1", "A3"), rank = 1:4)
seeded <- cfb_playoff_seeds(standings, rankings = rankings, playoff_seeds = 4)
seeded[!is.na(seeded$seed), c("team", "conference", "conf_champ", "seed")]
#> # A tibble: 4 × 4
#>   team  conference       conf_champ  seed
#>   <chr> <chr>            <lgl>      <int>
#> 1 A1    Alpha            TRUE           3
#> 2 A3    Alpha            FALSE          4
#> 3 B1    Beta             TRUE           1
#> 4 I1    FBS Independents FALSE          2

A small simulation

cfb_simulations() fills in the missing (NA) results week by week using a compute_results function - by default cfbseedR_compute_results(), an ELO-based generator adapted from nflseedR - then computes standings, champions, seeds, and (with sim_include = "POST") the playoff bracket.

games$result[games$week >= 3] <- NA
set.seed(42)
sim <- cfb_simulations(games, teams, simulations = 50, playoff_seeds = 4)
#> Start simulation of 50 seasons (4 weeks to simulate).
#> DONE!
sim$overall
#> # A tibble: 9 × 7
#>   conference       team   wins conf_champ playoff seed1 won_natty
#>   <chr>            <chr> <dbl>      <dbl>   <dbl> <dbl>     <dbl>
#> 1 Alpha            A1     3.26       0.88    0.9   0.02      0.34
#> 2 Alpha            A2     1.4        0.12    0.12  0.02      0.02
#> 3 Alpha            A3     2.72       0       0.88  0.7       0.06
#> 4 Alpha            A4     0.6        0       0     0         0   
#> 5 Beta             B1     3.24       0.68    0.7   0.06      0.14
#> 6 Beta             B2     1.8        0.32    0.34  0         0.04
#> 7 Beta             B3     1.66       0       0.34  0         0.06
#> 8 Beta             B4     0.68       0       0.04  0         0   
#> 9 FBS Independents I1     1.64       0       0.68  0.2       0.34

The returned cfbseedR_simulation list also carries per-simulation standings, all simulated games, team_wins win-total probabilities, per-matchup game_summary, and the sim_params used.

For deeper topics - custom compute_results functions, the tiebreaker_depth ladder, and the CFP-12 seeding rules - see the Simulating seasons article.

Acknowledgments

cfbseedR is an adaptation of nflseedR (MIT) by Lee Sharpe and Sebastian Carl. The entire architecture - the standings engine, the tiebreaking cascade design, the week-loop simulator with a pluggable compute_results contract, and the ELO-based default generator - is their design, re-derived here with college football semantics. Thank you to Lee, Sebastian, and the broader nflverse team whose open-source work this package builds on.