尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python赛后数据分析实战:从BP统计到可视化完整教程

Python赛后数据分析实战:从BP统计到可视化完整教程 NIP 2:1 战胜 WBG 的比赛刚落幕赛后采访里打野 guwon 说原本以为能轻松拿下结果 WBG 在 BP 环节明显有备而来。这个话题在粉丝社区里讨论得很热闹弹幕里也到处是“收留 WBG 心碎女孩”的调侃。不过作为技术博主我更关注的是另一个问题这轮 BO3 的 BP 到底是怎么影响胜负的手动翻网页统计英雄选择、队伍胜率、经济曲线实在太低效了。所以我把这类赛后数据分析流程整理成了一套 Python 实战方案从数据采集、清洗、分析到可视化全部用代码跑通。本文不是单一的比赛评论而是一篇可以直接复用的技术教程。哪怕你不是英雄联盟玩家只要能理解“比赛数据”这个概念就能用这套代码处理赛果、选手英雄、胜负关系这类结构化数据。读完你会掌握 requests 抓取网页数据、 BeautifulSoup 解析 HTML、 pandas 做聚合统计、 matplotlib 画图以及一套基本的赛后分析脚本。1. 背景与核心概念1.1 什么是赛后数据分析赛后数据分析简单说就是通过程序化方式收集一场或多场比赛的原始数据再把这些数据整理成可读、可比较、可展示的指标。对于比赛复盘来说这类数据通常包括对局双方队伍名称。每一小局的胜负结果。双方选择的英雄。对局时长。队伍整体和个人选手的经济、击杀、助攻等数据。单看一场比赛这些数据可能只是零散的数字。但如果我们把整个 BO3甚至整个赛季的数据放到一起就能看出队伍在战术风格上的偏好。比如 guwon 赛后说“WBG 的 BP 有备而来”这句话如果翻译成数据语言就是在说 WBG 在 Ban/Pick 阶段使用了不同于往日的英雄组合或禁用策略导致 NIP 前期的准备没有完全生效。1.2 数据从哪里来做赛后数据分析首先需要考虑数据来源。常见的方式有三种第一种是官方数据接口。赛事方通常会提供比赛历史数据数据字段最规范但接入门槛较高需要申请授权。第二种是第三方赛事数据站点。很多站点会把比赛结果、选手数据、BP 内容整理成网页或 JSON方便查看。作为学习用途我们可以分析这类公开页面但需要遵守网站条款和 robots 协议不能高频爬取更不能用非法手段绕过访问限制。第三种是自己维护的表格。比如手动把每局比赛的英雄选择、胜负结果录入 CSV 或数据库。虽然效率低但数据质量最可控。本文会采用“采集 本地示例数据”相结合的方式。我会给出一段通用的采集代码但不是针对某个具体网站写死而是把解析思路讲清楚。示例项目里则内置一份模拟数据保证你本地可以直接运行不受真实网站结构变化的影响。1.3 核心术语BP、Pick 率、Ban 率、胜率在阅读代码之前先快速熟悉几个比赛数据领域常用词BPBan Pick 的缩写指比赛前的禁用和选择英雄阶段。Pick选择英雄Pick 率表示某支队伍在多少局比赛中选用了某个英雄。Ban禁用英雄Ban 率表示某支队伍在多少局比赛中禁用了某个英雄。胜率队伍获胜局数除以总局数。KDA击杀、死亡、助攻的综合指标用来衡量选手个人表现。本文的重点不在 KDA而在队伍层面所以主要用胜率、Pick 次数、禁用情况、经济走势这几个指标。2. 环境准备与版本说明2.1 运行环境本文代码以 Python 为例。建议使用 Python 3.9 及以上版本因为代码中使用了list[dict]这类内置泛型语法低版本会因为语法不支持而报错。需要安装的第三方库如下requests发送 HTTP 请求获取网页内容。beautifulsoup4解析 HTML 页面。pandas处理表格数据和聚合统计。matplotlib绘制图表。numpy进行数值计算绘制模拟曲线时会用到。安装命令pip install requests beautifulsoup4 pandas matplotlib numpy如果你使用的是 Anaconda 环境pandas、matplotlib、numpy 通常已经预装只需要补装 requests 和 beautifulsoup4。conda install requests beautifulsoup42.2 项目结构为了让代码更容易维护项目按模块拆分analysis_project/ ├── data/ │ └── raw/ │ └── matches.json ├── output/ ├── data_loader.py ├── analysis.py ├── main.py └── requirements.txt说明data/raw/matches.json存放原始比赛数据模拟一场 BO3共三小局。output/存放生成的图片文件。data_loader.py负责读取本地 JSON 数据也可以对接采集结果。analysis.py负责统计、分析和可视化。main.py是主流程入口。如果你的环境里已经安装了 Jupyter Notebook也可以把analysis.py里的函数拆成单元格逐步执行方便观察每一步输出。2.3 为什么准备一份示例数据真实比赛数据的接口经常变动不同网站的字段命名也不一致。如果直接把代码绑定到某个线上页面一旦网站改版代码就无法运行。所以我准备了一份模拟数据matches.json里面包含三局比赛最终结果是 NIP 两胜、WBG 一胜对应 2:1。英雄名使用大家在比赛里常见的英雄简称但请注意这只是演示数据结构用的示例不代表真实比赛的具体 BP 组合。这份示例数据的意义在于你可以先把分析流程跑通之后再替换成自己采集到的真实数据。3. 数据采集与标准化3.1 设计原始数据格式拿到的比赛数据可能是 JSON也可能是网页表格。为了统一后续处理我们先把数据设计成下面这种结构[ { match_id: NIP_WBG_G1, blue_team: NIP, red_team: WBG, winner: NIP, duration_min: 31.5, blue_picks: [Jax, LeeSin, Ahri, Jinx, Nautilus], red_picks: [Ornn, Vi, Azir, Kalista, Renata], blue_bans: [Yuumi, Zeri, Taliyah], red_bans: [Rumble, Draven, Lulu] }, { match_id: NIP_WBG_G2, blue_team: WBG, red_team: NIP, winner: WBG, duration_min: 28.1, blue_picks: [Gnar, Sejuani, Tristana, Zeri, Karma], red_picks: [Rumble, Maokai, Sylas, Aphelios, Thresh], blue_bans: [LeeSin, Jinx, Nautilus], red_bans: [Ahri, Jax, Vi] }, { match_id: NIP_WBG_G3, blue_team: NIP, red_team: WBG, winner: NIP, duration_min: 34.0, blue_picks: [Renekton, Viego, Syndra, Draven, Lulu], red_picks: [KSante, Kindred, Corki, Xayah, Braum], blue_bans: [Azir, Kalista, Renata], red_bans: [Syndra, LeeSin, Jax] } ]每一局比赛都明确记录了蓝方队伍和红方队伍。最终胜利方。对局时长。双方 Pick 的英雄。双方 Ban 掉的英雄。有了这个结构后续统计 Pick 次数、Ban 次数、胜率都非常方便。3.2 从网页解析数据如果你的数据来源是网页通常会遇到两种情况一种是页面直接返回 JSON另一种是返回 HTML 表格。对应 JSON 类型可以直接用 requests 获取内容再用response.json()解析。对应 HTML 表格核心流程是请求页面。设置正确编码。用 BeautifulSoup 定位 table。遍历 tr/td 单元格提取文本。清洗数据并转换成字典列表。下面是一段通用的页面解析示例# scraper.py import logging import requests from bs4 import BeautifulSoup logger logging.getLogger(__name__) def fetch_html(url: str, headers: dict | None None) - str: 请求页面并返回 HTML 文本 default_headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } headers headers or default_headers resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding or utf-8 return resp.text def parse_match_table(html: str) - list[dict]: 解析比赛表格返回比赛记录列表 soup BeautifulSoup(html, html.parser) rows soup.select(table tbody tr) matches [] for row in rows: cells [td.get_text(stripTrue) for td in row.find_all(td)] if len(cells) 4: continue matches.append({ blue_team: cells[0], red_team: cells[1], winner: cells[2], duration_min: _to_float(cells[3]), }) return matches def _to_float(value: str) - float | None: 字符串转浮点数失败时返回 None try: return float(value) except (TypeError, ValueError): return None这个示例里的 CSS 选择器table tbody tr是非常通用的框架具体到某个网站时你需要打开开发者工具找到比赛表格对应的 class 属性再替换选择器。注意requests只是一个基础请求库如果对方站点有较强的反爬机制比如需要复杂 Cookie、动态渲染页面那 requests 就无能为力了需要换成 Playwright 或 Selenium 这类浏览器自动化工具。本文不展开这些内容先掌握基础流程即可。3.3 数据清洗与标准化无论数据来自本地 JSON 还是网页解析都需要做一次清洗目的是保证字段名统一、缺失值有默认处理、数据类型正确。以下函数可以解决两个常见问题网页解析出来的字段名可能与本地 JSON 不一致。部分字段可能存在缺失值参与计算时会报错。# data_loader.py import json from pathlib import Path RAW_DATA_PATH Path(__file__).parent / data / raw / matches.json def load_match_data(path: Path RAW_DATA_PATH) - list[dict]: 读取本地比赛数据返回字典列表 with open(path, r, encodingutf-8) as f: data json.load(f) normalized [] for match in data: normalized.append({ match_id: match.get(match_id, ), blue_team: _normalize_team(match.get(blue_team)), red_team: _normalize_team(match.get(red_team)), winner: _normalize_team(match.get(winner)), duration_min: float(match.get(duration_min, 0) or 0), blue_picks: match.get(blue_picks, []), red_picks: match.get(red_picks, []), blue_bans: match.get(blue_bans, []), red_bans: match.get(red_bans, []), }) return normalized def _normalize_team(team: str | None) - str: 统一队名格式去除首尾空格并转大写 if not team: return UNKNOWN return team.strip().upper()代码里使用了dict.get()方法即使原始数据缺少某个字段也不会直接报错而是返回默认值。_normalize_team会把队名统一成大写避免因为大小写问题导致分组统计出错。4. 比赛数据分析与可视化4.1 队伍胜负统计数据清洗完成之后第一件事通常是计算两支队伍的胜率。我们需要把每局比赛的胜负关系拆成“队伍维度的记录”。例如第一局 NIP 获胜那么对 NIP 来说这一局是胜对 WBG 来说这一局是负。这样每个队伍都能得到一个二维表总场次、胜场数、负场数。# analysis.py import pandas as pd import matplotlib.pyplot as plt from collections import Counter plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False def build_team_stats(matches: list[dict]) - pd.DataFrame: rows [] for match in matches: rows.append({ team: match[blue_team], win: 1 if match[winner] match[blue_team] else 0 }) rows.append({ team: match[red_team], win: 1 if match[winner] match[red_team] else 0 }) df pd.DataFrame(rows) stats df.groupby(team)[win].agg([sum, count]) stats[win_rate] stats[sum] / stats[count] stats stats.rename(columns{sum: wins, count: games}) return stats.reset_index()运行之后可以得到类似这样的结果team wins games win_rate 0 NIP 2 3 0.666667 1 WBG 1 3 0.333333这个结果对应了 NIP 2:1 战胜 WBG 的赛果数据上非常直观。4.2 BP 数据统计BP 分析是赛后复盘最复杂的部分。我们至少可以统计出每个英雄在几局中出现在了某支队伍里进而看出队伍对英雄的偏好。def build_pick_matrix(matches: list[dict]) - pd.DataFrame: pick_counter Counter() for match in matches: for hero in match[blue_picks]: pick_counter[(match[blue_team], hero)] 1 for hero in match[red_picks]: pick_counter[(match[red_team], hero)] 1 rows [] for (team, hero), cnt in pick_counter.items(): rows.append({team: team, hero: hero, picks: cnt}) df pd.DataFrame(rows) matrix df.pivot_table(indexhero, columnsteam, valuespicks, fill_value0) return matrix这个函数输出的是一张矩阵表。每一行是一个英雄每一列是一支队伍单元格数值表示该局 Pick 了该英雄的次数。如果某个英雄被某支队伍选过两次数值就会显示 2。类似的逻辑也可以写成 Ban 统计字段从blue_picks换成blue_bans即可就不再重复写了。4.3 经济曲线可视化除了 BP 和胜率经济曲线也是判断比赛走势的重要数据。但在示例数据里没有逐分钟的经济值因此我们用随机游走生成一条模拟曲线用于演示绘图流程。import numpy as np def simulate_gold_diff(duration: int 34, seed: int 1) - np.ndarray: 模拟每分钟经济差变化单位百金币 rng np.random.default_rng(seed) values np.cumsum(rng.normal(0, 150, duration 1)) return values def plot_gold_curve(duration: int 34, seed: int 1) - None: minute np.arange(0, duration 1) gold_diff simulate_gold_diff(duration, seed) plt.figure(figsize(10, 6)) plt.plot(minute, gold_diff, markero, markersize3, labelsimulated gold diff) plt.axhline(0, colorgray, linestyle--, linewidth1) plt.title(Blue team gold lead / minute) plt.xlabel(minute) plt.ylabel(gold diff) plt.legend() plt.grid(True, alpha0.3) plt.tight_layout() plt.savefig(output/gold_curve.png, dpi150) plt.close()如果未来拿到了真实比赛数据只需要把simulate_gold_diff这个函数替换成从数据源读取每分钟经济差的逻辑绘图代码可以保持不变。4.4 图表输出为了让结果更清晰我们把胜率柱状图和 Pick 次数对比图保存成图片。def plot_win_rate(stats: pd.DataFrame) - None: stats.plot.bar(xteam, ywin_rate, rot0, legendFalse) plt.title(队伍胜率对比) plt.ylabel(win_rate) plt.tight_layout() plt.savefig(output/win_rate.png, dpi150) plt.close() def plot_pick_compare(pick_matrix: pd.DataFrame) - None: pick_matrix.T.plot.bar(figsize(8, 5)) plt.title(英雄 Pick 次数对比) plt.ylabel(picks) plt.tight_layout() plt.savefig(output/pick_compare.png, dpi150) plt.close()这里的pick_matrix.T.plot.bar()是把矩阵转置后按队伍分组画柱状图横轴是队伍图例是不同英雄。5. 完整实战代码5.1 主流程 main.py分析脚本可以单独执行但为了工程化我们把主流程放在main.py里。执行顺序是加载数据 → 统计胜率 → 统计 BP → 生成图表。# main.py import logging from pathlib import Path from data_loader import load_match_data import analysis logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) logger logging.getLogger(__name__) def main() - None: Path(output).mkdir(exist_okTrue) matches load_match_data() logger.info(loaded %d matches, len(matches)) team_stats analysis.build_team_stats(matches) print(team_stats) print() pick_matrix analysis.build_pick_matrix(matches) print(pick_matrix) print() analysis.plot_win_rate(team_stats) analysis.plot_pick_compare(pick_matrix) analysis.plot_gold_curve() logger.info(charts saved to output/) if __name__ __main__: main()5.2 运行方式在项目根目录执行python main.py如果一切正常控制台会输出胜率统计表然后output/目录下会出现三张图片win_rate.pngpick_compare.pnggold_curve.png5.3 预期输出控制台输出示例2025-01-20 21:10:33 - INFO - loaded 3 matches team wins games win_rate 0 NIP 2 3 0.666667 1 WBG 1 3 0.333333 team NIP WBG hero Ahri 1 0 Azir 0 1 Braum 0 1 Corki 0 1 Draven 1 0 Gnar 0 1 Jax 1 0 Jinx 1 0 Karma 0 1 ... 2025-01-20 21:10:34 - INFO - charts saved to output/注意这里的输出内容是基于示例数据生成的不是真实比赛的全部英雄。当你替换成真实数据后结果会自动变化。5.4 把采集结果接入分析流程如果你已经通过爬虫拿到了真实比赛数据可以把scraper.py的解析结果转换成与matches.json相同的结构然后写回到 JSON或者直接传给analysis.build_team_stats()。这样采集和分析代码相对独立后续替换数据源时不需要改动统计逻辑。6. 常见问题与排查思路开发过程中最常遇到的问题集中在请求、编码、解析和图表显示四个方面。我把常见场景整理成了表格方便对照排查。问题现象常见原因解决思路请求页面返回 403缺少 User-Agent被服务器识别为脚本添加浏览器 User-Agent降低请求频率避免短时间内大量请求中文乱码页面是 GBK 编码但按 UTF-8 解析设置resp.encoding resp.apparent_encoding或根据页面 meta 指定编码解析结果为空列表CSS 选择器与真实页面结构不匹配用浏览器开发者工具检查表格的 class 和层级替换选择器JSON 字段缺失报 KeyError数据结构字段名不一致使用dict.get()提供默认值并在清洗阶段统一字段名图表中文显示成方框系统缺少中文字体设置plt.rcParams[font.sans-serif]为系统中文字体如 SimHei 或 Microsoft YaHei找不到 output 目录脚本在其他目录执行在main.py里使用Path(output).mkdir(exist_okTrue)或者用绝对路径一个更隐蔽的问题是运行环境换到 Linux 服务器后字体名可能变成WenQuanYi或Noto Sans CJK SC。这时可以直接修改analysis.py中的字体配置或者安装相应字体包。# Ubuntu 环境示例 sudo apt-get install fonts-wqy-zenhei开发中如果遇到代码报错建议按照以下顺序排查检查 Python 版本是否大于等于 3.9。检查 requests、beautifulsoup4、pandas、matplotlib、numpy 是否安装成功。检查 JSON 文件路径是否正确文件编码是否为 UTF-8。检查分析函数里传入的数据结构是否和matches.json一致。如果是爬虫问题先用浏览器访问目标页面确认页面结构是否正常返回。7. 最佳实践与工程建议7.1 数据获取要规范爬虫不是目的只是为了获取公开数据的手段。在采集任何网站之前都应该先确认以下三点数据是否属于公开可访问内容。网站是否有 robots 协议限制。是否需要申请 API 授权。实际项目中建议对已经获取的页面做好本地缓存避免重复请求。可以把采集结果保存成 JSON 文件后续所有分析都从本地文件读取。这样不仅能减少服务器压力也能让分析过程更快、更稳定。7.2 代码工程化这个项目的代码虽然短但工程化思路是可以复用的。第一日志要详细。logging.info可以记录加载了几条数据、生成了哪些文件排查问题时能快速定位阶段。第二路径要可配置。不要在代码里写死绝对路径使用Path(__file__).parent这样的相对路径更安全。第三函数职责要单一。data_loader只负责读取和清洗analysis只负责统计和画图main只负责串流程。后续如果增加数据库存储、Web 页面展示只需要在main中插入新模块。7.3 安全与合规边界在采集过程中需要注意接口返回的数据不能用于商业用途除非获得授权。如需保存数据到数据库也要做到最小权限避免在生产库上随意建表、删表。如果涉及自动化脚本的长期运行建议增加异常重试机制但在重试时一定要有间隔。例如失败后等待 5 秒再重试最多重试 3 次防止对目标站点造成压力。import time def fetch_with_retry(url: str, retries: int 3, interval: int 5) - str: for attempt in range(retries): try: return fetch_html(url) except Exception as exc: if attempt retries - 1: raise time.sleep(interval * (attempt 1)) raise RuntimeError(unreachable)这段代码在每次失败后增加等待时间属于比较温和的重试策略。7.4 分析模型的扩展方向当统计完胜率和 Pick 率之后下一步可以考虑更复杂的分析比如用逻辑回归预测某支队伍在当前版本下的胜率。用聚类分析发现队伍的英雄池相似度。对不同版本的英雄强度做时间序列分析。不过这些方向都需要更长时间的历史数据单从一个 BO3 是看不出规律来的。数据量越大模型的参考价值越高。8. 总结与下一步这篇文章从一个具体的比赛结果出发完成了一套 Python 赛后数据分析小工具。你学会了用 requests 获取网页、用 BeautifulSoup 解析表格、用 pandas 聚合胜率数据、用 matplotlib 生成可视化图表。现在可以动手做的一件事是把data/raw/matches.json替换成你感兴趣的任意比赛数据然后运行main.py观察输出结果。等到 iG 二番战前的数据准备阶段这套代码还可以继续复用。如果你想进一步增加深度下一步可以学习怎么用pandas做时间序列分析或者用scikit-learn做简单的胜负预测模型。比赛还没开始但数据脚本可以先跑起来。
返回列表