Skip to contents

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

Field 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.366

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