这篇博客是该项目的完整复盘,每个环节做了什么、为什么这么做、遇到了什么问题。
背景与目标
我们的目标是一个完整的数据分析系统:加载、清洗、统计、可视化、挖掘,并且针对一些特定的问题进行分析,比如球员的技术风格分类,用基础统计来预测得分。
技术栈:Python + pandas(表格处理)+ matplotlib(静态图表)+ scikit-learn(机器学习算法)。
后面又加上了网站,所以又加上了HTML + ES Modules+ ECharts 5
系统形态是命令行交互菜单,9 个功能项:数据概览、清洗、统计、可视化、聚类、预测、一键跑完,后来又新增了两个交互功能,球星生涯画像与双球星对比,和一个静态网站。
这是系统功能模块图

数据源与四个问题
数据来自 Kaggle 公开数据集 NBA Players stats since 1950(作者 drgilermo,爬取自 Basketball Reference 网站),覆盖 1950~2017 赛季共 68 个赛季:
| 文件 | 规模 | 内容 |
|---|---|---|
| player_data.csv | 4550 行 | 球员基本信息(姓名、起止年份、位置、身高、体重、出生日期、大学) |
| Seasons_Stats.csv | 24691 行 × 53 列 | 球员-赛季级统计(每行 = 一名球员一个赛季的全部指标) |
| Players.csv | 3922 行 | 球员出生地信息(扩展分析用) |
数据的核心单位是”球员 × 赛季”记录:一行就是一名球员在一个赛季的统计,是最小分析单元。
拿到数据先探查,总共发现四个问题,全都影响后续分析。这也是全项目最大价值点之一,下面逐个说。
问题 1:两个拼写错误的噪声列
列名 blanl、blank2(应为 blank)和一个匿名索引列,是爬虫遗留的垃圾列,直接删除。
问题 2:TOT 合计行
因为每行是”球员 × 赛季 × 球队”的统计,所以一名球员赛季中途转会的话,一年会占好几行。比如先在 A 队、再去 B 队,数据源按球队拆成两行,最后再加一行 TOT 合计行(Total,该球员当季跨所有球队的总和)。
拿艾弗森举例:2006 年 12 月他从 76 人交易到掘金,表里就是”76 人 15 场、掘金 50 场、TOT 65 场”三行,一个球员变成了三个”球员”。这类 TOT 行共有 2123 行,不处理的话,一人一赛季不唯一,后续的统计、聚类、预测全都会出问题。

原始数据里艾弗森 2007 赛季的 TOT 三行(WPS 打开效果)。
问题 3:累计值不等于场均
PTS(总得分)、MP(总上场分钟)、TRB(总篮板)、AST(总助攻)等 8 个字段是赛季累计值,不是场均。库里 2016 赛季出战 79 场、总分 2375,赛场上大家说的是”场均 30 分”(2375÷79≈30),表里存的是 2375。
拿累计值比谁得分多会被出场次数绑架(打 80 场的人天然占优),所以统一转成场均口径(PTS_PG = PTS / G 这类 *_PG 列)。
第一遍跑 AI 生成的代码时其实我们没有发现这个问题:AI 把累计值直接拿去预测,MAE 高达 44 分。场均得分正常不到 30 分,误差 44 分显然哪里错了。抽查库里这行数据(79 场 2375 分)确认是累计值,修正后 MAE 降到 0.77 分。
问题4:出场 0 场的赛季
有的赛季球员登记了却一场没打,算场均时”除以 0”会出问题,构建生涯表前要剔除。
数据清洗
清洗分为以下几步:删噪声列、删无效行、TOT 去重、位置规范化、异常值检查和单位换算

TOT 去重:有 TOT 就保 TOT
规则可以总结为:按(球员,赛季)分组,组内有 TOT 行就只保留 TOT 行。TOT是total的缩写,如果某位球员在赛季中从A队转会到B队,那么该赛季他就会有三条数据,但只有 TOT 才是“当季跨球队的完整总和”,单队行只是其中的一部分:
1 | # 清洗核心:TOT 去重(data_clean.py) |
排序让 TOT 行排最前,去重”保留第一行”就是保 TOT。清洗后 24624 → 20286 行,校验 0 组残留重复。
位置规范化与单位换算
复合位置(F-C 表示前锋或中锋都能打)取第一个前缀当主位置(F-C → F),空值记 U;身高 "6-9"(英尺-英寸)转成 81 英寸,两张表的身高体重单位统一。
产出结果:两张基础表 + 一张生涯表
清洗产出两张干净的基础表:
- 赛季表(20286 行):一行就是”某球员 × 某赛季”,TOT 去重后一人一赛季唯一;
- 球员信息表(4550 人):身高、体重、位置这些不变的基本属性。


因为我们的比较单位是”球员个体”,不是”赛季”,所以这里我们更进一步,把一个人所有赛季的行合成一行:场均类指标取生涯平均,累计类(总得分、总出场、征战赛季数)取总和,再拼上球员信息表的身高、体重、位置:
1 | def build_career_df(seasons, players): |
这就是生涯表,3921 名球员,每人一行。后面统计、画图、聚类、预测都以它为地基。注意:生涯表不单独落盘,是运行中实时生成的中间表(pandas DataFrame),真正导出的是两张清洗表加 4 张统计结果表。
简单来说,赛季表看”一个赛季”、信息表看”一个人”、生涯表看”整个生涯”。
四类统计分析
基于两组表做了四类统计,两类用赛季表、两类用生涯表。
1. 整体描述(赛季表)
全表平均年龄 26.6 岁、平均每赛季出战 56.35 场、效率值 PER 均值 12.84。

2. 按年代分组(赛季表):小球时代的数据证据
三分出手从 1990s 的 61 次/季涨到 2010s 的 108 次/季(+77%)。这是”小球时代”三分席卷联盟的量化证据。人均总得分与篮板从历史高位回落(防守强化、节奏变慢),PER 保持稳定(12.5~13.3)。


人均三分出手 / 得分 / 效率值的年代演变。
3. 按位置分组(生涯表):体型与分工吻合
中锋最高最重(82.93 英寸 ≈ 2.11 米、242.3 磅)且篮板最多(3.81);后卫最矮最轻(75.13 英寸、189.91 磅)但助攻显著最高(1.97 vs 0.71),得分反而略高于中锋。后卫承担大量外线得分。体型特征与篮球位置分工完全吻合。

4. 相关性分析(生涯表)
身高与体重 r=0.823(强正相关);身高与助攻 r=-0.454(负相关,越高越不像后卫);身高与篮板 r=0.383(正相关);得分与篮板 r=0.677、得分与助攻 r=0.669(核心球员各项指标同向强相关)。

注意:相关不等于因果。身高体重高度相关,不代表”长得高就长胖”,而是同一身体机制的结果。
可视化分析
在前面统计分析的基础上,我们进行了初步的可视化分析,生成了 8 张静态图,分别是位置分布饼图、身高体重分布直方图、身高体重散点、生涯总得分 TOP20、年代演变、位置身高箱线图、出生地分布图。








聚类分析
我们的一个目标是把球员的技术风格根据不同的特征分类,没有标准答案,属于无监督学习问题。
KMeans算法
- 选特征:7 个生涯场均特征,上场时间、得分、篮板、助攻、抢断、盖帽、效率值 PER;
- 筛样本:只挑生涯 ≥3 赛季的球员(只打一两个赛季的样本没代表性),3921 人 → 2008 人。这一步本身就是第一层调优:全样本聚类轮廓系数只有 0.230,过滤后才拿到 0.276;
- 标准化:指标单位差异大(上场时间二十几、得分十几、盖帽不到 1),不标准化的话 KMeans 按”数值大小”算距离,分钟数直接霸榜。所以要把 7 个指标拉到同一把尺子上(均值 0、方差 1);
- 调参:唯一核心参数是簇数 K。把 K 从 2 到 8 全部跑一遍,每档看两件事:① 轮廓系数,系数越高,分得越开,② 簇的可解读性(K=2
3 只有”强/弱”两堆,没篮球含义;K=78 出现几百人一团的模糊簇),最终拍板 K=5,正好对应五类有语义的群体。另外两个参数固定在代码里:n_init=10(随机初始化跑 10 次取最好,KMeans 对初始点敏感)、random_state=42(固定随机种子,不固定的话同一个 K 每次结果都可能不一样,和报告数字就对不上了); - 评估:轮廓系数 0.276(取值 -1
1,越接近 1 簇内越紧凑、簇间分离越好。0.250.5 算”弱到中等”,我们接受它,因为篮球风格本来就是渐变带,可解读性优先)。
产出是 5 个质心反标准化回原始单位,每个簇给出特征画像和得分 Top5 代表球员:
| 簇 | 人数 | 场均上场 | 场均得分 | 场均篮板 | 场均助攻 | 平均 PER | 人群画像 |
|---|---|---|---|---|---|---|---|
| 簇0 | 484 | 21.8 分钟 | 8.72 | 2.68 | 2.39 | 12.5 | 常规均衡型 |
| 簇1 | 376 | 21.2 分钟 | 7.96 | 5.32 | 1.08 | 13.5 | 蓝领内线(盖帽 0.73 最多) |
| 簇2 | 256 | 30.9 分钟 | 15.08 | 3.95 | 4.53 | 16.3 | 顶级持球得分手(乔丹/詹姆斯/艾弗森) |
| 簇3 | 125 | 31.0 分钟 | 15.07 | 8.89 | 2.13 | 18.1 | 高效内线(杜兰特/贾巴尔/奥尼尔) |
| 簇4 | 767 | 12.0 分钟 | 3.97 | 2.10 | 0.79 | 9.8 | 边缘角色球员 |
算法没有任何人工标签,仅凭 7 项统计,自动把乔丹、詹姆斯、艾弗森聚成一簇,把杜兰特、贾巴尔、奥尼尔聚成另一簇。”得分手”与”高效内线”两类风格截然分开,和篮球常识完全吻合。人数最多的簇 767 人对应联盟金字塔底层的角色球员。

GMM 与 DBSCAN算法
除了kmeans,我们又跑了 GMM 与 DBSCAN,同一份标准化数据、同一批球员,公平对比。
GMM(高斯混合)是”更灵活的 KMeans”:KMeans 分得很硬(一个球员要么这簇要么那簇,默认每簇是圆球),GMM 给每个球员算概率归属(八成属这簇、两成属那簇),簇形状也能拉长变形。跑出来轮廓系数只有 0.125。原因很直接:GMM 比 KMeans 多估一堆参数(每簇形状加权重),参数一多,摊到每个参数头上的样本就少了,2008 人 × 7 指标的数据量撑不起它的”灵活配置”。模型越灵活越吃数据,数据不够时反而不如参数少的 KMeans。
DBSCAN(密度聚类)是”不看中心、看密度”:以每个球员为中心画半径 eps 的圈,圈里有足够邻居说明”密度够”,一圈圈往外扩成一坨;圈里孤零零的球员标成噪声(拒绝归簇)。它不用预设簇数,但参数敏感。我们用网格搜索:输入范围 0.6~2.5、步长 0.1,每个候选半径跑一遍,淘汰”单簇/噪声>50%”的退化结果,剩下的用轮廓系数打分,取最高分(eps=1.1,轮廓 0.380,比 KMeans 还高)。
虽然轮廓系数很高,但是这个最高分只分出了 2 簇,并且噪声达到了6.5%。对于”给球员分成几类可解读风格”的目标来说,2 簇基本没有区分度,还会丢弃 6.5% 的球员。

三种算法用的是同一份标准化数据、同一批 2008 名球员,对比结果:
| 算法 | 簇数 | 轮廓系数 | 噪声占比 | 结论 |
|---|---|---|---|---|
| KMeans | 5 | 0.276 | 0 | 各簇规模可控,质心可反标准化逐簇画像 |
| GMM(高斯混合) | 5 | 0.125 | 0 | 软聚类,轮廓系数明显低于 KMeans |
| DBSCAN(eps=1.0,固定网格 0.2 步长) | 3 | 0.319 | 8.7% | 轮廓略高但簇数不可控、丢弃无法归簇的球员 |
| DBSCAN(eps=1.1,细网格 0.1 步长) | 2 | 0.380 | 6.5% | 轮廓最高却只剩 2 簇,区分度反而更差 |
结论:DBSCAN 轮廓可以很高但簇数不可控、丢球员;GMM 与 KMeans 同类但数据量小反而更差;KMeans 簇数可控、全员覆盖,对任务可解释性最好。
得分预测
除了给球员分类,我们还要尝试用基础统计预测赛季的场均得分,这是监督学习中的回归问题:给了”特征 + 答案”,学一个”基础统计 → 得分”的函数。
这里我们用的数据回到了赛季表取值。过滤出特征与目标都有效、出场 >0 的 15520 条球员-赛季记录。输入 9 个特征(出场数 G、上场时间、投篮出手、三分出手、篮板、助攻、抢断、盖帽,后 7 项场均口径,加年龄),预测目标是该赛季场均得分(PTS_PG = PTS / G)。
划分:15520 条按 80%/20% 随机切小一份训练集、一份测试集(固定 random_state=42),6 种算法共用同一份划分。同一份训练集训练、同一份测试集评分,考同一张试卷才公平。
6 种算法:线性回归(基线,可解释)、岭/套索回归(线性+正则化)、决策树/随机森林/梯度提升(非线性与集成)。评估三个指标:R²(解释了数据变动的比例,1 完美)、MAE(场均得分的偏差,分/场)、RMSE(放大大误差)。
| 模型 | R² | MAE | RMSE(均方根误差) |
|---|---|---|---|
| 线性回归(基线) | 0.9690 | 0.77 | 1.10 |
| 岭回归 | 0.9690 | 0.77 | 1.10 |
| 套索回归 | 0.9689 | 0.77 | 1.10 |
| 决策树回归 | 0.9567 | 0.89 | 1.30 |
| 随机森林 | 0.9708 | 0.74 | 1.07 |
| 梯度提升(GBRT) | 0.9716 | 0.73 | 1.05 |
结果解读:6 种算法 R² 全部超 0.95,线性回归(0.9690)几乎追平集成模型(0.9716)。基础统计与得分的关联基本是线性的,得分预测任务本身并不复杂。单决策树最弱(0.9567,单树方差大),森林与 GBRT 通过集成收敛到最好,GBRT 每棵专补上一棵的错,精度最高(误差控制在 ±0.8 分/场内)。



这里存在一个信息泄漏的问题:训练集和测试集80/20 切分切的是”球员-赛季记录”,不是”球员”。同一名球员的赛季会横跨训练集和测试集,比如库里 2016 赛季在训练集、2017 赛季在测试集,历史赛季给模型提供了”强先验”,分数比”无先验预测新秀”偏高。我们的改进方案:按年代切分(1990s 训练、2010s 测试)做时序验证。
交互式球星分析
到上面这里,我们这个项目还少了点交互的功能,于是下面我们尝试做了一些交互式球星分析:
1. 球星生涯画像
模糊匹配:数据源里很多人名带星号(Michael Jordan*),统一去星号归一化再匹配,候选按生涯总得分排序,检索 jordan 直接命中迈克尔·乔丹。
名人堂名录:输入 f 浏览 30 位名人堂球星(乔丹、詹姆斯、科比、贾巴尔、杜兰特、姚明等),按序号直接选。
选中后生成生涯汇总卡(赛季数、总得分、场均、巅峰赛季、转会记录)加 7 维能力雷达。雷达相对联盟平均水平归一化(生涯 ≥3 赛季的 2008 人有效样本均值为 1.0 倍基准,超出一倍就是高于联盟平均)。
这里拿艾弗森画像举例:14 年 914 场、24368 分,巅峰 2006 赛季场均 33.0 分;雷达显示得分 3.27 倍、助攻 3.35 倍、抢断 3.21 倍,盖帽 0.44 倍,”小个子持球得分手”的能力轮廓一目了然。


2. 转会前后对比:双规则转会检测
转会检测靠两个规则:
规则一(赛季中转会):某赛季存在 TOT 合计行,该球员赛季中换过球队,非 TOT 行按数据源行序拆成”转会前/转会后”两段(艾弗森 2007:76 人 15 场 → 掘金 50 场);
规则二(跨赛季换队):只看 TOT 会漏掉休赛期换队,比如勒布朗 2011 骑士→热火、2015 热火→骑士。这种相邻两个赛季球队记录不同的,用清洗后的赛季表逐季检查:不同且非转会年,判为跨赛季换队,对比取”换队前一个完整赛季 vs 新队首个完整赛季”的整季表现。
两类转会统一登记成一份”转会清单”(标注类型:赛季中/跨赛季),列年份、类型、球队、前后场次,任意选看。艾弗森 3 次赛季中、勒布朗 2 次跨赛季,都能查到。
选艾弗森 2007 赛季看结果:上场时间几乎不变(42.67→42.42),但场均得分 31.20→24.82(-20.4%)、PER 23.50→18.40(-21.7%)。他在掘金让渡了部分球权(掘金已有安东尼等得分点),数据形态随角色变化,这正是”转会不仅影响球队,也直接改变球员数据”的实证。

各项指标的变化(转会前 76 人 15 场 vs 转会后掘金 50 场):
| 指标 | 转会前(PHI,15 场) | 转会后(DEN,50 场) | 变化率 |
|---|---|---|---|
| 场均得分 | 31.20 | 24.82 | -20.4% |
| 效率值 PER | 23.50 | 18.40 | -21.7% |
| 场均抢断 | 2.20 | 1.80 | -18.2% |
| 场均助攻 | 7.27 | 7.18 | -1.2% |
| 场均上场时间 | 42.67 | 42.42 | -0.6% |
注:后来发现杜兰特 2009 年 SEA→OKC 也被标成换队,但那是超音速整体搬迁改名雷霆(同一支 franchise),不是转会。于是我们全量扫描数据(旧球队在搬迁年后彻底消失+多数球员跟随),定了 28 组”球队迁址/改名”名单(超音速→雷霆、灰熊搬家、篮网易址、国王历史长链等),检测时排除。真实转会保留,迁址不误标。
3. 双球星对比
选两人:7 项生涯指标画雷达图(按两人较大值归一化)加差值表,差值超 15% 自动点评。乔丹 vs 詹姆斯:得分 +7.9%、抢断 +27.8%(乔丹占优);篮板 -15.9%、助攻 -26.9%(詹姆斯占优),PER 27.37 vs 27.70 几乎持平。”得分手”对标”组织前锋”,各领风骚。

Web 可视化网站
命令行终究单调,我做了一个线上可视化网站,部署在 GitHub Pages上,属于我的个人站点下面附属的一个项目站点:https://tristan1116.github.io/nba-visualization/
技术选型
原生 HTML + 原生 JS(ES Modules)。ES Modules 是 JS 官方自带的模块化写法:代码拆成独立文件,页面需要哪个就 import 哪个(类比 C 的 #include),浏览器原生支持,不需要 webpack。网站就是一堆普通文件,拷到任何电脑直接打开,断网也能演示。
ECharts 5 是开源 JS 图表库,画折线、雷达、饼图、柱状图,而且能交互:鼠标悬浮看数值、点图例开关曲线、拖拽缩放,这是 matplotlib 静态 PNG 做不到的。
配色”暗夜红金”:黑蓝底 #070F1E、官方红 #C8102E、金字 #FDB927。
数据契约
网站本身不分析任何数据。我写了一个 web_generate.py,把前面所有模块(清洗/统计/挖掘/画像)跑出来的结果直接导出成一批 JSON 文件:生涯表拆成”球员列表”(3919 人一个文件)和”每人一份生涯明细”(三千多个文件),加上四类统计表、聚类结果、预测算法对比、名人堂名录,一共六类。
前端 api.js 统一按固定的地址规则读取:一个地址对应一类数据(球员列表一个地址、某球员明细一个地址)。因为所有数字都是 Python 预先算好存成文件的,网页只负责展示和查询,切页面根本不用重新计算,点哪页哪页秒开。而且和命令行同源,保存口径一致。

四个页面
- 首页:数据卡(3919 人 / 20286 条 / 1950~2017)+ 即时搜索(权重:全名>姓氏>名字>子串)+ 30 位名人堂 + 全量列表(位置筛选 + 分页);
- 球星页:生涯汇总卡 + 多指标叠加走势图(得分/效率值/上场时间同屏比,转会红/青竖线区分赛季中/跨赛季,球队时期色块直接标图上)+ 7 维雷达 + 转会对比卡片 + 聚类归属;
- 对比页:双选择器、双星雷达、差值表、自动点评;
- 总览页:模块图/流程图、四类统计表(中英双语表头)、图像画廊、三聚类对比表、6 算法结论条。
两个工程细节
缓存版本号机制:浏览器会把静态文件缓存住,改了数据刷新还是旧内容。在 api.js 顶部放一个 VER 常量,URL 全部带 ?v=VER,数据更新后改一下数字,全站强制取新数据。
部署:GitHub Pages 选 “Deploy from a branch”(零构建、仓库内容即站点、push 即更新)。这和博客(Hexo + GitHub Actions 云端构建)走了不同的路径,这个站点纯静态,不需要构建脚本。
站点所有数字与命令行完全一致(3919 人/轮廓 0.275/GBRT 0.9716/艾弗森 -20.4%),前端只是”展示层”,分析结论零二次加工。线上效果见:https://tristan1116.github.io/nba-visualization/
AI 协作与工程经验
整个项目,我们用 Claude Code接入 DeepSeek 模型生成代码、人工核验。下面是一些人机协作问题:
- 口径错误:AI 初版把累计值当场均拿去预测,MAE=44 分。抽查库里 2016 赛季(79 场 2375 分)确认是累计值,统一转场均后 MAE 从 44 分降到 0.77 分。数据项目必须人工确认数据语义,不能依赖 AI 生成内容的表面合理性;
- TOT 行差点被误删:初版清洗直接”去重复行”,会按原顺序留单队行、丢 TOT 行。改规则”有 TOT 保 TOT”后 24624→20286 行,校验 0 组残留;
- 中文乱码:matplotlib 默认字体没中文字形,标题全是方框。统一配置黑体(SimHei)放全局一处生效。
- ai味太浓、逻辑混乱:ai生成的文字具有非常浓烈的ai味道,并且逻辑看似成立,实则有时有很大的问题,我需要从头到尾一点点修改润色。
总结
本项目完成了一个覆盖”数据清洗 → 描述统计 → 可视化 → 聚类 → 预测 → 交互式球星分析”完整流程的 NBA 球员信息统计及可视化系统,交付了 15 张图表、4 张统计结果表与 4 页在线可视化网站。
项目创新点:
- 场均口径统一体系:识别并统一原始数据”赛季累计值”与”场均”口径,全部以
_PG列透明表达,保证聚类、预测、统计结论的一致性; - TOT 去重规则设计:针对”组建中多队合计行”的典型脏数据问题,设计按(球员,赛季)分组保留合计行的规则,验证后使球员-赛季记录唯一(20286 条);
- 聚类样本筛选策略:通过”生涯 ≥3 赛季”样本过滤将聚类轮廓系数从 0.230 提升至 0.276,并让聚类结果具有可解读的篮球意义(自动区分超级得分手与高效内线两类名人堂球员);
- 多算法横向对比:聚类(KMeans/GMM/DBSCAN)与预测(6 种算法)全部横向对比并说明选型理由,回应”为什么选这个算法”;
- 交互式球星分析:输入球员名即可查看生涯画像、转会前后对比(利用 TOT 行自动检测转会)与双球星对比——把数据报表变成用户能玩的场景;
- 工程可复现性:固定随机种子、默认参数集中于 config、模块化文件结构、一键演示流程(
python main.py后按 7); - 中文图表直出报告:matplotlib 中文字体配置 + 300dpi + “图序-图名”命名规范,图表可直接插入课程报告;
- Web 可视化与接口契约:4 页浏览器端网站(原生 ESM + ECharts 本地打包),与命令行同源同口径,前端只读 JSON 契约,为后续 C++ 后端(Drogon)预留替换点;转会检测覆盖赛季中转会与跨赛季换队两类,并维护 28 组球队迁址/改名名单避免误判。
可改进之处:
- 球员姓名消歧:数据源以姓名为主键,跨代同名球员存在小概率错配,可引入球员 ID 或身高体重辅助消歧;
- 时序公平评估:预测模型按赛季划分训练/测试集(如 1990s 训练、2010s 预测),可验证模型的跨年代泛化能力;
- 数据时效与服务化:当前数据截至 2017 赛季,后续可爬取最新赛季数据;Web 前端可进一步升级为服务化应用(用户账号、历史定制存档);
- 异常值处理与多算法对比:对 PER 异常值可设计检测-修正策略,聚类可增加 DBSCAN/GMM、骨架法选择 K 值,提升方法完备性;
- 完整业务化:可拓展为”球员检索 + 偏好推荐 + 生涯模拟”的小型应用。