ARTICLE DETAIL

资讯详情

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

Python课设实战:从爬虫到机器学习,构建NBA比赛胜负预测模型

Python课设实战:从爬虫到机器学习,构建NBA比赛胜负预测模型 简介一份聚焦NBA比赛数据爬取与机器学习预测的Python课程设计大作业资源面向具备一定Python基础、正在准备课设或答辩的高校学生。项目通过爬虫获取NBA历年比赛数据结合机器学习对球队交锋记录与赛季表现进行智能分析最终输出比赛结果预测曾获学期优秀项目可作为完整课程设计范例。资源共11个文件其中6个csv数据文件涵盖球队数据、赛程与比赛结果4个py脚本分别实现爬虫采集、数据处理、特征构建与模型训练另附1个docx说明文档整体压缩包约311KB结构清晰易于对照学习。资源还附带了提前准备好的NBA数据资料及项目说明读者可直接运行复现实验流程。目前已有1462人学习下载适合希望借助完整项目案例掌握爬虫与机器学习应用、并为课程设计答辩提供参考的读者。1. Python课设做NBA预测爬虫加机器学习凭什么拿下优秀项目学期末的Python课程设计最常见的情况是做个图书管理系统或者爬个天气数据答辩时老师问两句就冷场。这门课设走的是另一条路用爬虫把NBA公开的球队统计抓下来再用机器学习模型预测16-17赛季每场比赛的胜负。整个项目拿到了学期优秀项目评选资源包里包含三版爬虫脚本、机器学习主程序、15-16赛季和16-17赛季的数据文件以及答辩用的说明文档组成了一个可供复现的完整闭环。如果你也在准备Python大作业或课程设计想要一个既能体现爬虫能力、又能展示机器学习落地效果的题目这套资源可以直接拿来用。这篇文章把三个版本的爬虫分别做了什么、数据怎么拼成特征、模型怎么选怎么调、以及整套流程里最容易翻车的几个点按实际跑通的流程拆给你看。2. 爬虫的三个版本从赛程表到球队统计数据源与字段设计2.1 为什么写了三版爬虫数据不是一张表能解决的这个项目的数据需求分两层要预测的是一场比赛的胜负但模型依赖的特征来自球队在一个赛季内的场均表现。单靠爬一张赛程表不够得把赛程数据、球队每场统计、对手统计和杂项统计分别抓下来再按球队和日期对齐成一张特征表。三版爬虫各自的定位是这样的第一版负责抓取16-17赛季的赛程和比分16-17Schedule.csv的来源第二版抓取15-16赛季的球队场均统计Team_Per_Game_Stat和对手场均统计Opponent_Per_Game_Stat第三版补充杂项统计比如节奏、进攻效率这类进阶数据。拆成三个脚本而不是写一个大而全的爬虫思路是每个脚本独立可运行、独立排错。你在答辩时也可以这样讲分模块设计降低了单次请求失败的影响范围哪个环节挂了重跑哪个不用重来。看一下第一版爬虫的核心逻辑import requests from bs4 import BeautifulSoup import csv import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def crawl_schedule(year): url fhttps://www.basketball-reference.com/leagues/NBA_{year}_games.html resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) rows soup.select(table#schedule tbody tr) schedule_data [] for row in rows: if class in row.attrs and thead in row.attrs[class]: continue cells [td.get_text(stripTrue) for td in row.find_all(td)] if len(cells) 6: schedule_data.append({ date: cells[0], visitor: cells[2], visitor_pts: cells[3], home: cells[4], home_pts: cells[5], }) return schedule_data if __name__ __main__: data crawl_schedule(2017) with open(16-17Schedule.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[date, visitor, visitor_pts, home, home_pts]) writer.writeheader() writer.writerows(data) print(f共抓取 {len(data)} 场比赛)代码里select(table#schedule tbody tr)定位的是赛程表格get_text(stripTrue)用来去掉HTML标签和多余空格。这里有个细节Basketball Reference 的表格行有时带classthead的辅助行不跳过会导致解析失败。time.sleep没放进循环里如果你要跑全量赛季数据建议在循环内每次请求后加time.sleep(3)避免请求频率过高被服务端限流。2.2 球队场均统计的抓取与字段对齐第二版爬虫的目标是15-16赛季的球队每场统计。这个表的结构和赛程表不一样它是按球队分块的每支球队一张子表字段包括场次、首发、出场时间、投篮命中数、三分命中率、篮板、助攻、抢断、盖帽、失误、犯规、得分等。抓取时的一个关键操作是把球队缩写标准化因为赛程表里用的是球队代码比如BOS、LAL而统计表里的球队名是全称后续特征合并全靠这个映射。def crawl_team_stats(year): url fhttps://www.basketball-reference.com/leagues/NBA_{year}.html resp requests.get(url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) all_teams [] for team_header in soup.select(div#all_team_stats): for table in team_header.select(table): team_name table.get(id, ).replace(stats, ).strip() for row in table.select(tbody tr): if class in row.attrs and thead in row.attrs[class]: continue tds row.find_all(td) if not tds: continue stat {team: team_name} stat[fg_pct] tds[3].get_text(stripTrue) stat[fg3_pct] tds[7].get_text(stripTrue) stat[ft_pct] tds[11].get_text(stripTrue) stat[reb] tds[17].get_text(stripTrue) stat[ast] tds[18].get_text(stripTrue) stat[stl] tds[19].get_text(stripTrue) stat[blk] tds[20].get_text(stripTrue) stat[tov] tds[21].get_text(stripTrue) stat[pts] tds[23].get_text(stripTrue) all_teams.append(stat) return all_teams字段索引是写死的这是这类表格爬虫最常见的做法也是坑最多的地方。Basketball Reference 的表格列顺序基本稳定但偶尔会插入新列代码跑出来的数据明显不对时先检查是不是列索引移位了。我一般会先用print把表头打出来核对一遍再写死索引。数据抓下来后用pandas.read_csv看一下列名和行数确认和网站显示一致再进下一步。2.3 请求频率、User-Agent 与断点续抓的取舍课程设计作品里爬虫的健壮性是答辩老师比较关注的维度。这段时间的常见提问是网站反爬怎么办数据抓一半断了怎么办这套资源里的做法不一定是最优解但应付课设足够每个请求带User-Agent模拟浏览器两个请求之间间隔2到3秒抓取结果落盘为CSV跑挂了重新执行一次就行。面对课设这种明确单次抓取的数据量做断点续抓是过度设计真正需要的是重跑幂等——脚本重新跑一遍能覆盖旧文件、不报错这就够了。三种爬虫抓完之后你手上有三块数据赛程表、球队场均统计、对手场均统计。下一步就是把这个项目的重心特征工程。3. 特征工程把三张CSV拼成机器学习能用的训练集3.1 源数据字段盘点场均统计和对手场均统计的差异打开资源里的15-16赛季数据文件可以看到三张表Team_Per_Game_Stat球队场均统计、Opponent_Per_Game_Stat对手场均统计、Miscellaneous_Stat杂项统计。场均统计描述的是这支球队自己打出的数据对手场均统计描述的是这支球队场均让对手拿到的数据。两者是同一场比赛的两面比如一支球队失分113分说明它防守偏弱失分99分说明防守强度高。特征拼接的思路是要预测16-17赛季某场比赛的结果就用15-16赛季这两支球队的场均数据作为特征。这里有个逻辑问题需要注意15-16赛季的数据是完整赛季的累计均值用它预测16-17赛季的早期比赛其实存在时间窗口错位——严格的做法应该用截至前一天的数据滚动计算。但课设项目用上赛季整季数据也是站得住的毕竟球队整体实力是缓慢变化的作为特征足够了。3.2 按球队缩写映射拼接特征import pandas as pd team_stat pd.read_csv(15-16Team_Per_Game_Stat.csv) opp_stat pd.read_csv(15-16Opponent_Per_Game_Stat.csv) schedule pd.read_csv(16-17Schedule.csv) result_15_16 pd.read_csv(2015-2016_result.csv) team_stat[team_abbr] team_stat[team].map(teams_mapping) opp_stat[team_abbr] opp_stat[team].map(teams_mapping) def build_feature(team_df, prefix): df team_df.copy() feature_cols [c for c in df.columns if c not in [team, team_abbr]] df.columns [prefix c if c not in [team, team_abbr] else c for c in df.columns] return df home_stat build_feature(team_stat, home_).merge( opp_stat[[team_abbr] [c for c in opp_stat.columns if c ! team]], left_onteam_abbr, right_onteam_abbr, suffixes(, _opp) )到这里home_stat里每一行代表一支主队特征列包括主队自己的场均数据和主队让对手拿到的场均数据。同样的逻辑再给客队做一份然后按赛程表里的主客队缩写进行表连接。拼接完成后的训练集长这样每一行是一场16-17赛季的比赛特征列是主队和客队双方的上赛季场均数据目标列是这场比赛结果1代表主胜0代表客胜。3.3 目标列构造与训练集/预测集拆分schedule[home_win] (schedule[home_pts] schedule[visitor_pts]).astype(int) train pd.merge(schedule, home_stat, onhome_abbr, howleft) train pd.merge(train, away_stat, onaway_abbr, howleft) train train.dropna(subset[home_pts, visitor_pts]) feature_cols [c for c in train.columns if c.startswith(home_) or c.startswith(away_)] target_col home_win X train[feature_cols] y train[target_col]注意home_win是用最终比分算出来的这行代码跑在16-17赛季已经结束的时间点上是没问题的。真正做预测时赛程表里还没打的比赛是没有比分的这部分数据要单独拿出来home_win留空用来做预测集的输入。对这种流程设计上是把训练数据里home_pts不为空的行作为训练集为空的行作为预测集两者用同样的特征拼表逻辑生成只是目标列一个有值一个没值。3.4 特征筛选别一股脑全塞进去拼接完的特征可能有四五十列其中不少是强相关的比如投篮命中数和得分、防守篮板和总篮板。模型层面逻辑回归对多重共线性敏感随机森林则无所谓。但课设项目更看重解释性所以一般做法是人工挑一部分代表性特征而不是全丢给模型。我建议保留这几类得分、篮板、助攻、失误、投篮命中率、三分命中率、对手得分、对手篮板、对手助攻每类各取主队和客队两个版本大概十来个特征。这个数量在课设场景里足够支撑一次像样的训练和预测答辩时也更容易逐个特征解释业务含义。4. 机器学习建模逻辑回归和随机森林怎么选怎么调4.1 二分类问题的小样本场景为什么逻辑回归是首选NBA比赛胜负预测是一个典型的二分类问题主胜还是客胜。数据集大小大概是82轮乘每天几场比赛整个赛季也就一千多场有效样本。在这种量级下复杂模型过拟合风险远大于收益逻辑回归是性价比最高的起点训练速度快到秒级参数可解释性强配合标准化后的特征每个系数的正负和大小都能讲出球场上对应的含义。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score, classification_report scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42, stratifyy ) model LogisticRegression(max_iter1000, C1.0) model.fit(X_train, y_train) y_pred model.predict(X_test) print(准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))这里stratifyy是容易被忽略但很重要的参数它让训练集和测试集里的类别比例保持一致。NBA主队胜率通常在60%左右如果不做分层抽样测试集里主胜的比例可能会偏到70%准确率虚高但模型没学到东西。C1.0是正则化强度的默认值C越小正则化越强。你可以尝试C0.1和C10分别跑一次看测试集准确率的变化这个对比实验放到答辩PPT里是个加分项。4.2 随机森林作为对比树模型的边界逻辑回归的问题在于默认特征之间是线性关系而NBA里有不少非线性因素比如强队打弱队和中等队打中等队时的发挥稳定性差异。做一个随机森林对比模型能够体现你对模型选择做过比较分析而不是只背了一个算法。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth6, min_samples_split5, min_samples_leaf2, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) rf_pred rf.predict(X_test) print(随机森林准确率:, accuracy_score(y_test, rf_pred)) print(特征重要性Top10:) for name, imp in sorted(zip(feature_cols, rf.feature_importances_), keylambda x: x[1], reverseTrue)[:10]: print(f{name}: {imp:.4f})n_estimators300是树的数量课设数据量小300棵树的训练时间也就几秒。max_depth6限制树的深度防止单棵树学得太细导致过拟合。随机森林在课设这种小样本上准确率通常和逻辑回归差不多偶尔略低这很正常树的方差大对噪声敏感。你可以在答辩时这么说逻辑回归和随机森林在这份数据上表现接近说明马刺队那年的统治力太强强弱队差距明显时线性和非线性模型的边界差异不大。4.3 训练集时间切分不要随机打乱比赛顺序train_test_split默认的shuffleTrue会打乱数据顺序这在一般分类问题里没问题但比赛数据是有时间先后关系的。更严谨的做法是拿16-17赛季前两个月的比赛当训练集后一个月的比赛当测试集模拟真实的滚动预测schedule[date] pd.to_datetime(schedule[date]) train_mask schedule[date] 2017-03-01 test_mask schedule[date] 2017-03-01 X_train, X_test X[train_mask], X[test_mask] y_train, y_test y[train_mask], y[test_mask]这种切分方式比随机切分更接近真实场景准确率会稍微低一点但更有说服力。答辩老师如果问到“你这个模型到了赛季后半段还好用吗”你就可以把这段代码摆出来说明你考虑过时间迁移问题。4.4 预测结果输出直接生成CSV方便验收predicted_proba model.predict_proba(X_predict)[:, 1] pred_label (predicted_proba 0.5).astype(int) output pd.DataFrame({ date: predict_schedule[date], home: predict_schedule[home], away: predict_schedule[visitor], pred_home_win: pred_label, home_win_prob: predicted_proba }) output.to_csv(prediction_of_2016-2017.csv, indexFalse, encodingutf-8-sig)输出里同时保存预测类别和胜率概率这是个很实用的细节。只看0和1无从判断模型是否自信有home_win_prob之后你可以回看那些概率在0.45到0.55之间的比赛——模型其实认为五五开这比强行给出一个二分类结果更有分析价值。5. 避坑与排查从数据爬取到模型评估的五个常见坑5.1 爬虫返回503被限流了还以为是网站改版现象之前跑得好好的爬虫某天突然大量请求返回503状态码页面抓下来是空表格。原因请求频率太高同一个IP在短时间内发出了大量请求触发了服务端的限流保护。另一个常见原因是没有带User-Agent部分站点直接拒绝无UA的请求。解决每两个请求之间至少间隔3秒加随机的延时请求头里带上完整的User-Agent甚至可以模拟成浏览器的完整请求头。如果被抓了等待一段时间再继续别换着花样本机硬怼。课设数据量不大慢一点完全来得及。5.2 CSV打开中文乱码Excel和pandas的编码不一致现象pd.read_csv读出来是正常的但用Excel打开CSV文件时中文全部乱码。原因pandas默认的CSV写入编码是utf-8而Windows下的Excel默认按GBK解析CSV文件编码对不上就乱码。解决写CSV时指定encodingutf-8-sig这个编码会在文件开头加一个BOM头Excel读到BOM就知道用UTF-8解析了。代码里to_csv的地方统一加上encodingutf-8-sig即可读的时候保持utf-8不变。5.3 特征合并后大量NaN球队缩写对不上现象跑完merge之后发现训练集里有一半行的特征都是空值模型直接报错或准确率崩盘。原因赛程表里的球队代码和统计表里的球队名不是同一种格式比如赛程表用BOS统计表里是Boston Celticsmerge时没匹配上所有特征变成NaN。还有一种情况是赛季中途有球队改名或者新增球队比如那几年的夏洛特黄蜂和夏洛特山猫切换。解决建立一张完整的球队缩写映射表把三张源表里的队伍名全部对齐成同一种格式。映射表对照完成后value_counts检查每个队伍在训练集里的行数至少要有20行以上才算正常。5.4 准确率虚高特征里混进了结果列的影子现象模型在测试集上准确率高达95%以上明显不符合常理预测结果看起来“好得不真实”。原因特征列里包含了一些和比分强关联的字段比如最终比分、加时标识、或者某些间接等于比赛结果的统计。最常见的情况是把主队得分和客队得分直接当作特征塞进去了模型根本不需要学靠猜特征值就能知道标签。解决特征列要严格从源表里挑只保留比赛开始前就能确定的数据。比赛开始前的信息包括球队场均统计、对手场均统计、主客场安排不包括比分、进球数这类赛后数据。建议在拼接特征时打印一下feature_cols肉眼检查有没有混入可疑字段。5.5 结果全是主胜类别不平衡导致模型躺平现象预测结果里几乎所有比赛都预测主队赢准确率还不低但这样的模型没有任何实用价值。原因主队胜率大约六成模型发现全猜主胜就能拿到约60%准确率从损失函数角度看比努力学特征划算于是选择了躺平。解决换用更细粒度的评估指标别只看准确率。看混淆矩阵、精确率、召回率、AUC这些指标能反映模型在客胜这个少数类上的表现。计算逻辑回归预测概率时把阈值从默认的0.5降低到0.4甚至0.35看看客胜预测的召回率是否上升挑一个阈值让两类预测相对均衡。6. 项目验证与答辩演示用回溯法检查你的预测值靠不靠谱6.1 拿已结束的比赛对预测做回溯评估资源里有一份prediction of 2016-2017.csv这是整套流程跑完之后生成的预测结果。你可以打开这份文件和真实的16-17赛季赛后结果做一次对比校验。比对方式很简单输入真实赛程和比分然后状态对齐、按日期逐场标记预测与实际结果是否一致最后算总准确率。import pandas as pd from datetime import datetime pred pd.read_csv(prediction_of_2016-2017.csv, encodingutf-8) actual pd.read_csv(16-17Schedule.csv, encodingutf-8) actual[date] pd.to_datetime(actual[date]) pred[date] pd.to_datetime(pred[date]) actual[actual_home_win] (actual[home_pts] actual[visitor_pts]).astype(int) merged pd.merge( pred, actual[[date, home, visitor, actual_home_win]], on[date, home, visitor], howleft ) merged merged.dropna(subset[actual_home_win]) merged[is_correct] (merged[pred_home_win] merged[actual_home_win]).astype(int) print(回溯准确率:, merged[is_correct].mean())actual_home_win是我们用真实比分反向构造的目标列它代表了比赛的真实结果。预测文件里保存的pred_home_win是模型的预测值home_win_prob是预测主胜的概率。真正需要关心的不只是准确率而是home_win_prob在0.5附近的那批比赛到底猜对了多少——如果概率接近0.5的比赛实际准确率也接近50%说明模型对势均力敌的比赛有自知之明这个行为比单纯的高准确率更有说服力。6.2 答辩演示的推荐顺序答辩只有五到十分钟这套项目建议按下面的顺序演示先打开16-17Schedule.csv展示爬虫抓下来的原始赛程数据运行nba_spider.py现场抓一个赛季的赛程表让老师看到爬虫确实在跑再展示15-16Team_Per_Game_Stat.csv和16-17Schedule.csv是如何合并成特征表的然后跑NBA.py训练模型给出训练集准确率、测试集准确率和混淆矩阵最后打开prediction_of_2016-2017.csv抽查几场焦点比赛的预测结果再用刚才那段回溯代码打出整体准确率。技术演示部分更好用的策略是准备一个 error case找一场预测错了的比赛打开赛程表看实际比分再解释为什么模型会预测错。比方说模型预测主队赢但实际客队赢了可能因为该客队交易来了首发球员而上赛季的场均数据没有反映这层变化。这种回答展示了你对数据局限性的认识比只报一个漂亮的准确率更有说服力。6.3 一个有价值的扩展思路按主客场分模型从实际看这个项目的话主客场因素其实影响很大。有些球队主场表现明显好于客场有些则相反。比较深化的做法是分别训练一个主场比赛模型和一个客场比赛模型预测时按比赛场地选择对应模型。资源里没有这个拆分但这会是个很好的加分点。拆分后特征不变只是训练集被过滤成两个子集代码改动量极小。我做完这个项目后的习惯是每次拿到一份预测结果第一件事不是看准确率而是先按日期排序、看预测概率的分段分布。准确率只能说明某条阈值线划得好不好概率分布才是模型真实判断的依据。希望这个项目的完整流程和数据文件能帮到你特别是在期末时间紧、又要拿出一份能撑住答辩的作品的时候。本文还有配套的精品资源点击获取
返回列表