赛后数据雷达图制作全流程:Python从数据清洗到可视化实战 在 LPL 这类职业电竞赛事结束后数据网站上最常见的复盘配图之一就是“赛后数据雷达图”。类似“NIP 2-1 WBG 赛后数据雷达图”的标题在比赛结束后的复盘文章、数据统计站点和社区讨论里出现频率很高。雷达图之所以被高频使用是因为它能把 KDA、分均伤害、分均经济、视野得分、资源控制等不同维度的比赛数据压缩到一张图里让读者快速看出两支队伍在哪些方面占优。这篇博客以 NIP 2-1 WBG 这场系列赛的场景为例从数据理解、环境准备、数据整理、标准化、雷达图绘制到结果解读完整走一遍“赛后数据雷达图”的制作流程。文中的数据为演示用示例数据不是官方真实数据重点在于把整套方法讲清楚让读者可以拿任何一场比赛的数据直接套用。1. 先理解赛后数据雷达图到底在表达什么1.1 雷达图的本质在同一个坐标系里比较多个维度的相对强弱雷达图也叫蜘蛛网图、星形图本质上是一个极坐标系下的折线图。每个指标占据一条从圆心向外延伸的轴指标数值映射到轴的半径位置把所有指标对应的点连接起来就会形成一个闭合多边形。两支队伍的数据叠加在同一张图上形状越饱满代表这一维度数值越高两条折线的面积差和方向差代表了多维数据的整体差异。如果只看单个指标条形图或折线图比雷达图更精确。雷达图的价值在于同时呈现多个维度的“组合形态”。比如某支队伍 KDA 高、视野得分低在雷达图上会形成“有一部分凸出、有一部分凹陷”的不规则形状另一支队伍各维度都比较平均则接近正多边形。这种形状差异比单纯看数字更有直觉冲击力适合解说、图文复盘和短视频展示。但也要注意一个关键限制雷达图的面积和形状会受到指标排列顺序、指标数量、刻度范围影响。同样的数据指标顺序打乱后图形的“尖角”位置会变化但相对关系不变。因此做雷达图时最好固定指标顺序避免每次输出形态不一致。1.2 赛后数据雷达图适合放哪些指标英雄联盟的赛后统计字段很多但不能全部塞进雷达图。一个雷达图塞十多个指标视觉上会变成密密麻麻的蜘蛛网反而没有重点。常见做法是选取 5 到 7 个能反映队伍综合实力的核心指标。下面是一组常用指标及说明指标含义常见单位方向说明KDA击杀、助攻综合与死亡的关系比值越高越好常见计算为 (击杀 助攻) / 死亡死亡为 0 时需要特殊处理DPM分均伤害伤害值 / 分钟越高越好反映队伍团战和线上压制能力GPM分均经济金币 / 分钟越高越好反映资源获取和发育效率CSPM分均补刀补刀数 / 分钟越高越好反映线上基本功和边线处理能力VSPM分均视野得分视野得分 / 分钟越高越好反映视野布控和排眼能力资源控制率大小龙、先锋等地图资源控制情况百分比越高越好反映中立资源争夺能力这些指标放在一起基本可以覆盖“打架能力、发育能力、地图控制、视野博弈”四个维度。实际使用时可以去掉“分均补刀”因为分均经济已经部分包含补刀信息也可以把“一血率”“推塔数”加进来但指标总数尽量控制在 7 个以内。需要注意一点不是所有指标都是“越高越好”。比如场均死亡越低越好、场均用时越短越好。如果要把这类指标放进雷达图必须做方向转换例如使用“倒数”或“1 - 归一化值”否则会误导看图的人。1.3 为什么不能直接把原始数据画上去先看一组模拟数据维度变化KDA 通常在 2 到 6 之间。DPM 通常在 1500 到 3000 之间。VSPM 通常在 2 到 5 之间。资源控制率是 0 到 1 之间的小数。如果直接把原始数值作为半径画图DPM 会在外圈VSPM 会贴在内圈整张图几乎只有一项指标可见其他指标挤在圆心附近完全失去比较意义。所以必须先做标准化也就是把不同量纲的指标映射到同一个刻度范围。常用的标准化方式有两种方式计算方法适用场景缺点Min-Max 标准化(x - min) / (max - min)多个对象在同一批数据内比较受极端值影响大固定上限归一化x / 固定上限长期统一对比固定上限需要人为定义最大值归一化x / 当前数据最大值两队快速对比容易掩盖单队绝对差距对于单场比赛的两队雷达图最简单的方式是取双方指标列的最大值作为分母把数值缩放到 0 到 100然后绘制在同一张图上。这种方式能突出“谁更强”但会丢失“两队绝对水平都低”的信息。如果要做赛季长期对比建议使用固定业务上限比如 KDA 上限 10、DPM 上限 3000、GPM 上限 2000、CSPM 上限 10、VSPM 上限 5、资源控制率上限 1。上限明确后雷达图能在多个比赛日之间保持一致的可比性。注意任何雷达图都要注明标准化方式。否则读者看到 80 分和 90 分并不知道原始差距到底是 100 点伤害还是 1 点视野分。2. 准备分析环境、数据和依赖2.1 环境与依赖这套流程使用 Python 实现核心依赖是 pandas、numpy、matplotlib。如果希望生成网页端交互式雷达图可以额外安装 pyecharts。下面是推荐环境依赖用途安装命令Python运行环境建议 3.9 及以上版本pandas数据读取、清洗、标准化pip install pandasnumpy数值计算pip install numpymatplotlib绘制静态雷达图pip install matplotlibpyecharts绘制交互式 HTML 雷达图pip install pyecharts安装时没有强制锁定版本但实际项目落地前最好确认这几个包之间的兼容性。可以创建一个 requirements.txt 文件固定自己验证过的版本避免 pyecharts 升级后接口发生变化。2.2 数据从哪里来赛前赛后的比赛数据通常来自官方数据接口或第三方数据网站。个人做复盘时可以从数据网站导出 CSV也可以手动把每一局的击杀、助攻、死亡、补刀、分均伤害等字段录入表格。无论从哪里拿数据都要遵守网站的数据使用规则不要用高频率脚本抓取。本文下面使用的是一份构造出来的示例数据用来演示代码流程。数据结构尽量贴近常见赛后统计表但数值不是具体某场比赛的真实数据。读者只要把 CSV 文件替换成自己要分析的比赛数据代码逻辑可以基本不变。2.3 准备一份结构化的示例数据创建一个文件夹例如lol_radar在文件夹内准备match_data.csv。内容如下team,kda,dpm,gpm,cspm,vspm,objective NIP,4.20,2680.00,1850.00,8.40,3.60,0.78 WBG,3.10,2410.00,1775.00,7.90,3.10,0.55字段说明team队伍名。kda队伍场均 KDA越大越好。dpm分均伤害单位是数值/分钟。gpm分均经济。cspm分均补刀。vspm分均视野得分。objective大小龙资源控制率0 到 1 之间。由于这是一份只有两行的数据pandas 读取后会非常简单但足够说明雷达图的全流程。实际分析中如果展开到单局数据可以按“队伍 局数”拆分后分别绘制。import pandas as pd df pd.read_csv(match_data.csv) print(df.head()) print(df.info())读取后打印的df.info()会显示各列类型便于检查是否存在缺失值。3. 用 pandas 完成清洗和标准化3.1 读取数据并检查字段读取之后第一件事不是画图而是确认数据的列名、类型和范围。可以执行print(df.columns.tolist()) print(df.dtypes)正常输出应该包含 7 列数值列类型为 float64 或 int64。如果某列出现了 object 类型说明文件里有非数值内容需要定位到具体行检查。3.2 缺失值和异常值处理赛程数据里偶尔会出现缺失字段。常见处理方式如下场景处理方式某个指标缺失如果该项是队伍明显没有产生数据可填 0如果是数据采集遗漏可用前后场均值填充KDA 死亡数为 0常见统计中会记作 Perfect KDA或使用 (击杀 助攻) / 最大(死亡, 1)资源控制率大于 1数据不合法需要回到原始记录核对分均伤害为负数数据不合法删除或修正对于本文的示例数据没有缺失值但写代码时还是要养成检查习惯print(df.isna().sum())如果存在缺失值最简单的做法是使用fillna(0)或对当前列进行数值填充。3.3 标准化并转换为百分制下面定义需要参与雷达图绘制的指标列顺序这一步非常重要。雷达图绘制时会把列顺序作为轴顺序顺序不一致会导致标签和折线错位。score_cols [kda, dpm, gpm, cspm, vspm, objective]选择不同的标准化基准会直接影响雷达图形状。这里先实现“当前数据最大值归一化到 100 分”的版本适合快速比较两支队伍import numpy as np def normalize_to_max(df, cols): normalized df.copy() for col in cols: max_val normalized[col].max() if max_val 0: normalized[col _score] 0 else: normalized[col _score] normalized[col] / max_val * 100 return normalized df_score normalize_to_max(df, score_cols) print(df_score)输出结果中每支队伍会多出kda_score、dpm_score等字段数值范围在 0 到 100 之间。由于每列的最大值来自两支队伍中较大的一方因此每列至少有一支队伍是 100 分。如果希望按固定业务上限标准化可以使用upper_bounds { kda: 10, dpm: 3000, gpm: 2000, cspm: 10, vspm: 5, objective: 1, } def normalize_with_bounds(df, cols, bounds): normalized df.copy() for col in cols: bound bounds[col] normalized[col _score] normalized[col] / bound * 100 return normalized df_bound normalize_with_bounds(df, score_cols, upper_bounds) print(df_bound)固定上限方式的优点是不同比赛、不同日期的图表可以纵向比较缺点是如果上限设置过高所有队伍分数都会偏低视觉上差别不明显。两种方式都可以用关键是绘图前明确“这张图的 100 分代表什么”。注意如果采用最小值最大值归一化只有两支队伍时最大值队是 100 分最小值队是 0 分。0 分会让雷达图的某条边缩到圆心看起来像“缺了一个角”这在少数指标上确实能说明问题但也会让图形比例过度夸张。使用固定上限或最大值归一化能避免评分项变成 0。3.4 整理绘制雷达图所需的数据结构绘制雷达图时最好准备一个只包含队伍名和百分制得分的表plot_df df_score[[team] [col _score for col in score_cols]].copy() print(plot_df)此时plot_df的行是队伍列是评分。后面绘图函数可以基于这个表直接读取。4. 用 matplotlib 绘制可复用的雷达图4.1 最简绘制代码使用 matplotlib 绘制雷达图时常用projectionpolar创建极坐标轴。下面这段代码是基础版本import matplotlib.pyplot as plt import numpy as np # 中文字体设置 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False metrics [col _score for col in score_cols] num_vars len(metrics) # 角度计算把圆等分成 num_vars 份 angles np.linspace(0, 2 * np.pi, num_vars, endpointFalse).tolist() # 闭合图形首尾相连 angles angles[:1] fig, ax plt.subplots(figsize(8, 8), subplot_kw{projection: polar}) ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(1) for row in plot_df.itertuples(indexFalse): team_name row[0] values list(row[1:]) values values[:1] ax.plot(angles, values, labelteam_name, linewidth2) ax.fill(angles, values, alpha0.15) ax.set_xticks(angles[:-1]) ax.set_xticklabels([col.replace(_score, ).upper() for col in metrics]) ax.set_ylim(0, 100) ax.legend(locupper right, bbox_to_anchor(1.2, 1.0)) plt.title(NIP vs WBG 赛后数据雷达图) plt.show()代码的关键点有三个endpointFalse保证第一个指标不在最后一个点重复否则角度会把圆分成不均匀的段。angles angles[:1]和values values[:1]是为了闭合折线让最后一条边回到起点。set_theta_offset(np.pi / 2)让第一根轴朝向正上方set_theta_direction(1)让角度按顺时针排列更符合常见雷达图的阅读习惯。4.2 双队对比与样式优化双队对比时两个多边形可能重叠为了区分可以给每支队伍指定不同颜色、线型和透明度colors {NIP: #d62728, WBG: #1f77b4} fig, ax plt.subplots(figsize(8, 8), subplot_kw{projection: polar}) ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(1) for row in plot_df.itertuples(indexFalse): team_name row[0] values list(row[1:]) values values[:1] ax.plot(angles, values, labelteam_name, linewidth2.5, colorcolors[team_name]) ax.fill(angles, values, alpha0.12, colorcolors[team_name]) ax.set_xticks(angles[:-1]) ax.set_xticklabels([col.replace(_score, ).upper() for col in metrics], fontsize12) ax.set_ylim(0, 100) ax.grid(linestyle--, linewidth0.6) ax.legend(locupper right, bbox_to_anchor(1.2, 1.0)) plt.title(NIP vs WBG 赛后数据雷达图, fontsize15) plt.tight_layout() plt.show()颜色选择上尽量选色差明显的两个颜色避免红绿色盲无法区分。填充透明度不要太高否则重叠区域会变成深色块反而看不清两条折线边界。4.3 保存图片保存图片时注意分辨率和边距fig.savefig(nip_vs_wbg_radar.png, dpi300, bbox_inchestight)dpi300适合发布到网页或PPT清晰度足够。bbox_inchestight会裁掉多余白边让图片更紧凑。如果图片要放在深色背景的页面里可以增加facecolorwhite参数避免 PNG 背景透明导致文字和图形混在一起。5. 用 pyecharts 生成交互式雷达图5.1 为什么需要交互版本matplotlib 输出的静态 PNG 适合直接嵌入到图文里但交互性不足。如果希望读者在网页上悬停查看具体数值或者切换显示某一支队伍可以使用 pyecharts 生成 HTML 文件。pyecharts 底层封装了 ECharts渲染效果好适合数据复盘站、日报页面和实时展示场景。5.2 pyecharts 实现示例pyecharts 的 Radar 组件需要先声明indicator也就是每个指标的名称和最大值。由于我们已经在前面把数据转成百分制这里可以把最大值统一设置为 100。from pyecharts import options as opts from pyecharts.charts import Radar indicator [ {name: KDA, max: 100}, {name: DPM, max: 100}, {name: GPM, max: 100}, {name: CSPM, max: 100}, {name: VSPM, max: 100}, {name: OBJECTIVE, max: 100}, ] data [] for row in plot_df.itertuples(indexFalse): team_name row[0] values list(row[1:]) data.append((team_name, values)) c Radar() c.add_schema(schemaindicator) for team_name, values in data: c.add( series_nameteam_name, data[values], linestyle_optsopts.LineStyleOpts(width2), areastyle_optsopts.AreaStyleOpts(opacity0.1), ) c.set_global_opts( title_optsopts.TitleOpts(titleNIP vs WBG 赛后数据雷达图), legend_optsopts.LegendOpts(), ) c.render(nip_vs_wbg_radar.html)运行后会生成一个nip_vs_wbg_radar.html文件浏览器打开即可交互。需要注意 pyecharts 1.x 和 2.x 的 API 有差异上面写法以常见的 1.x 版本为基础。如果安装的是较新版本最好参考当前版本的官方示例调整参数。5.3 两种工具对比对比项matplotlibpyecharts输出形式PNG、JPG、SVGHTML、Web 组件交互能力弱强支持悬停、图例切换中文字体需要手动配置浏览器自动处理适合场景公众号、图文、PPT网页、数据平台、实时看板学习成本较低中等需要了解 ECharts 概念批量生成方便同样方便但文件大一些实际生产项目中建议两者结合一批图用 matplotlib 生成静态图片用于汇报另一批用 pyecharts 生成 HTML 用于网页端投放。6. 读懂雷达图从形状差异到比赛内容6.1 先看整体面积再看单项突出雷达图的第一眼信息是“两块多边形谁的面积更大”。在示例数据中NIP 在 KDA、DPM、GPM、CSPM、VSPM、OBJECTIVE 六个维度上都比 WBG 高所以 NIP 的多边形会明显更饱满这对应了系列赛 2-1 的胜负关系。但面积本身不是绝对结论。雷达图的每个轴权重相同而实际比赛中视野、资源控制等维度的重要性并不是等价的。因此读图时要先判断哪些指标主导了比赛再回到比赛录像或逐局数据里验证。6.2 找差异最大的维度作为切入点差异最大的维度是复盘时最值得展开的切入点。假设示例数据中两队资源控制率差异很大那么可以优先回看大小龙时间点是先锋团、小龙团的阵容优势导致的必然结果是打野在关键时间点提前落位形成了视野和位置差还是某一波团战中抢龙导致的偶然结果这些判断无法从一张雷达图直接得出但雷达图能快速提示“应该优先看哪个环节”。KDA 的差异则可以结合击杀时间线判断是前期对线打出优势还是中后期团战才拉开差距。6.3 警惕“数据因果化”误区雷达图是统计结果不是因果证据。它不能反映下面这些信息阵容前中后期曲线比如某队拿到后期阵容前期经济落后但中后期翻盘。单局波动比如第一局碾压、第二局被碾压、第三局险胜汇总后可能“看起来差距不大”。选手或队伍的失误操作、大小龙被抢、关键技能空掉等细节。BP 阶段的针对性选择比如为限制对手中路而牺牲了部分前期对线能力。因此做赛后复盘时建议把雷达图作为“索引”而不是最终结论。先看雷达图找差异维度再看分均经济和击杀时间线最后回到比赛录像确认产生差异的具体回合。6.4 可以搭配哪些图一起使用只放一张雷达图信息量有限。推荐搭配以下图表图表说明经济曲线折线图看单局节奏和翻盘点单局伤害对比柱状图看选手输出分布视野得分时间序列图看视野布控节奏变化资源控制事件表看大小龙、先锋刷新和归属时间点这样组合后雷达图承担“概览”职责其他图表承担“细节证据”职责复盘内容会更扎实。7. 常见坑与排查路径7.1 中文标签变成方块现象雷达图指标名称出现“□□”或乱码。原因matplotlib 默认字体不支持中文。检查方式查看当前环境可用字体或直接打印plt.rcParams[font.sans-serif]。解决方式plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False如果系统没有 SimHei可以指定其他中文字体路径from matplotlib import font_manager font_manager.fontManager.addfont(path/to/font.ttf) plt.rcParams[font.family] YourFontName7.2 雷达图没有闭合成环现象折线从第一个点画到最后一个点就结束图形是开口的。原因没有把第一个点的数值复制到序列末尾。解决方式绘图前对 values 和 angles 都追加第一个元素values values[:1] angles angles[:1]7.3 标准化后出现 0 分图形塌陷现象某个指标贴着圆心看起来像“少了半边”。原因如果使用 Min-Max 标准化两支队中数值最小的队伍会得到 0 分。解决方式改用固定上限归一化或对 0 分做下限保护比如限制最低得分为 5 分。这样图形不会完全贴地视觉上也更容易保留潜在信息。7.4 多队折线重叠无法区分现象三支以上队伍画在一张雷达图里颜色接近看不出谁是谁。解决方式使用明显色差。使用不同线型如实线、虚线、点划线。给折线增加标记点如圆形、方形。降低填充透明度保留主要折线。如果队伍超过 4 支建议拆分成多个雷达图单独展示或改用平行坐标图。7.5 pyecharts 版本不同导致 API 不生效现象add_schema不存在或Radar初始化报错。原因pyecharts 1.x 和 2.x 的接口差异较大。检查方式运行pip show pyecharts查看版本。解决方式按版本查找官方示例。建议在 requirements.txt 中固定一个验证过的版本避免后续升级破坏脚本。问题现象常见原因检查方式处理建议中文乱码字体缺少中文字符检查 matplotlib 字体配置设置中文字体或指定字体路径图形不闭合首尾点未拼接检查 values 长度是否比 angles 少 1绘图前追加第一个点指标错位列顺序和标签顺序不一致打印 score_cols 和 xticklabels固定指标列表按同一顺序传入数值超出网格未设置 ylim检查数据最大值set_ylim(0, 100)HTML 文件无法打开渲染路径错误检查 render 输出路径使用绝对路径或确认当前目录8. 最佳实践与扩展方向8.1 把可复用函数写出来手动复制绘图代码很容易出错。推荐封装成一个函数def draw_radar(plot_df, metrics, titleRadar, output_pathradar.png): num_vars len(metrics) angles np.linspace(0, 2 * np.pi, num_vars, endpointFalse).tolist() angles angles[:1] fig, ax plt.subplots(figsize(8, 8), subplot_kw{projection: polar}) ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(1) for row in plot_df.itertuples(indexFalse): team_name row[0] values list(row[1:]) values values[:1] ax.plot(angles, values, labelteam_name, linewidth2) ax.fill(angles, values, alpha0.12) ax.set_xticks(angles[:-1]) ax.set_xticklabels(metrics) ax.set_ylim(0, 100) ax.legend(locupper right, bbox_to_anchor(1.2, 1.0)) ax.set_title(title) fig.savefig(output_path, dpi300, bbox_inchestight) return output_path使用这个函数时plot_df必须已经包含“队伍名列 百分制评分列”并且评分列的顺序与metrics一致。这样每次赛后只需要替换 CSV 文件调用一次函数就能生成新的雷达图。8.2 自动化复盘怎么落地如果要持续产出赛后雷达图可以将流程脚本化从数据源导出 CSV或通过合规接口定时拉取。脚本执行数据清洗、标准化、绘图。生成 PNG 和 HTML 文件输出到固定目录。由前端页面或文档系统引用生成的图片。自动化流程中必须注意每次运行前确认数据文件是否完整避免空文件导致脚本报错。保留原始 CSV 和历史版本图片方便回溯对比。日志要记录标准化基准和生成时间防止不同比赛的图被混在一起比较。如果脚本部署在服务器上需要控制文件访问权限避免他人任意修改数据文件。8.3 扩展方向雷达图只是比赛数据可视化的起点。后续可以扩展的方向包括方向说明逐局雷达图把每一局比赛单独生成雷达图观察队伍状态波动选手个人雷达图将选手的分均伤害、参团率、承伤占比等指标单独绘制赛季分位基准使用赛季所有比赛数据计算中位数、分位数形成统一基准动态时间轴用时间轴展示经济差、经验差的演变配合雷达图整体观感前端图表组件使用 ECharts 的 Radar 组件接入数据平台实现点选交互对新手来说最有价值的练习是找一场自己已经看过完整回放的比赛按照本文流程手动整理数据生成雷达图后看自己能不能从雷达图上找到与比赛记忆一致的信息。如果雷达图显示某个指标异常突出但实际比赛里并没有对应记忆通常说明数据整理或标准化方式有问题。这种“用比赛记忆验证图表”的练习比单纯抄代码更有利于建立数据感觉。8.4 生产发布前检查清单发布赛后数据雷达图前建议按下面清单检查一遍[ ] 数据来源是否可信是否标注了“示例数据”或官方数据。[ ] 指标列顺序是否固定标签与数据是否一一对应。[ ] 是否说明了标准化方式比如“按本场比赛最大值归一化为 100”。[ ] 中文标签是否正常显示导出图片是否有乱码。[ ] 图片分辨率足够是否用 300 dpi 导出。[ ] 是否补充了比赛进程信息避免读者误把雷达图差异当作直接获胜原因。[ ] 如果是自动化脚本是否记录了数据文件路径、生成时间和版本。雷达图的价值不在“画得多好看”而在于能否把多维数据压缩成可读的形状差异并引导读者回到比赛细节中去验证。把绘制流程固定成脚本之后每周赛后只需要更新 CSV就能在几分钟内产出一张规范的赛后数据雷达图。这套流程同样可以迁移到选手数据、赛季积分、战队历史状态对比等场景。