一条“910 20杀 1.56Rating 率队碾压 magic”的比赛战报,在很多观众眼里只是一场精彩的开幕战:明星选手状态火热,队伍配合默契,团队轻取开门红。但如果站在数据分析的角度看,这短短几行字背后藏着一套完整的比赛数据链路:单回合击杀、死亡、助攻、存活、首杀、多杀回合、KAST、ADR,最后才汇总成一个像 1.56 Rating 这样的综合指标。
这篇文章要做的事情,就是把这套链路拆开:从比赛数据指标的定义讲起,再说明数据怎么获取、怎么清洗、怎么用 Python 计算一个简化评分,最后用图表还原“超神表现”到底强在哪里。案例会围绕 EWC 揭幕战里 910 的 20 杀、1.56 Rating 展开,代码示例全部基于通用数据结构,实际项目需要根据数据源字段调整。
1. 先看懂比赛数据里的基础指标
1.1 “20杀 1.56Rating”只是一个汇总结果
在 FPS 电竞项目里,单场比赛会产生两类数据:一类是回合层面的事件,比如谁击杀了谁、使用什么武器、发生在什么位置;另一类才是观众看到的选手统计,比如击杀数、死亡数、助攻数、Rating。
“20 杀”是击杀总数,这里的“杀”通常指一次成功的击杀事件。如果比赛是 24 回合,那么 20 杀意味着这名选手平均每回合贡献 0.83 个击杀,也就是 KPR(Kills Per Round)接近 0.83。这个数字已经不算低,在职业比赛里 KPR 超过 0.70 就是一线选手的稳定水平。
“1.56 Rating”是综合评分。Rating 不是一个单独事件,而是把击杀、助攻、存活、多杀、首杀、团队配合等多个维度压缩成一个便于比较的数字。不同数据平台采用的公式不同,常见的有 HLTV Rating、VLR Rating 等,因此不能把一个平台的 Rating 直接拿到另一个平台对比。
下面列出比赛复盘中最常用的统计口径:
| 指标 | 英文全称 | 含义 | 常见高水平区间 |
|---|---|---|---|
| KPR | Kills Per Round | 平均每回合击杀数 | 0.70 以上 |
| DPR | Deaths Per Round | 平均每回合死亡数 | 0.60 以下 |
| ADR | Average Damage Per Round | 平均每回合造成伤害 | 75 以上 |
| KAST | Kill, Assist, Survive, Trade | 有贡献回合占比 | 70% 以上 |
| 首杀 | Opening Kill | 回合开始阶段完成首次击杀 | 越高越好 |
| 多杀回合 | Multi-kill Rounds | 单回合击杀 2 人以上的回合数 | 2 杀、3 杀、4 杀 |
这些指标之间不是完全独立的。一个高 Rating 选手通常至少满足两个条件:KPR 高,DPR 低。910 那场比赛 20 杀、1.56 Rating,可以推断他的击杀贡献和回合存活率都明显高于团队平均水平。
1.2 为什么不能用“击杀多”来衡量选手价值
很多业余复盘只看击杀数,但这会漏掉大量信息。以 20 杀为例,同样是 20 杀,选手 A 可能在 24 个回合里稳定输出,选手 B 可能靠两个回合的残局四杀拿到击杀数,但其他回合频繁白给。前者对团队的贡献远高于后者。
所以现代比赛数据分析会把事件拆得更细:击杀价值是否发生在关键回合、是否有队友补枪配合、自己是否存活到回合结束、是否拿到首杀并帮团队打开局面。这些信息光看战报是不完整的,需要拿到逐回合事件数据。
这也解释了为什么 Rating 比单纯击杀数更可靠:它尝试把“回合内的综合贡献”量化,而不是只数人头。
注意:Rating 是描述性指标,不是因果指标。它告诉你一个选手打得好不好,但不直接告诉你为什么打得好。要回答“为什么”,需要看回合事件、站位、枪械、经济、对手状态等更多数据。
2. 比赛数据从哪来:接口、统计站、还是自己抓
2.1 常见数据来源和适用场景
做比赛数据分析的第一步是先拿到数据。不同来源的字段完整度、获取难度和合规要求差别很大。
| 数据来源 | 数据形态 | 适合场景 | 需要注意的问题 |
|---|---|---|---|
| 官方赛事 API | JSON / XML 接口 | 赛事方授权的数据应用 | 接口权限申请门槛高 |
| 第三方统计网站 | HTML 页面或开放接口 | 快速查看历史数据、做学习项目 | 字段口径可能不同,访问频率有限制 |
| 公开数据仓库 | CSV / Parquet 文件 | 批量建模、离线分析 | 时效性差,需要确认更新周期 |
| 赛事回放文件 | 专用回放格式 | 最细粒度的逐帧事件分析 | 解析复杂,需要配合专用工具 |
对于个人学习项目,优先建议使用第三方统计网站的导出台账,或者社区维护的数据集。爬取完整页面也可以做,但必须遵守网站的 robots 协议和服务条款,控制请求频率,不要对目标站点造成压力。
这里有一个容易踩的坑:不同网站对“Rating”的算法不完全一致。甲站显示 1.56,乙站可能显示 1.48。原因不是数据错,而是计算口径不同。做分析时一定要记录数据来源,不要把不同平台的数值混在一个模型里。
2.2 一次简单的页面抓取示例
如果只是想快速拿到一张比赛数据表,可以用 Python 的requests配合pandas.read_html。下面示例用于说明思路,实际字段名和目标地址要根据真实站点调整:
import pandas as pd import requests url = "https://example.com/matches/ewc-opening" html = requests.get( url, headers={"User-Agent": "Mozilla/5.0 (compatible;>from bs4 import BeautifulSoup soup = BeautifulSoup(html.text, "html.parser") player_rows = [] for row in soup.select("div.player-row"): name = row.select_one(".player-name").get_text(strip=True) kills = row.select_one(".kills").get_text(strip=True) rating = row.select_one(".rating").get_text(strip=True) player_rows.append({"name": name, "kills": kills, "rating": rating}) print(player_rows)关键点有两个:第一,给请求加上合理的User-Agent,避免被目标站点误判为脚本;第二,解析逻辑要绑定在具体页面的 HTML 结构上,页面改版后代码很可能失效。
2.3 获取数据的红线
抓取公开页面用于个人学习通常问题不大,但如果要发布数据、对外提供服务,就需要注意合规问题。建议遵循以下原则:
- 优先寻找官方开放接口或数据授权。
- 不绕过登录限制、验证码和访问频率控制。
- 不批量抓取后再公开转售。
- 不在文章里展示目标站点的完整数据快照。
- 记录数据抓取时间,因为比赛数据会随着统计口径修正而更新。
3. 用 pandas 把原始记录整理成比赛分析表
3.1 数据结构设计:比赛、选手、回合
抓下来的数据往往是一张大宽表,字段混乱、缺失值多。分析前要先设计清晰的结构。一个最小可用的比赛分析库至少包含三张表:
| 表名 | 字段示例 | 粒度 |
|---|---|---|
| matches | match_id, team_a, team_b, event, date | 一场比赛一条记录 |
| players | player_id, player_name, team_name, match_id | 一个选手在一场比赛一条记录 |
| rounds | match_id, round_num, player_id, kills, deaths, assists, survived | 一个选手在一个回合一条记录 |
rounds是最底层的事件明细,players是聚合结果。Rating 的推导逻辑放在聚合阶段实现,这样既方便复现,也方便调整算法。
一个 JSON 示例结构如下:
{ "match_id": "ewc-001", "team_a": "team-a", "team_b": "team-b", "round_data": [ { "round_num": 1, "events": [ {"player_id": "p910", "kills": 1, "deaths": 0, "assists": 1, "survived": true} ] } ] }这里的事件对象只记录了结果字段,没有记录“击杀了谁、使用什么武器”。如果需要做更细致的复盘,事件表还要增加目标选手、武器、位置、回合经济等字段。
3.2 清洗和聚合的代码骨架
拿到原始数据后,先做字段规整和类型转换:
import pandas as pd round_df = pd.read_csv("rounds_raw.csv") round_df.columns = round_df.columns.str.strip().str.lower().str.replace(" ", "_") required_cols = ["match_id", "round_num", "player_id", "kills", "deaths", "assists", "survived"] for col in required_cols: if col not in round_df.columns: raise ValueError(f"缺少必要字段: {col}") round_df["kills"] = pd.to_numeric(round_df["kills"], errors="coerce").fillna(0) round_df["deaths"] = pd.to_numeric(round_df["deaths"], errors="coerce").fillna(0) round_df["assists"] = pd.to_numeric(round_df["assists"], errors="coerce").fillna(0) round_df["survived"] = round_df["survived"].astype(bool)然后按选手聚合出常用的基础指标:
player_stats = round_df.groupby("player_id").agg( rounds=("round_num", "nunique"), kills=("kills", "sum"), deaths=("deaths", "sum"), assists=("assists", "sum"), survived_rounds=("survived", "sum"), ).reset_index() player_stats["kpr"] = player_stats["kills"] / player_stats["rounds"] player_stats["dpr"] = player_stats["deaths"] / player_stats["rounds"] player_stats["total_contributions"] = ( player_stats["kills"] + player_stats["assists"] + player_stats["survived_rounds"] ) player_stats["kast_like"] = player_stats["total_contributions"] / player_stats["rounds"]这里的kast_like只是简化近似。真正的 KAST 需要按回合判断选手是否至少完成一次击杀、助攻或存活,不能直接把总数相加后除以回合数,否则会重复计数。正确做法是先按“回合内是否发生贡献”做布尔判断,再聚合。
round_df["has_contribution"] = ( (round_df["kills"] > 0) | (round_df["assists"] > 0) | (round_df["survived"]) ) kast = ( round_df.groupby(["player_id", "round_num"])["has_contribution"] .any() .groupby("player_id") .mean() .reset_index() ) kast.columns = ["player_id", "kast"]这一步是关键:很多初学者把指标算错,就是因为没有区分“总次数”和“有贡献回合数”。
4. 从指标到综合评分:一个可实现的 Rating 近似模型
4.1 简化模型要覆盖的维度
官方 Rating 算法通常包含多个维度,不同平台权重不同。为了让评分更接近“选手综合贡献”,至少要覆盖以下四个方向:
- 击杀贡献:KPR 越高,对回合获胜的直接影响越大。
- 生存贡献:DPR 越低,说明选手减少给对手送经济,存活也能保留枪械和装备。
- 团队贡献:助攻、补枪、残局处理都对回合结果有正向作用。
- 爆发贡献:单回合多杀能力,是“carry”比赛的重要来源。
一个简化的 Rating 公式可以这样设计:
rating_simple = kpr * 1.2 - dpr * 0.8 + (1 - dpr) * 0.3 + kast * 0.4这个公式不是官方算法,只是用来分析“相对表现”的近似分。它的意义在于把多个维度的信息压缩成一个数值,方便排序和对比。实际项目里如果要复刻某个平台的精确 Rating,需要拿到该平台公开的权重定义。
4.2 Python 实现
用前面聚合出来的字段,可以构造一个simple_rating列:
def compute_simple_rating(row): base = row["kpr"] * 1.2 survival = (1.0 - row["dpr"]) * 0.3 teamwork = row["kast"] * 0.4 return round(base - row["dpr"] * 0.8 + survival + teamwork, 3) player_stats["simple_rating"] = player_stats.apply(compute_simple_rating, axis=1) player_stats = player_stats.sort_values("simple_rating", ascending=False)注意,公式中的权重系数需要根据实际数据校验。如果系数设置不合理,可能会出现“击杀极高但死亡也极高的激进选手”和“稳定但参与度低的选手”得分一样的情况。调参时可以先看排名是否符合观赛直觉,再逐步调整。
4.3 案例代入:一个 20 杀 1.56 Rating 的复盘样例
为了演示计算过程,这里构造一份接近 EWC 揭幕战场景的模拟选手数据。注意这是演示数据,不是官方精确统计:
| 选手 | 回合 | 击杀 | 死亡 | 助攻 | KAST | KPR | DPR |
|---|---|---|---|---|---|---|---|
| 910 | 24 | 20 | 12 | 6 | 0.75 | 0.833 | 0.500 |
| magic | 24 | 14 | 16 | 4 | 0.62 | 0.583 | 0.667 |
代入上面的公式:
- 910: 0.833 * 1.2 - 0.5 * 0.8 + (1 - 0.5) * 0.3 + 0.75 * 0.4 = 1.0 - 0.4 + 0.15 + 0.3 = 1.05
- magic: 0.583 * 1.2 - 0.667 * 0.8 + (1 - 0.667) * 0.3 + 0.62 * 0.4 = 0.7 - 0.533 + 0.1 + 0.248 = 0.515
这里的 910 明显高于对手,但数值和官方 Rating 1.56 之间没有直接对应关系,因为公式和权重不同。这个例子的价值在于体现“数据结构 -> 指标 -> 评分”的完整链路。
注意:任何自建 Rating 都只能用于趋势分析、选手排序和复盘辅助,不能替代官方数据对外发布。对外输出时一定要标注“简化评分模型”,避免误导。
5. 可视化复盘:图表说明“碾压”来自哪里
5.1 用横向柱状图对比关键指标
数据算完之后,最好用图表呈现。以下代码用 matplotlib 对比 910 和 magic 的 KPR、DPR、KAST:
import matplotlib.pyplot as plt import numpy as np players = ["910", "magic"] kpr = [0.833, 0.583] dpr = [0.500, 0.667] kast = [0.75, 0.62] x = np.arange(len(players)) width = 0.25 fig, ax = plt.subplots(figsize=(8, 5)) bar1 = ax.bar(x - width, kpr, width, label="KPR") bar2 = ax.bar(x, dpr, width, label="DPR") bar3 = ax.bar(x + width, kast, width, label="KAST") ax.set_xticks(x) ax.set_xticklabels(players) ax.set_ylabel("value") ax.set_title("Player Key Metrics Comparison") ax.legend() plt.tight_layout() plt.show()这里需要解释一点:DPR 是“越低越好”,所以把它和 KPR、KAST 放在同一张柱状图里会造成视觉误导。更严谨的做法是单独绘制 DPR 反向对比,或者把 DPR 转换为“1 - DPR”的存活率。
5.2 用雷达图展示选手能力结构
如果想看一个选手的“能力形状”,可以画雷达图:
import numpy as np import matplotlib.pyplot as plt from math import pi def radar_plot(labels, values, title): angles = [n / float(len(labels)) * 2 * pi for n in range(len(labels))] angles += angles[:1] values += values[:1] fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True)) ax.plot(angles, values, linewidth=2) ax.fill(angles, values, alpha=0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(labels) ax.set_title(title) plt.show() radar_plot( ["KPR", "Survival", "KAST", "Assist", "First Kill"], [0.833, 0.5, 0.75, 0.25, 0.33], "910 Performance Radar", )雷达图适合直观观察强项和短板,但不适合精确对比,因为不同维度的量纲可能不同。建议只对同一场比赛、同一批选手使用,避免跨比赛比较。
可视化的真正价值不是“画得好看”,而是让异常值暴露出来:比如 KPR 很高但 KAST 很低,说明选手存在“强击杀但低参与度”的特征;比如 DPR 低但 KAST 高,说明选手更偏辅助和存活位。
6. 常见问题排查:数据对不上、抓不到、指标算不对
实际做比赛数据分析时,最常遇到的几个问题集中在数据获取、字段口径和算法差异上。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 页面抓下来没有表格 | 目标数据由 JavaScript 动态渲染 | 查看页面源码或 Network 请求 | 改用接口请求,或使用浏览器自动化工具 |
| 击杀数和战报不一致 | 统计口径不含加时赛,或统计了错误地图 | 核对比赛 ID、地图编号、范围 | 明确数据范围,统一过滤条件 |
| Rating 算出来和平台不一致 | 自建公式与平台公式不同 | 对比平台公示的算法和权重 | 标注统计口径,避免混用 |
| KAST 超过 100% | 按“次数”相加后除以回合数 | 检查是否有重复计数 | 改为按回合内是否贡献做布尔判断 |
| 数据更新后结果变化 | 原始事件被修正或补充 | 记录数据抓取时间 | 保存数据快照,追踪版本 |
| 请求被拒绝或封禁 | 请求频率过高,缺少合规请求头 | 查看响应状态码和错误信息 | 降低频率,加延时,优先使用官方接口 |
其中“重复计数”是最容易被忽略的问题。比如一个回合里选手既杀了人又活到最后,如果同时把 kills 和 survived 都算进“贡献总次数”,就比按回合计数的逻辑高出不少。前面代码里用groupby(["player_id", "round_num"]).any()就是为了把多事件回合折叠成“是否参与”的布尔值。
排查时建议按这个顺序来:
- 先确认比赛 ID、对手、地图、赛事范围是否正确。
- 再确认数据是否包含加时赛,统计口径是否一致。
- 然后检查聚合字段有没有重复计数。
- 最后对比平台给出的人群分布,判断自己的算法是否偏离常识。
7. 生产环境里做比赛数据分析的工程化建议
7.1 从学习脚本到可维护项目
学习阶段用 Jupyter Notebook 或单个 Python 脚本就够,但进入生产环境后,数据处理链路要拆成模块:
match_data_pipeline/ ├── collector/ │ └── fetcher.py ├── cleaning/ │ └── normalizer.py ├── metrics/ │ └── rating.py ├── visualizer/ │ └── charts.py └── config/ └── settings.yaml生产环境至少需要额外考虑:
- 数据存储:用 PostgreSQL 或 ClickHouse 保存比赛和回合明细,避免每次重新抓取。
- 调度:用 Airflow 或 cron 按比赛日程定时拉取数据。
- 监控:统计抓取失败率、字段缺失率、指标异常波动。
- 版本管理:公式调整后要能回溯历史评分。
- 权限:不要在内网任务里存储第三方站点账号密码。
7.2 学习环境与生产环境对照
| 环节 | 学习环境 | 生产环境 |
|---|---|---|
| 数据获取 | 手动抓取页面 | 官方接口、定时任务、数据仓库 |
| 数据存储 | CSV / 内存 DataFrame | PostgreSQL、对象存储、数仓 |
| 指标计算 | 脚本内直接计算 | 独立服务或任务,带版本号 |
| 可视化 | matplotlib | 前端图表库、BI 报表 |
| 错误处理 | 抛异常后手动调试 | 日志、告警、自动重试 |
核心思路是:学习环境追求快速验证,生产环境追求稳定、可追溯、可回滚。两者不要混用,否则会在数据口径变化时很难排查。
8. 扩展方向:离开战报,往更深处分析
一条战报数据能做到的分析并不止于此。如果对比赛数据方向感兴趣,可以从下面几个方向继续深入。
- 回合级事件建模:把每个击杀关联到地图位置、武器、经济状态,建立回合获胜概率模型。
- 选手趋势预测:把一段赛程内的 Rating、KPR、ADR 做时间序列,观察状态起伏。
- 战术模式识别:通过击杀事件的时间点聚类,识别队伍惯用战术是快攻、控图还是赌点。
- 自定义评分系统:结合队伍角色定位,为一号位、狙击手、指挥分别设计不同权重评分。
- 复盘工具沉淀:把清洗、聚合、可视化打包成工具库,后续比赛换数据源即可复用。
对新手来说,最有价值的练习不是追最新战报,而是把一场旧比赛的数据完整跑通:从原始页面抓取,到清洗成结构化表格,再到自建评分并画图。这样走完一遍,比看十篇战报更能理解 Rating 到底在衡量什么。
回到开头的 EWC 揭幕战,910 的 20 杀和 1.56 Rating 是一条精彩战报的浓缩结果,而数据分析师要做的,是还原这些数字背后的事件、回合和贡献结构。只要把数据链路搭起来,下一次看到新的比赛数据时,你就不再只能喊“太强了”,而是能说出:强在 KPR、强在生存、还是强在关键回合的爆发贡献。