又到了复盘比赛内容的时候。每次聊到 VIT,弹幕里总能看到类似“VIT 的 BP 正在进步”“Fiesta 打得很有冒险精神”“队内气氛好像不错”这样的评论。这些说法听起来主观,但如果把问题剥开,其实是可以用数据去验证的:队伍在蓝色方和红色方各拿什么英雄?前两手是不是永远锁同一套?版本热门英雄被 Ban 之后有没有备用方案?连续几场失利之后,BP 优先级有没有出现方向性的调整?
与其凭印象争论一支战队的 BP 到底进步没有,不如自己动手写一套小脚本,把比赛数据拉下来、清洗、统计、可视化,用最终的图表说话。这篇文章就围绕“英雄联盟战队 BP 数据分析”这个方向展开。不需要很重的框架,Python + pandas + matplotlib 就能完成。无论你是想分析 LEC 的 VIT,还是想分析自己参与的业余联赛,这套思路都可以直接复用。
阅读本文你会得到三样东西:一是看懂比赛 BP 数据结构的基本方法;二是完整可运行的 Python 分析代码;三是一套判断“ BP 是否正在变好”的量化思路,而不是只盯着比分牌说话。
1. 为什么要量化战队的 BP 表现
英雄联盟比赛里的 BP(Ban/Pick)是赛前博弈最重要的环节。Ban 掉什么英雄,通常取决于版本强度、选手英雄池、对手惯用体系以及红蓝方的博弈策略。Pick 什么英雄,则决定了一支队伍在游戏前中后期的节奏模型。
一支战队的 BP 水平很难从单场比赛直接判断。举个例子:某一场比赛里 VIT 在蓝色方一抢了强势上单,解说夸这个 BP 做得舒服;但下一场比赛对手同样在蓝色方一抢了同一个英雄,VIT 应对得却很差。这时候我们就会发现,单独看一两场比赛的“感觉”并不可靠,必须把一段时间内的 BP 采样放在一起看。
量化 BP 表现有几个非常直接的好处:
- 可以区分“选手个人发挥好”和“BP 本身好”。如果一套阵容选出来,线上打出了巨大优势,那是选手状态问题;如果每场都是逆版本选人,前期没有任何主动性,那就值得怀疑 BP 是不是拖了后腿。
- 可以判断 BP 是否真的在调整。通过对比队伍前 10 场的英雄优先级和后 10 场的变化,就能验证“进步”到底是客观事实,还是赛后采访里的客套话。
- 可以发现队伍的英雄池短板。某个版本热门英雄在对面永远不 Ban 也不抢,那大概率是这个英雄不在本方体系里,或者选手根本不玩。
换句话说,BP 分析就是一支队伍赛训策略的“体检报告”。与其只看结果,不如把选人逻辑拆开。
2. 环境准备与数据说明
本文以 Python 为主要工具。需要的类库不复杂,核心是三个:
- pandas:负责数据清洗和聚合统计,是分析阶段最常用的工具。
- matplotlib:负责画图,生成趋势图、频率柱状图。
- numpy:辅助计算和数组操作,某些统计场景会用到。
建议先创建一个独立的项目目录,避免脚本文件散落各处。
mkdir lol-bp-analysis cd lol-bp-analysis python -m venv venvWindows 系统激活虚拟环境:
venv\Scripts\activatemacOS / Linux 系统激活虚拟环境:
source venv/bin/activate然后安装依赖:
pip install pandas matplotlib numpy关于版本,我的建议是不必追求最新。只要 pandas 和 matplotlib 能正常导入,并且支持 DataFrame 的基本操作,教学示例就可以顺利执行。如果你在本机已经安装了 Anaconda,前面的步骤可以省略,直接用 Jupyter Notebook 跟着写即可。
接下来是数据问题。
英雄联盟赛事数据的获取渠道比较多,常见方式包括官方赛事 API、第三方电竞数据平台、开发者社区维护的数据集。不同渠道的数据字段和更新频率存在差异,而且 API 的访问权限和调用规则经常调整,所以我在这篇教程里不写死某个接口的调用方式,而是先构造一份和真实赛事数据格式非常接近的本地 CSV 数据,用这份数据跑通分析流程。等流程通了,你需要做的只是把自己获取到的真实数据替换进 CSV 文件即可。
这种方式最大的好处是稳定:无论外界 API 怎么变化,分析代码的核心逻辑不会受影响。
3. 读懂 BP 数据:从一局比赛到一张数据表
在写代码之前,先建立几个基本概念。
一场比赛在正式进入对局之前,会经历 BP 阶段。首先是 Ban 人阶段,一般分为三轮,总计每方 5 个 Ban 位;然后是 Pick 阶段,双方交错选人,最终每方选出 5 个英雄。BP 完成后进入召唤师峡谷。
如果我们要量化分析,就不能只把 BP 当作一段无法保存的直播画面,而需要把这场博弈转写成结构化数据。常见的最小数据粒度是一行代表一个英雄事件。这里有两个方案:
方案 A:一行记录代表一支战队在某场比赛里的完整 BP。
字段可能是:
- match_id:比赛编号
- team:战队名
- side:蓝色方 / 红色方
- ban_1 到 ban_5:5 个被 Ban 的英雄
- pick_1 到 pick_5:5 个被选出的英雄
- result:本场比赛胜负
方案 B:一行记录代表一次 Ban 或一次 Pick。
字段可能是:
- match_id:比赛编号
- team:战队名
- side:蓝色方 / 红色方
- action:ban / pick
- champion:英雄名
- order:第几个执行
- result:比赛胜负
方案 B 是“长表”,每个行为单位,很适合用 pandas 做 groupby 聚合,也更容易计算某个英雄的 Ban 率、Pick 率、胜率。因此本文选择方案 B 作为核心数据结构。
下面我们模拟一份示例数据。这里的队伍名称使用“TeamA”“TeamB”,英雄名使用贴近真实风格的英文名,比如“Azir”“LeeSin”。实际分析 VIT 或 Fiesta 时,你只需要把真实比赛数据替换进来即可。
为了让你快速理解数据长什么样,可以先看下面的 Python 构造逻辑。
import pandas as pd import numpy as np rows = [] # 模拟 20 场 VIT 相关比赛 np.random.seed(42) team_names = ["VIT", "TeamA", "TeamB", "TeamC"] champions_pool = [ "Azir", "LeeSin", "Ornn", "KaiSa", "Thresh", "Rakan", "Gnar", "Syndra", "Taliyah", "Aphelios", "Nautilus", "Elise", "Camille", "Lucian", "Renata", "Vi", "Yone", "Jinx", "Leona", "Aatrox" ] # 构造约 700 行 BP 事件数据 for game in range(20): for team_index, team in enumerate(["VIT", "TeamA"]): side = "blue" if team_index == 0 else "red" result = 1 if (game + team_index) % 2 == 0 else 0 # 每队 5 个 ban 和 5 个 pick for order in range(5): rows.append({ "match_id": f"GAME_{game+1:03d}", "team": team, "side": side, "action": "ban", "champion": np.random.choice(champions_pool), "order": order + 1, "result": result }) for order in range(5): rows.append({ "match_id": f"GAME_{game+1:03d}", "team": team, "side": side, "action": "pick", "champion": np.random.choice(champions_pool), "order": order + 1, "result": result }) df = pd.DataFrame(rows) print(df.head()) print(df.shape)执行上面的代码,你会看到一个包含约 700 行的 DataFrame。其中前几行大概是:
match_id team side action champion order result 0 GAME_001 VIT blue ban Vi 1 1 1 GAME_001 VIT blue ban Rakan 2 1 ...这只是一份演示数据,它的随机性不代表任何真实赛况。它的作用是方便后续代码运行。
4. 核心代码实现:统计 VIT 的 BP 风格
当我们拿到了格式规整的数据后,第一步不是画图,而是先完成几个基础统计。
4.1 计算 Ban / Pick 频率
Ban 率反映的是“这个英雄值得被尊敬的程度”。Pick 率反映的是“队伍主动选择该英雄的频率”。如果某英雄的 Pick 率非常高,说明它有可能是这支队伍的核心体系英雄。
def calc_frequency(df, team_name, action_type): team_df = df[(df["team"] == team_name) & (df["action"] == action_type)] total = len(team_df) freq = ( team_df.groupby("champion") .size() .reset_index(name="count") ) freq["freq"] = freq["count"] / total freq = freq.sort_values("freq", ascending=False).reset_index(drop=True) return freq vit_pick_freq = calc_frequency(df, "VIT", "pick") print(vit_pick_freq.head(10))这部分输出的含义是:在 VIT 所有 Pick 行为中,哪些英雄出现占比最高。比如某英雄占比 0.08,意味着 VIT 每 100 次选人里大约有 8 次会选到这个英雄。
这里需要注意一个细节:如果样本量很小,比如只有 5 场比赛,那么每队总共只有 25 个 Pick 事件,频率结果会很不稳定,一分钟一个版本。实际分析时要先确认样本量足够,最好至少累计 10 场以上。
4.2 统计英雄胜率
只看 Pick 频率还不够。如果队伍经常选某个英雄,但这个英雄胜率很低,说明这个选择可能存在“舒适区陷阱”:选手爱玩,但赢不了。
def calc_win_rate(df, team_name, action_type="pick"): team_df = df[(df["team"] == team_name) & (df["action"] == action_type)] stats = ( team_df.groupby("champion")["result"] .agg(count="count", win_sum="sum") .reset_index() ) stats["win_rate"] = stats["win_sum"] / stats["count"] stats = stats[stats["count"] >= 3].sort_values("win_rate", ascending=False) return stats vit_pick_stats = calc_win_rate(df, "VIT") print(vit_pick_stats.head(10))这里我设置了count >= 3的过滤条件。原因是如果某个英雄只出现了 1 次,胜率只有 0% 或 100%,这种数据没有统计意义。实际项目中,这个阈值可以根据赛事场次调整,比赛场次更多时建议把阈值提高到 5 次以上。
4.3 区分红蓝方的 BP 差异
职业比赛里,红色方通常需要承担更多 Ban 位压力,因为蓝色方有一抢权。因此,分析 BP 时把红蓝方混在一起会掩盖很多细节。
def calc_side_freq(df, team_name, action_type="pick"): team_df = df[(df["team"] == team_name) & (df["action"] == action_type)] side_stats = ( team_df.groupby(["side", "champion"]) .size() .reset_index(name="count") ) total = team_df.groupby("side").size().reset_index(name="total") side_stats = side_stats.merge(total, on="side") side_stats["freq"] = side_stats["count"] / side_stats["total"] side_stats = side_stats.sort_values(["side", "freq"], ascending=[True, False]) return side_stats vit_side_pick = calc_side_freq(df, "VIT", "pick") print(vit_side_pick.head(10))这组数据如果做出来之后发现,VIT 在蓝色方经常一抢某个打野,而红色方几乎不选同一个打野,那么红色方的 BP 分析就值得单独往下挖了。
5. 用可视化和“前后期对比”验证 VIT 的 BP 是否在进步
统计完频率和胜率后,我们会得到一堆静态结果,但它还不能回答“BP 正在进步”这个问题。因为“进步”是一个动态概念,需要一个时间维度的对比。
最简单的办法是:把 VIT 的比赛按时间分成前 N 场和后 N 场,对比两者在英雄选择、胜负、不同位置 Pick 率上的差异。如果窗口足够大,也可以使用滚动平均。
5.1 按比赛顺序对比两个阶段
我们为 DataFrame 增加一个简单的序号,用于表示比赛先后。
df["game_order"] = df["match_id"].str.extract(r"(\d+)").astype(int)接着把整支队伍的比赛切分成前半段和后半段。
game_count = df.drop_duplicates("match_id")["match_id"].count() split_point = game_count // 2 df["period"] = df["game_order"].apply( lambda x: "latter" if x > split_point else "former" ) vit_df = df[df["team"] == "VIT"].copy() former_pick = vit_df[(vit_df["period"] == "former") & (vit_df["action"] == "pick")] latter_pick = vit_df[(vit_df["period"] == "latter") & (vit_df["action"] == "pick")] former_win_rate = former_pick["result"].mean() latter_win_rate = latter_pick["result"].mean() print("前半段胜率:", former_win_rate) print("后半段胜率:", latter_win_rate)这里的胜率是按“BP 事件对应比赛结果”计算的。每场比赛的胜负会同时作用于该场比赛中的 5 个 Pick 事件,因此在统计时要明白:某一个英雄胜率更高,并不完全等于这个英雄带飞了比赛,它也可能只是说明队伍在选出这套阵容时整体状态更好。
这其实是一个很常见的误区:把 BP 胜率理解成了英雄 solo 胜率。在比赛数据中,选人阶段只是游戏的第一步,后续的战术执行、选手发挥同样会作用于比赛结果。
5.2 绘制英雄选择频率的变化图
画图可以帮助我们直观感受一个阶段的变化。下面的代码统计 VIT 前半段和后半段 Pick 次数前 8 的英雄,并做可视化对比。
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Arial Unicode MS"] plt.rcParams["axes.unicode_minus"] = False def top_n_freq(pick_df, n=8): total = len(pick_df) freq = pick_df.groupby("champion").size().reset_index(name="count") freq["freq"] = freq["count"] / total return freq.sort_values("freq", ascending=False).head(n) former_top = top_n_freq(former_pick) latter_top = top_n_freq(latter_pick) fig, axes = plt.subplots(1, 2, figsize=(12, 5)) formers = dict(zip(former_top["champion"], former_top["freq"])) laters = dict(zip(latter_top["champion"], latter_top["freq"])) all_champs = list(dict.fromkeys( list(formers.keys()) + list(laters.keys()) )) former_vals = [formers.get(c, 0) for c in all_champs] latter_vals = [laters.get(c, 0) for c in all_champs] axes[0].barh(all_champs, former_vals, color="#4C72B0") axes[0].set_title("VIT former period pick freq") axes[1].barh(all_champs, latter_vals, color="#DD8452") axes[1].set_title("VIT latter period pick freq") plt.tight_layout() plt.savefig("vit_period_pick_compare.png", dpi=150) plt.show()图表的作用不是让文章变复杂,而是让你在看到“后半段蓝色柱子明显变短”“某个英雄从列表顶端消失”的一瞬间,找到 BP 改变最明显的证据。例如,如果某位以“冒险精神”著称的打野选手,前半段经常选出螳螂、豹女这类高风险的野核英雄,而后半段突然开始频繁拿猪妹、大树这种工具人打野,那这个变化未必是退步,也可能是教练组根据当前版本重新定义了阵容核心。
6. 用事件序列拆解队伍的“关键调整能力”
比统计更高级的分析方向,是观察队伍在失利之后如何调整 BP。这在赛训里有更实际的价值。毕竟一支队伍 BP 是不是在进步,最核心的判断指标不是简单的胜率,而是“自我修正速度”。
一个可操作的量化方法是:把连续失利的比赛找出来,看下一场比赛中,BP 阶段发生了哪些变化。具体可以看三点:
一,上一场输掉的阵容中,是否有英雄在下一场直接被 Ban 掉或不再优先 Pick。这说明教练组意识到某个选角无效并及时止损。
二,上一场的蓝色方首抢位置是否调整。如果第一局蓝色方一抢输出型英雄但被对手节奏碾压,下一场改成先抢节奏型打野或强势辅助,说明教练组的优先级思路发生了变化。
三,上一场被对手限制的关键点,是否在下一场主动 Ban 掉。这是一种反向学习,也就是从失败中获知对手的核心打法,并在下次交锋前做出响应。
下面的代码可以演示如何找出“位于失利场次之后的首选英雄变化”。
# 找出 VIT 失利的比赛 lose_games = ( vit_df[vit_df["action"] == "pick"] .groupby("match_id")["result"] .max() .reset_index() ) lose_games = lose_games[lose_games["result"] == 0]["match_id"].tolist() # 为下一场比赛设置一个前驱场次标识 vit_df["next_game_flag"] = vit_df["match_id"].map( lambda x: f"GAME_{int(x.split('_')[1]) + 1:03d}" )真实分析时,这里还要考虑不同比赛日、不同版本之间的间隔。如果两场比赛间隔数周,中间游戏版本更新过,那 BP 变化就不能全部归因于队伍修正,很可能是版本把某些英雄推下了舞台。
因此,做赛训归因时一定要记录数据对应的游戏版本号。没有版本信息,就很难把“队伍变强”和“版本红利”分开。
7. 常见问题与排查思路
在跑这份分析脚本时,新手大概率会遇到下面几个问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 导入中文标签时图形出现方块 | matplotlib 默认字体不含中文字符 | 设置plt.rcParams["font.sans-serif"],比如 SimHei 或 Microsoft YaHei;Linux 服务器可安装文泉驿字体 |
| 统计出来的英雄名大小写不统一 | 不同来源的数据没有清洗干净 | 分析前统一转小写或统一首字母大写,并对同一英雄的多别名做映射 |
| 某英雄胜率异常高或低 | 样本量太少 | 增加count >= 3或count >= 5的过滤条件 |
| 前后半段对比结果没有差异 | 比赛场次太少,或者切分点不合理 | 改成滚动窗口分析,每 5 场计算一次移动平均 |
| 分析结果与真实观赛感受不一致 | 数据只反映 BP,不反映选手操作 | 结合比赛内的首杀、一塔、小龙控制率做联合分析 |
出现第一个问题时,可以先检查运行环境中的可用字体,再执行:
import matplotlib.font_manager as fm fonts = [f.name for f in fm.fontManager.ttflist if "Hei" in f.name or "Yai" in f.name] print(fonts)如果输出为空,说明当前环境没有合适的中文字体,需要先安装。这些问题的解决思路都是通用的,不依赖具体客户端或网站。
8. 数据分析落到赛训建议时的工程实践
数据分析并不是跑完代码就结束了。从代码到“能指导队伍 BP 优化”的结论之间,还隔着非常重要的工程规范。这里整理几个我在实际分析电竞数据时比较重视的要点。
第一点是数据字段的标准化。不管是自己手工录的数据,还是写爬虫抓取的数据,入库前都要先统一字段。champion 不要一会叫 “LeeSin” 一会叫 “lee sin”,match_id 不要一会是数字,一会又是字符串,更不能出现一行的 side 写成 “blue” 而另一行写成 “Blue” 的情况。前期不花时间清洗,后期所有统计都会受影响。
第二点是版本信息的记录。英雄联盟职业比赛和游戏版本高度绑定。分析结果要在版本大更新之后及时重置。我自己一般会在每份导出数据里增加patch字段,并定期对版本号做一次校验。
第三点是样本量意识。分析战队 BP 时,人的直觉经常会被最近一两场极端对局带偏。例如 Fiesta 某场选出盲僧踢出精彩操作,观众会习惯性认为“VIT 的 BP 鼓励高风险操作”,但回过头看过去 10 场数据,也许盲僧只出场了一次。为了避免这类偏误,我通常会先打印一个总体样本量,再决定结论的置信程度。
第四点是不把单个指标神化。BP 分析中常用的 Pick 率、Ban 率、胜率终究是简化指标。它们无法精准反映“指挥型辅助的带动作用”或“某个英雄面对特定 Counter 关系的隐性收益”。因此数据报告里不要只堆指标,而是要回到比赛内容去理解异常点。
第五点是安全问题。在获取赛事数据时,原则上应该优先使用官方渠道和获准的公开数据源,不要使用未经授权、包含恶意脚本的爬虫包。如果你的本地爬虫触发了网站的反爬机制,应当立刻停止并及时联系数据提供方,而不是继续做高频率请求。电竞数据分析是辅助赛训和看比赛的工具,前提是合法合规、不破坏目标站点。
9. 更进一步:从 BP 数据入口建立自己的赛训观察面板
到这里,一套基于 Python 的 BP 数据分析链路已经跑通了。我们真正完成的,不只是数一数英雄出现次数,而是建立了一个能回答“VIT 的 BP 正在进步吗”这一类问题的分析框架。
当然,这个框架还有很多可以延伸的方向。例如引入比赛内前 15 分钟的领先数据来判断阵容前期强度;结合选手个人排位记录来预测英雄池变化;或者抓取游戏版本改动公告,把英雄补丁和队伍 BP 优先级放在同一张时间轴上。
数据分析在这里最大的价值不是取代教练组和选手的判断,而是把模糊的赛训感觉转化为可追溯的记录。哪怕今天只是分析了自己主队最近 5 场的小数据,也会对 BP 背后的逻辑有新的理解。看过图表之后如果能发现一个“原来这套阵容在某个侧路选出来胜率这么低”的瞬间,这篇教程就没有白写了。
我自己一般在跑完一次完整分析后,会把脚本固定成三种模式:单队伍 BP 总览、红蓝方差异对比、失利后调整响应。日常赛训只维护这三种图表,数据量小,产出也稳定。你需要时可以顺着这个思路继续往下做,把它扩展成适合自己主队的观察面板。