box_scores <- hoopR::load_nba_team_box(seasons = 2019:2026)Predicting NBA Game-level Results via a Machine-Learning Pipeline
DATA 607 - Data Science in Context Presentation
Agenda
- Data Pipeline and Rolling Metrics
- Machine Learning Workflow
- Findings and Model Evaluation
Background and Motivation
Inspiration:
‘Exploring Machine Learning: Predicting the NBA’ by author Kishore Annambhotla.
https://medium.com/@k4annam/exploring-machine-learning-predicting-the-nba-607aea31c955
Objective:
Build a Machine Learning pipeline using real-time data to predict NBA game outcomes.
R Packages utilized:
hoopR, tidyverse, slider, and tidymodels
Data and Feature Engineering
Data Source:
Game-level box score data from hoopR package (2019-2026 seasons)
Rolling Metrics:
18-game rolling averages are computed for:
— Offensive output and shooting metrics (Effective FG%, True Shooting %)
— Advanced box stats (Rebounds, Assists, Turnovers, Steals, Blocks)
Contextual factors:
Days of rest and home court advantage (HCA)
Click to view feature engineering R code
#Empirically derived rolling-calc-window
n_rolling_days <- 18
full_features_df <- box_scores |>
arrange(team_id, game_date) |>
group_by(team_id) |>
mutate(
#define home-game advantage
hga = if_else(str_detect(team_home_away, "away"),0,1),
#define last game outcome
lgo = lag(as.integer(team_winner), n = 1, default = NA),
#define effective field goal percentage
efg = (field_goals_made + 0.5*three_point_field_goals_made)/(field_goals_attempted),
#define turnover rate percentage
tov = (total_turnovers)/(field_goals_attempted + 0.44 * free_throws_attempted + total_turnovers),
#define free throw rate
ftr = free_throws_attempted/field_goals_attempted,
#define true shooting percentage
ts = team_score / (2* (field_goals_attempted + (0.44 * free_throws_attempted))),
# Rolling n-game average of offensive rating and shooting %
roll_pts_scored = slide_dbl(team_score, mean, .before = n_rolling_days, .after = -1),
roll_field_goal_pct = slide_dbl(field_goal_pct, mean, .before = n_rolling_days, .after = -1),
roll_oreb = slide_dbl(offensive_rebounds, mean, .before = n_rolling_days, .after = -1),
roll_dreb = slide_dbl(defensive_rebounds, mean, .before = n_rolling_days, .after = -1),
roll_reb = slide_dbl(total_rebounds, mean, .before = n_rolling_days, .after = -1),
roll_ast = slide_dbl(assists, mean, .before = n_rolling_days, .after = -1),
roll_stl = slide_dbl(steals, mean, .before = n_rolling_days, .after = -1),
roll_blk = slide_dbl(blocks, mean, .before = n_rolling_days, .after = -1),
roll_tov = slide_dbl(turnovers, mean, .before = n_rolling_days, .after = -1),
roll_efg = slide_dbl(efg, mean, .before = n_rolling_days, .after = -1),
roll_tovp = slide_dbl(tov, mean, .before = n_rolling_days, .after = -1),
roll_ftr = slide_dbl(ftr, mean, .before = n_rolling_days, .after = -1),
roll_ts = slide_dbl(ts, mean, .before = n_rolling_days, .after = -1),
days_rest = as.numeric(game_date - lag(game_date))
) |>
ungroup()Machine Learning Pipeline
Train/Test split:
— Training: Historical seasons (2019-2025)
— Testing: 2026 season
Tidymodels Recipe:
Data normalization, feature disentanglement, and removal of collinear variables (i.e. non-independent)
Algorithm:
Random forest classification model (ranger) with 500 trees, evaluating Gini impurity
rf_spec <- rand_forest(trees=500, mode = "classification") |>
set_engine("ranger", importance = "impurity")nba_workflow <- workflow() |>
add_recipe(nba_recipe) |>
add_model(rf_spec)
nba_fit <- fit(nba_workflow, data = train_data)Findings and Model Evaluation
2026 Season Metrics:
- Accuracy: 63%
(compared to 50% random baseline)
- ROC-AUC: 0.669
Key Drivers:
* Differential TS%
* Differential eFG%
2027 NBA Regular Season Predictions
| Rank | Team | Projected | 95% CI Lower | 95% CI Upper | SE |
|---|---|---|---|---|---|
| 1 | Knicks | 56.1 | 47.8 | 64.4 | 4.21 |
| 2 | Nuggets | 50.5 | 41.9 | 59.1 | 4.40 |
| 3 | Thunder | 50.4 | 41.8 | 59.0 | 4.41 |
| 4 | Celtics | 48.8 | 40.1 | 57.5 | 4.44 |
| 5 | Heat | 48.0 | 39.3 | 56.7 | 4.46 |
| 6 | Spurs | 47.8 | 39.0 | 56.6 | 4.47 |
| 7 | Clippers | 47.6 | 38.8 | 56.4 | 4.47 |
| 8 | Rockets | 46.9 | 38.1 | 55.7 | 4.48 |
| 9 | Raptors | 45.5 | 36.7 | 54.3 | 4.50 |
| 10 | Hornets | 45.4 | 36.6 | 54.2 | 4.50 |
Monte Carlo Simulation Results
Summary and Conclusions
Summary:
An end-to-end Machine Learning pipeline was built using Random Forest classification
Achieved an accuracy of 63% on 2026 NBA Season game-level predictions
Takeaways:
Rolling features capture team momentum better than static seasonal stats.
Random Forest methodology provides a stable probability estimate for game outcomes.
Next steps:
- Player-level tracking, injury awareness, XGBoost comparison, and star match-up differentials.
Citations and Acknowledgements
- Medium article on NBA predictions using ML:
— https://medium.com/@k4annam/exploring-machine-learning-predicting-the-nba-607aea31c955
- Core R Packages:
— tidymodels, hoopR, slider, and tidyverse
— hoopR package: https://hoopr.sportsdataverse.org
- Code structuring and
tidymodelssyntax debugging were assisted by Google Gemini (Flash model 3.6, 2026).
— All logic, data pipeline design, and outputs were verified by the author.