简介:这份资源面向具备Python基础、希望进入体育数据分析领域的学习者与开发者,围绕NBA比赛数据展开完整实战。内容覆盖数据抓取、清洗、统计指标计算、可视化与预测建模等环节,帮助读者理解球队表现、球员贡献与赛季趋势,可作为课程设计或数据分析练手项目。压缩包共13个文件,约238KB,以11个CSV数据表为主,涵盖赛程、比分、球队与对手场均统计、综合数据及多个赛季结果,另有1个Python分析脚本和1份Elo等级分说明文档,便于直接运行与查阅。目前已有1022人学习下载。通过该资源,读者可掌握用requests与BeautifulSoup获取数据、用pandas与numpy整理与计算效率值等指标、用matplotlib与seaborn呈现胜负关系与得分分布,并借助时间序列与scikit-learn尝试比赛结果预测,形成从数据到结论的完整分析思路。
1. 从一份 17-18 赛季 CSV 说起:这套 Python 分析 NBA 数据的资源到底能干什么
很多人第一次做体育数据分析,卡住的不是代码,而是数据从哪来。网上找的赛程表要么缺字段,要么格式乱到没法直接喂给 pandas,更别提跨赛季对比了。这份资源最实在的地方,是它把 2015-2016、2016-2017、2017-2018 三个赛季的原始数据打包好了,包括每场比分、球队场均统计、对手场均统计、杂项统计和赛程表,一共十来份 CSV,外加一份 Elo 等级分定义的 PDF 说明和一个Analysis_NBA_Data.py主脚本。拿到手就能跑,不用先去折腾爬虫。
它适合两类人:一类是想练 pandas 清洗和 matplotlib 可视化的新手,另一类是想验证 Elo 模型在 NBA 场景下怎么落地的熟手。数据覆盖了主客场、得失分、篮板助攻等基础字段,足够你算出球队实力排名、赛季走势,甚至做胜负预测的基线模型。下面我按实际拆包的顺序,把这份资源怎么用、参数怎么调、哪里容易翻车讲清楚。
2. 数据文件结构与字段含义:先搞懂每列在说什么
2.1 三类 CSV 的分工
拆开压缩包,data目录下的文件可以分成三组。第一组是比赛结果类,2015-2016_results.csv、2016-2017_results.csv、17-18Result.csv记录每场比赛的日期、主客队和最终比分。第二组是球队统计类,16-17Team_Per_Game_Stats.csv和16-17Opponent_Per_Game_Stats.csv分别是从进攻方和对手视角统计的场均数据,字段包括得分、篮板、助攻、命中率等。第三组是杂项和赛程,16-17Miscellaneous_Stats.csv放了一些不常看的进阶指标,16-17Schedule.csv和17-18Schedule.csv则是完整赛程。
这里有个容易忽略的点:Team_Per_Game_Stats和Opponent_Per_Game_Stats是成对出现的,前者是你自己打出的数据,后者是让对手打出的数据。做净效率分析时,两者相减才有意义。很多人只读一份就开始算,结果得出“每支球队防守都一样”的荒谬结论。
2.2 用 pandas 快速摸清字段类型
拿到数据别急着画图,先跑一遍info()和head()。下面这段代码是我每次开新数据集的固定动作:
import pandas as pd # 读取 2016-2017 赛季球队场均统计 team_stats = pd.read_csv('data/16-17Team_Per_Game_Stats.csv') # 查看字段类型和非空数量 print(team_stats.info()) # 看前五行,确认列名和数值格式 print(team_stats.head()) # 检查是否有重复的球队名 print(team_stats['Team'].duplicated().sum())逻辑说明:info()能一眼看出哪些列是 object 类型(通常是球队名),哪些是 float 或 int。如果发现本该是数值的列显示为 object,多半是 CSV 里混了逗号或百分号,需要后续清洗。duplicated().sum()用来确认球队维度是否唯一,如果大于 0,说明数据里可能有交易截止日后的拆分记录,得先聚合。
参数方面,pd.read_csv默认用逗号分隔,如果遇到制表符分隔的文件,要加sep='\t'。另外encoding参数在 Windows 上经常要改成gbk,否则中文队名会报错。我一般会先试utf-8,报错再换gbk,这个顺序能省不少时间。
2.3 跨赛季合并的键怎么选
三个赛季的结果文件列名不完全一致,比如 17-18 赛季用的是Result而不是Score。合并前必须统一列名,否则concat之后会出现大量 NaN。常见做法是写一个映射字典:
rename_map = { 'Result': 'Score', 'Home': 'Home_Team', 'Away': 'Away_Team' } df_1718 = pd.read_csv('data/17-18Result.csv').rename(columns=rename_map) df_1617 = pd.read_csv('data/2016-2017_results.csv') # 按行合并,忽略索引 all_results = pd.concat([df_1617, df_1718], ignore_index=True) print(all_results.shape)这里ignore_index=True很重要,否则合并后的索引会重复,后续用iloc取值时容易取错行。合并完先看shape,确认行数等于两个赛季比赛数之和,差太多就说明列对齐出了问题。
3. 用 Elo 等级分给球队排座次:从 PDF 定义到代码实现
3.1 Elo 的核心参数与 NBA 场景的调整
资源里那份Elo等级分定义.pdf给的是通用公式,但直接套到 NBA 会有一个问题:主场优势没体现。通用 Elo 假设双方场地中立,而 NBA 主队胜率长期在 60% 左右,不修正的话主队会被系统性高估。我一般会在预期胜率公式里加一个主场常数:
def expected_win_rate(rating_a, rating_b, home_advantage=100): # 主队评分加上主场优势后再算期望 return 1 / (1 + 10 ** ((rating_b - (rating_a + home_advantage)) / 400))home_advantage=100是我试过比较稳的值,相当于给主队加 100 分。这个数不是拍脑袋,你可以用历史主客场胜率反推:如果主队胜率约 60%,对应 Elo 差值大概在 70 到 120 之间。调这个参数时,建议固定其他变量,只看排名变化是否合理。
3.2 逐场比赛更新评分的完整脚本
Elo 是迭代算法,必须按时间顺序一场一场更新。下面这段代码可以直接跑,前提是结果表里有日期、主队、客队和胜负:
def update_elo(ratings, home_team, away_team, home_win, k=20): # 获取当前评分,初始 1500 r_home = ratings.get(home_team, 1500) r_away = ratings.get(away_team, 1500) # 计算主队预期胜率 exp_home = expected_win_rate(r_home, r_away) # 实际结果:主队赢为 1,输为 0 actual = 1 if home_win else 0 # 更新评分 ratings[home_team] = r_home + k * (actual - exp_home) ratings[away_team] = r_away + k * ((1 - actual) - (1 - exp_home)) return ratings # 按日期排序后逐行应用 ratings = {} for _, row in all_results.sort_values('Date').iterrows(): ratings = update_elo(ratings, row['Home_Team'], row['Away_Team'], row['Home_Score'] > row['Away_Score'])k=20是更新幅度,值越大排名波动越剧烈。NBA 一个赛季 82 场,用 20 比较合适;如果做跨赛季长期跟踪,可以降到 10 左右,让评分更稳定。跑完之后把ratings转成 DataFrame 排序,就能看到赛季末的实力榜。这里有个血泪经验:一定要先按日期排序,否则 Elo 会变成“随机漫步”,排名毫无意义。
3.3 用排名验证 Elo 是否合理
跑完 Elo 别只看第一名是谁,要做两件事验证。第一,把 Elo 排名和实际胜场排名做斯皮尔曼相关系数,正常应该在 0.8 以上。第二,看赛季初和赛季末的评分变化,如果某支球队开局连败但 Elo 没怎么掉,说明k值太小,更新滞后。我一般会输出一个对比表:
| 球队 | Elo 评分 | 实际胜率 | 排名差 |
|---|---|---|---|
| 勇士 | 1720 | 0.707 | 0 |
| 火箭 | 1685 | 0.659 | +1 |
| 猛龙 | 1650 | 0.646 | -1 |
排名差超过 3 位就要回头检查主场优势参数和k值。这份资源的数据量够你做三轮交叉验证,别浪费。
4. 可视化与时间序列:把赛季走势画成能看的图
4.1 用 matplotlib 画球队得分分布
拿到清洗后的数据,第一张图建议画得分分布直方图,快速判断数据有没有异常值。下面代码用 seaborn 加核密度曲线:
import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体,Windows 用 SimHei,Mac 用 Arial Unicode MS plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False sns.histplot(team_stats['PTS'], kde=True, bins=15) plt.title('2016-2017 赛季球队场均得分分布') plt.xlabel('场均得分') plt.ylabel('球队数量') plt.show()bins=15是因为 NBA 30 支球队,分 15 组每组大概 2 支,粒度刚好。如果设成 30,图会碎成锯齿;设成 5,又看不出双峰。中文字体那两行必须加,否则标题全是方框,这个坑新手几乎必踩。
4.2 时间序列看单队战绩波动
想跟踪某支球队整个赛季的胜率变化,可以用 pandas 的rolling做滑动窗口。下面以勇士为例:
# 筛选勇士比赛 warriors = all_results[(all_results['Home_Team'] == 'GSW') | (all_results['Away_Team'] == 'GSW')].copy() # 标记胜负 warriors['Win'] = ((warriors['Home_Team'] == 'GSW') & (warriors['Home_Score'] > warriors['Away_Score'])) | \ ((warriors['Away_Team'] == 'GSW') & (warriors['Away_Score'] > warriors['Home_Score'])) # 按日期排序后算 10 场滑动胜率 warriors = warriors.sort_values('Date') warriors['Rolling_Win_Rate'] = warriors['Win'].rolling(window=10).mean() plt.plot(warriors['Date'], warriors['Rolling_Win_Rate']) plt.title('勇士队 10 场滑动胜率') plt.xticks(rotation=45) plt.show()window=10是经验值,太小噪音大,太大反应迟钝。看这张图能发现连败或连胜的拐点,再结合赛程表看那段时间的对手强度,就能判断是实力问题还是赛程太难。
4.3 热力图对比球队攻防
把Team_Per_Game_Stats和Opponent_Per_Game_Stats合并后,可以画一张攻防四象限图。横轴是场均得分,纵轴是场均失分,每支球队一个点。右上角是攻强守弱,左下角是攻弱守强。这种图比表格直观得多,汇报时尤其好用。注意失分轴要反转,让“好”的方向朝右上,否则读图的人会懵。
5. 避坑与排查:这份数据我踩过的五个坑
5.1 日期格式不统一导致排序错乱
现象:按Date排序后,2017 年 1 月的比赛排到了 2016 年 12 月前面。原因是 CSV 里日期是mm/dd/yyyy格式,pandas 默认按字符串排序。解决:读入时加parse_dates=['Date'],或者手动pd.to_datetime(df['Date'], format='%m/%d/%Y')。这个坑不修,Elo 和滑动窗口全废。
5.2 球队缩写不一致
现象:GSW和Golden State Warriors同时出现,导致同一支球队被拆成两条记录。原因:不同赛季的数据源用了不同的命名习惯。解决:先统一成缩写,写一个映射表把全称转成三字母代码。我一般会打印unique()看一眼,确认没有漏网之鱼。
5.3 百分号字段被当成字符串
现象:FG%列做mean()时报错,提示无法将 str 转 float。原因:CSV 里存的是45.6%这种带百分号的文本。解决:用str.replace('%', '').astype(float) / 100转成小数。注意除不除 100 取决于你后续怎么用,如果只是比较大小,不除也行,但做加权计算时必须统一。
5.4 主客场字段在合并后丢失
现象:concat之后发现分不清哪队是主队。原因:两个赛季的结果文件列名不同,一个用Home,一个用Home_Team,合并时没对齐。解决:合并前先rename统一列名,合并后打印columns确认。这个错误很隐蔽,因为数据不会报错,只是分析结果全反了。
5.5 Elo 初始评分设成 0 导致前期排名失真
现象:赛季前十场,所有球队 Elo 都在 0 附近乱跳。原因:初始值设成了 0,而 Elo 公式里分母是 400,0 分和 1500 分的期望胜率计算完全不同。解决:初始评分统一设 1500,这是 Elo 系统的惯例。如果你做跨赛季跟踪,第二赛季可以直接沿用上赛季末评分,不用重置。
6. 进阶技巧:用 scikit-learn 做胜负预测的基线模型
Elo 能排名,但预测单场胜负还得靠分类模型。这份资源的数据量不大,三个赛季约 3600 场比赛,用逻辑回归就够了,别一上来就上 XGBoost,容易过拟合。特征我一般选四个:主队 Elo、客队 Elo、主队近 10 场胜率、客队近 10 场胜率。标签就是主队是否获胜。
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score # 假设 features 是四列特征,target 是 0/1 X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=42) model = LogisticRegression() model.fit(X_train, y_train) pred = model.predict(X_test) print('准确率:', accuracy_score(y_test, pred))random_state=42是为了结果可复现,不然每次跑出来的准确率都不一样,没法对比调参效果。test_size=0.2是常规切分,如果数据有季节性,最好按时间切,用前两个赛季训练、第三个赛季测试,这样更接近真实预测场景。
跑完看准确率,NBA 主队胜率基线大概 60%,如果你的模型低于这个数,说明特征没选好或者数据泄漏了。我一般会再输出一个混淆矩阵,看看是主队赢预测准还是客队赢预测准。如果一边倒,多半是主场优势特征太强,把客队 Elo 的权重压没了。
从那以后我每次拿到新的赛季数据,都强制先跑一遍字段检查和日期排序,再动任何分析代码。这份资源的价值不在于代码多复杂,而在于它把三个赛季的原始数据整理好了,省掉最耗时的找数据环节。希望帮到你。
本文还有配套的精品资源,点击获取