0%

NBA 球员信息统计及可视化项目

这篇博客是该项目的完整复盘,每个环节做了什么、为什么这么做、遇到了什么问题。

背景与目标

我们的目标是一个完整的数据分析系统:加载、清洗、统计、可视化、挖掘,并且针对一些特定的问题进行分析,比如球员的技术风格分类,用基础统计来预测得分。

技术栈:Python + pandas(表格处理)+ matplotlib(静态图表)+ scikit-learn(机器学习算法)。

后面又加上了网站,所以又加上了HTML + ES Modules+ ECharts 5

系统形态是命令行交互菜单,9 个功能项:数据概览、清洗、统计、可视化、聚类、预测、一键跑完,后来又新增了两个交互功能,球星生涯画像与双球星对比,和一个静态网站。

这是系统功能模块图

image-20260910105151048

数据源与四个问题

数据来自 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 行,不处理的话,一人一赛季不唯一,后续的统计、聚类、预测全都会出问题。

TOT 行的三行记录
原始数据里艾弗森 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
2
3
4
5
# 清洗核心:TOT 去重(data_clean.py)
df["_is_tot"] = df["Tm"] == "TOT"
df = df.sort_values("_is_tot", ascending=False)\
.drop_duplicates(subset=["Player", "Year"], keep="first")
df = df.drop(columns="_is_tot")

排序让 TOT 行排最前,去重”保留第一行”就是保 TOT。清洗后 24624 → 20286 行,校验 0 组残留重复。

位置规范化与单位换算

复合位置(F-C 表示前锋或中锋都能打)取第一个前缀当主位置(F-C → F),空值记 U;身高 "6-9"(英尺-英寸)转成 81 英寸,两张表的身高体重单位统一。

产出结果:两张基础表 + 一张生涯表

清洗产出两张干净的基础表:

  • 赛季表(20286 行):一行就是”某球员 × 某赛季”,TOT 去重后一人一赛季唯一;
  • 球员信息表(4550 人):身高、体重、位置这些不变的基本属性。

赛季表(WPS 打开效果)
球员信息表

因为我们的比较单位是”球员个体”,不是”赛季”,所以这里我们更进一步,把一个人所有赛季的行合成一行:场均类指标取生涯平均,累计类(总得分、总出场、征战赛季数)取总和,再拼上球员信息表的身高、体重、位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def build_career_df(seasons, players):
"""构造球员级生涯汇总表:每名球员一行,含生涯平均表现 + 球员基本信息。"""
tmp = seasons[seasons["G"] > 0].copy() # 排除出场 0 场(除零会出 NaN)
for c in ["MP", "PTS", "TRB", "AST", "STL", "BLK", "FGA", "3PA"]:
tmp[c + "_PG"] = tmp[c] / tmp["G"] # 转为场均口径

career = (
tmp.groupby("Player")
.agg(**{c + "_PG": (c + "_PG", "mean") for c in
["MP", "PTS", "TRB", "AST", "STL", "BLK"]},
PER=("PER", "mean"),
PTS_TOT=("PTS", "sum"), # 生涯总得分
Games=("G", "sum"), Seeds=("Year", "nunique"))
.reset_index().rename(columns={"Player": "name"})
)
info = players.drop_duplicates(subset=["name"], keep="first")
return career.merge(info, on="name", how="left")

这就是生涯表,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、年代演变、位置身高箱线图、出生地分布图。

fig01_位置分布饼图

fig02_身高分布直方图

fig03_体重分布直方图

fig04_身高体重散点图

image-20260910110019325

fig06_年代风格演变

fig07_位置身高箱线图

fig08_球员出生地分布

聚类分析

我们的一个目标是把球员的技术风格根据不同的特征分类,没有标准答案,属于无监督学习问题。

KMeans算法

  1. 选特征:7 个生涯场均特征,上场时间、得分、篮板、助攻、抢断、盖帽、效率值 PER;
  2. 筛样本:只挑生涯 ≥3 赛季的球员(只打一两个赛季的样本没代表性),3921 人 → 2008 人。这一步本身就是第一层调优:全样本聚类轮廓系数只有 0.230,过滤后才拿到 0.276;
  3. 标准化:指标单位差异大(上场时间二十几、得分十几、盖帽不到 1),不标准化的话 KMeans 按”数值大小”算距离,分钟数直接霸榜。所以要把 7 个指标拉到同一把尺子上(均值 0、方差 1);
  4. 调参:唯一核心参数是簇数 K。把 K 从 2 到 8 全部跑一遍,每档看两件事:① 轮廓系数,系数越高,分得越开,② 簇的可解读性(K=23 只有”强/弱”两堆,没篮球含义;K=78 出现几百人一团的模糊簇),最终拍板 K=5,正好对应五类有语义的群体。另外两个参数固定在代码里:n_init=10(随机初始化跑 10 次取最好,KMeans 对初始点敏感)、random_state=42(固定随机种子,不固定的话同一个 K 每次结果都可能不一样,和报告数字就对不上了);
  5. 评估:轮廓系数 0.276(取值 -11,越接近 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 人对应联盟金字塔底层的角色球员。

KMeans 聚类散点图

GMM 与 DBSCAN算法

除了kmeans,我们又跑了 GMM 与 DBSCAN,同一份标准化数据、同一批球员,公平对比。

GMM(高斯混合)是”更灵活的 KMeans”:KMeans 分得很硬(一个球员要么这簇要么那簇,默认每簇是圆球),GMM 给每个球员算概率归属(八成属这簇、两成属那簇),簇形状也能拉长变形。跑出来轮廓系数只有 0.125。原因很直接:GMM 比 KMeans 多估一堆参数(每簇形状加权重),参数一多,摊到每个参数头上的样本就少了,2008 人 × 7 指标的数据量撑不起它的”灵活配置”。模型越灵活越吃数据,数据不够时反而不如参数少的 KMeans。image-20260910110439541

DBSCAN(密度聚类)是”不看中心、看密度”:以每个球员为中心画半径 eps 的圈,圈里有足够邻居说明”密度够”,一圈圈往外扩成一坨;圈里孤零零的球员标成噪声(拒绝归簇)。它不用预设簇数,但参数敏感。我们用网格搜索:输入范围 0.6~2.5、步长 0.1,每个候选半径跑一遍,淘汰”单簇/噪声>50%”的退化结果,剩下的用轮廓系数打分,取最高分(eps=1.1,轮廓 0.380,比 KMeans 还高)。

虽然轮廓系数很高,但是这个最高分只分出了 2 簇,并且噪声达到了6.5%。对于”给球员分成几类可解读风格”的目标来说,2 簇基本没有区分度,还会丢弃 6.5% 的球员。

DBSCAN 聚类散点图

三种算法用的是同一份标准化数据、同一批 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 倍,”小个子持球得分手”的能力轮廓一目了然。image-20260910110957139

fig11_球星生涯画像_ALLEN_IVERSON

艾弗森 7 维雷达

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 预先算好存成文件的,网页只负责展示和查询,切页面根本不用重新计算,点哪页哪页秒开。而且和命令行同源,保存口径一致。

Web 可视化前端架构图

四个页面

  • 首页:数据卡(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 模型生成代码、人工核验。下面是一些人机协作问题:

  1. 口径错误:AI 初版把累计值当场均拿去预测,MAE=44 分。抽查库里 2016 赛季(79 场 2375 分)确认是累计值,统一转场均后 MAE 从 44 分降到 0.77 分。数据项目必须人工确认数据语义,不能依赖 AI 生成内容的表面合理性;
  2. TOT 行差点被误删:初版清洗直接”去重复行”,会按原顺序留单队行、丢 TOT 行。改规则”有 TOT 保 TOT”后 24624→20286 行,校验 0 组残留;
  3. 中文乱码:matplotlib 默认字体没中文字形,标题全是方框。统一配置黑体(SimHei)放全局一处生效。
  4. ai味太浓、逻辑混乱:ai生成的文字具有非常浓烈的ai味道,并且逻辑看似成立,实则有时有很大的问题,我需要从头到尾一点点修改润色。

总结

本项目完成了一个覆盖”数据清洗 → 描述统计 → 可视化 → 聚类 → 预测 → 交互式球星分析”完整流程的 NBA 球员信息统计及可视化系统,交付了 15 张图表、4 张统计结果表与 4 页在线可视化网站。

项目创新点:

  1. 场均口径统一体系:识别并统一原始数据”赛季累计值”与”场均”口径,全部以 _PG 列透明表达,保证聚类、预测、统计结论的一致性;
  2. TOT 去重规则设计:针对”组建中多队合计行”的典型脏数据问题,设计按(球员,赛季)分组保留合计行的规则,验证后使球员-赛季记录唯一(20286 条);
  3. 聚类样本筛选策略:通过”生涯 ≥3 赛季”样本过滤将聚类轮廓系数从 0.230 提升至 0.276,并让聚类结果具有可解读的篮球意义(自动区分超级得分手与高效内线两类名人堂球员);
  4. 多算法横向对比:聚类(KMeans/GMM/DBSCAN)与预测(6 种算法)全部横向对比并说明选型理由,回应”为什么选这个算法”;
  5. 交互式球星分析:输入球员名即可查看生涯画像、转会前后对比(利用 TOT 行自动检测转会)与双球星对比——把数据报表变成用户能玩的场景;
  6. 工程可复现性:固定随机种子、默认参数集中于 config、模块化文件结构、一键演示流程(python main.py 后按 7);
  7. 中文图表直出报告:matplotlib 中文字体配置 + 300dpi + “图序-图名”命名规范,图表可直接插入课程报告;
  8. Web 可视化与接口契约:4 页浏览器端网站(原生 ESM + ECharts 本地打包),与命令行同源同口径,前端只读 JSON 契约,为后续 C++ 后端(Drogon)预留替换点;转会检测覆盖赛季中转会与跨赛季换队两类,并维护 28 组球队迁址/改名名单避免误判。

可改进之处:

  1. 球员姓名消歧:数据源以姓名为主键,跨代同名球员存在小概率错配,可引入球员 ID 或身高体重辅助消歧;
  2. 时序公平评估:预测模型按赛季划分训练/测试集(如 1990s 训练、2010s 预测),可验证模型的跨年代泛化能力;
  3. 数据时效与服务化:当前数据截至 2017 赛季,后续可爬取最新赛季数据;Web 前端可进一步升级为服务化应用(用户账号、历史定制存档);
  4. 异常值处理与多算法对比:对 PER 异常值可设计检测-修正策略,聚类可增加 DBSCAN/GMM、骨架法选择 K 值,提升方法完备性;
  5. 完整业务化:可拓展为”球员检索 + 偏好推荐 + 生涯模拟”的小型应用。