
如果你问老粉丝“是否还会想起曾经那只EDG”大概率会得到一段很长的故事2015 年的 MSI、2021 年冰岛的 BO5、双冠王之后的阵容更替、厂长到 Meiko 的队魂传承。这些记忆在社交平台上是情感和话题但从技术人角度看它完全可以被拆成一组可查询、可统计、可可视化的数据对象比赛日期、对手、蓝红方、英雄选择、B/P 顺序、经济曲线、推塔数、龙魂归属、MVP 归属、版本号。本文不以怀旧为目的而是把“EDG 战队历史复盘”当作一个典型的数据分析项目来做落地一套本地方便重复运行的赛事数据复盘工具。流程上会覆盖数据获取、清洗、统计分析、可视化、批量更新、接口化访问和常见问题排查适合熟悉 Python、想用真实体育电竞场景练手数据分析的开发者参考。先说明一点这个项目不是传统意义上的 AI 模型部署没有显存、CUDA、推理步骤这些概念。它的核心是“数据怎么拿、怎么存、怎么算、怎么展示”。EDG 之所以适合作为案例是因为它的历史跨度足够长比赛数据量大且公开可查夺冠周期、版本更替、选手转会都能形成清晰的统计特征。你可以把本文给出的框架换到任何一支战队、任何一届赛事上只需要替换数据源即可。1. 核心能力速览能力项说明项目类型公开赛事数据复盘与分析工具核心场景EDG 战队历史比赛数据整理、胜率趋势、英雄池、选手表现、版本影响分析数据来源公开赛事数据站点、出版社提供的赛事存档、官方比赛记录页需自行确认授权与合规开发语言Python 3.9主要依赖requests、pandas、numpy、matplotlib、plotly、Flask启动方式命令行脚本 Jupyter Notebook / Flask 本地仪表盘是否支持 API支持本地 Flask 服务可输出 JSON 接口是否支持批量任务支持按赛季或赛程批量抓取、批量清洗、批量导出输出格式CSV、Excel、Markdown 报告、HTML 可视化页面适合场景电竞数据分析、赛事复盘、数据可视化练习、本地轻量 Web 服务搭建需要强调的是不同数据源的接口规则差异很大。本文提供的是通用工程框架实际使用时你要根据所选公开数据源调整字段解析逻辑不要照搬固定请求地址。2. 适用场景与使用边界这个项目适合三类读者。第一类是 EDG 老粉丝想用一种更“硬核”的方式回顾战队变化比如统计厂长期间的胜率、Scout 登场后中单英雄池变化、S11 夺冠赛季的经济曲线规律。第二类是数据分析初学者想找一套真实的、有语境的数据练手而不是反反复复分析鸢尾花或房价数据。第三类是赛事内容作者做赛前前瞻、赛后退伍复盘时需要从数据维度补充素材。用真实电竞数据做分析边界非常重要。EDG 比赛记录、选手姓名、英雄选择、击杀助攻等数据属于赛事公开信息在个人学习、非商业研究和合理引用范围内使用风险较低。但要注意几点不要大规模爬取并批量分发原始数据不要使用非公开渠道获取的内部数据涉及选手肖像、视频画面、解说音轨时要遵循平台版权要求写文章、做视频、做商业报告时需要标注数据来源必要时获得授权。尤其不要把这些数据用于任何形式的投注辅助或赌博预测这是法律红线。从技术上看这个项目还不适合做什么不适合做实时比分推送因为比赛流数据需要专门的数据通道公开页面不一定稳定不适合做选手个人隐私分析比如选手实况位置、个人社交信息等这类数据不在项目范围内也不适合直接当作生产级数据中台它只是一个本地分析工具缺少任务调度、错误队列、监控告警等组件。3. 环境准备与项目结构设计开始写代码之前先把项目目录规划好。数据类项目最怕“跑完一次下次找不到数据文件”。下面是一套推荐的项目结构edg_analysis/ ├── data/ │ ├── raw/ # 原始数据文件按赛季存放 │ ├── processed/ # 清洗后的结构化数据 │ └── output/ # 统计结果、图表、报告 ├── src/ │ ├── fetcher.py # 数据获取模块 │ ├── cleaner.py # 数据清洗模块 │ ├── analyzer.py # 统计指标模块 │ ├── exporter.py # 导出报告模块 │ ├── webapp.py # Flask 可视化服务 │ └── config.py # 公共配置 ├── notebooks/ │ └── edg_review.ipynb # 分析探索用 Notebook ├── requirements.txt └── README.md目录设计的基本原则是原始数据不动清洗数据单独存放输出文件按时间或版本命名。这样可以反复追溯数据变更不会因为一次重新抓取而破坏历史分析结果。Python 环境建议使用虚拟环境避免和系统 Python 冲突。下面给出依赖清单按需安装pip install requests pandas numpy matplotlib plotly flask tqdm如果你是第一次做类似项目建议把这些库全部装上。requests 负责请求公开数据页面pandas 负责结构化处理matplotlib 和 plotly 负责静态图和交互图flask 用来提供本地接口tqdm 用来观察批量抓取进度。操作系统方面Windows、macOS、Linux 都可以Python 脚本本身是跨平台的。唯一要注意的是文件路径分隔符建议在代码里统一使用pathlib处理别直接写死\或/。4. 数据获取以公开赛事记录为起点数据获取是整个项目的基础。EDG 的比赛数据主要包含三个层级战队级数据赛季胜场、排名、BO5 战绩比赛级数据每一局的蓝红方、时长、击杀、经济、推塔数选手级数据KDA、分均经济、英雄使用次数。如果目标只是做战队视角的分析优先拿比赛级数据如果要细化到选手状态则要补充选手级字段。下面给出一段通用请求框架用来获取公开赛事页面并解析比赛列表。注意具体 URL 和页面结构取决于数据源本文代码是示意模板你需要根据实际页面调整选择器或 JSON 字段。import requests import pandas as pd from pathlib import Path # 通用请求模板实际 URL 需要替换为公开合法数据源 DATA_URL https://example.com/api/team_matches PARAMS { team: EDG, season: 2024, } def fetch_match_list(season: str) - pd.DataFrame: params PARAMS.copy() params[season] season response requests.get( DATA_URL, paramsparams, headers{User-Agent: Mozilla/5.0}, timeout15, ) response.raise_for_status() # 这里根据实际返回格式解析可能是 JSON 也可能是 HTML # 以 JSON 为例 data response.json() records data.get(matches, []) return pd.DataFrame(records) if __name__ __main__: raw_dir Path(data/raw) raw_dir.mkdir(parentsTrue, exist_okTrue) df fetch_match_list(2024) df.to_csv(raw_dir / matches_2024.csv, indexFalse, encodingutf-8-sig) print(df.head())这段代码里面有几个关键点请求头尽量带上浏览器 UA避免部分站点拒绝空 UA 请求。超时时间要设置不能无限等待。返回后立刻存储为原始文件不要直接做清洗保留现场。每个赛季一个 CSV文件名里带上赛季信息方便后续回溯。如果你选的数据源不是 JSON API而是 HTML 页面那就要用到BeautifulSoup或lxml做解析。这里给一个通用的 HTML 解析思路from bs4 import BeautifulSoup def parse_match_rows(html_text: str): soup BeautifulSoup(html_text, lxml) table soup.find(table, class_match-table) if not table: return [] rows [] for tr in table.select(tbody tr): cells [td.get_text(stripTrue) for td in tr.find_all(td)] if cells: rows.append(cells) return rows关于抓取频率建议每次请求之间至少间隔 1 到 2 秒连续大批量抓取时使用随机延时避免对目标站点造成压力。批量抓取时还要记录日志至少包含请求 URL、状态码、抓取条数和失败原因。5. 数据清洗与字段标准化原始数据拿到后不要直接用因为不同来源的字段格式差异很大。以 EDG 的比赛记录为例你会遇到这些常见问题比赛日期格式不统一有的是2024.06.15有的是2024-06-15还有的带星期。英雄名称缩写不一致比如“皇子”可能写成“嘉文四世”、“J4”。战绩字段是字符串12-3-8需要拆成击杀、死亡、助攻三列。比赛结果有“胜利”、“失败”、“胜”、“负”、“W”、“L”多种写法。部分场次缺少英雄选择数据需要标记为空值不能直接删除整行。清洗的核心思路是先定义标准字段再统一转换。下面是一套适用于 EDG 比赛记录的标准化结构# 标准字段定义 STANDARD_COLUMNS [ match_id, # 比赛唯一标识 date, # 比赛日期格式 YYYY-MM-DD season, # 赛季 event_name, # 赛事名称如 LPL 夏季赛 stage, # 阶段常规赛/季后赛/世界赛 opponent, # 对手战队名 side, # 蓝方/红方 result, # win/loss game_duration, # 比赛时长秒 kills, # 总击杀 deaths, # 总死亡 assists, # 总助攻 towers, # 推塔数 dragons, # 小龙数 barons, # 大龙数 gold, # 结束经济 team_hp, # 战队缩写 player_names, # 出场选手 ]清洗模块可以做成分步函数每个函数只处理一个职责import pandas as pd def normalize_result(value: str) - str: mapping { 胜利: win, 胜: win, W: win, WIN: win, 失败: loss, 负: loss, L: loss, LOSS: loss, } return mapping.get(str(value).strip(), unknown) def split_kda(kda_str: str): if pd.isna(kda_str): return None, None, None parts str(kda_str).replace( , ).split(-) if len(parts) ! 3: return None, None, None return parts[0], parts[1], parts[2] def clean_matches(raw_df: pd.DataFrame) - pd.DataFrame: df raw_df.copy() df[date] pd.to_datetime(df[date], errorscoerce).dt.strftime(%Y-%m-%d) df[result] df[result].apply(normalize_result) df[[kills, deaths, assists]] df[kda].apply( lambda x: pd.Series(split_kda(x)) ) df df.dropna(subset[match_id, date]) return df清洗完的数据要统一保存到data/processed目录命名可以带“处理时间”或“数据版本”clean_df.to_csv(data/processed/edg_matches_2024_clean.csv, indexFalse, encodingutf-8-sig)这一步做完以后后面所有的统计和可视化都基于这份标准数据不要每次分析都重新清洗。6. 统计指标设计与分析示例数据清洗完成后可以开始定义分析指标。针对 EDG 战队历史复盘建议按四个维度来做战队维度、比赛维度、版本维度、选手维度。战队维度最常用的指标是总胜率、近 10 场胜率、主客场蓝红方胜率、打满 BO5 的胜率。这些指标能快速回答“EDG 在某个赛季到底是强队还是弱队”“红色方胜率是不是更低”之类的问题。比赛维度关注节奏和终结能力比如场均时长、场均击杀、大龙控制率、一血胜率、经济领先时的胜率。比较有意思的一个指标是“经济领先但输掉比赛的比例”这个指标能反映战队在中期运营和团战执行的稳定性。版本维度要结合比赛日期所在的版本号来统计比如“12.18 版本下 EDG 的胜率”和“13.20 版本下 EDG 的胜率”。LCK、LPL 等联赛的数据源不一定直接提供版本号需要根据比赛日期和版本更新时间做映射。这个映射表可以手工维护也可以从公开的版本更新记录页抓取。选手维度主要看登场次数、整体 KDA、分均补刀、英雄池宽度、MVP 次数。通过“选手出战 vs 未出战时战队胜率”的对比可以做一个很基础的轮换效果分析。下面给出一个统计胜率趋势的 Python 示例def win_rate_by_stage(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[is_win] (df[result] win).astype(int) grouped df.groupby(stage).agg( matches(match_id, count), wins(is_win, sum), ).reset_index() grouped[win_rate] grouped[wins] / grouped[matches] return grouped # 按季度或月份聚合观察趋势 def win_rate_by_month(df: pd.DataFrame) - pd.DataFrame: df df.copy() df[month] pd.to_datetime(df[date]).dt.to_period(M) df[is_win] (df[result] win).astype(int) grouped df.groupby(month).agg( matches(match_id, count), wins(is_win, sum), ).reset_index() grouped[win_rate] grouped[wins] / grouped[matches] return grouped输出结果是一个 DataFrame可以直接在 Notebook 里展示也可以转成 CSV 存档。做统计时要注意样本量如果某个月只有 3 场比赛胜率从 100% 变成 33%统计意义有限。建议在可视化图表上标注样本量或者对低样本月份做降权处理。7. 可视化从表格到趋势图数据只停留在表格上很难引起共鸣尤其是 EDG 这种时间跨度长的战队趋势图比数据表直观得多。下面用 matplotlib 画一个按月胜率变化的图同时标注比赛场次。import matplotlib.pyplot as plt import pandas as pd def plot_monthly_win_rate(df: pd.DataFrame, save_path: str data/output/win_rate.png): monthly win_rate_by_month(df) monthly[month_str] monthly[month].astype(str) fig, ax1 plt.subplots(figsize(14, 6)) ax1.plot( monthly[month_str], monthly[win_rate], markero, linewidth2, labelWin Rate, ) ax1.set_ylabel(Win Rate) ax1.set_ylim(0, 1.05) ax1.set_title(EDG Monthly Win Rate Trend) ax2 ax1.twinx() ax2.bar( monthly[month_str], monthly[matches], alpha0.25, colorgray, labelMatch Count, ) ax2.set_ylabel(Match Count) fig.autofmt_xdate(rotation45) fig.tight_layout() plt.savefig(save_path, dpi150) plt.show()如果希望图表支持交互可以改用 plotlyimport plotly.express as px def plot_win_rate_interactive(df: pd.DataFrame): monthly win_rate_by_month(df) fig px.line( monthly, xmonthly[month].astype(str), ywin_rate, titleEDG Monthly Win Rate Trend, labels{x: Month, win_rate: Win Rate}, ) fig.add_bar( xmonthly[month].astype(str), ymonthly[matches], nameMatch Count, yaxisy2, opacity0.3, ) fig.update_layout(yaxis2dict(overlayingy, sideright)) fig.write_html(data/output/win_rate_interactive.html) fig.show()可视化输出要同时保留.png和.html前者可以放进 PPT 或文档后者方便在浏览器里交互查看。除了胜率趋势值得做的图还有EDG 在不同对手下的胜率热力图、A 级比赛中英雄选择频率条形图、每场比赛经济差随时间变化的走势图。这些图并不复杂但能很直观地反映战队风格。8. 接口 API 与批量更新能力本地分析做完以后如果还想把结果接进自己的博客、小程序或工作流就需要把统计分析封装成 API。用 Flask 做一个轻量接口输入赛季参数返回该赛季的统计结果 JSON。from flask import Flask, jsonify, request import pandas as pd app Flask(__name__) DATA_PATH data/processed/edg_matches_clean.csv def load_clean_data(): return pd.read_csv(DATA_PATH, encodingutf-8-sig) app.route(/api/edg/stats, methods[GET]) def get_stats(): season request.args.get(season) df load_clean_data() if season: df df[df[season] season] if df.empty: return jsonify({error: no data}), 404 total len(df) wins int((df[result] win).sum()) loss total - wins win_rate round(wins / total, 4) if total else 0 result { season: season, total_matches: total, wins: wins, losses: loss, win_rate: win_rate, } return jsonify(result) if __name__ __main__: app.run(host127.0.0.1, port8000, debugFalse)启动接口服务python src/webapp.py访问测试curl http://127.0.0.1:8000/api/edg/stats?season2024预期返回类似{ season: 2024, total_matches: 48, wins: 28, losses: 20, win_rate: 0.5833 }这里要说明接口路径、参数名、返回结构都是自定义的正式使用时要按你的数据源和业务需求调整。这个模板的意义在于把分析能力变成可复用服务之后可以做更多接口比如/api/edg/heroes返回英雄使用频率/api/edg/players返回选手 KDA。批量更新方面可以写一个定时任务脚本按赛季循环抓取并全量重算from src.fetcher import fetch_match_list from src.cleaner import clean_matches from src.analyzer import win_rate_by_stage SEASONS [2021, 2022, 2023, 2024] def batch_update(): all_records [] for season in SEASONS: raw_df fetch_match_list(season) clean_df clean_matches(raw_df) clean_df[season] season all_records.append(clean_df) print(f[OK] {season} matches: {len(clean_df)}) combined pd.concat(all_records, ignore_indexTrue) combined.to_csv(data/processed/edg_matches_clean.csv, indexFalse) print([DONE] batch update finished)批量更新时一定要处理失败重试。单赛季抓取失败不应该让整个任务中断常见做法是捕获异常并记录跑完所有赛季后统一输出失败列表。更稳妥的做法是把抓取任务拆成独立子进程或使用队列但本地项目里先保证日志完整即可。9. 资源占用、缓存与性能观察这个项目不涉及 GPU 显存但数据抓取和分析仍然有性能瓶颈。第一是网络请求耗时。如果一次性抓取 10 个赛季的数据每个赛季几十场比赛单场请求一次加上延时和超时重试整体耗时可能达到几分钟到十几分钟。解决办法是增加本地缓存第一次抓取的结果存成 CSV 或 JSON后续分析优先使用本地文件只有当需要更新时才重新抓取。第二是 pandas 内存占用。EDG 的全部比赛数据量不会太大几十万行以内 pandas 都能轻松处理。但如果你扩展到了全球所有战队所有赛季行数会成倍增长这时候就要引入分块读取或 SQLite 存储。下面给出一个简单的 SQLite 使用思路import sqlite3 def save_to_sqlite(df: pd.DataFrame, db_path: str edg.db): conn sqlite3.connect(db_path) df.to_sql(matches, conn, if_existsreplace, indexFalse) conn.close()第三是可视化渲染耗时。plotly 生成的 HTML 文件如果包含大量数据点浏览器打开会卡顿。建议对图表数据做降采样或者只展示最近 N 个月的趋势。本地观察性能时可以使用 Python 的time模块记录每一步耗时import time start time.time() raw_df fetch_match_list(2024) print(ffetch time: {time.time() - start:.2f}s)把耗时信息统一写入logs/performance.log方便后续对比不同网络环境和数据源下的表现。还有一个容易被忽略的问题进程残留。Flask 服务如果没正常关闭会占用端口下次启动时提示端口冲突。检查端口占用可以用系统命令# Windows netstat -ano | findstr :8000 # Linux/macOS lsof -i :8000发现问题后把对应进程结束掉或者修改 Flask 的启动端口。10. 常见问题与排查方法问题现象可能原因排查方式解决方案请求返回 403请求头不完整或触发站点反爬检查返回状态码和响应内容补充浏览器 UA、增加延时、检查来源是否允许爬取数据解析为空页面结构变化或选择器失效打印响应 HTML 片段更新 BeautifulSoup 选择器或改用 JSON 数据源日期字段全是 NaT日期格式不统一打印原始日期值使用 pandasto_datetime的format参数或先手工清洗胜率计算偏高/偏低result 字段映射错误抽查原始结果字段值完善normalize_result映射表批量任务中途卡住单赛季请求超时未捕获查看日志中最后一条成功记录捕获超时异常加入重试机制失败后跳过继续Flask 接口返回 404数据文件路径不对或赛季参数无数据检查DATA_PATH是否存在调整路径或补充“data not found”提示图表中文乱码matplotlib 缺少中文字体查看 PDF 或 PNG 输出安装中文字体并设置plt.rcParams更新数据后旧报告被覆盖输出文件名固定检查输出目录命名规则文件名加入时间戳或版本号CSV 打开乱码编码问题检查文件编码encodingutf-8-sig写出Excel 可直接打开数据源字段命名变化上游结构变更对比最近一次成功数据维护字段映射配置不要写死在代码里下面给出 matplotlib 中文字体设置示例import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False处理“批量任务卡住”的一个实用技巧在循环里加入超时包装用tqdm显示进度同时把每个赛季的抓取状态写入日志from tqdm import tqdm for season in tqdm(SEASONS, descFetching seasons): try: raw_df fetch_match_list(season) raw_df.to_csv(fdata/raw/matches_{season}.csv, indexFalse) except Exception as exc: print(f[FAIL] {season}: {exc})这样即使某个赛季失败你也能在终端明确看到失败位置而不是整个程序卡死。11. 最佳实践与合规使用建议这个项目虽然技术难度中等但如果要长期维护有一些工程习惯值得建立。第一数据文件分开管理。原始抓取文件、清洗后文件、图表报告文件分别放在raw、processed、output目录。每次更新数据前对旧文件备份避免误覆盖。建议在文件名中附带日期或版本号比如edg_matches_2024_20250115.csv。第二把请求参数、文件路径、字段映射统一放到config.py不要散落在各个脚本里。后续要换数据源或改字段时只改一个文件。# config.py DATA_RAW_DIR data/raw DATA_PROCESSED_DIR data/processed OUTPUT_DIR data/output USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 REQUEST_INTERVAL 1.5 SEASONS [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024]第三批量抓取要遵守数据源规则。优先查看站点是否提供官方 API、robots 协议或开放数据说明。如果站点明确禁止抓取不要强行绕过本项目所有代码都只适用于你有权访问的公开数据。第四涉及选手姓名、比赛画面、商标标识等内容时注意标识使用边界。EDG 战队名称、Logo 是俱乐部商标资产分析报告中出现队名合理但不能做误导性使用、不能暗示官方合作。第五发布分析结论时不要过度解读。不要因为“近 10 场胜率下降”就断言队伍内部有矛盾也不要因为“某个英雄胜率 100%”就推断是版本答案。样本量、对手强度、版本变化都会影响数据写结论时标注统计口径比较稳妥。第六如果要部署 Flask 接口到公网必须做访问控制。至少要绑定127.0.0.1在受信任的内网环境里使用如果一定要公网访问建议加简单 Token 校验或使用反向代理限制来源 IP。不要把本地服务直接暴露在公网上避免数据被滥用。12. 总结与下一步这篇文章从“EDG 往事”这个话题出发构建了一套完整的赛事数据复盘工具链环境准备、项目结构、数据获取、清洗标准化、统计指标、可视化、Flask API、批量更新和性能排查。整套方案不依赖 GPU不依赖特定云服务一台普通电脑、一份公开赛事数据源、Python 环境就可以跑起来。如果你只想做一步建议先从“拉取一个赛季的比赛记录”开始。验证逻辑很简单能拿到原始数据、能把它整理成标准字段、能算出一个赛季的胜率。这三步跑通后面所有功能都是增量叠加。最容易踩的坑有两个一是数据源页面结构变化导致解析失败解决思路是尽量找结构化 JSON 接口少用 HTML 解析二是批量任务没有日志和重试机制一个赛季失败就中断整个流程。把异常处理和日志记录提前做好比多写十个统计指标更重要。接下来可以扩展的方向很明确加入更多战队的横向对比比如同时分析 EDG、RNG、JDG 的历史数据自动生成赛季全景报告把选手转会时间线纳入数据观察阵容变动对胜率的影响也可以把分析模型换成更复杂的机器学习分类器尝试从比赛早期数据预测最终胜负。每次扩展都会遇到新问题但底层的数据规范和分析框架可以复用这才是这个项目真正的价值所在。