sdvmodels ships the fitted models behind cfbfastR and the wider
SportsDataverse as data. Each model is the raw UBJ byte vector of the
released file, so you load it with xgboost::xgb.load.raw()
and score it with predict(). Nothing is downloaded at run
time.
The feature contract
Every model expects its features in a fixed order. The order is recorded in the model itself and in the bundle manifest, so read it rather than typing it:
ep <- xgb.load.raw(cfb_ep_model)
getinfo(ep, "feature_name")
#> [1] "TimeSecsRem" "yards_to_goal" "distance"
#> [4] "down_1" "down_2" "down_3"
#> [7] "down_4" "pos_score_diff_start"
cfb_model_manifest$assets[["ep_model.ubj"]]$features
#> [1] "TimeSecsRem" "yards_to_goal" "distance"
#> [4] "down_1" "down_2" "down_3"
#> [7] "down_4" "pos_score_diff_start"Expected points
The expected points model is a 7-class classifier over the next
scoring event. The manifest’s ep_class_contract gives the
class order and the points each class is worth; expected points is the
probability-weighted sum.
contract <- cfb_model_manifest$ep_class_contract
contract$class_order
#> [1] "TD" "Opp_TD" "FG" "Opp_FG" "Safety"
#> [6] "Opp_Safety" "No_Score"
contract$point_values
#> [1] 7 -7 3 -3 2 -2 0
# 1st and 10 at the opponent's 25, start of the second half, tied game
play <- data.frame(
TimeSecsRem = 1800, yards_to_goal = 25, distance = 10,
down_1 = 1, down_2 = 0, down_3 = 0, down_4 = 0,
pos_score_diff_start = 0
)
probs <- predict(ep, xgb.DMatrix(as.matrix(play[getinfo(ep, "feature_name")])))
round(setNames(as.vector(probs), contract$class_order), 3)
#> TD Opp_TD FG Opp_FG Safety Opp_Safety No_Score
#> 0.574 0.088 0.298 0.035 0.002 0.001 0.003
sum(probs * contract$point_values)
#> [1] 4.19562Field goal make probability
The field goal model takes the distance to the goal line plus a one-hot era. The era cuts are part of the model card, so the encoding comes from the card:
era <- cfb_model_cards$fg_model$era_contract
era$buckets
#> [1] "<=2006" "2007-2013" "2014-2020" ">=2021"
fg <- xgb.load.raw(cfb_fg_model)
kicks <- data.frame(yards_to_goal = c(5, 15, 25, 35, 45))
# a 2024 season falls in the last bucket (>= 2021)
kicks[era$columns] <- as.list(c(0, 0, 0, 1))
data.frame(
yards_to_goal = kicks$yards_to_goal,
kick_distance = kicks$yards_to_goal + 17,
p_make = round(predict(fg, xgb.DMatrix(as.matrix(kicks[getinfo(fg, "feature_name")]))), 3)
)
#> yards_to_goal kick_distance p_make
#> 1 5 22 0.939
#> 2 15 32 0.823
#> 3 25 42 0.675
#> 4 35 52 0.495
#> 5 45 62 0.366Model cards
Each model has a card that records what it predicts, how it was trained and on which seasons:
card <- cfb_model_cards$ep_model
card[c("model_type", "objective", "label", "n_features", "training_seasons", "trained_date")]
#> $model_type
#> [1] "ep"
#>
#> $objective
#> [1] "multi:softprob"
#>
#> $label
#> [1] "next_score_label"
#>
#> $n_features
#> [1] 8
#>
#> $training_seasons
#> [1] 2004 2025
#>
#> $trained_date
#> [1] "2026-08-02"Where the models come from
The models are the cfb_model_artifacts release of sportsdataverse-data,
trained by the sportsdataverse-py model
pipeline. The same bytes are scored by cfbfastR and sportsdataverse-py,
so a prediction made with sdvmodels matches theirs exactly.