尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

NBA 15-18赛季数据包实战:Python数据分析与Elo等级分计算

NBA 15-18赛季数据包实战:Python数据分析与Elo等级分计算 简介这份资源面向具备一定Python基础、希望上手真实数据分析项目的高校学生与数据爱好者围绕NBA比赛数据展开提供从数据采集到可视化呈现的完整实践素材。压缩包共14个文件约245KB以11个CSV数据表为主涵盖球队与对手的场均统计、赛程、赛果及杂项数据另含1个Python分析脚本、1份PDF说明文档和1个TXT文件分别用于数据处理、指标解释与辅助说明。资源已积累506人学习下载热度较为稳定。读者可借助现成的多赛季数据表直接练习数据清洗、指标计算与图表绘制结合脚本理解爬取与分析流程并通过Elo等级分定义文档补充评价模型知识适合作为课程作业、练手项目或数据分析入门的参考案例。1. 从一份 15-18 赛季的 NBA 数据包说起它到底能跑出什么很多人第一次做体育数据分析卡住的不是代码而是数据。网上找的 CSV 要么字段残缺要么赛季对不上要么爬下来的表头全是英文缩写看不懂。这份资源包解决的就是这个前置问题它把 2015-2016、2016-2017、2017-2018 三个赛季的 NBA 常规赛数据整理成了可直接读取的 CSV配套一个Analysis_NBA_Data.py分析脚本、一份 Elo 等级分定义的 PDF 说明文档以及一个data目录做数据归档。换句话说你拿到手就能跳过「找数据、洗字段」这两步直接进入 Python 数据分析和可视化环节。它适合三类人正在学 pandas 想找个真实数据集练手的入门者、需要快速验证某个分析思路的从业者、以及想用 NBA 数据做可视化大屏或课程作业的学生。三个赛季的跨度不算长但足够做趋势对比、球队强弱演变、主客场胜率差异这类分析。下面我按「数据长什么样 → 怎么读进来 → 怎么算 → 怎么画 → 坑在哪」的顺序拆一遍。2. 数据包结构拆解三张核心表和两个辅助文件怎么配合2.1 文件清单与字段职责先把包里的东西按功能分个类不然后面读文件容易乱。核心数据分三块赛季结果表、球队场均统计表、对手场均统计表。辅助文件是赛程表和杂项统计表。文件覆盖赛季主要用途2015-2016_results.csv/2016-2017_results.csv/17-18Result.csv15-18每场比赛的胜负结果、比分15-16Team_Per_Game_Stats.csv/16-17Team_Per_Game_Stats.csv15-17球队场均得分、篮板、助攻等15-16Opponent_Per_Game_Stats.csv/16-17Opponent_Per_Game_Stats.csv15-17对手视角的场均数据用于对比16-17Schedule.csv/17-18Schedule.csv16-18赛程、主客场、日期15-16Miscellaneous_Stats.csv/16-17Miscellaneous_Stats.csv15-17杂项统计如犯规、失误等Analysis_NBA_Data.py—主分析脚本Elo等级分定义.pdf—Elo 算法的定义和计算说明注意文件名不统一15-16 和 16-17 赛季用Team_Per_Game_Stats命名17-18 赛季的结果文件叫17-18Result.csv没有对应的 Team Stats 文件。这意味着做跨赛季对比时17-18 赛季只能从结果表和赛程表入手场均统计需要另找或从结果表反推。这是第一个要留意的边界。2.2 用 pandas 批量读取并统一列名文件名不统一直接写死路径会很难维护。我一般用 glob 匹配加字典映射的方式批量读顺便把列名统一成小写下划线格式。import pandas as pd import glob import os # 按赛季分组读取避免文件名不一致导致漏读 season_files { 2015-2016: { results: 2015-2016_results.csv, team_stats: 15-16Team_Per_Game_Stats.csv, opp_stats: 15-16Opponent_Per_Game_Stats.csv, misc: 15-16Miscellaneous_Stats.csv }, 2016-2017: { results: 2016-2017_results.csv, team_stats: 16-17Team_Per_Game_Stats.csv, opp_stats: 16-17Opponent_Per_Game_Stats.csv, misc: 16-17Miscellaneous_Stats.csv }, 2017-2018: { results: 17-18Result.csv, schedule: 17-18Schedule.csv } } def load_season(season, file_map, base_dirdata): 读取单个赛季的所有 CSV统一列名格式 dfs {} for key, fname in file_map.items(): path os.path.join(base_dir, fname) if not os.path.exists(path): print(f[跳过] {season} 的 {key} 文件不存在: {path}) continue df pd.read_csv(path) # 列名统一去空格、转小写、空格换下划线 df.columns [c.strip().lower().replace( , _) for c in df.columns] dfs[key] df print(f[读取] {season} {key}: {df.shape[0]} 行 x {df.shape[1]} 列) return dfs all_data {} for season, fmap in season_files.items(): all_data[season] load_season(season, fmap)这段代码的关键点有三个。第一用字典把赛季和文件映射起来而不是用glob.glob(*.csv)一把梭因为包里混了赛程、杂项、结果多种表全读进来后面还得靠列名猜哪张是哪张。第二os.path.exists做存在性检查17-18 赛季缺 Team Stats 文件时不会直接抛异常中断。第三列名统一处理是必须的原始 CSV 里列名可能带空格或大小写不一致不统一后面df[Team]和df[team]会来回翻车。读完之后建议先看一眼每张表的列名和前几行确认字段含义。比如结果表里通常有date、visitor、home、visitor_pts、home_pts这类字段球队统计表里是team、fg%、3p%、reb、ast等。字段名对不上时以实际读出来的为准不要照搬记忆。3. 从胜负表到 Elo 等级分核心计算逻辑与脚本参数3.1 Elo 算法的输入输出与 K 值选择包里那份Elo等级分定义.pdf是整个分析的理论核心。Elo 原本用于国际象棋核心思想是每场比赛后根据预期胜率和实际结果调整双方分数。预期胜率公式是E_home 1 / (1 10^((R_away - R_home HFA) / 400))其中R_home和R_away是主客队当前等级分HFA是主场优势加成400 是缩放常数。赛后更新R_home_new R_home K * (S_home - E_home)S_home是实际结果胜 1 负 0K是更新幅度。K 值越大分数波动越剧烈。NBA 一个赛季 82 场我一般把 K 设在 20 左右主场优势 HFA 设在 100 分上下。这两个参数没有绝对标准但 K 太大排名会抖K 太小赛季末排名又太黏。3.2 用结果表跑一遍 Elo 并输出排名下面这段代码从结果表出发按日期排序逐场更新最后输出赛季末排名。def compute_elo(results_df, k20, hfa100, init_rating1500): 基于比赛结果计算 Elo 等级分 results_df: 需包含 date, home, visitor, home_pts, visitor_pts 列 k: 更新系数控制单场波动 hfa: 主场优势加成 init_rating: 初始分 ratings {} # 按日期排序确保按时间顺序更新 df results_df.sort_values(date).reset_index(dropTrue) for _, row in df.iterrows(): home row[home] away row[visitor] # 首次出现的球队给初始分 ratings.setdefault(home, init_rating) ratings.setdefault(away, init_rating) r_home ratings[home] r_away ratings[away] # 预期胜率 e_home 1 / (1 10 ** ((r_away - r_home hfa) / 400)) # 实际结果 s_home 1.0 if row[home_pts] row[visitor_pts] else 0.0 # 更新分数 ratings[home] r_home k * (s_home - e_home) ratings[away] r_away k * ((1 - s_home) - (1 - e_home)) # 转成 DataFrame 并按分数降序 elo_df pd.DataFrame(list(ratings.items()), columns[team, elo]) elo_df elo_df.sort_values(elo, ascendingFalse).reset_index(dropTrue) elo_df[rank] elo_df.index 1 return elo_df # 对 2015-2016 赛季跑一遍 elo_1516 compute_elo(all_data[2015-2016][results]) print(elo_1516.head(10))逻辑说明sort_values(date)保证按时间顺序更新如果日期格式不统一比如有的带时间有的不带先用pd.to_datetime转一下。setdefault处理首次出现的球队避免 KeyError。更新公式里客队的实际结果是1 - s_home预期胜率是1 - e_home两者相减就是客队的调整量。参数方面k20适合常规赛如果做季后赛可以调到 30 让波动更大。hfa100是经验值你可以跑几组不同 HFA 看排名变化如果主场胜率明显高于 55%HFA 可以往上加。init_rating1500是通用起点所有球队从同一起跑线开始赛季初几场波动大是正常的。跑完之后可以拿排名和实际战绩对比如果某支球队 Elo 排名和胜率排名差很多说明它赢的比赛含金量高对手强这本身就是个有意思的分析点。4. 可视化落地用 matplotlib 和 seaborn 出四张能看的图4.1 球队场均得分分布与主客场差异数据可视化不是把数字画出来就完事得让图能回答问题。第一张图我一般做球队场均得分的分布顺便对比主客场。import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体避免中文乱码 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False def plot_team_scoring(team_stats_df, top_n15): 画球队场均得分横向条形图 df team_stats_df.copy() # 找到得分列不同赛季列名可能不同 pts_col [c for c in df.columns if pts in c or points in c] team_col [c for c in df.columns if team in c] if not pts_col or not team_col: print(未找到得分或球队列请检查列名) return df df[[team_col[0], pts_col[0]]].dropna() df.columns [team, pts] df df.sort_values(pts, ascendingFalse).head(top_n) fig, ax plt.subplots(figsize(10, 6)) sns.barplot(datadf, xpts, yteam, axax, paletteviridis) ax.set_title(f球队场均得分 Top {top_n}) ax.set_xlabel(场均得分) ax.set_ylabel(球队) plt.tight_layout() plt.savefig(team_scoring.png, dpi150) plt.show() plot_team_scoring(all_data[2015-2016][team_stats])这段代码里pts_col和team_col用列表推导找列是因为不同赛季 CSV 的列名可能有细微差异写死df[pts]容易 KeyError。dropna去掉缺失值head(top_n)只画前 15 名避免图太挤。paletteviridis是配色方案换成coolwarm或Blues也行。保存用dpi150够清晰又不至于文件太大。4.2 Elo 排名随时间变化的折线图第二张图做 Elo 分数的时间序列能看出哪支球队赛季中段发力、哪支后程崩盘。这需要在计算 Elo 时把每场比赛后的分数存下来而不是只存最终值。def compute_elo_history(results_df, k20, hfa100, init_rating1500): 计算 Elo 并记录每场比赛后的分数变化 ratings {} history [] # 每场比赛后记录一次 df results_df.sort_values(date).reset_index(dropTrue) for _, row in df.iterrows(): home, away row[home], row[visitor] ratings.setdefault(home, init_rating) ratings.setdefault(away, init_rating) r_home, r_away ratings[home], ratings[away] e_home 1 / (1 10 ** ((r_away - r_home hfa) / 400)) s_home 1.0 if row[home_pts] row[visitor_pts] else 0.0 ratings[home] r_home k * (s_home - e_home) ratings[away] r_away k * ((1 - s_home) - (1 - e_home)) history.append({ date: row[date], team: home, elo: ratings[home] }) history.append({ date: row[date], team: away, elo: ratings[away] }) return pd.DataFrame(history) hist_df compute_elo_history(all_data[2015-2016][results]) # 选几支球队画折线 focus_teams [Golden State Warriors, Cleveland Cavaliers, San Antonio Spurs] focus_df hist_df[hist_df[team].isin(focus_teams)] fig, ax plt.subplots(figsize(12, 6)) for team in focus_teams: team_df focus_df[focus_df[team] team].sort_values(date) ax.plot(team_df[date], team_df[elo], labelteam, linewidth1.5) ax.set_title(2015-2016 赛季 Elo 等级分变化) ax.set_xlabel(日期) ax.set_ylabel(Elo 分数) ax.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(elo_trend.png, dpi150) plt.show()注意focus_teams里的队名必须和 CSV 里的写法完全一致大小写、缩写都要对。如果 CSV 里是GSW而不是Golden State Warriors这里就得改。日期列如果是字符串ax.plot也能画但排序可能不对建议先pd.to_datetime转成日期类型。另外两张图可以做主客场胜率对比的热力图、球队得分与失分的散点图。思路类似核心是先把数据整理成x和y两列再交给 seaborn。不要一上来就追求花哨先把每张图要回答的问题写清楚再选图型。5. 避坑与排查跑这份数据包时最容易翻车的五个地方5.1 日期格式不统一导致排序错乱现象Elo 计算出来的排名和实际战绩对不上强队分数偏低。原因结果表里date列可能是2015-10-27和10/27/2015混着来字符串排序会把10/27/2015排到2015-10-27前面。解决读进来后统一df[date] pd.to_datetime(df[date], errorscoerce)再sort_values(date)。errorscoerce把解析失败的变成 NaT后面可以dropna掉。5.2 队名写法不一致导致 Elo 分数分裂现象同一支球队在排名表里出现两次分数各算各的。原因CSV 里有的写New York Knicks有的写NY Knickssetdefault会当成两支球队。解决先跑一遍df[home].unique()和df[visitor].unique()把队名列表打印出来人工核对或者建一个映射字典统一缩写。这个坑不排查后面所有分析都是错的。5.3 17-18 赛季缺 Team Stats 文件现象想画三个赛季的场均得分对比发现 17-18 赛季没有Team_Per_Game_Stats.csv。原因资源包本身就没放这个文件只有结果表和赛程表。解决要么只用 15-17 两个赛季做对比要么从17-18Result.csv里按球队分组算场均得分。后者需要先确认结果表里有home_pts和visitor_pts两列然后groupby球队求均值。5.4 列名带空格或特殊字符现象df[FG%]报 KeyError。原因原始 CSV 列名可能是FG%或fg %或Field Goal Percentage。解决读进来先print(df.columns.tolist())看一眼然后用df.columns [c.strip().lower().replace( , _) for c in df.columns]统一。百分号列名建议改成fg_pct这种避免后续引用时转义麻烦。5.5 matplotlib 中文显示为方块现象图标题和轴标签里的中文全是方框。原因matplotlib 默认字体不含中文。解决plt.rcParams[font.sans-serif] [SimHei]Windows 下一般有 SimHeiMac 下换成[Arial Unicode MS]Linux 下如果没装中文字体需要先安装或换成英文标签。另外plt.rcParams[axes.unicode_minus] False解决负号显示问题。6. 进阶技巧把 Elo 和场均数据交叉验证判断排名可信度跑完 Elo 和场均统计后我习惯做一件事把两个排名放在一起对比。如果某支球队 Elo 排第 3 但场均得分排第 12说明它赢球靠的是防守或关键球而不是火力压制。这种交叉验证能帮你判断数据包的分析结果是否自洽。具体做法是先拿到 Elo 排名表elo_df再从team_stats里算出场均得分排名然后按球队名 merge。def cross_validate(elo_df, team_stats_df): 交叉对比 Elo 排名和场均得分排名 df team_stats_df.copy() pts_col [c for c in df.columns if pts in c][0] team_col [c for c in df.columns if team in c][0] df df[[team_col, pts_col]].dropna() df.columns [team, pts] df[pts_rank] df[pts].rank(ascendingFalse) merged elo_df.merge(df, onteam, howinner) merged[rank_diff] merged[rank] - merged[pts_rank] # 差异大的球队值得单独看 merged[abs_diff] merged[rank_diff].abs() return merged.sort_values(abs_diff, ascendingFalse) result cross_validate(elo_1516, all_data[2015-2016][team_stats]) print(result[[team, elo, rank, pts, pts_rank, rank_diff]].head(10))rank_diff为正说明 Elo 排名比得分排名靠后得分强但赢球少为负说明 Elo 排名更靠前得分一般但能赢。abs_diff排序后前几支球队就是最值得深挖的对象。这个技巧不复杂但能让你从「跑出结果」进阶到「解释结果」。从那以后我每次拿到新的赛季数据包都会先跑一遍队名一致性检查和日期格式检查再开始算 Elo。这两个检查花不了五分钟但能省掉后面几小时的排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表