π College football with sportsdataverse-py
Saturdays in autumn, condensed into tidy DataFrames. π In a few lines of Python you're about to pull a decade of play-by-play, full rosters, schedules, team info, plus live ESPN scoreboards, standings, polls and recruiting boards β all as clean polars frames ready to model.
CFB has no single native premium API, so our premium path is two-pronged:
- ποΈ Release loaders (
load_cfb_*) β pre-built, EPA/WPA-enriched datasets served straight from the cfbfastR-data GitHub release. Fast, reliable, no key needed. - π‘ ESPN families (
espn_cfb_*) β live scoreboards, team pages, standings, rankings, recruiting and per-play participants.
R user? Every verb here has a twin in cfbfastR. Let's kick off! π
π§° The toolboxβ
Everything returns a tidy polars DataFrame by default β pass
return_as_pandas=True for pandas, or (on the espn_cfb_* wrappers)
return_parsed=False for the raw JSON. β marks the premium path.
| Function | What it gives you | Source |
|---|---|---|
load_cfb_pbp | Full play-by-play with EPA/WPA, since 2003 | β release |
load_cfb_rosters | Season rosters (bio, position, hometown) | β release |
load_cfb_schedule | Season schedule + results + Elo | β release |
load_cfb_team_info | Team metadata: conference, colors, venue | β release |
load_cfb_betting_lines | Historical betting market lines (spread/total/ML) | β release |
espn_cfb_scoreboard | Live + recent scoreboard for a date/week | β ESPN |
espn_cfb_schedule | ESPN schedule frame for a date/week | β ESPN |
espn_cfb_teams | Every FBS/FCS team (grab team_ids) | β ESPN |
espn_cfb_team_roster | One team's roster | β ESPN |
espn_cfb_team_schedule | One team's schedule | β ESPN |
espn_cfb_standings | Conference / division standings | β ESPN |
espn_cfb_rankings | AP / Coaches / CFP polls | β ESPN |
espn_cfb_leaders | League stat leaders by category | β ESPN |
espn_cfb_recruits | Season recruiting class | β ESPN |
espn_cfb_play_participants | Per-play athletes (passer/rusher/tacklerβ¦) | β ESPN |
CFBPlayProcess | Full ESPN PBP pipeline (EPA/WPA + box) | β ESPN |
most_recent_cfb_season | The current season year helper | helper |
π Setupβ
pip install sportsdataverse
No API key required. The load_cfb_* loaders read public parquet from the
cfbfastR-data release, and the espn_cfb_* wrappers hit ESPN's public
endpoints.
import polars as pl
import sportsdataverse as sdv
from sportsdataverse.cfb import most_recent_cfb_season
SEASON = most_recent_cfb_season()
print('most recent CFB season:', SEASON)
most recent CFB season: 2025
ESPN's live endpoints are seasonal and occasionally rate-limited, so a tiny
safe() helper runs the riskier calls defensively β you get the frame when
the feed is up, and a friendly one-liner when it isn't (never a scary
traceback). The release loaders are reliable, so we call those directly. π
def safe(label, thunk):
try:
out = thunk()
print(f'β
{label}')
return out
except Exception as e: # noqa: BLE001 -- demo resilience
print(f'βοΈ {label}: unavailable right now ({type(e).__name__})')
return None
ποΈ Premium loaders: a whole season in one callβ
The load_cfb_* family is the fastest way to get clean, complete
season data. Each takes a seasons= int or list (β₯ 2003) and returns one
tidy frame. Let's start with the schedule β one row per game, with final
scores, conference flags, and pre/post-game Elo ratings baked in.
| Function | Grain | Highlights |
|---|---|---|
load_cfb_schedule | one row / game | scores, Elo, neutral-site & conference flags |
schedule = sdv.cfb.load_cfb_schedule(seasons=[2023])
print('schedule shape:', schedule.shape)
schedule.select([
'game_id', 'week', 'home_team', 'away_team',
'home_points', 'away_points', 'home_conference', 'neutral_site',
]).head()
schedule shape: (3734, 31)
shape: (5, 8)
βββββββββββββ¬βββββββ¬ββββββββββββββ¬ββββββββββββββ¬βββββββββββββ¬βββββββββββββ¬βββββββββββββ¬βββββββββββββ
β game_id β week β home_team β away_team β home_point β away_point β home_confe β neutral_si β
β --- β --- β --- β --- β s β s β rence β te β
β i32 β i32 β str β str β --- β --- β --- β --- β
β β β β β i32 β i32 β str β bool β
βββββββββββββͺβββββββͺββββββββββββββͺββββββββββββββͺβββββββββββββͺβββββββββββββͺβββββββββββββͺβββββββββββββ‘
β 401525434 β 1 β Notre Dame β Navy β 42 β 3 β FBS Indepe β true β
β β β β β β β ndents β β
β 401540199 β 1 β Mercer β North β 17 β 7 β Southern β true β
β β β β Alabama β β β β β
β 401520145 β 1 β Jacksonvill β UTEP β 17 β 14 β Conference β false β
β β β e State β β β β USA β β
β 401532392 β 1 β San Diego β Ohio β 20 β 13 β Mountain β false β
β β β State β β β β West β β
β 401540628 β 1 β UAlbany β Fordham β 34 β 13 β CAA β false β
βββββββββββββ΄βββββββ΄ββββββββββββββ΄ββββββββββββββ΄βββββββββββββ΄βββββββββββββ΄βββββββββββββ΄βββββββββββββ
π₯ Premium loaders: rostersβ
load_cfb_rosters gives you
every listed player for a season β name, position, jersey, physicals and
hometown. Perfect for joining onto play-by-play or building depth tables.
rosters = sdv.cfb.load_cfb_rosters(seasons=[2023])
print('rosters shape:', rosters.shape)
rosters.select([
'athlete_id', 'first_name', 'last_name', 'team',
'position', 'jersey', 'home_state',
]).head()
rosters shape: (22467, 18)
shape: (5, 7)
ββββββββββββββ¬βββββββββββββ¬ββββββββββββ¬ββββββββββββββββββββ¬βββββββββββ¬βββββββββ¬βββββββββββββ
β athlete_id β first_name β last_name β team β position β jersey β home_state β
β --- β --- β --- β --- β --- β --- β --- β
β str β str β str β str β str β i32 β str β
ββββββββββββββͺβββββββββββββͺββββββββββββͺββββββββββββββββββββͺβββββββββββͺβββββββββͺβββββββββββββ‘
β 102597 β Will β Rogers β Mississippi State β QB β 7 β MS β
β 107494 β Trey β Sanders β TCU β RB β 2 β FL β
β 146583 β John β Adams β Temple β WR β 17 β NJ β
β 160900 β Will β Johnson β Michigan β null β null β null β
β 169499 β Ryan β Johnson β Akron β DL β 4 β MS β
ββββββββββββββ΄βββββββββββββ΄ββββββββββββ΄ββββββββββββββββββββ΄βββββββββββ΄βββββββββ΄βββββββββββββ
ποΈ Premium loaders: team infoβ
load_cfb_team_info
carries the reference metadata you'll want to label every chart: school
name, conference, classification (FBS/FCS), team colors, and venue.
team_info = sdv.cfb.load_cfb_team_info(seasons=[2023])
print('team_info shape:', team_info.shape)
team_info.select([
'team_id', 'school', 'conference', 'classification',
'venue_name', 'city', 'state', 'dome',
]).head()
team_info shape: (1840, 28)
shape: (5, 8)
βββββββββββ¬ββββββββββββββββ¬ββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββ¬ββββββββ¬ββββββββ
β team_id β school β conference β classificati β venue_name β city β state β dome β
β --- β --- β --- β on β --- β --- β --- β --- β
β i32 β str β str β --- β str β str β str β bool β
β β β β str β β β β β
βββββββββββͺββββββββββββββββͺββββββββββββββββͺβββββββββββββββͺβββββββββββββββͺβββββββββββͺββββββββͺββββββββ‘
β 2000 β Abilene β UAC β fcs β Wildcat β Abilene β TX β false β
β β Christian β β β Stadium (TX) β β β β
β 2001 β Adams State β Rocky β ii β Rex Stadium β Alamosa β CO β false β
β β β Mountain β β β β β β
β 2003 β Adrian β Michigan β iii β Docking β Adrian β MI β false β
β β β β β Stadium β β β β
β 2005 β Air Force β Mountain West β fbs β Falcon β Colorado β CO β false β
β β β β β Stadium β Springs β β β
β 2006 β Akron β Mid-American β fbs β Summa Field β Akron β OH β false β
β β β β β at β β β β
β β β β β InfoCision β β β β
β β β β β Stadβ¦ β β β β
βββββββββββ΄ββββββββββββββββ΄ββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββ΄βββββββββββ΄ββββββββ΄ββββββββ
π¬ Premium loaders: play-by-play with EPAβ
The crown jewel. load_cfb_pbp
returns every play of a season with hundreds of engineered columns β
down & distance, win probability, and Expected Points Added (EPA)
already computed. (It's a big pull, so we grab a single season and peek.) π
# The release serves PBP for whichever seasons are currently published.
# Try a few recent-ish seasons and keep the first one that comes back full,
# so the EPA recipes below always have real plays to chew on.
pbp = pl.DataFrame()
for yr in (2023, 2022, 2021, 2020):
cand = safe(f'load_cfb_pbp {yr}', lambda yr=yr: sdv.cfb.load_cfb_pbp(seasons=[yr]))
if cand is not None and cand.width > 0 and cand.height > 0:
pbp, PBP_SEASON = cand, yr
break
else:
PBP_SEASON = None
print('pbp season:', PBP_SEASON, '| pbp shape:', pbp.shape)
cols = ['game_id', 'start.pos_team.name', 'down', 'distance',
'play_type', 'EPA', 'wpa']
have = [c for c in cols if c in pbp.columns]
pbp.select(have).head() if have else 'pbp not published for these seasons right now'
β
load_cfb_pbp 2023
β
load_cfb_pbp 2022
β
load_cfb_pbp 2021
pbp season: 2021 | pbp shape: (137046, 370)
shape: (5, 6)
βββββββββββββ¬ββββββββββββββββββββββ¬βββββββ¬βββββββββββ¬ββββββββββββ¬ββββββββββββ
β game_id β start.pos_team.name β down β distance β EPA β wpa β
β --- β --- β --- β --- β --- β --- β
β i64 β str β f64 β f64 β f64 β f64 β
βββββββββββββͺββββββββββββββββββββββͺβββββββͺβββββββββββͺββββββββββββͺββββββββββββ‘
β 401281942 β Miami β null β null β -1.024319 β -0.605042 β
β 401281942 β Alabama β null β null β -0.345141 β -0.434851 β
β 401281942 β Alabama β null β null β -0.666759 β -0.000523 β
β 401281942 β Alabama β null β null β -0.298288 β -0.000052 β
β 401281942 β Alabama β null β null β -0.20101 β -0.000039 β
βββββββββββββ΄ββββββββββββββββββββββ΄βββββββ΄βββββββββββ΄ββββββββββββ΄ββββββββββββ
π‘ Live from ESPN: the scoreboardβ
When you need today's slate (or a specific date), the ESPN wrappers shine.
espn_cfb_scoreboard takes a
dates=YYYYMMDD (or season year) and returns the games on the board. We wrap
it in safe() since live endpoints can be quiet in the offseason.
board = safe(
'ESPN scoreboard',
lambda: sdv.cfb.espn_cfb_scoreboard(dates=20231125), # rivalry Saturday
)
if board is not None and getattr(board, 'height', 0):
keep = [c for c in board.columns
if c in ('game_id', 'name', 'short_name', 'status_type_description',
'home_team_abbreviation', 'away_team_abbreviation')]
out = board.select(keep).head() if keep else board.head()
else:
out = 'no games on the board for that date'
out
β
ESPN scoreboard
shape: (5, 4)
βββββββββββββ¬ββββββββββββββββββββββββββββββββββ¬βββββββββββββ¬ββββββββββββββββββββββββββ
β game_id β name β short_name β status_type_description β
β --- β --- β --- β --- β
β str β str β str β str β
βββββββββββββͺββββββββββββββββββββββββββββββββββͺβββββββββββββͺββββββββββββββββββββββββββ‘
β 401520430 β Georgia Bulldogs at Georgia Teβ¦ β UGA @ GT β Final β
β 401520434 β Ohio State Buckeyes at Michigaβ¦ β OSU @ MICH β Final β
β 401524068 β Washington State Cougars at Waβ¦ β WSU @ WASH β Final β
β 401520429 β Florida State Seminoles at Floβ¦ β FSU @ FLA β Final β
β 401520427 β Alabama Crimson Tide at Auburnβ¦ β ALA @ AUB β Final β
βββββββββββββ΄ββββββββββββββββββββββββββββββββββ΄βββββββββββββ΄ββββββββββββββββββββββββββ
π« Live from ESPN: teams (and their team_ids)β
espn_cfb_teams lists every
team in a division (groups=80 FBS, groups=81 FCS). The team_id column
is the key you feed into every team-scoped ESPN call below.
teams = safe('ESPN teams', sdv.cfb.espn_cfb_teams)
if teams is not None and teams.height:
cols = [c for c in ('team_id', 'team_location', 'team_name',
'team_abbreviation') if c in teams.columns]
out = teams.select(cols).head(8)
else:
out = 'teams unavailable right now'
out
β
ESPN teams
shape: (8, 4)
βββββββββββ¬ββββββββββββββββββββ¬βββββββββββββββ¬ββββββββββββββββββββ
β team_id β team_location β team_name β team_abbreviation β
β --- β --- β --- β --- β
β str β str β str β str β
βββββββββββͺββββββββββββββββββββͺβββββββββββββββͺββββββββββββββββββββ‘
β 2000 β Abilene Christian β Wildcats β ACU β
β 2001 β Adams State β Grizzlies β ADSU β
β 2003 β Adrian β Bulldogs β ADR β
β 2005 β Air Force β Falcons β AF β
β 2006 β Akron β Zips β AKR β
β 2010 β Alabama A&M β Bulldogs β AAMU β
β 333 β Alabama β Crimson Tide β ALA β
β 2011 β Alabama State β Hornets β ALST β
βββββββββββ΄ββββββββββββββββββββ΄βββββββββββββββ΄ββββββββββββββββββββ
π³ Cookbook: common CFB tasksβ
Now the fun part β real questions, answered with a few expressions. The loaders are reliable so these recipes lean on them, reaching for ESPN where it adds something live.
Recipe 1 β Highest-scoring games of the season π₯β
Straight from the loaded schedule: add the two scores and sort. No casting needed β the release frame already stores points as integers.
(schedule
.with_columns(
(pl.col('home_points') + pl.col('away_points')).alias('total_points')
)
.sort('total_points', descending=True)
.select(['week', 'home_team', 'away_team',
'home_points', 'away_points', 'total_points'])
.head(10))
shape: (10, 6)
ββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬βββββββββββββββ
β week β home_team β away_team β home_points β away_points β total_points β
β --- β --- β --- β --- β --- β --- β
β i32 β str β str β i32 β i32 β i32 β
ββββββββͺβββββββββββββββββββββββͺβββββββββββββββββββββͺββββββββββββββͺββββββββββββββͺβββββββββββββββ‘
β 9 β Colby College β Middlebury β null β null β null β
β 9 β Bowdoin β Trinity (CT) β null β null β null β
β 9 β Bates β Williams β null β null β null β
β 11 β Worcester St β Framingham State β null β null β null β
β 10 β Defiance College β Rose-Hulman β 54 β 78 β 132 β
β 10 β Muskingum University β Wilmington (OH) β 64 β 63 β 127 β
β 2 β Coast Guard β Anna Maria College β 93 β 24 β 117 β
β 13 β Oklahoma β TCU β 69 β 45 β 114 β
β 3 β Texas State β Jackson State β 77 β 34 β 111 β
β 10 β Cornell College (IA) β Illinois College β 34 β 76 β 110 β
ββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄βββββββββββββββ
Recipe 2 β Team offensive EPA/play leaderboard πβ
This is what premium EPA-tagged play-by-play unlocks. Filter to real scrimmage plays, group by the offense, and average the EPA per play β a clean efficiency ranking in five lines.
team_col = 'start.pos_team.name' # human-readable offense on each play
epa_cols = {team_col, 'EPA', 'play'}
if epa_cols.issubset(pbp.columns):
leaderboard = (
pbp
.filter(pl.col('play') & pl.col('EPA').is_not_null())
.group_by(team_col)
.agg(
pl.len().alias('plays'),
pl.col('EPA').mean().round(3).alias('epa_per_play'),
)
.filter(pl.col('plays') >= 500)
.sort('epa_per_play', descending=True)
.rename({team_col: 'offense'})
.head(15)
)
out = leaderboard
else:
out = 'expected EPA columns not present in this pbp build'
out
shape: (15, 3)
ββββββββββββββββββββ¬ββββββββ¬βββββββββββββββ
β offense β plays β epa_per_play β
β --- β --- β --- β
β str β u32 β f64 β
ββββββββββββββββββββͺββββββββͺβββββββββββββββ‘
β Ohio State β 972 β 0.28 β
β Western Kentucky β 1021 β 0.195 β
β Virginia β 914 β 0.178 β
β Oregon State β 897 β 0.17 β
β Oklahoma β 970 β 0.154 β
β β¦ β β¦ β β¦ β
β Wake Forest β 1071 β 0.121 β
β Alabama β 1202 β 0.115 β
β Nevada β 980 β 0.114 β
β Missouri β 906 β 0.113 β
β TCU β 730 β 0.112 β
ββββββββββββββββββββ΄ββββββββ΄βββββββββββββββ