cfbfastR 3.0.0.9000 (development version)
The development version is numbered 3.0.0.9000 so it can be told apart from the CRAN 3.0.0 release, which ships the previous model generation. Until now both builds reported 3.0.0, so packageVersion("cfbfastR") could not distinguish them — while producing different EPA/WPA for the same play. A four-component version means you are on the GitHub build. See the model note below before mixing outputs from the two.
Air yards side the catch spot by the game’s own text abbreviations
The 2025+ ESPN vendor text spots the catch with each school’s own abbreviation (UHM, GSO, USC for South Carolina, Sac St, BC.), which is frequently not the payload’s home_team_abbreviation / away_team_abbreviation. The helper behind air_yards, air_yardsToEndzone and yards_after_catch now learns each game’s abbreviations from its own "... to the ABC nn" end spots against yards_to_goal_end (majority vote per abbreviation) before falling back to the payload abbreviations, reads every observed token shape (UA 10, BC.41, Sac St10, NC ST19) and resolves a spot at the 50 without one. On 2025 new-template games this lifts in-game air-yards coverage from 72% to 90% of pass plays; every spot-phrase play now resolves. Same logic as sdv-py #418; the parity oracle was re-captured from sdv-py 9efee9f1 and extended with five affected 2025/26 games (tests/testthat/fixtures/parity/README.md).
Expected Points model now comes from the shared cfb_model_artifacts bundle
The EP model is now the XGBoost artifact published in cfb_model_artifacts — the same artifact sportsdataverse-py scores with, so both libraries agree on EPA for a given play and a retrain updates both from one publish (#138).
-
Fixes #5 —
epa_wpa = TRUEno longer aborts withpredict.nnet(): missing values in 'x'on mid-era CFBD data (seasons ~2006–2013), in either engine. - EP scoring is consolidated behind one internal helper, so the seven next-score probability columns keep their historical names and order and no downstream code changed. The bundle’s class order differs from the retired model’s with no fixed point between them; the permutation is read from the bundle’s
MANIFEST.jsonand asserted in tests, because getting it wrong yields EP that is wrong yet plausible-looking. - Model artifacts are cached under the package cache dir and refreshed on the
cfbfastR.cache_durationTTL (default 24h), so a republished model is picked up without a package update. An expired cached copy is still used if the release is unreachable. -
xgboost(Suggests) is now required to score EP and its floor moved to>= 1.7for.ubjsupport. Without it — or offline with no cached copy — the retirednnetmodel is loaded as a fallback and still works. - The Win Probability model moved to the bundle’s
wp_naive.ubjon the same terms. Eleven of its twelve features already existed on the frame; onlyis_homeis derived. - The Field Goal model moved to the bundle’s era-aware
fg_model.ubj(yards_to_goal+ one-hotera0..era3).seasonis now threaded through.run_epa_wpa()and the exportedcreate_epa()/epa_fg_probs()gain aseasonargument (defaulting toNULL). Scoring the era-aware model without a season is an error rather than a silent all-zero one-hot. -
New: completion probability.
cpandcpoecolumns are added on pass plays,cpoeon the percentage-point scale100 * (completion - cp), matchingsportsdataverse-py. The model loads lazily on first use and the stage degrades toNAcolumns rather than failing. -
New: spread-aware win probability. A
vegas_wpcolumn is added alongside the unchanged naivewp_before— the same split nflfastR draws betweenwpandvegas_wp. It isNAwherever the game has no pre-game line (the ESPN path, and CFBD before 2013). Sign convention verified over 83 games: CFBD’sspreadis negative when the home team is favoured, and the model reads positivespread_timeas the team in possession being favoured.vegas_wpaandvegas_wp_afterare derived alongside it, using the same turnover and half/period-end overlays as the naivewpa. -
New: expected pass rate.
xpassandpass_oecolumns are added on scrimmage plays (nflfastR’sxpass/pass_oe),pass_oeon the percentage-point scale100 * (pass - xpass). Note this model uses an ordinal rule-era feature cutting at 2006/2013/2017, which is a different encoding from the FG model’s one-hotera0..era3(2006/2013/2020). - Existing EPA/WPA values will change: this is a different model generation. Rebuild rather than mixing old and new outputs in one dataset.
Decision surfaces and QBR from the same bundle
The three remaining cfb_model_artifacts artifacts are now used: the two-point, fourth-down and QBR models (#140). Unlike the model columns above, these are analytic surfaces — they build the game state that WOULD follow a choice, score it, and compare. They are ports of cfb4th and degrade to NA columns rather than failing when a model, the punt table, arrow, or a pre-game line is missing.
-
New: the two-point decision. On offensive touchdowns,
two_pt_wp/xp_wp/two_pt_wp_diff/two_pt_recommendationcompare going for two against kicking, using the conversion probabilityprob_2ptand the opponent’s ensuing-drive win probability for each outcome. Verified bit-identical tosportsdataverse-pyto eight decimals. Readtwo_pt_wp_diffrather than the bare recommendation: cfb4th’s rule has no margin, and the bundled model is optimistic about college conversion rates. -
New: the fourth-down decision. On fourth downs,
go_wp/punt_wp/fg_wpand the comparison columnsgo_boost,go_wp_diff,fg_wp_diff,punt_wp_diffandfourth_down_recommendation. The go branch expands the fourth-down model’s 76-class yards-gained distribution to one state per possible outcome; the punt branch joins the empirical end-yardline distribution (NAinside the 31, where cfb4th’s table is empty); the field-goal branch carries cfb4th’s policy clamps (zero beyond 42 yards-to-goal, 0.9x from 35 out).first_down_prob,wp_succeed,wp_fail,make_fg_wp,miss_fg_wpandfourth_down_fg_make_probare exposed alongside. Note the last of those is namespaced:fg_make_probalready means the make probability of the field goal that was actually attempted. -
New:
create_qbr(). Per-quarterback, per-game leverage-weighted EPA components scored through the bundled QBR model. It emits its own table rather than columns on the play-by-play frame, so it is an entry point you call on a modeled frame, not a pipeline stage. - These three surfaces have no cross-language oracle for the fourth-down branches —
sportsdataverse-py’sget_go_wp()raises on pandas 3 — so they were ported from thecfb4thR source and gated behaviourally against known college conversion and field-goal rates. Three sign conventions were found inverted in the Python port while doing so; each produced a confident wrong recommendation rather than an error, and each is pinned by test here. - Every decision column is
NAon the ESPN engine, which carries no pre-game spread.
cfbfastR v3.0.0
CRAN release: 2026-08-24
New release-dataset loaders (48 functions)
cfbfastR now loads every published CFB dataset on the sportsdataverse-data release repo, closing the gap with sportsdataverse-py’s loader surface. All loaders return cfbfastR_data-tagged tibbles, accept seasons = TRUE for the full published range, and support the dbConnection/tablename database write-through.
-
load_espn_cfb_*()— 27 loaders for the ESPN-derived family:pbp,schedules,team_box,player_box,drives,game_rosters,linescores,betting,play_participants,power_index,percentiles,passing,rushing,receiving,team_summaries,model_pbp, and the elevenadv_*advanced-stats datasets. -
load_cfb_ratings(),load_cfb_ratings_weekly(),load_cfb_fpi_weekly(),load_cfb_team_summaries_weekly(),load_cfb_team_talent(),load_cfb_recruits(),load_cfb_recruiting_proj(),load_cfb_returning_production()— season/weekly ratings, talent, and recruiting datasets. -
load_cfb_schedule_crosswalk(),load_cfb_teams_crosswalk(),load_cfb_rosters_crosswalk()— CFBD/ESPN id crosswalks. -
load_ncaa_mfb_*()— 10 loaders for the stats.ncaa.org men’s football family (covers FCS and lower divisions):pbp,pbp_cfbfastr(the cfbfastR-schema-shaped variant),drives,linescore,officials,player_stats,rosters,schedule,team_stats,teams. - New internal
parquet_from_url()helper backs the parquet-only datasets (requires the suggestedarrowpackage).
Which play-by-play loader do I want? Nothing is deprecated — the classic functions are unchanged; the new families add sources:
-
load_cfb_pbp()(unchanged) — the classic cfbfastR EPA/WPA play-by-play, FBS 2014+. -
load_espn_cfb_pbp()(new) — the ESPN-derived play-by-play (469 columns, EPA/WPA + participant ids), 2004+. -
load_ncaa_mfb_pbp()(new) — stats.ncaa.org play-by-play incl. FCS and lower divisions, 2013+;load_ncaa_mfb_pbp_cfbfastr()is the same data reshaped onto cfbfastR pbp column conventions for cross-source binds.
This release adds a 65-function ESPN college-football API layer, expanding cfbfastR’s ESPN surface from 8 wrappers to 73. The new wrappers expose ESPN’s core-v2 endpoints in ESPN’s own ID space — complementary to the CollegeFootballData (cfbd_*) wrappers, and the natural join partners for espn_cfb_pbp() / espn_cfb_scoreboard(). Every wrapper was verified live against the 2023, 2024, and 2025 seasons.
Naming alignment with the sportsdataverse convention (this dev cycle, never on CRAN): espn_cfb_player_statistics() is renamed to espn_cfb_player_career_stats() (the core-v2 /athletes/{id}/statistics career view, matching hoopR/wehoop/sportsdataverse-py). New espn_cfb_player_stats_v3() wraps the comprehensive web-common-v3 /athletes/{id}/stats payload (all categories, long format) — the _v3 companion to espn_cfb_player_stats() (core-v2 season statistics).
New Fox Sports API wrappers (fox_cfb_*)
A read-only Fox Sports “Bifrost” college-football layer (api.foxsports.com/bifrost/v1/cfb/*), complementary to the espn_cfb_* and cfbd_* families. Eight wrappers flatten Fox’s layout-oriented JSON (sections → tables → rows → cells) into tidy cfbfastR-tagged tibbles. Reverse-engineering notes and an OpenAPI 3.1 spec live in the sdv-internal-refs repo. Verified live against the 2025 season.
-
fox_cfb_pbp()— game play-by-play (quarters → drives → plays), one row per play. -
fox_cfb_boxscore()— per-team player stat tables, tidy long (one row per player-stat). -
fox_cfb_odds()— matchup six-pack (spread / to-win / total) per team. -
fox_cfb_team_roster()— roster by position group, one row per player. -
fox_cfb_team_stats()— team stat leaders by category. -
fox_cfb_team_gamelog()— per-game team stats, tidy long (game × category × stat), each game keyed to its Fox event id. -
fox_cfb_standings()— a team’s conference standings table. -
fox_cfb_league_leaders()— statistical leaderboards by category (stats-con).
New ESPN wrappers — football-specific metrics
-
espn_cfb_powerindex()— ESPN’s College Football Power Index (FPI): every predictive metric and efficiency component, in long format. -
espn_cfb_qbr()— Total Quarterback Rating (QBR) and the full set of clutch-weighted EPA components, one row per qualified passer. -
espn_cfb_futures()— the season betting-futures board (national championship, conference, and award markets) with each sportsbook’s American odds. -
espn_cfb_recruits()— ESPN’s recruiting board for a class, one row per recruit with grade, position/state/region rank, committed school, and hometown.
New ESPN wrappers — players
-
espn_cfb_players(),espn_cfb_player(),espn_cfb_player_eventlog(),espn_cfb_player_gamelog(),espn_cfb_player_statistics(),espn_cfb_player_splits(),espn_cfb_player_overview(), andespn_cfb_player_seasons()— player index, biographical detail, per-game logs, season statistics, and split breakdowns. The season-level wrappers resolveathlete_idto human-readable name/position columns via anathlete_detailargument.
New ESPN wrappers — teams
-
espn_cfb_teams(),espn_cfb_team(),espn_cfb_team_roster(),espn_cfb_team_schedule(),espn_cfb_team_record(), andespn_cfb_team_leaders()— team index, team-in-season detail, roster, schedule, records, and statistical leaders. -
espn_cfb_team_ats(),espn_cfb_team_powerindex(),espn_cfb_team_events(),espn_cfb_team_ranks(),espn_cfb_team_awards(), andespn_cfb_team_coaches()— team against-the-spread records, single-team power index, season event log, poll-rank history, player awards, and coaching staff.
New ESPN wrappers — game detail
-
espn_cfb_game_teams(),espn_cfb_game_team_linescores(),espn_cfb_game_team_leaders(),espn_cfb_game_team_roster(),espn_cfb_game_team_statistics(), andespn_cfb_game_team_records()— per-game team breakdowns. -
espn_cfb_game_odds(),espn_cfb_game_broadcasts(),espn_cfb_game_predictor(),espn_cfb_game_probabilities(),espn_cfb_game_powerindex(), andespn_cfb_game_pbp()— per-game odds, broadcasts, pre-game predictor, live win-probability, matchup power index, and play-by-play. -
espn_cfb_game_drives(),espn_cfb_game_drive_plays(),espn_cfb_game_play(),espn_cfb_game_leaders(),espn_cfb_game_situation(),espn_cfb_game_status(),espn_cfb_game_player_statistics(), andespn_cfb_game_player_box()— drive log, drive-scoped plays, single-play detail, game statistical leaders, situation, status, and per-player game box lines. - The play-level wrappers (
espn_cfb_game_pbp(),espn_cfb_game_drive_plays(),espn_cfb_game_play()) extract every field ESPN returns for a play and expose its nested child collections through opt-in parameters:participants("none"/"wide"/"long") andparticipants_listsurface per-play athlete involvement (passer, rusher, tackler, …), andteam_participants/team_participants_listsurface the offense/defense team participants —"wide"modes pivot to one row per play, the*_listflags keep the raw detail as a list-column. -
espn_cfb_game_team_records(detail = TRUE)unpacks each record’s full statistic breakdown;espn_cfb_game_odds(line_history = TRUE)returns the open/close/current line-movement history. Roster and player-stats wrappers join ESPN position-catalog detail whenposition_detail = TRUE. -
espn_cfb_game_drives()gains aplaysargument —"list"nests each drive’s plays (full play-by-play schema, with the participant pass-through options) as a list-column,"expand"returns the flat one-row-per-play table withdrive_*context columns. The newespn_cfb_unnest_plays()performs the same drives-to-play-by-play transform on an already-fetched nested frame. - The game wrappers join human-readable team detail when
team_detail = TRUE(default): every team-id column (team_id,home_team_id,start_team_id,leader_team_id, …) gains sibling*_name,*_abbreviation,*_location,*_display_name,*_color,*_logo_href, … columns from the ESPN team catalog.espn_cfb_game_teams(format = "wide")collapses the two competitor rows into a single per-game row withhome_*/away_*columns for direct joining onto one-row-per-game tables. -
espn_cfb_pbp_v2()— a core-v2-sourced successor toespn_cfb_pbp(): assembles play-by-play in one structured request (vs. the legacy site-v2 summary parse) and, withepa_wpa = TRUE, runs cfbfastR’s full EPA/WPA model pipeline — producing EPA/WPA columns identical to the legacy modeled feed.
New ESPN wrappers — catalogs and season metadata
-
espn_cfb_seasons(),espn_cfb_season_info(),espn_cfb_season_types(),espn_cfb_season_weeks(),espn_cfb_groups(), andespn_cfb_standings()— season structure, conferences, and standings. -
espn_cfb_coaches(),espn_cfb_coach(),espn_cfb_venues(),espn_cfb_positions(),espn_cfb_awards(),espn_cfb_rankings(), andespn_cfb_week_rankings()— league catalogs and poll rankings. -
espn_cfb_coach_record(),espn_cfb_franchises(),espn_cfb_franchise(),espn_cfb_venue(),espn_cfb_position(), andespn_cfb_award()— coach season win/loss records, the league franchise catalog, and single-record venue / position / award detail.
New Yahoo Sports wrappers
- Added Yahoo Sports college football wrappers:
yahoo_cfb_player_season_stats(),yahoo_cfb_team_season_stats(),yahoo_cfb_player_season_stats_legacy(),yahoo_cfb_team_season_stats_legacy(),yahoo_cfb_scoreboard(), andyahoo_cfb_boxscore()(scaffold), wrapping Yahoo’s shangrila stats graph and editorial feed.
New CollegeFootballData wrappers
-
cfbd_betting_ats()— season against-the-spread (ATS) summary records by team, wrapping the CollegeFootballData/teams/atsendpoint. -
cfbd_stats_game_havoc()— per-game havoc statistics (total / front-seven / defensive-back havoc events and rates, offense and defense), wrapping the CollegeFootballData/stats/game/havocendpoint. -
cfbd_pbp_data_v2()is a new public function: a modular successor tocfbd_pbp_data()that runs the same EPA/WPA pipeline through a single shared engine (.run_epa_wpa()) and a canonical play-type taxonomy (.pbp_play_types()). The legacycfbd_pbp_data()is unchanged. -
espn_cfb_pbp_v2()now sources play-by-play and meta through the shared engine, requestsparticipants = "wide"andteam_participants = "wide"fromespn_cfb_game_drives(), and adds the meta columnshome_team_name,home_team_color,home_team_alternate_color,home_team_rank(andaway_*) via the new.espn_pbp_game_meta()bridge. Output is a strict superset of legacyespn_cfb_pbp()on the meta columns.
Play-by-play engine — v2 is now the default
cfbd_pbp_data()andespn_cfb_pbp()now run the v2 engine by default. Both gain penalty enforcement resolution, ESPN-resolved player names, the*_player_idcolumns and theoutputtier selector without a code change. The previous behaviour is one argument away —engine = "legacy"per call, oroptions(cfbfastR.pbp_engine = "legacy")for a session — and a once-per-session message says so.engine = "auto"continues to mean “whatever this release considers current”.tests/testthat/test-pbp_equivalence.Rasserts v2 reproduces the legacy frames column-for-column, with an explicit allow-list of intentional deltas.Play-by-play now overwrites the regex-extracted
*_player_namevalues with ESPN’s ownparticipants[]names (2014 onward), so a capture that trailed narration ("Rod Smith 3 Yd"), abbreviated, or carried a team code becomes the real name. Ported fromsportsdataverse’sCFBPlayProcess.__join_participantsand verified against a 60-game offline oracle (5 games from each of 2004, 2006, 2008, 2010, 2013, 2014, 2017, 2019, 2020, 2021, 2023, 2025, including the postseason): 1,236 of 9,545 × 11 name cells change, with zero divergence from the Python. The stage runs before id resolution, so the roster matcher gets a clean key rather than narration.espn_cfb_pbp_v2()gainsresolve_names(defaultTRUE). Whenepa_wpa = TRUEit spends one memoised request per game on ESPN’s play-by-play sidecar, which supplies (a) full athlete names —"Jalen Mitchell"rather than the core-v2 roster’s"J. Mitchell"— and (b) the per-player box score as a second identity source. The box score is the only identity source on the large share of games where ESPN 404s the roster resource; adding it cuts*_player_iddivergence from sdv-py from 52/163,656 to 26/166,482 — fewer mismatches over more plays. Setresolve_names = FALSEfor a bulk sweep that would rather have the short names than the requests.-
Play-by-play now resolves which team each event belongs to, adding 30 columns cfbfastR could not previously produce: the special-teams flip (
kicking_team,return_team,punt_return_team,kick_return_team,fg_team,punt_team), the event-credit columns (sack_team,interception_team,pass_breakup_team,forced_fumble_team,fumble_recovery_team), the fumble/recovery chain (fumble_or_muff,fumbling_team,recovery_team,recovery_team_2), the per-side turnover model (is_turnover,turnover_team,int_turnover,pos_fumble_lost,def_fumble_lost,is_pos_team_turnover,is_def_pos_team_turnover,is_st_turnover,is_blocked_punt_turnover,is_blocked_fg_turnover), penalty attribution (penalized_team,penalty_team_id,penalty_yards_signed) and the id-keyedpos_team_id/def_pos_team_id. Ported fromsportsdataverse’sCFBPlayProcess.__add_attribution_colsand verified against the 60-game offline oracle at zero divergence over 267,260 cells.The turnover flags are framed per side because one play can lose the ball twice — the offense fumbles, the defense recovers and fumbles back — and a single boolean cannot say that both teams turned it over. Blocked punts and blocked field goals deliberately stay out of
is_turnover: ESPN’s official box counts only giveaways, so folding them in would break the reconciliation against it. They get their own flags instead.On the ESPN path, ESPN’s own per-play turnover flag is preserved as
espn_is_turnoverrather than being silently overwritten. The two legitimately differ — ESPN’s also fires on blocked kicks. -
Play-by-play gains
air_yards,air_yardsToEndzoneandyards_after_catch, splitting a completed pass into the yards thrown and the yards run after the catch. ESPN states the catch point as"caught at OU35"; the stated yard line belongs to whichever team owns that side of the field, so the abbreviation is sided against the possessing and defending teams with the same prefix-tolerant matcher the recovery and penalty teams use.yards_after_catchis computed for completions only. Ported fromsportsdataverse’sCFBPlayProcess.__add_air_yards_cols.One deliberate divergence from the Python: sdv-py’s pattern has no article, so it resolves
"caught at OU35"and silently drops"thrown to the ARK30". Both forms occur in ESPN’s text — and cfbfastR’s core-v2 feed uses the article form almost exclusively, where a verbatim port would have matched nothing at all. This implementation accepts both. The parity test is partitioned accordingly: exact agreement on the 137 oracle rows sdv-py resolves, plus an explicit assertion that the 7 article-form rows it drops are recovered here. Play-by-play gains
pass_depth,pass_direction,rush_directionandqb_hurry, read from the play text ("short"/"deep","left"/"middle"/"right", and ESPN’s"QB hurried by"annotation). Null where ESPN omits the phrase — sacks, screens, and older seasons that never annotated depth or direction.espn_cfb_pbp_v2()now refuses to run the EPA/WPA models on an obviously malformed feed — no plays, or implausibly few or many for a game that has finished — and returns the unmodeled frame with a warning instead. A truncated game models perfectly cleanly and produces EPA, drive results and a box score that all look reasonable and are all wrong, and nothing downstream can distinguish that from a real blowout. The count rules apply only to completed games so a live feed is never rejected. Ported fromCFBPlayProcess.corrupt_pbp_check.-
A pbp-to-boxscore parity gate now guards the test suite. The per-play parity tests check a play against itself; this aggregates play-by-play by team and compares it against ESPN’s own team box, which is the only cheap end-to-end judge of whether parsing put the right events on the right team. An attribution bug leaves every per-play assertion green and shows up here immediately. Ported from
sportsdataverse’stools/validation/checks/boxscore_parity, adapted from a per-season harness check to a per-game library gate.It encodes three conventions proven against the box, two of which are the opposite of the NFL’s: NCAA charges a sack to rushing (attempt and yardage both), pass attempts exclude sacks, and a penalty belongs to the team that committed it — a positive
penalty_yards_signedmeans the offence gained, so the defence was flagged. Floors are measured from the shipped 60-game corpus, never guessed, and are per-stat because parity is strongly era-dependent (interceptions reconcile at 97%, 2004-inclusive rushing yardage at 31%).
CFBD API coverage
Audited against the CollegeFootballData OpenAPI spec (5.24.1, 74 endpoints).
15 endpoints that had no wrapper now have one: cfbd_playoffs_cfp(), cfbd_playoffs_cfp_games(), cfbd_playoffs_cfp_participants(), cfbd_conference_affiliations(), cfbd_conference_changes(), cfbd_coaches_profile(), cfbd_coaches_seasons(), cfbd_coaches_tenures(), cfbd_ratings_core(), cfbd_ratings_srs_expanded(), cfbd_teams_fbs(), cfbd_stats_player_success(), cfbd_stats_player_success_game(), cfbd_player_season_overview() and cfbd_info_usage(). Every one was exercised against the live API before being committed.
26 parameters added to existing wrappers — most importantly division on ten more functions, plus defense / offense_conference / defense_conference / conference / division on cfbd_pbp_data(), competition and round on cfbd_game_info() (College Football Playoff filtering), line_provider on cfbd_betting_lines(), conference on cfbd_play_stats_player() and recruit_type on cfbd_recruiting_position(). cfbd_conferences() previously took no arguments at all and now accepts year and division.
New validate_division() covering fbs / fcs / ii / ii/iii / iii. This validates locally because CFBD ignores an unrecognised filter value rather than rejecting it — so without it a typo silently returns every division.
Two spec parameters were deliberately not exposed after testing them: /rankings declares latest and final as booleans, but the API returns HTTP 400 for every form of both. poll is validated to "cfp", the only value it accepts.
Bug fixes
espn_cfb_team_coaches()— theyearargument is deprecated. ESPN’s core-v2 coaches endpoint returns the current coach whatever season is requested, echoing the requested year back in the response, so historical calls silently returned today’s coach labelled with the old season (#125).yearnow defaults tomost_recent_cfb_season(); passing any other season warns and is coerced, rather than returning misattributed data. Existing calls keep working.-
espn_cfb_teams()returned zero rows, becausesite.api.espn.comnow answers HTTP 403 to a spoofed browserUser-Agent. The failure was silent — the wrapper caught it and returned an empty frame — and every consumer degraded toNA, which tookhome,away,pos_team,def_pos_team,offense_play,defense_playand every team abbreviation on the ESPN play-by-play path down with it. Measured 2026-08-19: the endpoint answers 200 with httr2’s default UA, withcurl/8.5.0, or withAccept/Origin/Refererand no UA at all, and 403 with the Chrome string. TheUser-Agentheader is dropped.Probing every ESPN host the package uses narrowed the blast radius to exactly two callers —
espn_cfb_teams()andespn_cfb_team_schedule(), the only two that combinesite.api.espn.comwith the spoofed header.espn_cfb_team_schedule()was returning zero rows for the same reason and now returns data. The other ~65 occurrences of the header sit onsports.core.api.espn.comandsite.web.api.espn.com, which answer 200 either way, so they are left alone.Worth knowing for anyone adding an ESPN call:
cdn.espn.comdoes something worse than a 403 under the browser UA — it answers 200 with a zero-byte body, so nothing raises and the parse silently yields nothing.tests/testthat/test-espn_http_headers.Ris a source-level guard that fails if the header is re-added to asite.apicaller. defense_playwas a copy-paste duplicate ofoffense_playin the ESPN adapter — bothcase_when()branches returned the home team — so it named the team with the ball on every ESPN play.Play-by-play gains
pos_team_id/def_pos_team_id/offense_play_id/defense_play_id.pos_team,def_pos_team,offense_playanddefense_playare team NAMES resolved through the ESPN teams catalog, and when that catalog is unavailable they all go NA together — which silently disabled team-aware roster matching, dropping every player-id lookup to the global-unique fallback. The ids come straight off the play and are always present. (espn_cfb_teams()currently returns zero rows, so this is the live condition, not a hypothetical.).espn_cfb_participant_roster()is now memoised alongside the ESPN catalog helpers.espn_cfb_pbp_v2()needs one game’s roster twice — once to name participants, once to resolve player ids — and a season sweep asked for it once per game; both now cost a single request. The memoised-helper list is a single constant shared by.onLoad()andespn_cfb_clear_cache(), which had been a second hand-maintained copy that could silently drift into caching a helper it never cleared..run_epa_wpa_by_game()had no roster argument, socfbd_pbp_data_v2()could never resolve*_player_idcolumns however the roster was supplied. It now threadsrostersandparticipantsthrough, sliced pergame_id.espn_cfb_pbp()now builds its request URL with the?event=query separator (previously concatenated assummaryevent=, which returned HTTP 404 for every game) and initializes its return frame before thetryCatchso an upstream failure no longer throwsobject 'plays_df' not found.cfbd_pbp_data_v2()andespn_cfb_pbp_v2()preserve characterid_playprecision through the EPA/WPA pipeline. The legacy shared helper used unquoted numeric literals in twoifelsecalls (a historicalid_playswap for one game), which silently coerced characterid_playto numeric and then lost precision past 2^53 — breaking the play-id join-back inespn_cfb_pbp_v2(). The modular.pbp_clean_pbp_dat()quotes those literals soid_playstays character; the legacyclean_pbp_dat()is unchanged.
Internal changes
-
httr -> httr2 migration. cfbfastR’s HTTP layer now uses the modern
httr2package (>= 1.0.0) instead of the legacyhttr. End users running existing wrapper calls (cfbd_*,espn_cfb_*) should see no behavioural change – the migration is internal. Custom code that callsget_req()orcheck_status()directly must update fromhttr::content(res, as = "text")tohttr2::resp_body_string(res)and fromhttr::status_code(res)tohttr2::resp_status(res). -
Proxy support.
get_req()now resolves a proxy in the order: explicitproxyargument ->getOption("cfbfastR.proxy")->http_proxy/https_proxyenv vars. The proxy value accepts either a URL string or a named list withurl/port/username/password/authfor authenticated proxies. -
Dependency footprint trimmed.
lubridate,progressr,memoise,cachem, andmagrittrhave moved out ofImports(21 -> 16).lubridateis gone entirely – its twoymd_hm() |> with_tz()calls inespn_cfb_schedule.Rare now base-Ras.POSIXct(format = "%Y-%m-%dT%H:%M", tz = "UTC")+attr(., "tzone").progressr,memoise, andcachemmoved toSuggestsand the helpers degrade gracefully when missing:load_cfb_pbp()/cfbd_pbp_data()/pbp_epa_wpa_engine()run without a progress bar whenprogressris absent; ESPN catalog wrappers run uncached whenmemoise/cachemare absent (espn_cfb_clear_cache()becomes a no-op). Drops theImportscount below the >20R CMD checkNOTE threshold. -
Native pipe migration. All 1,419
%>%chains inR/, plus 137 acrossvignettes/andtests/, were converted to the base-R native pipe|>.magrittris no longer anImports; downstream consumers that loadcfbfastRpurely for its functions don’t get%>%re-exported anymore. User-visible impact is minimal – the public API is unchanged anddplyr(which is inImports) still re-exports%>%for users who want to keep writing it. Two non-mechanical fixes were needed during the sweep: three|> [[("url")chains incfbd_betting.Randcfbd_coaches.R(rejected as RHS in R 4.1’s|>) became|> purrr::pluck("url"); seven|> tibble::tibble(col = .data$.)constructs were a magrittr quirk that silently duplicated the LHS into both a.and the named column – rewritten totibble::tibble(col = <lhs>), which drops the redundant.column. -
Test-time CFBD throttle. A new
tests/testthat/setup-cfbd-throttle.Radds a 1-second sleep before every CFBD request made bydevtools::test()/R CMD check. It works by monkey-patchingcfbfastR:::get_req()for the duration of the test session (restored viawithr::defer(., teardown_env())) – the package code is unchanged, so interactive and production calls pay no penalty. Tunable viaoptions(cfbfastR.test_request_delay = N)(default 1; set to 0 for unthrottled local runs). Resolves the cascadingHTTP 429skip-if-empty results that were turning otherwise-green test runs into “all green, mostly skipped.”withrjoinsSuggeststo declare the test-side dependency cleanly (it was already a transitive dep oftestthat).
cfbfastR v2.2.0
- Fixes a bug in
validate_week()utility function where some inputs were not being handled correctly (i.e. week 16). Fixes trickle down tocfbd_pbp_data()and other functions. - Default value for
season_typeparameter incfbd_game_info()andcfbd_play_stats_player()function changed from “regular” to “both” to align with other functions in the package.
cfbfastR v2.1.0
- Fixes a bug in
cfbd_pbp_data()where play-by-play data for some games were not as expected. - Improves
add_yardage()where plays with missing yardage values were not being handled correctly.
cfbfastR v2.0.0
CRAN release: 2025-09-09
Breaking Changes to Loading Functions
- All
load_cfb_*()functions now usesportsdataverse-datareleases or the CollegeFootballData.com API as their underlying data source to remain in compliance with CFBD API terms and conditions (See Note below). - Updated
load_cfb_pbp()dataset to include various team- and game-level ID’s and flags that were not being included, likehome_team_id,away_team_id,season_type,venue_id, somedrive_*columns, a half-dozen player stat columns, etc. Essentially, all the leg-work users have undoubtedly had to do while using these datasets is mostly just included now. The downside: this means end users need to check their pipelines which build off these datasets to ensure behavior is as expected and all your joins are doing what is intended.
Now upgraded to the CFBD v2 API
Special thanks are in order for our newest contributor, Brad Hill (@bradisbrad) for providing most of the v2 upgrade via his first PR to cfbfastR!! 🙌🏽 👑 🥇 Your contributions are most appreciated by the community.
Note: The free-tier API key for the CFBD v2 API has a strict 1k calls/month limit, so plan your workflows accordingly! If you receive errors mentioning r Request failed [429], you have most likely run out of API calls for the month in your membership tier.
-
Added all new
cfbd_*()functions accommodated by the new College Football Data API v2. This includes the following functions:- Added
cfbd_metrics_fg_ep()function to access the new field goal expected points added metric from the API. - Added
cfbd_metrics_wepa_team_season()function to access the new opponent adjusted team season predicted points added metric from the API. - Added
cfbd_metrics_wepa_players_passing()function to access the new opponent adjusted players passing predicted points added metric from the API. - Added
cfbd_metrics_wepa_players_rushing()function to access the new opponent adjusted players rushing predicted points added metric from the API. - Added
cfbd_metrics_wepa_players_kicking()function to access the new Points Added Above Replacement (PAAR) ratings for kickers from the API. - Added
cfbd_ratings_fpi()function to access the new FPI ratings from the API. - Added
cfbd_live_scoreboard()function to access live scoreboard data from the API. - Added
cfbd_live_plays()function to access live play-by-play data from the API. - Added
cfbd_api_key_info()function to get information about your API key, including your Patreon level and usage limits.
- Added
-
Minor changes to the existing
cfbd_*()functions under the hood to accommodate the new API v2 structure. Please see below for a list of all updated functions:- Updated
cfbd_betting_lines()function - Updated
cfbd_coaches()function - Updated
cfbd_conferences()function - Updated
cfbd_drives()function - Updated
cfbd_calendar()function - Updated
cfbd_game_box_advanced()function - Updated
cfbd_game_info()function - Updated
cfbd_game_media()function - Updated
cfbd_game_player_stats()function - Updated
cfbd_game_records()function - Updated
cfbd_game_team_stats()function - Updated
cfbd_metrics_ppa_games()function - Updated
cfbd_metrics_ppa_players_games()function - Updated
cfbd_metrics_ppa_players_season()function - Updated
cfbd_metrics_ppa_predicted()function - Updated
cfbd_metrics_ppa_teams()function - Updated
cfbd_metrics_wp()function - Updated
cfbd_metrics_wp_pregame()function - Updated
cfbd_pbp_data()function - Updated
cfbd_play_stats_player()function - Updated
cfbd_play_stats_types()function - Updated
cfbd_play_types()function - Updated
cfbd_plays()function - Updated
cfbd_player_info()function - Updated
cfbd_player_returning()function - Updated
cfbd_player_usage()function - Updated
cfbd_rankings()function - Updated
cfbd_ratings_sp()function - Updated
cfbd_ratings_sp_conference()function - Updated
cfbd_ratings_srs()function - Updated
cfbd_recruiting_player()function - Updated
cfbd_recruiting_position()function - Updated
cfbd_recruiting_team()function - Updated
cfbd_stats_categories()function - Updated
cfbd_stats_game_advanced()function - Updated
cfbd_stats_season_advanced()function - Updated
cfbd_stats_season_player()function - Updated
cfbd_stats_season_team()function - Updated
cfbd_team_info()function - Updated
cfbd_team_matchup()function - Updated
cfbd_team_matchup_records()function - Updated
cfbd_team_roster()function - Updated
cfbd_team_talent()function - Updated
cfbd_venues()function
- Updated
-
Fixed the following functions and/or documentation:
- Documentation
cfbd_team_info()addressing #97 - Ensuring
cfbd_stats_game_advanced()returns an empty data frame when there are no results - Documentation
cfbd_game_team_stats()updated to reflect all parameter requirement scenarios. - Fixed
athlete_idparametercfbd_player_usage()so that it works as users would expect. There was an API query-parameter mismatch - Fixed
athlete_idparameter forcfbd_play_stats_player()function and added more thorough documentation. - Fixed returned
positionto correct value (instead of NA) fromcfbd_stats_season_player() - Added more thorough
season_typeparameter documentation across many functions - Changed behavior of
cfbd_pbp_data()to substitute 3 timeouts per half when the data is missing from the API.
- Documentation
cfbfastR v1.9.5
- fixed breaking bug related to
stringiv1.8 update incfbd_play_pbp_data()EPA and WPA processing - Minor documentation and test updates
cfbfastR v1.9.4
- Improve date parsing for
espn_cfb_scoreboard()andespn_cfb_schedule()functions while addinglubridatedependency - Made a minor tweak to the returns of the
espn_ratings_fpi()function
cfbfastR v1.9.2
-
espn_cfb_player_stats()function added.
cfbfastR v1.9.1
- Improved drive_pts logic in play-by-play data.
- Fixed an issue that occasionally made the
cfbd_game_team_stats()function return data in a long format - Minor documentation and test updates
cfbfastR v1.9.0
CRAN release: 2022-06-13
Added functions to access ESPN API:
espn_cfb_calendar()espn_cfb_schedule()- Added EPA and WPA processing to
espn_cfb_pbp() espn_cfb_team_stats()
cfbfastR v1.8.0
- All functions now default to return tibbles.
- Added S3 method to print outputs with data info and retrieval timestamps. Thank you to Tan Ho (@tanho36) for the idea.
cfbfastR v1.7.1
- Added
espn_ratings_fpi()to exports.
cfbfastR v1.6.7
- Fixed bug in
cfbd_game_team_stats()with _allowed columns duplicating team stats instead of showing opponent stats. - Updated tests and documentation for
cfbd_game_team_stats().
cfbfastR v1.6.6
- Updated function
cfbd_pbp_data()to account for additional timeout cases (namely, kickoffs/extra point attempts).
cfbfastR v1.6.5
- Updated tests and documentation for
cfbd_betting_lines() - API call in
espn_ratings_fpi()now requires headers in httr request
cfbfastR v1.6.4
CRAN release: 2021-10-27
- Changed options to revert to old options on exit of function.
- Removed check_github functions.
cfbfastR v1.6.2
- Switched package urls in README and DESCRIPTION files to https://
cfbfastR v1.6.1
- Removed source urls from many package documentation entries.
- Updated a test to skip on CRAN
cfbfastR v1.6.0
- Added
cfbd_ratings_elo()function - Fixed a bug in
update_cfb_db()where the function failed when trying to load recent games from the data repo. (#35) - Added the option
cfbfastR.dbdirectorythat allows to set the database directory inupdate_cfb_db()globally.
cfbfastR v1.5.1
Minor release
- Removed calculated columns from
cfbd_stats_season_team()that were not behaving correctly - Fixed bug where
only_fbsinput incfbd_team_info()was ignored. It is now possible to get the team info for all the colleges in the API instead of only FBS schools. - Removed default year from
cfbd_metrics_ppa_teams.cfbd_metrics_ppa_teamsandcfbd_metrics_ppa_players_seasonnow require one ofteamoryearto be specified
cfbfastR v1.4.0
Added cfbd_game_weather()
cfbfastR v1.3.3
Hotfix cfbd_game_player_stats()
cfbfastR v1.3.2
Added ID linking to cfbd_recruiting_players()
cfbfastR v1.3.0-1
Added three NFL draft functions:
-
cfbd_draft_teams()- Get list of NFL teams -
cfbd_draft_positions()- Get list of NFL positions for mapping to collegiate -
cfbd_draft_picks()- Get list of NFL Draft picks
cfbfastR v1.2.1
Minor release
Added headshot_url to outputs of
cfbd_team_roster()-
Renamed returns in
cfbd_game_box_advanced():-
rushing_line_yd_avgto pluralrushing_line_yds_avg -
rushing_second_lvl_yd_avgto pluralrushing_second_lvl_yds_avg -
rushing_open_field_yd_avgto pluralrushing_open_field_yds_avg
-
Completed documentation for all returns except
cfbd_pbp_data()Continued work on intro vignette
cfbfastR v1.2.0-1
Add significant documentation to the package
- Added mini-vignettes pertaining to CFB Data functionality:
- Introductory vignette stub added
ESPN/CFBD metrics function variable return standardization
- Change
idvariable toteam_idinespn_ratings_fpi() - Changed
espn_game_idvariable togame_idinespn_metrics_wp(), corrected theaway_win_percentagecalculation and addedtie_percentageto the returns. - Change
idvariable toathlete_idincfbd_metrics_ppa_players_season()
cfbfastR v1.1.0
Add loading from Data Repository functionality
- Added
load_cfb_pbp()andupdate_cfb_db()functions. Pretty much cherry-picking thenflfastRmethodology of loading data from thecfbfastR-datarepository.
cfbfastR v1.0.0
Function Naming Convention Change
All functions sourced from the College Football Data API will start with
cfbd_as opposed tocfb_(as in cfbscrapR). One additionalcfbd_function has been added that corresponds to the result whencfbd_pbp_data()has the parameterepa_wpa=FALSE. It has now been separated into its own function for claritycfbd_plays(). The parameter and functionality still exists incfbd_pbp_data()but we expect this function will still exist but made obsolete in favor of a function more closely matchingnflfastR’s naming conventions.Similarly, data and metrics sourced from ESPN will begin with
espn_as opposed tocfb_. In particular, the two functions are nowespn_ratings_fpi()andespn_metrics_wp()Data generated from any of the
cfbfastRmethods will usecfb_
College Football Data API Keys
The CollegeFootballData API now requires an API key, here’s a quick run-down:
To get an API key, follow the directions here: College Football Data Key Registration.
Using the key: You can save the key for consistent usage by adding
CFBD_API_KEY=XXXX-YOUR-API-KEY-HERE-XXXXXto your .Renviron file (easily accessed viausethis::edit_r_environ()). Runusethis::edit_r_environ(), a new script will pop open named.Renviron, THEN paste the following in the new script that pops up (without quotations)
CFBD_API_KEY = XXXX-YOUR-API-KEY-HERE-XXXXXSave the script and restart your RStudio session, by clicking Session (in between Plots and Build) and click Restart R (n.b. there also exists the shortcut Ctrl + Shift + F10 to restart your session). If set correctly, from then on you should be able to use any of the cfbd_ functions without any other changes.
- For less consistent usage: At the beginning of every session or within an R environment, save your API key as the environment variable
CFBD_API_KEY(with quotations) using a command like the following.
{r} Sys.setenv(CFBD_API_KEY = "XXXX-YOUR-API-KEY-HERE-XXXXX")
- Added API Key methods. If you forget to set your environment variable, functions will give you a warning and ask for one.
