ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python课程设计:NBA比赛数据抓取与机器学习预测实战

Python课程设计:NBA比赛数据抓取与机器学习预测实战 简介这份资源面向高校学生与Python初学者提供一套完整的NBA比赛结果预测课程设计项目可用于学期大作业、项目答辩或机器学习入门实战。项目通过爬虫抓取NBA历史比赛数据再借助机器学习对数据进行处理分析最终输出比赛胜负预测结果曾获学期优秀项目评选。压缩包共11个文件约311KB包含6个CSV数据文件、4个Python脚本和1份说明文档分别对应历史赛程与球队统计数据、爬虫与预测代码以及项目使用说明结构清晰便于按模块学习。目前已有1462人学习下载。读者可获得从数据采集、特征整理到模型训练预测的完整代码与现成数据配合说明文档快速理解项目流程适合直接用于课程设计提交或二次修改扩展也能作为爬虫与机器学习结合的练手案例。1. 从一份课程设计说起NBA 比赛数据抓取与机器学习预测到底怎么做很多同学拿到「Python课程设计大作业获取NBA比赛数据并进行机器学习智能预测NBA的比赛结果」这个题目时第一反应是打开 python 官网下载安装包然后对着空白的 PyCharm 发呆。这个题目看起来简单实际上它是一条完整的链路数据获取、数据清洗、特征工程、模型训练、结果评估每一步都有坑。我见过太多人卡在第一步——数据拿不到后面全是空谈。这篇文章面向的是正在做课程设计、想用 python 机器学习入门、或者需要一个完整机器学习项目练手的人。我会把这条链路拆开告诉你数据从哪来、特征怎么造、模型怎么选、参数怎么调以及那些只有真正跑过一遍才会知道的翻车点。不需要你有很深的数学背景但需要你会基本的 python 语法知道 pandas 和 sklearn 是干什么的。读完你至少能跑出一个准确率在 65% 以上的基线模型并且知道怎么继续往上推。2. 数据获取从 API 到本地 CSV 的完整落地路径2.1 为什么选 NBA 官方数据接口而不是爬虫做数据获取第一件事是选数据源。常见做法有三种爬取篮球参考网站、调用 NBA 官方统计接口、使用开源数据集。我一般会优先选 NBA 官方统计接口原因是数据结构稳定、字段含义明确、不需要处理反爬。爬虫方案虽然看起来自由但篮球参考类网站对频繁请求有封锁策略课程设计周期短不值得把时间耗在对抗反爬上。NBA 官方统计接口的调用方式是通过nba_api这个 python 包它封装了官方 endpoints返回 JSON 格式数据。安装命令如下pip install nba_api pandas scikit-learn matplotlib seaborn这里一次性把后续要用的包都装了。nba_api负责取数pandas做清洗和特征工程scikit-learn做模型训练和评估matplotlib和seaborn用来画图。如果你用的是 vscode python 环境配置记得在终端里确认 pip 对应的解释器是你当前项目用的那个否则会出现「装了但 import 报错」的经典问题。2.2 拉取比赛数据的最小可用脚本下面这段代码是我常用的起手式拉取最近三个赛季的常规赛比赛数据from nba_api.stats.endpoints import leaguegamefinder import pandas as pd import time def fetch_season_games(season: str, season_type: str Regular Season) - pd.DataFrame: 拉取指定赛季的比赛数据 season: 格式如 2022-23 season_type: Regular Season 或 Playoffs gamefinder leaguegamefinder.LeagueGameFinder( season_nullableseason, season_type_nullableseason_type, league_id_nullable00 # 00 表示 NBA ) df gamefinder.get_data_frames()[0] time.sleep(1) # 控制请求频率避免被限流 return df seasons [2021-22, 2022-23, 2023-24] all_games [] for s in seasons: print(f正在拉取 {s} 赛季...) df fetch_season_games(s) all_games.append(df) raw pd.concat(all_games, ignore_indexTrue) raw.to_csv(nba_games_raw.csv, indexFalse) print(f共获取 {len(raw)} 条记录已保存到 nba_games_raw.csv)这段代码的逻辑很直接循环三个赛季每个赛季调一次接口拼成一个大的 DataFrame最后落盘成 CSV。time.sleep(1)是关键官方接口虽然没有严格的频率限制但连续快速请求容易触发临时封禁加一秒间隔是最低成本的安全措施。league_id_nullable00表示 NBA 联盟如果以后要拉发展联盟数据可以改成 20。拉下来的数据里每一行代表的是一支球队在一场比赛中的表现所以一场比赛会出现两行主队一行、客队一行。字段包括GAME_ID、TEAM_ID、TEAM_NAME、MATCHUP、WL胜负、PTS得分以及一堆投篮命中数、篮板、助攻等统计。这个结构对后续特征工程很友好因为你可以直接按GAME_ID分组把两行合并成一行构造出「主队 vs 客队」的样本。2.3 把两行合并成一行构造比赛级样本原始数据是「球队-比赛」粒度但预测目标是「某场比赛谁赢」所以需要把同一GAME_ID的两行合并。常见做法是def reshape_to_match_level(df: pd.DataFrame) - pd.DataFrame: 将球队-比赛粒度数据转换为比赛粒度 每场比赛一行包含主队和客队的核心统计 # 提取主客队标识 df df.copy() df[IS_HOME] df[MATCHUP].apply(lambda x: 1 if vs. in x else 0) # 分离主客队 home df[df[IS_HOME] 1].copy() away df[df[IS_HOME] 0].copy() # 选择需要保留的统计列 stat_cols [PTS, FG_PCT, FG3_PCT, FT_PCT, REB, AST, STL, BLK, TOV] # 重命名主队列 home_cols {col: fHOME_{col} for col in stat_cols} home home[[GAME_ID, TEAM_NAME, WL] stat_cols].rename(columnshome_cols) home home.rename(columns{TEAM_NAME: HOME_TEAM, WL: HOME_WIN}) # 重命名客队列 away_cols {col: fAWAY_{col} for col in stat_cols} away away[[GAME_ID, TEAM_NAME] stat_cols].rename(columnsaway_cols) away away.rename(columns{TEAM_NAME: AWAY_TEAM}) # 合并 merged pd.merge(home, away, onGAME_ID, howinner) merged[HOME_WIN] merged[HOME_WIN].apply(lambda x: 1 if x W else 0) return merged match_df reshape_to_match_level(raw) match_df.to_csv(nba_matches.csv, indexFalse) print(f合并后共 {len(match_df)} 场比赛)这里有几个细节值得说。MATCHUP字段的格式是「球队缩写 vs. 对手缩写」表示主队「球队缩写 对手缩写」表示客队所以用vs. in x判断主客是可靠的。统计列我选了得分、投篮命中率、三分命中率、罚球命中率、篮板、助攻、抢断、盖帽、失误这九个它们是最直接影响比赛结果的指标。合并之后每一行就是一场完整的比赛HOME_WIN是标签列1 表示主队赢0 表示客队赢。注意FG_PCT等命中率字段在原始数据里可能是字符串或空值合并前最好做一次pd.to_numeric(errorscoerce)转换否则后面模型训练会报类型错误。3. 特征工程让模型从「谁得分高」进化到「谁更可能赢」3.1 基础特征之外必须构造滚动窗口特征如果你只用当场比赛的得分、篮板、助攻去预测胜负模型准确率大概在 60% 左右因为「当场得分高」和「赢球」几乎是同义反复但比赛开始前你并不知道当场得分。真正有预测价值的特征是比赛开始前就能获取的信息比如球队近五场的平均得分、近十场的胜率、背靠背作战情况等。这就是滚动窗口特征。def add_rolling_features(df: pd.DataFrame, window: int 5) - pd.DataFrame: 为每支球队计算近 window 场的滚动统计 注意必须按时间排序且不能包含当前场次的数据 df df.copy() df[GAME_DATE] pd.to_datetime(df[GAME_DATE]) df df.sort_values(GAME_DATE).reset_index(dropTrue) # 构建球队-比赛的长表方便按球队分组 home_view df[[GAME_ID, GAME_DATE, HOME_TEAM, HOME_PTS, HOME_WIN]].rename( columns{HOME_TEAM: TEAM, HOME_PTS: PTS, HOME_WIN: WIN} ) away_view df[[GAME_ID, GAME_DATE, AWAY_TEAM, AWAY_PTS]].rename( columns{AWAY_TEAM: TEAM, AWAY_PTS: PTS} ) away_view[WIN] 1 - df[HOME_WIN].values team_games pd.concat([home_view, away_view], ignore_indexTrue) team_games team_games.sort_values([TEAM, GAME_DATE]) # 滚动平均shift(1) 确保不泄露当前场次信息 team_games[ROLL_PTS] team_games.groupby(TEAM)[PTS].transform( lambda x: x.shift(1).rolling(window, min_periods1).mean() ) team_games[ROLL_WIN] team_games.groupby(TEAM)[WIN].transform( lambda x: x.shift(1).rolling(window, min_periods1).mean() ) # 合并回主客队视角 home_feat team_games[team_games[GAME_ID].isin(df[GAME_ID])].copy() # 这里简化处理实际项目中需要按 GAME_ID 和球队分别 merge return team_games team_features add_rolling_features(match_df)这段代码的核心是shift(1)。如果不加 shift滚动窗口会包含当前比赛的结果造成数据泄露模型在训练集上准确率虚高一到测试集就原形毕露。这是机器学习项目里最经典的翻车点之一血泪经验。min_periods1保证赛季初第一场比赛也有值否则会出现大量 NaN。3.2 特征选择哪些该留哪些该扔构造完滚动特征后特征数量会膨胀到几十个。不是越多越好冗余特征会拖慢训练速度还可能引入噪声。我一般用三种方法做筛选方法适用场景操作要点相关性分析快速剔除高度线性相关的特征计算皮尔逊相关系数阈值设 0.85方差阈值剔除几乎不变的特征VarianceThreshold(threshold0.01)模型重要性用树模型输出特征重要性随机森林feature_importances_排序实际课程设计中我建议先用相关性分析去掉明显冗余的列比如HOME_PTS和HOME_FG_PCT可能高度相关保留一个即可。然后用随机森林跑一遍看特征重要性排序把重要性低于 0.01 的列删掉。这样通常能把特征从 40 多个压缩到 15 到 20 个模型训练时间减半准确率反而可能微升。提示特征选择必须在训练集上做然后把筛选规则应用到测试集。如果全量数据一起做测试集信息会泄露到训练过程中。4. 模型训练与调参从逻辑回归到梯度提升的实战对比4.1 基线模型逻辑回归为什么值得先跑很多人一上来就上 XGBoost 或神经网络结果调参调到怀疑人生。我的习惯是先跑一个逻辑回归作为基线原因是它训练快、可解释性强、不容易过拟合。如果逻辑回归能到 65%说明特征工程基本到位如果只有 55%那问题出在特征上换再复杂的模型也救不了。from sklearn.model_selection import train_test_split, cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.metrics import accuracy_score, classification_report # 假设 feature_cols 是筛选后的特征列 feature_cols [HOME_ROLL_PTS, AWAY_ROLL_PTS, HOME_ROLL_WIN, AWAY_ROLL_WIN, HOME_FG_PCT, AWAY_FG_PCT, HOME_REB, AWAY_REB, HOME_TOV, AWAY_TOV, HOME_AST, AWAY_AST] X match_df[feature_cols] y match_df[HOME_WIN] # 按时间切分不能用随机切分 split_idx int(len(X) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, C1.0)) ]) pipe.fit(X_train, y_train) y_pred pipe.predict(X_test) print(f逻辑回归准确率: {accuracy_score(y_test, y_pred):.4f}) print(classification_report(y_test, y_pred))这里有两个关键决策。第一按时间切分而不是随机切分。NBA 比赛有强烈的时间序列属性随机切分会让未来数据泄露到训练集导致评估结果偏乐观。第二用 Pipeline 把标准化和分类器串起来避免在交叉验证时标准化参数泄露。C1.0是正则化强度的倒数值越小正则化越强默认 1.0 通常够用如果过拟合可以降到 0.1 试试。4.2 梯度提升树调参顺序比参数本身更重要逻辑回归跑通后换梯度提升树Gradient Boosting通常能提升 3 到 5 个百分点。sklearn 自带GradientBoostingClassifier不需要额外安装。调参顺序我一般按这个来先定n_estimators和learning_rate再调max_depth最后调min_samples_leaf和subsample。from sklearn.ensemble import GradientBoostingClassifier from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], learning_rate: [0.05, 0.1, 0.2], max_depth: [3, 4, 5], min_samples_leaf: [5, 10, 20] } gb GradientBoostingClassifier(random_state42) grid GridSearchCV(gb, param_grid, cv5, scoringaccuracy, n_jobs-1, verbose1) grid.fit(X_train, y_train) print(f最佳参数: {grid.best_params_}) print(f最佳交叉验证准确率: {grid.best_score_:.4f}) print(f测试集准确率: {grid.score(X_test, y_test):.4f})n_estimators是树的数量太少欠拟合太多过拟合且训练慢。learning_rate是每棵树的贡献权重通常和n_estimators反向搭配学习率小就多来几棵树。max_depth控制单棵树复杂度NBA 数据这种中等规模数据集3 到 5 层足够。min_samples_leaf防止叶子节点样本过少设 10 左右比较稳。subsample如果加上可以引入随机性进一步防过拟合但课程设计里不加也能跑。注意GridSearchCV 的cv5在时间序列数据上应该用TimeSeriesSplit否则还是会有轻微泄露。课程设计里如果追求严谨把cv换成TimeSeriesSplit(n_splits5)即可。5. 避坑与排查那些让准确率一夜回到解放前的操作5.1 数据泄露准确率 95% 的模型为什么不能用现象训练集准确率 95%测试集准确率 52%两者差距巨大。原因特征里包含了比赛结束才能获取的信息比如当场得分、当场命中率。解决把所有「当场统计」列从特征中删掉只保留赛前可获取的滚动特征、历史交锋记录、休息天数等。检查方法是问自己一句这场比赛开打之前这个特征的值能算出来吗5.2 时间切分错误随机切分让评估结果虚高现象用train_test_split随机切分准确率 70%但按时间切分只有 62%。原因随机切分让模型在训练时「看到」了未来比赛的数据评估结果不可信。解决始终按GAME_DATE排序后取前 80% 做训练、后 20% 做测试。如果要做交叉验证用TimeSeriesSplit。5.3 类别不平衡主队胜率偏高带来的偏差现象模型把所有样本都预测为主队赢准确率也有 58%。原因NBA 主队胜率天然在 58% 到 60% 左右类别不平衡导致模型倾向于多数类。解决用class_weightbalanced让模型对少数类加权或者用stratifyy在切分时保持类别比例。评估指标不要只看准确率要看classification_report里的 F1 和 AUC。5.4 空值和类型错误赛季初数据缺失导致训练中断现象ValueError: Input contains NaN, infinity or a value too large。原因滚动窗口在赛季初几场会产生 NaN或者原始数据里命中率字段有空字符串。解决在特征工程最后加一步X X.fillna(X.median())或者用SimpleImputer做填充。类型转换用pd.to_numeric(errorscoerce)把无法转换的值变成 NaN再统一填充。5.5 特征顺序错乱训练和预测时列顺序不一致现象模型训练时准确率正常但用新数据预测时结果完全不对。原因训练时特征列顺序是 A、B、C预测时传进去的是 B、A、Csklearn 不会报错但结果会乱。解决把feature_cols保存成一个列表训练和预测都用同一个列表取列。更稳妥的做法是用sklearn.compose.ColumnTransformer固定列映射。6. 进阶技巧用 AUC 和校准曲线判断模型是否真的可用准确率只能告诉你模型「猜对了多少」但不能告诉你「猜得有多自信」。一个 65% 准确率的模型如果它对每场比赛都给出 51% 的胜率预测那它其实没有提供任何有价值的信息。这时候需要看 AUC 和校准曲线。from sklearn.metrics import roc_auc_score, roc_curve from sklearn.calibration import calibration_curve import matplotlib.pyplot as plt # 获取预测概率 y_prob grid.predict_proba(X_test)[:, 1] # AUC auc roc_auc_score(y_test, y_prob) print(fAUC: {auc:.4f}) # 校准曲线 prob_true, prob_pred calibration_curve(y_test, y_prob, n_bins10) plt.figure(figsize(8, 6)) plt.plot(prob_pred, prob_true, markero, label模型校准曲线) plt.plot([0, 1], [0, 1], linestyle--, label完美校准) plt.xlabel(预测胜率) plt.ylabel(实际胜率) plt.title(NBA 比赛预测模型校准曲线) plt.legend() plt.savefig(calibration_curve.png, dpi150) plt.show()AUC 的取值范围是 0.5 到 1.00.5 表示模型和随机猜没区别0.7 以上算可用0.75 以上算不错。校准曲线看的是「模型说 70% 胜率的比赛实际是不是真的有 70% 主队赢了」。如果曲线在对角线附近说明模型概率输出可靠如果偏离严重说明模型过度自信或过度保守需要做概率校准CalibratedClassifierCV。我自己的习惯是课程设计报告里至少放三样东西时间切分下的准确率、AUC 值、校准曲线。这三样能说明你不仅跑通了流程还理解了模型评估的层次。很多人只放一个准确率就交差遇到答辩提问「为什么不用 AUC」就答不上来。最后一个技巧把模型保存下来用joblib.dump(grid.best_estimator_, nba_model.pkl)下次预测新比赛时直接加载不用重新训练。预测时构造特征的方式必须和训练时完全一致否则前功尽弃。这个习惯在课程设计里可能用不上但到了实际工作中模型持久化是基本操作。希望帮到你。本文还有配套的精品资源点击获取
返回列表