Python电竞赛事数据分析实战:从KDA到可视化全流程
2026/9/2 10:46:57 网站建设 项目流程

提到电竞赛事分析,很多朋友第一时间想到的是靠眼睛一帧一帧看录像、凭印象做点评。其实这类工作完全可以交给 Python 来做,从数据采集、清洗到可视化对比,一条流水线就能把“清梦 VS Simon”这样的循环赛对阵分析得明明白白。本文就围绕电竞赛事数据实战展开,以“叶祁夏季赛循环赛”为业务背景,完整演示一场 BO3 对战的数据分析过程,包括指标设计、代码实现、图表输出和常见避坑方案,零基础也能跟着一步步复现。

1. 背景与核心概念

1.1 什么是电竞赛事数据分析

电竞赛事数据分析,简单说就是把一场或多场比赛中选手的操作行为、团队节奏和资源分配等信息,转成结构化数据,再用统计方法和可视化工具解释“谁赢了、为什么赢、赢在哪里”。

以“清梦 VS Simon”这场循环赛为例,如果只看最终比分,你只能知道胜负。但如果我们把双方每个英雄的击杀、死亡、助攻、补刀、经济、视野得分都记录下来,就能进一步回答:

  • 前期哪一方建立了经济优势?
  • 中单选手的支援节奏是否领先对面?
  • 团队在 20 分钟时的视野控制是否压制了对手?
  • 输掉的那一局,崩盘是从哪一波团战开始的?

这些问题的答案,就是数据分析的价值所在。

1.2 常见的赛事指标

在竞技类项目中,有几个核心指标是分析比赛绕不开的:

指标含义说明
KDA击杀、死亡、助攻的复合指标衡量选手综合表现,常见公式为 (击杀+助攻)/死亡
GPM每分钟经济反映选手发育速度和资源获取能力
XPM每分钟经验反映等级成长速度
团队经济差双方总经济差值描述比赛节奏的重要信号
视野得分真假眼布置与反眼能力影响地图信息控制力
首杀时间全场第一滴血发生时间判断前期进攻性
推塔数量防御塔摧毁数量客观反映地图推进进度

专业比赛里还有更多深层指标,比如参团率、分均伤害、伤害转化率等。本文先聚焦在 KDA、经济、视野和推塔这几类基础但核心的维度上。

1.3 为什么要自己写代码分析

市面上的赛事网站和 App 已经提供了不少数据,但很多场景下,我们需要的是“按自己需求组合的定制化分析”。比如:

  • 把不同场次的同队选手数据合并对比。
  • 把某个选手在循环赛中所有场的 KDA 变化趋势画出来。
  • 把当前比赛与历史平均数据放在同一坐标系里对比。

这些需求靠手动整理非常痛苦,写成脚本以后,只要数据格式不变,下次比赛结果出来以后直接跑一遍就能自动更新报表。这正是 Python 在数据分析领域的核心优势。

2. 环境准备与版本说明

2.1 工具清单

本文示例以常见的数据分析环境为准,具体版本可根据你的项目实际情况调整,重点演示配置思路。

工具/库用途建议版本
Python运行环境3.8+
pandas数据处理与分析1.5+
numpy数值计算1.23+
matplotlib数据可视化3.6+
Jupyter Notebook 或 VSCode编写与调试按个人习惯选择

如果你的本机还没有安装这些库,可以用以下命令统一安装:

pip install pandas numpy matplotlib

如果网络较慢,可以换成国内镜像源:

pip install pandas numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

2.2 示例项目结构

为了便于管理代码和数据,建议先创建一个项目目录:

esports-analysis/ ├── data/ │ ├── match_1.csv │ ├── match_2.csv │ └── match_3.csv ├── outputs/ │ └── charts/ ├── analysis.py └── README.md
  • data目录存放原始比赛数据。
  • outputs目录存放脚本生成的图表。
  • analysis.py是主分析脚本。

3. 数据结构设计

3.1 数据字段规划

在写分析代码之前,首先要确定数据表的结构。以一场比赛为单位,每条记录对应“某队某名选手在某一局中的表现”。字段设计如下:

字段名类型示例值说明
match_id字符串VS001比赛编号
team字符串清梦队伍名称
player字符串Simin选手 ID
hero字符串法刺使用英雄/定位,可自定义
kill整数8击杀数
death整数3死亡数
assist整数12助攻数
gpm浮点数512.6每分钟经济
xpm浮点数486.3每分钟经验
vision_score浮点数42.8视野得分
tower_kills整数2推塔数
duration整数26比赛持续分钟数(保留整数即可)
result字符串win本局该队胜负:win/loss

在这个结构里,match_idteam共同标识一条数据的归属。这样做的好处是:后续不管新增多少场比赛,只要按照同样格式追加数据,分析逻辑完全不需要改动。

3.2 从公开数据平台取数

在正规场景下,比赛数据通常来自官方赛事 API、俱乐部数据平台或经过授权的数据服务商。个人学习一般有两种做法:

  • 从官方站点开放的数据接口获取 JSON,再转为 CSV。
  • 手动整理比赛录像中的关键节点,填入固定表格。

需要注意的是,使用任何第三方赛事数据时,都要先确认数据授权范围。个人学习、非商用研究是常见场景,但如果要对外发布或商用,必须获得授权并注明来源。

4. 核心分析与可视化实战

4.1 创建示例数据集

由于真实赛事数据存在版权和授权限制,我们这里使用一组模拟数据来演示完整的分析流程。数据结构与真实场景一致,你需要跑真实数据时,只需要替换 CSV 文件内容即可。

创建data/match_1.csv,内容如下:

match_id,team,player,hero,kill,death,assist,gpm,xpm,vision_score,tower_kills,duration,result VS001,清梦,清梦选手A,战士,6,2,10,482.5,455.2,38.6,3,28,win VS001,清梦,清梦选手B,射手,10,1,8,601.3,520.1,41.2,4,28,win VS001,清梦,清梦选手C,辅助,1,4,18,288.6,312.8,72.4,2,28,win VS001,Simon,Simon选手A,战士,4,5,6,412.3,398.7,30.5,1,28,loss VS001,Simon,Simon选手B,射手,7,4,5,532.8,489.3,35.1,2,28,loss VS001,Simon,Simon选手C,辅助,0,6,11,265.4,290.6,58.3,0,28,loss

再创建data/match_2.csv,内容如下:

match_id,team,player,hero,kill,death,assist,gpm,xpm,vision_score,tower_kills,duration,result VS002,清梦,清梦选手A,战士,5,3,7,451.2,430.8,32.1,2,31,loss VS002,清梦,清梦选手B,射手,8,5,6,520.7,489.2,36.8,1,31,loss VS002,清梦,清梦选手C,辅助,1,4,14,275.3,301.5,62.7,0,31,loss VS002,Simon,Simon选手A,战士,6,2,9,486.5,452.3,35.4,3,31,win VS002,Simon,Simon选手B,射手,11,3,7,588.2,510.8,39.9,4,31,win VS002,Simon,Simon选手C,辅助,2,2,17,290.1,318.4,66.2,2,31,win

这两份 CSV 对应的是同一轮循环赛中两支队伍的两局比赛,一胜一负,方便我们做汇总对比。

4.2 数据读取与检查

新建analysis.py,首先读取并检查数据。

import pandas as pd df1 = pd.read_csv("data/match_1.csv") df2 = pd.read_csv("data/match_2.csv") df = pd.concat([df1, df2], ignore_index=True) print("数据形状:", df.shape) print(df.head()) print(df.dtypes)

这段代码的作用:

  • read_csv读取 CSV 文件。
  • concat将两局比赛数据拼接成一张总表。
  • shape输出总行数和列数,确认没有少数据。
  • dtypes查看各列数据类型,方便发现解析问题。

预期输出中,killdeath等列应是int64gpmxpm等列应是float64。如果发现某些数值列被解析成了字符串,说明源数据里可能存在空值或脏字符,需要先清洗。

4.3 计算 KDA 与派生指标

KDA 是最直观的选手效率指标。我们在df上新增一列kda

df["kda"] = (df["kill"] + df["assist"]) / df["death"].replace(0, 1)

这里replace(0, 1)是为了避免除零错误。如果某个选手死亡数为 0,我们用一个极小惩罚值替代,匹配常见电竞数据平台的算法思路。真实平台对“未死亡”有不同处理方式,有些直接记为满分,你可以根据自己的统计口径调整。

继续构造“表现分”或“效率指数”之类的派生指标。比如:

df["damage_per_minute"] = df["gpm"] * 0.8 + df["xpm"] * 0.2

这个指标只是演示如何通过既有列生成新特征。实际项目中,分均伤害应该来自单独的伤害统计列,这里用经济经验加权只是为了说明“派生特征”这种数据处理方法。

4.4 队伍整体聚合对比

分析比赛时,我们不能只看单个选手,还要看团队整体表现。对队伍进行分组聚合:

team_stats = df.groupby(["match_id", "team"]).agg( total_kills=("kill", "sum"), total_deaths=("death", "sum"), total_assists=("assist", "sum"), avg_gpm=("gpm", "mean"), avg_xpm=("xpm", "mean"), total_vision=("vision_score", "sum"), total_towers=("tower_kills", "sum"), ).reset_index() team_stats["team_kda"] = ( (team_stats["total_kills"] + team_stats["total_assists"]) / team_stats["total_deaths"].replace(0, 1) ) print(team_stats)

这里使用groupby按比赛编号和队伍分组,再通过agg同时计算总击杀、平均经济、总视野等多个指标。reset_index会把分组键恢复为普通列,方便后续筛选。

4.5 选手表现横向对比

回到清梦与 Simon 这场循环赛本身,我们通常要回答一个核心问题:两队的核心输出位谁更强势?

key_players = df[df["hero"].isin(["射手", "法刺"])] for _, match in key_players.groupby("match_id"): print("比赛编号:", _) print(match[["team", "player", "hero", "kill", "death", "assist", "kda"]].to_string(index=False)) print("-" * 50)

hero字段里标记“射手”的选手是队伍的主要输出位。把两局比赛里双方的输出位数据并列打印,可以快速看到谁在团队中承担了更高的击杀任务。

4.6 可视化比赛节奏

使用 matplotlib 绘制对比图,帮助直观呈现两队在各个维度上的差异。

4.6.1 团队经济对比柱状图

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(12, 4)) for i, match_id in enumerate(team_stats["match_id"].unique()): match_data = team_stats[team_stats["match_id"] == match_id] axes[i].bar(match_data["team"], match_data["avg_gpm"], color=["#4C72B0", "#DD8452"]) axes[i].set_title(f"{match_id} 平均每分钟经济") axes[i].set_ylabel("GPM") axes[i].set_ylim(0, 700) plt.tight_layout() plt.savefig("outputs/charts/gpm_compare.png", dpi=200) plt.show()

这里设置了 matplotlib 中文字体,避免图表上出现方块乱码。如果你的系统没有SimHeiMicrosoft YaHei,可以改成系统里已安装的中文字体名称。

4.6.2 选手视野能力雷达图

雷达图适合对比单个选手或多个维度上的综合能力。以下示例绘制清梦与 Simon 双方辅助选手的视野、推塔、经济三个维度。

import numpy as np def radar_chart(players, labels, output_path): angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() angles += angles[:1] fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True)) for _, player in players.iterrows(): values = player[labels].values.tolist() values += values[:1] ax.plot(angles, values, linewidth=2, label=player["player"]) ax.fill(angles, values, alpha=0.15) ax.set_xticks(angles[:-1]) ax.set_xticklabels(labels) ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.1)) plt.savefig(output_path, dpi=200, bbox_inches="tight") plt.show() support_players = df[df["hero"] == "辅助"] radar_chart( support_players, ["vision_score", "total_towers", "avg_gpm"] if len(support_players) else ["vision_score"], "outputs/charts/support_radar.png", )

上面的写法有一个问题:total_towersavg_gpm并不是行级字段。为了避免误导读者,我们在真实项目中应该先做数据整形再画雷达图,尤其是把“每个选手一局的总推塔”与“整队平均经济”放在同一行中时,要确保数据口径一致。这里保留示例,是为了让新手理解雷达图的数据组织方式,实际应用时请按下节的数据透视思路先整理。

4.7 数据透视与匹配度分析

很多比赛数据是“一对一局”的,如果想把选手多局数据合并成一条记录,可以使用pivot_table

player_summary = df.pivot_table( index=["team", "player"], values=["kill", "death", "assist", "gpm", "vision_score"], aggfunc={ "kill": "sum", "death": "sum", "assist": "sum", "gpm": "mean", "vision_score": "mean", }, ).reset_index() player_summary["kda"] = ( player_summary["kill"] + player_summary["assist"] ) / player_summary["death"].replace(0, 1) print(player_summary.sort_values("kda", ascending=False))

pivot_table的作用是按照“队伍 + 选手”重新组织数据。击杀、死亡、助攻这类累计数值用求和,经济和视野这类均值型数据用平均。这样每个选手就只剩下一条汇总记录,后续直接用于排名或画图都非常方便。

5. 完整分析流程整合

把前面拆开的步骤整合成一个完整脚本,方便一次性运行。

import pandas as pd import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False # 读取数据 df1 = pd.read_csv("data/match_1.csv") df2 = pd.read_csv("data/match_2.csv") df = pd.concat([df1, df2], ignore_index=True) # 派生指标 df["kda"] = (df["kill"] + df["assist"]) / df["death"].replace(0, 1) # 队伍汇总 team_stats = df.groupby(["match_id", "team"]).agg( total_kills=("kill", "sum"), total_deaths=("death", "sum"), total_assists=("assist", "sum"), avg_gpm=("gpm", "mean"), avg_xpm=("xpm", "mean"), total_vision=("vision_score", "sum"), total_towers=("tower_kills", "sum"), ).reset_index() team_stats["team_kda"] = ( team_stats["total_kills"] + team_stats["total_assists"] ) / team_stats["total_deaths"].replace(0, 1) print("===== 队伍汇总 =====") print(team_stats) # 选手汇总 player_summary = df.pivot_table( index=["team", "player"], values=["kill", "death", "assist", "gpm", "vision_score"], aggfunc={ "kill": "sum", "death": "sum", "assist": "sum", "gpm": "mean", "vision_score": "mean", }, ).reset_index() player_summary["kda"] = ( player_summary["kill"] + player_summary["assist"] ) / player_summary["death"].replace(0, 1) print("\n===== 选手汇总 =====") print(player_summary.sort_values("kda", ascending=False)) # 图表输出 fig, ax = plt.subplots(figsize=(8, 5)) for team in team_stats["team"].unique(): team_data = team_stats[team_stats["team"] == team] ax.bar(team_data["match_id"] + "-" + team_data["team"], team_data["avg_gpm"], label=team) ax.set_title("队伍平均GPM对比") ax.set_ylabel("GPM") ax.legend() plt.tight_layout() plt.savefig("outputs/charts/team_gpm.png", dpi=200) print("\n图表已保存到 outputs/charts/team_gpm.png")

运行方式:

python analysis.py

该脚本会输出两个表格,并在outputs/charts目录下生成一张柱状图。

6. 常见问题与排查思路

在进行赛事数据分析时,下面几个问题出现频率很高。

问题现象常见原因解决思路
中文乱码绘图字体不支持中文设置plt.rcParams["font.sans-serif"]为系统已安装中文字体
除零错误某选手死亡数为 0数据清洗时将 0 替换为 1,或使用自定义口径
CSV 解析后数值列变成字符串源数据有,"等特殊符号检查原始 CSV,使用pd.to_numeric强制转换
matplotlib 绘图不显示没有调用plt.show()在脚本末尾添加plt.show()
数据量太大跑得很慢循环里逐行处理groupbypivot_table代替循环
各平台数据字段不一致不同来源统计口径不同统一字段映射表,先做标准化再分析

如果“CSV 解析后数值列变成字符串”这种情况出现,可以使用:

df["gpm"] = pd.to_numeric(df["gpm"], errors="coerce")

errors="coerce"会把无法转换的内容变成NaN,方便我们后面统一填充缺失值。

另外,当你发现聚合结果和你手算不一致时,先检查是不是重复行导致的:

print(df.duplicated().sum())

重复记录会直接拉高击杀、助攻等累计指标,必须提前排查。

7. 最佳实践与工程建议

7.1 统一数据规范

赛事数据来源往往不统一,建议在项目里维护一张“字段映射表”。接收数据时先做字段名标准化,再进入分析流程。例如:

field_map = { "kills": "kill", "deaths": "death", "assists": "assist", "player_name": "player", } df = df.rename(columns=field_map)

统一字段名以后,后续所有脚本都只跟规范字段打交道,不会因为不同来源的命名差异而反复修改代码。

7.2 数据留痕与可复现性

分析比赛数据最怕“这次能跑,下次跑不出来”。建议做到:

  • 原始数据文件不要手动修改,新版本另存。
  • 脚本中记录数据读取路径和生成时间。
  • 图表文件名带比赛编号,比如VS001_team_gpm.png

可以在脚本顶部加一个输出日志:

import datetime print(f"[{datetime.datetime.now()}] 开始分析比赛数据")

这样排查问题时能知道数据是什么时候生成的、对应哪批比赛。

7.3 谨慎使用第三方数据

正规赛事数据版权通常归赛事方或数据服务商所有。个人学习和研究没问题,但如果要公开发布分析报告、做成商用产品,务必确认授权范围。尽量从赛事官网开放接口或经过授权的数据平台获取数据,不要爬取未经授权的站点,更不要把受限数据打包转发。

7.4 指标口径要写清楚

分析报告或博客里给出 KDA、伤害转化率这种指标时,一定要写明计算公式和口径。同一场比赛,不同平台给出的 KDA 可能不同,原因往往出在“未死亡选手如何处理”“助攻是否加权”等细节上。明确口径,结果才可比较、可信赖。

7.5 代码结构保持模块化

不要把所有逻辑堆在一个文件里。建议拆分为:

src/ ├── loader.py # 数据读取与清洗 ├── metrics.py # 指标计算 ├── visualize.py # 图表绘制 └── report.py # 汇总输出

每个模块只做一件事,后续扩展“从数据库读数据”“自动生成 Markdown 报告”都会容易很多。

8. 总结与下一步方向

围绕“清梦 VS Simon”这场循环赛,本文完整演示了怎样用 Python 做一场电竞对阵数据分析。你如果完整跑完示例,应该已经掌握:

  • 赛事数据分析的常用指标与字段设计。
  • 使用 pandas 读取、拼接、清洗 CSV 数据。
  • 使用 groupby 和 pivot_table 聚合队伍与选手数据。
  • 使用 matplotlib 输出中文化对比图表。
  • 常见数据坑点与工程化整理方法。

下一步可以顺着两个方向继续深入:一是接入更多数据源,把循环赛多轮数据全部汇总成一份自动化周报;二是引入更复杂的分析模型,比如用时间序列分析经济曲线拐点,或用聚类算法划分选手打法风格。

如果你正在准备自己的赛事分析项目,建议从“固定赛制、固定数据源”的小范围开始,先把一条数据链跑通,再逐步扩展。这样无论后续是加指标、加图表还是换数据源,整个流程都会更稳。如果这篇文章对你有帮助,可以收藏备用,等实战中遇到问题再回来对照排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询