ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Python实现选手赛区数据对比分析:从数据清洗到统计检验

用Python实现选手赛区数据对比分析:从数据清洗到统计检验 最近一段时间“欧美选手 vs 亚洲选手到底谁更强”这类话题又出现在赛事讨论区里。说实话这类讨论几乎每个大赛周期都会来一轮但大多数争论停留在印象和情绪层面有人看几场高光集锦有人盯着某个选手的单项数据还有人把某一次关键比赛的胜负放大成“赛区整体实力”的结论。如果你也是数据分析师、比赛分析师或者只是想理性吃瓜的开发者会发现更靠谱的做法是把这场“论战”变成一个数据分析项目让代码替你处理成千上万条选手记录用统计指标替代个人偏好。这篇文章不会给你一个“谁更强”的最终裁决而是要给你一套完整可复用的 Python 分析流程。从数据加载、清洗、赛区标签统一到分组统计、可视化对比、显著性检验每一步都配有可运行的代码。你拿到这套方法后可以把它套用到任何赛区的公开赛事数据自己做一份客观对比报告。1. 背景与核心概念1.1 为什么“选手对比”总是争论不休“论战”类话题有一个共同特征参与讨论的人很多但大家对“更强”的定义并不统一。在竞技体育和电竞里选手表现至少包含三个维度个人操作能力、团队协作贡献、稳定性。欧美选手和亚洲选手虽然身处不同赛区但比赛版本、战术体系、队伍磨合程度都存在差异单看一个维度很难下结论。更麻烦的是很多讨论依赖的是片段式信息比如一次极限操作集锦、一场决赛的胜负或者解说员的一句评价。这些信息适合制造话题却不适合用来比较选手真实水平。真正有参考价值的数据应该来自较长周期内、同一规则下、口径一致的比赛记录然后通过统计方法把随机因素和真实差距区分开来。1.2 数据分析能解决什么数据不能告诉你“谁天赋更高”但可以告诉你“谁在哪些具体指标上表现更稳定”“差异在统计上是否显著”。举例来说如果我们观察到亚洲赛区选手的场均 KDA 更高第一反应不应该是“亚洲选手更强”而要检查样本量是否足够、比赛版本是否一致、对手强度是否相当。如果欧美赛区选手的分均补刀更高也需要思考这是个人能力差异还是两个赛区的战术节奏不同导致的结果。数据分析最大的价值是让每一个结论都能被追溯、被检验、被推翻。当你说“数据显示欧美选手在某个维度上有优势”时别人可以查看你的代码和数据验证你的结论是否成立。这种可复现性是网络论战中最缺失的东西。1.3 本文的阅读收益读完这篇文章你会掌握以下能力用 pandas 清洗选手比赛数据处理缺失值和异常值。用 Python 划分“欧美赛区”和“亚洲赛区”并做分组统计。用 matplotlib 和 seaborn 绘制箱线图、分布图、雷达图直观展示组间差异。用 scipy 做独立样本 t 检验评估差异是否显著。知道哪些坑最容易让对比分析失真以及如何避免。这篇文章适合有 Python 基础、想学习数据分析实战或者对竞技数据感兴趣的同学。如果你是纯数据分析新手也可以照着代码一步步运行遇到不理解的函数再查相关文档。2. 环境准备与数据说明2.1 数据来源与字段规范在做这类赛区对比分析时数据源直接影响结论是否可信。常见的公开数据来源包括赛事官网统计接口、社区维护的选手数据库、以及 Kaggle 上沉淀的赛事数据集。无论你从哪获取数据至少需要包含以下字段字段名含义示例player_id选手唯一标识EU_1024region所属赛区EU / NA / CN / KR / SEAmatch_id比赛场次标识M201kda击杀死亡助攻比5.2cs_per_min分均补刀8.4wards_per_min分均插眼0.7真实数据往往比这复杂还有伤害转化率、参团率、视野得分等字段。本文为了聚焦分析流程先使用最基础的三类指标。你拿到真实数据后可以按相同思路扩展更多维度。需要注意不同数据源的字段命名和指标口径可能完全不同。比如 KDA 有的数据源按(K A) / D计算有的则把助攻权重调整过。所以第一步永远是先写数据字典把每个指标的定义固定下来否则后续所有对比都建立在“误差叠加”之上。2.2 Python 环境准备本文代码基于 Python 3建议使用以下版本组合Python 3.8 及以上pandas 1.x 或 2.xnumpy 1.xmatplotlib 3.xseaborn 0.12 或更高scipy 1.x版本不一定要完全一致但如果你使用的是较新环境部分 API 可能有细微调整。建议用虚拟环境安装依赖避免和系统环境冲突。pip install pandas numpy matplotlib seaborn scipy如果你使用 Anaconda也可以直接用 conda 安装conda install pandas numpy matplotlib seaborn scipy安装完成后可以在 Python 交互环境里验证版本import pandas as pd import numpy as np import matplotlib import seaborn as sns import scipy print(pandas:, pd.__version__) print(numpy:, np.__version__) print(matplotlib:, matplotlib.__version__) print(seaborn:, sns.__version__) print(scipy:, scipy.__version__)2.3 项目目录结构建议把代码和文件分离方便后续维护。player-comparison/ ├── data/ │ └── players_demo.csv ├── analysis/ │ ├── 01_clean.py │ ├── 02_visualize.py │ └── 03_test.py ├── output/ │ ├── boxplot_kda.png │ └── radar_compare.png └── README.md本文为了保持教程连贯先把代码按功能分成多个片段讲解最后在实战章节汇总成完整脚本。你可以按上面的目录结构保存文件。3. 数据加载与清洗3.1 加载数据并查看总体情况清洗是数据分析中最耗时但最关键的环节。脏数据会让所有统计结果失真因此我们先加载数据看看整体质量。假设我们已经有一份 CSV 文件players_demo.csv用 pandas 读取import pandas as pd df pd.read_csv(players_demo.csv) print(df.head()) print(df.info())df.head()输出前几行快速确认字段名和数据格式。df.info()会显示每列的字段类型以及非空值数量。举例来说如果kda列只有 1800 条非空记录而总行数是 2000说明有缺失值需要处理。接着可以用describe()查看数值型字段的分布print(df.describe())这一步能让你快速发现异常值比如某个选手的分均补刀是 99.9或者 KDA 是负数都属于显然不合理的数据。3.2 处理缺失值与异常值处理缺失值没有绝对正确的方法取决于缺失比例和业务含义。如果缺失比例很小比如小于 5%并且字段是连续型指标可以用中位数或均值填充。如果缺失比例较大填充反而会引入偏见建议删除对应记录或单独建一个“缺失组”。对于异常值建议先确认是数据录入错误还是真实极端表现。# 查看缺失情况 print(df.isna().sum()) # 使用中位数填充 KDA 列 df[kda] df[kda].fillna(df[kda].median()) # 删除数值明显不合理的记录 df df[df[cs_per_min] 0] df df[df[kda] 0] print(df.shape)这里有两点值得解释为什么不选均值填充因为均值对极值敏感一个超神选手的离谱 KDA 会把均值拉高进而掩盖缺失值的正常水平。中位数更稳健。为什么要删除cs_per_min 0的记录正常选手整场比赛的分均补刀不应该是 0这大概率是数据未上报或比赛退场导致的残缺数据。3.3 统一赛区标签原始数据里赛区字段可能是EU、Europe、NA、北美这种混杂写法。如果不统一后面按赛区分组就会把事情搞乱。我们新增一列region_group把欧美赛区合并为一个组把亚洲赛区合并为另一个组。def map_region_group(region): # 假设欧美赛区包括 EU 和 NA亚洲赛区包括 CN、KR、SEA if region in [EU, NA, Europe, North America]: return 欧美赛区 elif region in [CN, KR, SEA, VNM, TV]: return 亚洲赛区 else: return 其他 df[region_group] df[region].apply(map_region_group)建议先看看赛区字段到底有几种取值再写映射函数print(df[region].value_counts())如果发现未知赛区可以单独查看后决定归到哪一组或者直接标记为“其他”并在分析时排除。不要强行把所有赛区生硬分成两组那样会掩盖数据本身的结构差异。4. 核心指标设计与对比口径4.1 从“谁强”到“强在哪”拆解指标对比分析最容易犯的错误是“先有结论再找数据”。为了避免这种情况建议在分析前先把指标拆成几个可解释的维度再逐个对比。以选手表现分析为例可以把指标分成三类输出能力KDA、分均伤害、击杀数。运营能力分均补刀、参团率、野区控制率。团队贡献分均插眼、视野得分、助攻数。不同位置的选手指标含义也不同。ADC 的分均伤害自然比辅助高辅助的视野得分自然比 ADC 高。因此做赛区对比时如果直接把所有位置混在一起平均很容易得出“某个赛区打的更好”的伪结论。更严谨的做法是先按位置分组再在不同位置内比较。当然这意味着数据量会被拆得更薄对样本量要求更高。如果样本不够宁可缩小结论范围比如“分析只针对 ADC 选手不代表全部位置”。4.2 均值、中位数与极值比较分组后我们可以快速计算每组在某个指标上的均值、中位数、标准差和四分位数grouped df.groupby(region_group)[kda].describe() print(grouped)输出会包含count、mean、std、min、25%、50%、75%、max等字段。简单解读方法如果两组中位数接近但均值差异大说明差异主要来自极值。如果两组的标准差差异很大说明一组选手表现更稳定。如果最小值和最大值差异悬殊要注意异常选手对整体结论的干扰。只靠均值和标准差也不够还需要可视化来确认分布形态。4.3 标准化处理如果把 KDA、分均补刀、分均插眼放在一起来对比量纲完全不同直接画雷达图会导致数值大的指标主导图形。这时需要做 min-max 标准化把每个指标压缩到 0 到 1 之间。features [kda, cs_per_min, wards_per_min] grouped_numeric df.groupby(region_group)[features].mean() normalized (grouped_numeric - grouped_numeric.min()) / (grouped_numeric.max() - grouped_numeric.min()) print(normalized)标准化之后0 表示该赛区在该指标上的平均表现是所有对比组中最低的1 表示最高。这样比较的是“相对位置”而不是原始数值大小。5. 完整实战用 Python 完成一次选手对比分析5.1 生成示例数据为了让教程可以立即运行我先构造一份演示数据。注意以下数据是随机生成的只用于演示流程不代表任何真实赛区水平。你可以用自己获取的真实赛事数据替换这部分。import pandas as pd import numpy as np np.random.seed(42) rows [] region_centers { EU: {kda: 5.0, cs: 7.8, wards: 0.75}, NA: {kda: 5.2, cs: 8.0, wards: 0.80}, CN: {kda: 5.5, cs: 8.2, wards: 0.78}, KR: {kda: 5.6, cs: 8.3, wards: 0.82}, SEA: {kda: 5.1, cs: 7.7, wards: 0.72}, } for region, center in region_centers.items(): for i in range(200): rows.append({ player_id: f{region}_{i}_2024, region: region, kda: max(0, np.random.normal(center[kda], 1.2)), cs_per_min: max(0, np.random.normal(center[cs], 1.0)), wards_per_min: max(0, np.random.normal(center[wards], 0.15)), }) df pd.DataFrame(rows) df.to_csv(players_demo.csv, indexFalse) print(df.head())这段代码用正态分布模拟了五个赛区各 200 名选手的指标。每个赛区的均值略有不同但随机性很大所以即使有差异也不一定统计显著。运行后会在当前目录生成players_demo.csv。5.2 清洗与分组统计接下来我们加载数据完成第 3 节中提到的清洗步骤import pandas as pd df pd.read_csv(players_demo.csv) # 查看缺失值和基本结构 print(df.isna().sum()) print(df.describe()) # 清洗中位数填充缺失值 df[kda] df[kda].fillna(df[kda].median()) # 清洗删除不合理记录 df df[(df[cs_per_min] 0) (df[kda] 0)] # 新增赛区分组 def map_region_group(region): if region in [EU, NA]: return 欧美赛区 elif region in [CN, KR, SEA]: return 亚洲赛区 else: return 其他 df[region_group] df[region].apply(map_region_group) # 只保留欧美和亚洲两个组 df df[df[region_group] ! 其他] # 分组统计 grouped df.groupby(region_group)[[kda, cs_per_min, wards_per_min]].agg( [count, mean, median, std] ) print(grouped)运行输出后你会看到两组在样本量、均值、中位数和标准差上的差异。这些数字是后续分析和讨论的基础。5.3 可视化对比统计数字不够直观下面用箱线图看看两个大区在各指标上的分布情况。import matplotlib.pyplot as plt import seaborn as sns # 解决中文显示问题 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False sns.set_style(whitegrid) features [kda, cs_per_min, wards_per_min] fig, axes plt.subplots(1, 3, figsize(15, 5)) for ax, feature in zip(axes, features): sns.boxplot(xregion_group, yfeature, datadf, axax) ax.set_title(f{feature} 分布对比) plt.tight_layout() plt.savefig(output_boxplot.png, dpi150) plt.show()箱线图能直观显示中位数、四分位距和异常点。如果两个大区的箱体位置错开明显说明差异不只是偶然波动如果箱体重叠程度很高则要谨慎下结论。除了箱线图还可以用直方图看分布形状fig, axes plt.subplots(1, 3, figsize(15, 5)) for ax, feature in zip(axes, features): for region_group in df[region_group].unique(): subset df[df[region_group] region_group] sns.kdeplot(subset[feature], labelregion_group, axax) ax.set_title(f{feature} 分布对比) ax.legend() plt.tight_layout() plt.show()KDE 曲线可以帮助观察分布是否近似正态、是否存在多峰结构。两组曲线重叠面积越大差异越不明显。5.4 显著性检验可视化之外我们还需要一个统计量来判断两组差异是否可能是抽样误差造成的。这里使用独立样本 t 检验。from scipy.stats import ttest_ind eu_na df[df[region_group] 欧美赛区][kda] asia df[df[region_group] 亚洲赛区][kda] t_stat, p_value ttest_ind(eu_na, asia) print(fKDA 独立样本 t 检验t{t_stat:.3f}, p{p_value:.4f})解读方式如果 p 值小于 0.05通常认为两组差异在统计上显著。如果 p 值大于 0.05则说明当前数据不足以支持“两组有真实差异”的结论。但要注意t 检验假设数据近似正态分布且两组方差相近。如果数据分布偏斜严重或者两组样本量差异过大可以考虑用非参数的 Mann-Whitney U 检验from scipy.stats import mannwhitneyu u_stat, p_value_u mannwhitneyu(eu_na, asia, alternativetwo-sided) print(fKDA Mann-Whitney U 检验U{u_stat:.3f}, p{p_value_u:.4f})在做最终结论时建议同时报告 t 检验和 U 检验结果并结合效应量一起看。只用 p 值容易忽略实际差异大小。5.5 雷达图对比最后用雷达图把多个指标综合展示。雷达图适合观察“形态差异”但不适合精确比较数值。import numpy as np import matplotlib.pyplot as plt features [kda, cs_per_min, wards_per_min] grouped_mean df.groupby(region_group)[features].mean() normalized (grouped_mean - grouped_mean.min()) / (grouped_mean.max() - grouped_mean.min()) angles np.linspace(0, 2 * np.pi, len(features), endpointFalse).tolist() angles angles[:1] fig, ax plt.subplots(figsize(6, 6), subplot_kwdict(polarTrue)) for label, row in normalized.iterrows(): values row.values.tolist() values values[:1] ax.plot(angles, values, labellabel) ax.fill(angles, values, alpha0.2) ax.set_xticks(angles[:-1]) ax.set_xticklabels(features) ax.legend(locupper right, bbox_to_anchor(1.2, 1.1)) plt.show()如果两个赛区的雷达图在某个维度上明显外扩说明该维度是其中一个赛区的相对优势项。不过雷达图只显示“相对高低”不能替代前面步骤里的统计检验。6. 常见问题与排查思路问题现象常见原因解决思路图表中文显示为方块系统缺少中文字体或 matplotlib 默认字体不支持中文设置plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei]并执行plt.rcParams[axes.unicode_minus] False分组统计里出现“其他”组原始赛区字段存在未统一写法的值用value_counts()查看所有取值完善映射函数缺失值很多结果和预期不一致直接删除或均值填充导致样本偏差先评估缺失比例小比例用中位数填充大比例考虑单独分析或删除两组均值差异很大但 p 值大于 0.05样本量不足或数据波动太大增加样本量或改用非参数检验并报告效应量雷达图某指标看起来很大未做标准化原始数值量纲不同先做 min-max 标准化再绘图KDA 出现负值原始数据录入异常或计算口径不同排除负值记录并检查数据源定义7. 最佳实践与工程建议7.1 指标口径先行任何对比分析开始前先写清楚指标定义。比如 KDA 的口径、分均补刀的时间边界、插眼数的单位都必须统一。建议建立一个data_dictionary.md文件记录每个字段的格式和业务含义。这样将来别人看你的分析时能快速理解你在比较什么。7.2 样本量与置信区间样本量越小结论越不可靠。当样本量不足时与其给出一个不稳定的点估计不如提供置信区间。示例from scipy import stats def mean_ci(data, confidence0.95): n len(data) mean_val np.mean(data) se stats.sem(data) h se * stats.t.ppf((1 confidence) / 2, n - 1) return mean_val - h, mean_val h eu_kda eu_na.values ci mean_ci(eu_kda) print(f欧美赛区 KDA 均值 95% 置信区间{ci})如果两个区间的重叠度很高说明从统计角度看目前无法分辨哪个赛区更有优势。7.3 可视化设计避免只用红绿两色对比因为色盲用户无法区分。可以改用蓝橙或带材质区分的样式。箱线图适合展示分布条形图适合展示均值雷达图适合展示多位形态。不要用一张图表达所有信息。每张图都要加标题和坐标轴说明否则读者只能靠猜。7.4 保证报告可复现工程化分析的底线是可复现。建议把整个流程写成一个 Python 脚本加上明确的--input、--output参数方便重复运行。同时固定随机种子例如np.random.seed(42)保证每次随机生成的演示数据一致。下面是一个简化版的可执行入口# analysis/run_analysis.py import pandas as pd import numpy as np def main(): df pd.read_csv(players_demo.csv) df[kda] df[kda].fillna(df[kda].median()) df df[(df[cs_per_min] 0) (df[kda] 0)] df df[df[region].isin([EU, NA, CN, KR, SEA])] df[region_group] df[region].apply( lambda r: 欧美赛区 if r in [EU, NA] else 亚洲赛区 ) grouped df.groupby(region_group)[[kda, cs_per_min, wards_per_min]].mean() print(grouped) if __name__ __main__: main()到这里你已经有一套完整的赛区选手对比分析工具了。后续可以扩展的方向包括按比赛版本切片分析、加入对手强度因子、用 bootstrap 方法计算差异置信区间或者训练一个简单的分类模型判断“某条选手记录更接近哪个赛区”并输出特征重要性。数据对比不会终结“欧美选手 vs 亚洲选手”这场论战但它能让论战升级从“我觉得谁强”变成“数据显示在哪些维度上存在可检验的差异”。下次再遇到类似的争论不妨把数据和代码拿出来用分析说话。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进