最近 LPL 常规赛里 NIP 2:1 拿下 WBG,赛后 NIP 打野 guwon 的采访很有意思:一边说“本来以为会轻松拿下”,一边承认“WBG 的 BP 有备而来”。这两句话放在一起,其实点出了一个非常值得用技术手段去验证的问题:一支队伍的 BP 到底是不是“有备而来”?是赛前功课做足了,还是临场手感好?
普通观众看比赛,记住的往往是选手操作和团战结果。但如果你要做战队分析、内容复盘,或者想用小规模数据验证赛前判断,纯肉眼不够用。这篇文章就用 NIP 对 WBG 这场系列赛做引子,讲一套可以复用的 BP 数据分析工作流:数据从哪来、怎么清洗、怎么算优先级、怎么批量出图。整个方案用 Python 就能跑,不需要服务器,普通笔记本足够。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目定位 | 赛事 BP 数据分析与复盘工具,不是爬虫脚本单点,而是完整的分析工作流 |
| 主要功能 | 比赛数据抓取、BP 字段解析、Ban/Pick 优先级计算、红蓝方胜率对比、批量赛果归档 |
| 输入数据 | 公开赛事网站导出的比赛记录,或手工整理的赛后 BP 数据表 |
| 技术栈 | Python 3.8+、pandas、requests、BeautifulSoup、matplotlib、openpyxl |
| 运行环境 | Windows / macOS / Linux 均可,CPU 即可运行 |
| 显存需求 | 无,纯数据分析任务不涉及 GPU |
| 启动方式 | Jupyter Notebook 分步执行,或命令行脚本一键生成报告 |
| 接口 API | 不依赖第三方付费 API,对公网赛事页面做低频请求即可;也可以完全离线分析 |
| 批量任务 | 支持整轮比赛批量导入,输出统一格式的 BP 分析表 |
| 适合场景 | 战队赛后复盘、自媒体内容制作、赛前 BP 预判验证、电竞数据教学 |
这套东西的价值不在于“预测比赛输赢”,而是把“BP 有没有备而来”这种主观判断,尽量转成可量化的指标:英雄优先级、前三手选择倾向、红蓝方胜率差、关键 Counter 关系。
2. 适用场景与使用边界
先说清楚:这篇文章是数据分析方法教学,不是菠菜工具,也不保证预测准确率。它适合三类人。
第一类是电竞自媒体作者。赛后想快速产出“第三局 BP 解析”内容,靠手工截屏和口述太慢。用脚本把这一轮的 BP 数据拉下来,生成热力图和统计表,直接作为视频或图文素材。
第二类是战队分析师或业余战队教练。虽然 LPL 正赛队伍有自己的专业数据系统,但对业余赛事、高校联赛、平台杯赛来说,自己维护一套轻量 BP 分析流程完全够用。
第三类是 LOL 数据爱好者。想验证“蓝色方前两手到底抢什么”“某个版本热门英雄的优先级是否被高估”这类问题,这套工作流能给你一个可复现的答案。
使用边界也要明确:赛事数据网站的页面结构和接口可能调整,脚本需要定期维护;抓取频率不要过高,避免给目标站点造成压力;如果涉及选手个人信息、肖像、赛事版权素材,只用于个人学习研究,不要做商业传播。涉及任何版权敏感内容时,遵守平台规则和赛事方授权要求。
另外要提醒一句:比赛复盘存在明显的“事后归因”偏差。BP 结果好未必是策略好,也可能是选手英雄池恰好克制。数据分析只能提供参考维度,不能替代教练组的赛训判断。
3. 环境准备与前置条件
先说硬件,这类任务真的不挑设备。只要电脑能装 Python,4G 内存都够跑。最吃资源的是批量出图环节,几百场比赛的 BP 数据做热力图,内存占用也不会超过 1G,主要是 CPU 计算。没有 GPU 要求,没有显存要求,也没有 50 系显卡适配问题。
软件层面建议准备:
| 软件 | 版本建议 | 用途 |
|---|---|---|
| Python | 3.8 及以上 | 主开发语言 |
| pandas | 1.3 及以上 | 数据处理 |
| requests | 2.x | 抓取公开页面数据 |
| BeautifulSoup4 | 4.x | 解析 HTML |
| matplotlib | 3.5 及以上 | 生成可视化图表 |
| openpyxl | 3.x | 导出 Excel 报告 |
| Jupyter Notebook | 可选 | 分步调试更方便 |
安装命令:
pip install pandas requests beautifulsoup4 matplotlib openpyxl jupyter如果是在国内网络环境,建议配置镜像源,避免下载超时。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas requests beautifulsoup4 matplotlib openpyxl jupyter动手之前,先建目录,把后续要用的文件归类好:
lol_bp_analysis/ ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── output/ │ └── figures/ # 生成的图表 ├── scripts/ │ ├── fetch.py # 数据获取 │ ├── clean.py # 数据清洗 │ └── report.py # 生成报告 └── notebook/ └── bp_analysis.ipynb4. 赛事数据获取
BP 分析的第一步是拿到比赛原始数据。有两条路,一条是从公开赛事数据站点抓取,另一条是手工整理。这里重点讲公开数据源的方法,但注意:站点结构随时可能变化,下面的代码是通用思路,不要把选择器写死当成万能方案。
抓取数据时,需要用到的核心字段包括:比赛 ID、系列赛双方战队、红蓝方归属、每一局的 Ban 列表、Pick 列表、比赛结果。
这里用 requests 加 BeautifulSoup 做一次低频请求示例。目标站点的 HTML 结构需要你按实际情况调整。
import requests from bs4 import BeautifulSoup def fetch_bp_page(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36" } try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 部分站点是中文或 UTF-8 编码,这里做统一处理 resp.encoding = resp.apparent_encoding return resp.text except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return None def parse_bp_table(html): soup = BeautifulSoup(html, "html.parser") # 注意:这里的选择器是示例,必须按目标站点实际结构调整 table = soup.find("table", class_="bp-table") if not table: print("未找到 BP 表格,请检查站点结构") return [] rows = [] for tr in table.find_all("tr"): cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])] if cells: rows.append(cells) return rows if __name__ == "__main__": # 示例 URL,实际请替换为可访问的公开赛事页面 url = "https://example.com/match/12345" html = fetch_bp_page(url) if html: bp_data = parse_bp_table(html) for row in bp_data: print(row)这里强调一个原则:不要对同一个站点做高频并发请求,也不要拿这种脚本去爬全量历史数据然后对外分发。个人学习用途低频访问即可。如果站点有 API 接口,优先用接口,比解析 HTML 稳定得多。
如果找不到合适的公开数据源,手工整理也不丢人。做一个 CSV,每一行记录一场比赛的 BP 细节,字段如下:
match_id,team,side,game_num,ban1,ban2,ban3,ban4,ban5,pick1,pick2,pick3,pick4,pick5,result M001,NIP,blue,1,Azir,Elise,Jayce,Rakan,Poppy,Vi,Orianna,Lucian,Nautilus,Karma,WIN M001,WBG,red,1,Pikachu,Rumble,Kaisa,etc,...手工整理的工作量很大,但数据质量可控,适合单场重点复盘。如果只是做 NIP 对 WBG 这一轮比赛的 BP 分析,手工整理三局数据也只需要半小时左右。
5. BP 数据字段与清洗逻辑
拿到原始数据后,不能直接分析。需要先明确字段含义。
| 字段 | 含义 | 示例 |
|---|---|---|
| match_id | 比赛唯一标识 | NIP_WBG_2025SPRING |
| team | 战队名 | NIP |
| side | 阵营 | blue / red |
| game_num | 局数 | 1、2、3 |
| ban1 - ban5 | 五个禁选英雄 | Azir、Elise ... |
| pick1 - pick5 | 五个选用英雄 | Vi、Orianna ... |
| result | 本局结果 | WIN / LOSS |
数据清洗主要做三件事。
英雄名称标准化。同一个英雄在不同数据源可能有不同写法,比如“悟空”和“Wukong”、“皇子”和“Jarvan IV”。必须先统一成英文名或者中文官方名,否则统计会出现重复项。
import pandas as pd def normalize_hero_name(name): hero_map = { "悟空": "Wukong", "猴子": "Wukong", "皇子": "Jarvan IV", "嘉文四世": "Jarvan IV", "瑞尔": "Rell", } return hero_map.get(name, name) def clean_bp_data(df): ban_cols = ["ban1", "ban2", "ban3", "ban4", "ban5"] pick_cols = ["pick1", "pick2", "pick3", "pick4", "pick5"] for col in ban_cols + pick_cols: df[col] = df[col].apply(normalize_hero_name) return df补全缺失值。有些比赛数据源可能只记录前三个 Ban,因为部分赛制是前两手 Ban 再选。要根据赛制补成标准六 Ban 结构,或者统一只分析前三个 Ban。
新增阵营与胜负标记。分析红蓝方胜率前,必须确保每一行都有 side 和 result 字段。统计脚本里直接对缺失字段报错,不要静默跳过,否则最终结果可能被污染。
def validate_bp_data(df): required = ["match_id", "team", "side", "game_num", "result"] for col in required: if col not in df.columns: raise ValueError(f"缺少必需字段: {col}") assert set(df["side"].unique()).issubset({"blue", "red"}), "阵营字段非法" assert set(df["result"].unique()).issubset({"WIN", "LOSS"}), "结果字段非法" return df清洗完成后,把处理好的数据另存一份。
def save_processed(df, path="data/processed/bp_clean.csv"): df.to_csv(path, index=False, encoding="utf-8-sig") print(f"已保存清洗后数据: {path}")6. BP 分析维度
数据准备好,正式进入“验证 BP 是否有备而来”的分析环节。这里的核心思路是计算英雄优先级指数。
一个英雄的 BP 优先级不能只看 Ban 率或 Pick 率,要把两者合成一个指数。常用公式:
优先级指数 = Ban率 * 0.6 + Pick率 * 0.4权重可以按版本调整。如果某个版本禁位压力很大,Ban 率权重应该更高;如果是强调抢英雄的版本,Pick 率权重应该更高。从 NIP 和 WBG 这场比赛的材料看,guwon 赛后提到“BP 有备而来”,意思是 WBG 可能在特定英雄上做了针对性设计。用优先级指数就能发现:WBG 在前三手是否高频 Ban 掉 NIP 打野位强势英雄,或者是否自己抢下某一手关键英雄。
代码实现:
def calculate_priority(df): ban_cols = ["ban1", "ban2", "ban3", "ban4", "ban5"] pick_cols = ["pick1", "pick2", "pick3", "pick4", "pick5"] hero_stats = {} all_heroes = set() for col in ban_cols + pick_cols: all_heroes.update(df[col].dropna().unique()) total_games = df["game_num"].nunique() for hero in all_heroes: ban_count = 0 pick_count = 0 win_count = 0 for _, row in df.iterrows(): if hero in row[ban_cols].values: ban_count += 1 if hero in row[pick_cols].values: pick_count += 1 result = row["result"] if hero in row[pick_cols].values and result == "WIN": win_count += 1 ban_rate = ban_count / total_games pick_rate = pick_count / total_games win_rate = pick_count > 0 and win_count / pick_count or 0 priority = ban_rate * 0.6 + pick_rate * 0.4 hero_stats[hero] = { "ban_count": ban_count, "pick_count": pick_count, "ban_rate": ban_rate, "pick_rate": pick_rate, "win_rate": win_rate, "priority": priority } return pd.DataFrame(hero_stats).T.sort_values("priority", ascending=False)除了英雄优先级,还可以拆四个维度看 BP 策略。
红蓝方胜率对比。LPL 很多版本里蓝色方胜率会更高,因为先手抢英雄有信息优势。统计红色方和蓝色方在样本比赛里的胜率,能判断某个队伍是否特别擅长红色方后手 Counter。
前三手选择倾向。前三手通常决定了阵容核心思路,是保上中野节奏,还是打下路对线,还是先拿野区强势英雄。统计每支队伍前三手最常出的英雄组合,能看出战术体系。
关键局 BP 变化。比如第二局输了,第三局 BP 做了哪些调整。重点看 Ban 位变化和 Pick 顺序调整。guwon 说“本来以为会轻松拿下”,说明第一局可能 NIP 比较顺利;而“WBG 的 BP 有备而来”,说明后续局 WBG 可能调整了 BP 策略,抢到了一些关键英雄。
Counter 关系验证。两个位置的英雄存在明显相克关系。用样本数据统计“当 A 英雄被 Pick 后,对方下一手是否立刻 Pick B 英雄”的次数,可以判断对手是否在临场做针对性反制。
7. 可视化复盘
数据算出来之后,出图才是内容生产者最关心的环节。这里给三张图:BP 优先级 Top10、红蓝方胜率对比、战队 Ban/Pick 热力图。
BP 优先级条形图:
import matplotlib.pyplot as plt import pandas as pd plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] plt.rcParams["axes.unicode_minus"] = False def plot_priority(df_priority, top_n=10): top = df_priority.head(top_n) fig, ax = plt.subplots(figsize=(10, 6)) ax.barh(top.index, top["priority"], color="#1E90FF") ax.set_xlabel("优先级指数") ax.set_title("BP 优先级 Top10") ax.invert_yaxis() plt.tight_layout() plt.savefig("output/figures/priority_top10.png", dpi=150) plt.show()红蓝方胜率堆叠图:
def plot_side_winrate(df): side_stats = df.groupby("side")["result"].apply( lambda x: (x == "WIN").sum() / len(x) ).reset_index() side_stats.columns = ["side", "winrate"] fig, ax = plt.subplots(figsize=(6, 6)) colors = ["#D32F2F", "#1976D2"] ax.pie( side_stats["winrate"], labels=side_stats["side"], autopct="%.1f%%", colors=colors ) ax.set_title("红蓝方胜率占比") plt.savefig("output/figures/side_winrate.png", dpi=150) plt.show()战队 BP 热力图。这张图做出来最有视频封面感。横轴是英雄,纵轴是战队,颜色深浅表示该战队对这个英雄的 Ban/Pick 偏好:
import numpy as np def plot_team_hero_heatmap(df, teams=["NIP", "WBG"], top_heroes=None): if top_heroes is None: top_heroes = ["Azir", "Elise", "Vi", "Orianna", "Lucian", "Rakan", "Poppy"] matrix = np.zeros((len(teams), len(top_heroes))) for i, team in enumerate(teams): team_df = df[df["team"] == team] for j, hero in enumerate(top_heroes): count = 0 for _, row in team_df.iterrows(): if hero in row[["ban1", "ban2", "ban3", "ban4", "ban5"]].values: count += 1 if hero in row[["pick1", "pick2", "pick3", "pick4", "pick5"]].values: count += 1 matrix[i, j] = count fig, ax = plt.subplots(figsize=(12, 4)) im = ax.imshow(matrix, cmap="YlOrRd", aspect="auto") ax.set_xticks(range(len(top_heroes))) ax.set_xticklabels(top_heroes, rotation=45, ha="right") ax.set_yticks(range(len(teams))) ax.set_yticklabels(teams) for i in range(len(teams)): for j in range(len(top_heroes)): ax.text(j, i, int(matrix[i, j]), ha="center", va="center", color="black") ax.set_title("战队 BP 关注度热力图") plt.colorbar(im) plt.tight_layout() plt.savefig("output/figures/team_bp_heatmap.png", dpi=150) plt.show()出图的时候注意中文字体设置。Linux 服务器上通常没有 SimHei 和 Microsoft YaHei,需要装中文字体,否则图里中文会变成方框。展示图片时,优先输出 SVG 或高 DPI PNG,方便后续剪视频时放大裁切。
8. 批量分析与报告导出
单场分析只是热身。做赛事复盘往往要拉整个常规赛的数据,或者连续分析某支队伍最近五场。批量任务的设计思路是:读入多场比赛数据,循环计算,汇总输出。
import os import pandas as pd def batch_process(raw_dir="data/raw", output_path="data/processed/all_matches.csv"): all_files = [f for f in os.listdir(raw_dir) if f.endswith(".csv")] df_list = [] for f in all_files: file_path = os.path.join(raw_dir, f) df = pd.read_csv(file_path) df_list.append(df) if not df_list: print("raw 目录下没有 CSV 文件") return None combined = pd.concat(df_list, ignore_index=True) combined.to_csv(output_path, index=False, encoding="utf-8-sig") print(f"已合并 {len(all_files)} 个文件,共 {len(combined)} 行") return combined报告导出用 Excel 多 Sheet 结构:第一个 Sheet 放比赛总览,第二个 Sheet 放英雄优先级排名,第三个 Sheet 放红蓝方胜率,第四个 Sheet 放逐局 BP 明细。这样给战队看或者自己复盘时,一个文件就够。
def export_excel(df, priority_df, filename="output/bp_report.xlsx"): with pd.ExcelWriter(filename, engine="openpyxl") as writer: df.to_excel(writer, sheet_name="比赛总览", index=False) priority_df.to_excel(writer, sheet_name="英雄优先级") side_stats = df.groupby("side")["result"].apply( lambda x: (x == "WIN").sum() ).reset_index() side_stats.to_excel(writer, sheet_name="红蓝方胜率", index=False) print(f"报告已导出: {filename}")批量任务要特别注意失败重试和日志记录。如果某场比赛的原始数据格式有问题,不能让它卡住整批任务,要记录到失败列表,最后统一处理。
fail_log = [] for file in all_files: try: clean_bp_data(pd.read_csv(file)) except Exception as e: fail_log.append({"file": file, "error": str(e)}) if fail_log: pd.DataFrame(fail_log).to_csv("output/fail_log.csv", index=False)接口调用方面,这套工作流本身不涉及对外 API,但如果要接公众号或者微博自动发布脚本,批量导出的 CSV 和 Excel 文件可以直接被上层调度系统读取。简单来说,它输出的数据天然就是机器可读的。
9. 资源占用与性能观察
虽然这个项目不需要 GPU,但也需要关注性能,否则处理大样本时会慢。
显存占用:零。整个流程依赖 CPU 和内存,所以 50 系显卡、核显、老显卡都不影响。
内存占用:单场 BP 数据量很小,一场三局比赛也就几十行。瓶颈通常出在“把所有历史比赛读进内存”这一步。如果分析三个赛季的数据,原始 CSV 可能有几万行,pandas 读入后内存占用通常在 500M 到 1G 之间,普通笔记本可以接受。
计算耗时:英雄优先级计算里用了两层循环,如果样本达到几万行,计算时间会明显上升。建议引入 numpy 或 pandas 的向量化操作来重写统计逻辑。
def calculate_priority_vectorized(df): hero_list = [] for col in ["ban1", "ban2", "ban3", "ban4", "ban5"]: hero_list.append(df[col]) ban_stack = pd.concat(hero_list, axis=1) all_heroes = pd.unique(ban_stack.values.ravel()) hero_rows = [] for hero in all_heroes: ban_count = (ban_stack == hero).sum().sum() hero_rows.append({"hero": hero, "ban_count": ban_count}) return pd.DataFrame(hero_rows)出图性能不是问题,matplotlib 单张图渲染时间在 1 秒以内,批量出几十张图也只是几秒的事。
如果发现程序越来越慢,优先检查是否在循环里反复读 CSV,或者反复创建 DataFrame。正确做法是一次性读入全部数据,分析完成后统一释放。还可以增加进程内缓存,但这类项目规模不需要上 Redis,简单的变量缓存就够。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 抓取页面返回 403 | User-Agent 被识别,或被站点封禁 | 查看响应状态码和页面内容 | 更换 User-Agent,降低请求频率,等待几分钟再试 |
| 解析不到 BP 表格 | 站点 HTML 结构调整,选择器失效 | 在浏览器里查看元素,确认 class 名 | 更新 BeautifulSoup 选择器,优先改用官方 API |
| 中文显示为方框 | 系统缺少中文字体 | 运行 fc-list 查看已安装字体 | 安装 Noto Sans CJK 或 wqy-microhei |
| 英雄名称统计重复 | 同一英雄名称不统一 | 打印 pd.unique() 查看全部英雄名 | 完善 normalize_hero_name 映射表 |
| 计算优先级时报 KeyError | 脏数据里有空值或非法字段 | 检查原始 CSV | 在 validate_bp_data 里提前拦截空值 |
| 批量处理中单场比赛报错 | 某文件格式不规范 | 查看 fail_log | 单独处理失败文件,修复后重新合并 |
| Excel 打开后中文乱码 | CSV 编码问题 | 用文本编辑器查看文件编码 | 保存时使用 utf-8-sig 编码 |
| 多次运行结果不一致 | 数据源页面内容更新或抓取不全 | 对比两次抓取的行数 | 确保在数据源数据稳定后再抓取,必要时缓存原始页面 |
| 图片模糊 | DPI 设置过低 | 打开图片属性查看分辨率 | 保存时增大 dpi 参数,建议 300 |
最容易踩的坑还是数据源变动的坑。今天能跑的解析代码,下周可能失效。解决思路是:把原始 HTML 存到本地做缓存,解析代码改动后再重新跑一遍历史数据,保证分析结果可复现。
11. 最佳实践与使用建议
先说这套流程在当前比赛版本下怎么用最舒服。第一次跑通不要贪多,先选单独一个系列赛,比如 NIP 对 WBG 三局,手工整理或者抓取下来,完成“清洗 — 优先级计算 — 出图”全流程,确认数据质量没问题,再扩大到整个赛区。
数据管理上建议统一文件命名规则,例如LPL_2025_Spring_W1D1.csv,月份和轮次写清楚。原始数据、清洗后数据、图表、Excel 报告分目录存放,避免三个月之后找不到原始文件。
批量分析时必须加日志。每一场比赛在合并前记录文件名、行数、成功状态。宁可多写一条日志,也不要等全部跑完才发现某一步有数据污染。
输出图表时,应该统一风格。战队颜色建议直接用官方配色:NIP 主色偏黑金,WBG 主色偏粉白。颜色统一后,不同的复盘视频和图文作品放在一起更专业。 matplotlib 样式可以在一个配置文件里统一定义。
接口这块,如果后续要接入自动发布平台,建议把分析结果转换成 JSON 格式,而不是直接处理 CSV。JSON 便于程序间传输,字段语义也更清楚。
def export_json_summary(priority_df, output_path="output/bp_summary.json"): summary = priority_df.head(10).to_dict(orient="records") import json with open(output_path, "w", encoding="utf-8") as f: json.dump(summary, f, ensure_ascii=False, indent=2) print(f"JSON 摘要已导出: {output_path}")关于合规,再强调一次:赛事数据和选手数据只用于个人学习、非商业内容制作和战队内部复盘。如果要对外发布包含选手肖像、比赛录像截图或赛事独家数据的视频和文章,需要确认是否符合赛事方和平台的使用规范。涉及比赛录像的二次创作,也要注意版权边界。
12. 总结与下一步
从 guwon 那句“WBG 的 BP 有备而来”入手,这篇文章把 BP 复盘这件事变成了一个可执行的数据分析流程:公开数据抓取、字段清洗、优先级指数计算、批量出图、Excel 报表导出。整套方法不挑机器,不依赖 GPU,普通笔记本跑起来很轻松。
最值得先验证的功能是英雄优先级指数和红蓝方胜率对比。前者能直接把“BP 有备而来”这种话转化成英雄数据排名,后者能看出队伍在选边上的真实优劣势。最容易踩的坑是数据源结构和英雄名称不统一,提前做好清洗规则能省很多时间。
下一步可以做的事很多:给这个流程加一个置信区间分析,用历史数据判断某个英雄的高优先级是短期版本红利还是长期强势;也可以接一个简单的网页爬虫调度,每周自动拉取赛果,生成周报;还可以把输出图表整合到视频剪辑流程里,直接作为 B 站或抖音的内容素材。
如果你手里正好有 NIP 或 WBG 最近几场的赛后数据,建议直接打开 Jupyter Notebook 跑一遍上面的脚本。数据量不用大,三场比赛就能看出这套流程和纯肉眼复盘的区别。