用Python构建Dota2赛前数据准备工具:以Spirit vs Liquid为例

发布时间:2026/9/6 14:09:04
用Python构建Dota2赛前数据准备工具:以Spirit vs Liquid为例 TI2026 瑞士轮阶段会吸引大量 Dota2 玩家和数据爱好者的关注Spirit vs Liquid 这样的强队对阵更是许多人为 BP、观赛和复盘准备资料的对象。相比只看最终胜负更有价值的是在比赛开始前把两支队伍的近期战绩、英雄池、选手状态和常见 BP 倾向汇总成一份可查询、可展示的情报。这篇文章围绕 Spirit vs Liquid 这一对阵场景介绍如何用 Python 搭建一套轻量的赛前数据准备工具覆盖数据获取、字段清洗、特征统计、可视化输出四个环节并加入接口不可用时的本地示例数据兜底方案。这套实现并不复杂但可以复用到其他战队、其他赛事也可以扩展成赛程提醒、阵容对比或赛后复盘工具。实际开发中第一优先级不是把统计模型做得多复杂而是先保证数据来源可靠、字段口径一致、异常分支能处理。以下内容以公开接口和示例数据为基础具体接口字段和访问限制以你实际使用的文档为准。1. 为什么需要围绕比赛做数据准备1.1 比分之外数据能回答什么问题观赛时比分只能告诉你谁赢了。数据能回答的是两队最近在用什么战术、哪条线的状态起伏最大、哪些英雄属于两队都不愿意放出来的点。以 Spirit vs Liquid 这种强队对决为例赛前情报至少包含五个维度近期胜率和胜负走势判断两队当前竞技状态。常用英雄池和禁用倾向为 BP 观察提供背景。近期比赛平均时长判断一血、团战节奏和后期能力。核心选手最近的数据变化比如 KDA、场均死亡、英雄选择。在瑞士轮同阶段、同版本下的战绩避免混入版本完全不同或线上赛性质差异过大的场次。这些信息不承担“预测谁一定赢”的任务更多是帮助你在比赛中快速理解局面为什么某个英雄被先手抢下为什么某支队伍突然改变节奏为什么解说会把某个选手的经济单独拿出来讲。有了数字背景观赛判断会从“感觉这局有希望”变成“这支队伍在近十场天辉方的胜率超过七成所以先选边倾向天辉”。1.2 目标工具包括哪几项能力这篇文章要实现的工具会围绕一场比赛分析场景完成以下能力从公开数据接口拉取指定战队的近期比赛列表。对原始字段做标准化避免把天辉胜利错误当成队伍胜利。过滤无效数据比如未完结比赛、空列表、字段缺失严重的记录。统计胜率、平均时长、英雄出场次数和选手近期数据。输出柱状图、热力图和一份 HTML 报告便于本地查看或直接分享。工具不会直接给出“谁更强”的结论而是提供一组口径清晰、可追溯的指标。后续如果要做预测模型也是基于这套清洗后的数据而不是直接拿原始接口数据训练。1.3 适用读者和前置要求适合以下读者阅读和实操有一定 Python 基础能运行脚本和安装依赖包。了解 pandas 的 DataFrame 基本操作不熟悉也没关系文中代码会给出解释。知道 API 的基本概念比如请求 URL、参数、JSON 响应。对 Dota2 比赛数据感兴趣想做一个能实际运行的数据分析小工具。不需要机器学习背景也不需要提前准备数据库。整个项目在本地 Python 环境中即可跑通比赛数据量也不大个人电脑完全能够处理。2. 环境准备与数据接口选择2.1 运行环境与 Python 依赖建议使用 Python 3.10 或更高版本。较新版本的 pandas 和 matplotlib 在 Python 3.10 以上环境中的兼容性更好也不容易出现numpy版本冲突。第一步创建虚拟环境并安装依赖python -m venv .venv source .venv/bin/activate pip install requests pandas matplotlib jinja2Windows 环境下激活命令是.venv\Scripts\activate依赖包的作用如下requests发起 HTTP 请求获取比赛数据和英雄信息。pandas处理比赛列表完成字段清洗和统计。matplotlib生成胜率对比图和英雄使用热力图。jinja2把统计结果渲染成 HTML 报告方便分享。安装完成后可以用一行命令确认版本避免后续代码在不同大版本下行为不一致python -c import pandas as pd; print(pd.__version__)2.2 常见 Dota2 数据接口做赛前数据准备首先要想清楚数据从哪里来。常见的数据来源有 OpenDota API、Stratz API、Steam Web API。它们各有特点适合不同场景。接口请求方式是否需要密钥适合场景主要风险OpenDota APIREST / JSON公共接口可按需取用个人脚本、社区分析未认证场景限流较严格Stratz APIGraphQL需要 Token更完整的统计指标Token 获取和配额需要确认Steam Web APIREST / JSON需要 Key官方英雄信息、基础比赛数据字段较底层需要二次加工从实现成本来看OpenDota API 的社区资料最丰富返回结构也相对清晰适合作为第一个版本的数据源。Stratz 的指标更细但 GraphQL 的查询写法会多一层学习成本。Steam Web API 算是官方底层接口能做但不适合直接拿来统计胜率因为很多比赛概念需要自己拼接。需要特别提醒接口能力、限流策略和返回结构都会随平台调整。写代码之前先手动请求一次接口把返回 JSON 打印出来确认字段名后再进入开发。2.3 接口字段差异与选型建议同一场比赛在不同接口里返回结构可能不一样。比如 OpenDota 的战队比赛接口中一场比赛通常包含match_id、start_time、duration、radiant、radiant_win、league等字段但有些接口可能把radiant_win替换成winner或者额外返回players子列表。因此代码结构需要做一层标准化而不是在采集函数里写死字段名。推荐的做法是先构建统一的内部 DataFrame 字段比如match_id、start_time、duration、is_radiant、team_win。对不同接口写适配函数把原始字段映射成统一样式。字段缺失时不要直接抛异常而是填充None到清洗阶段再做过滤。这样做的好处是后续切换数据源时只需要改写适配层分析和可视化代码不需要大范围变动。3. 获取 Spirit 与 Liquid 的近期比赛数据3.1 请求结构设计与超时处理以 OpenDota API 为例获取战队比赛列表的请求路径通常是/api/teams/{team_id}/matches。示例代码如下import requests import pandas as pd API_BASE https://api.opendota.com/api def fetch_team_matches(team_id: int, limit: int 20) - list: url f{API_BASE}/teams/{team_id}/matches params {limit: limit} resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()这里有三个设计点需要说明。第一timeout10是必须的。没有超时时间脚本会在网络异常时长时间卡住尤其是在批量请求多支队伍时某个卡住会导致整个流程不可用。第二raise_for_status()会在返回 HTTP 4xx 或 5xx 时抛出异常。这比手动判断resp.status_code更简洁也方便后续统一捕获。第三team_id不是写死固定的。实际使用前先通过战队搜索接口确认 ID比如利用/api/teams返回的team_id字段。直接把 ID 写死在脚本里一旦战队信息变更分析结果就会出错。3.2 本地示例数据兜底公开接口可能因为网络、限流或赛事数据暂未收录而返回空结果。为了让分析代码可以先开发、先验证建议准备一份本地示例数据格式与最终接口返回保持一致。def load_sample_matches() - list: return [ { match_id: 1001, start_time: 1723612800, duration: 2860, radiant: True, radiant_win: True, league: TI2026 Swiss, hero_id: 1, }, { match_id: 1002, start_time: 1723699200, duration: 3120, radiant: False, radiant_win: False, league: TI2026 Swiss, hero_id: 2, }, { match_id: 1003, start_time: 1723785600, duration: 2440, radiant: True, radiant_win: False, league: TI2026 Swiss, hero_id: 3, }, { match_id: 1004, start_time: 1723872000, duration: 2980, radiant: False, radiant_win: True, league: TI2026 Swiss, hero_id: 4, }, { match_id: 1005, start_time: 1723958400, duration: 2710, radiant: True, radiant_win: True, league: TI2026 Swiss, hero_id: 5, }, ]这里的start_time是 Unix 时间戳单位为秒。radiant表示该队伍是否天辉方radiant_win表示天辉方是否胜利。通过这两列才能正确计算队伍胜负这在 3.3 节会详细说明。如果接口暂时不可用主函数可以这样切换数据源def get_matches(team_id: int) - list: try: data fetch_team_matches(team_id) if data: return data except requests.RequestException as exc: print(ffetch matches failed: {exc}) print(use local sample data instead) return load_sample_matches()这种兜底不是让你在生产环境里长期使用而是保证开发阶段流程可以继续跑通避免被外部接口的临时问题阻塞。3.3 数据清洗与字段口径拿到原始比赛列表后不能直接开始算胜率。原始字段只是描述这场比赛还需要转换成“这支队伍是否获胜”的布尔字段。转换逻辑要结合双方阵营。def clean_matches(raw_matches: list) - pd.DataFrame: df pd.DataFrame(raw_matches) required_cols [ match_id, start_time, duration, radiant, radiant_win, league, ] for col in required_cols: if col not in df.columns: df[col] None # Unix 时间戳转 UTC 时间避免丢失时区信息 df[start_time] pd.to_datetime(df[start_time], units, utcTrue) # 队伍是否天辉方 df[is_radiant] df[radiant].astype(bool) # 天辉方胜利 ! 当前队伍胜利 def compute_team_win(row): if row[is_radiant]: return bool(row[radiant_win]) return not bool(row[radiant_win]) df[team_win] df.apply(compute_team_win, axis1) return df这里最关键的是compute_team_win。如果只判断radiant_win那么当 Spirit 或 Liquid 在夜魇一方时胜率会被完全算反。很多初学数据分析的人在这个字段上踩坑原因就是没有区分“天辉胜利”和“所选队伍胜利”。清洗后的 DataFrame 还应该过滤明显无效的记录比如duration小于 0 或缺失的记录以及比赛尚未完成的记录。过滤条件可以单独封装def filter_valid_matches(df: pd.DataFrame) - pd.DataFrame: df df.dropna(subset[match_id, start_time, duration]) df df[df[duration] 0] return df.reset_index(dropTrue)生产环境还需要根据赛事、版本、日期做更严格的过滤学习阶段先保留完整数据即可。4. 赛前情报分析胜率、英雄池与选手状态4.1 胜率与近期状态统计口径胜率是赛前分析最直接的指标但必须明确统计窗口。用最近 10 场和用最近 50 场得到的结论完全不同。def summarize_recent(df: pd.DataFrame, window: int 10) - dict: recent df.tail(window) return { total: len(recent), wins: int(recent[team_win].sum()), win_rate: round(float(recent[team_win].mean()), 2), avg_duration: round(float(recent[duration].mean()), 1), }窗口选择建议如果只有 20 场左右数据用 10 场窗口比较稳定。如果数据量超过 100 场可以同时输出近 5 场、近 10 场、近 20 场三个口径。瑞士轮阶段 BO1 和 BO3 混合只看单场胜负可能低估队伍在系列赛中的调整能力有条件时可以把系列赛结果合并后再统计。胜率之外场均时长也有参考价值。平均时长偏长的队伍通常擅长中期运营和后期团战平均时长较短的队伍则可能更依赖前中期节奏。把胜率和时长放在一起看能初步判断两支队伍的节奏差异。4.2 英雄池与 BP 倾向分析英雄池分析需要把比赛数据里的hero_id映射成英雄名称。若接口直接返回hero_id可以通过英雄列表接口构建映射表。def fetch_heroes() - dict: resp requests.get(f{API_BASE}/heroes, timeout10) resp.raise_for_status() return {hero[id]: hero[localized_name] for hero in resp.json()}然后统计英雄使用频率def hero_usage(df: pd.DataFrame, heroes: dict) - pd.Series: if hero_id not in df.columns: return pd.Series(dtypeint) return ( df[hero_id] .dropna() .map(heroes) .value_counts() .head(10) )需要注意战队比赛列表接口不一定直接返回hero_id。如果原始数据里没有就需要先从比赛列表拿到match_id再请求每场比赛的详情接口从players子列表中取出对应选手的英雄选择。这会显著增加请求次数建议先分析 5 到 10 场再决定是否全量补充。英雄池分析的产出是一份名单比如“近期使用最多的五个英雄”“最近三场没有使用但胜率较高的英雄”。在 BP 观察时这些名单能帮助你判断某个英雄被抢或被禁是否符合两队近期的习惯。4.3 选手近期数据对比选手维度的分析比战队维度多一层需要先获取选手的account_id。比赛详情的players子列表会包含每个选手的账号信息、击杀、死亡、助攻、输出等数据。def fetch_player_recent(account_id: int, limit: int 10) - pd.DataFrame: url f{API_BASE}/players/{account_id}/recentMatches params {limit: limit} resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return pd.DataFrame(resp.json())返回数据中的关键字段可以整理成选手状态摘要近 5 场平均 KDA。场均死亡是否升高死亡升高通常说明个人站位或团队保护存在问题。常用英雄的使用次数和胜率。近几场的辅助、核心位置变化。选手数据请求会成倍放大接口压力。一个合理的策略是把选手 ID 保存到本地文件或 SQLite每天只更新一次而不是每次分析都实时请求全部选手数据。5. 可视化报表输出5.1 用柱状图展示胜率和场均时长可视化不是必须的但在对比 Spirit 和 Liquid 两支队伍时一张柱状图比一堆数字更直观。下面代码生成胜率对比图import matplotlib.pyplot as plt def plot_win_rate(teams: dict): names list(teams.keys()) rates [teams[name][win_rate] for name in names] plt.bar(names, rates, color[#1f77b4, #ff7f0e]) plt.ylim(0, 1) plt.ylabel(win rate) plt.title(Recent Win Rate Comparison) plt.savefig(win_rate.png, bbox_inchestight) plt.close()用plt.close()关闭当前图形避免在脚本循环生成多张图时造成内存累积。bbox_inchestight可以避免标题和坐标轴标签被截断。同样的方式也可以输出场均时长对比图只要把rates替换成avg_duration即可。5.2 用热力图展示英雄使用频率英雄使用热力图的行列可以设计为“队伍 x 英雄”。先构造一个二维矩阵行是 Spirit、Liquid列是英雄名值是出场次数。import numpy as np def plot_hero_heatmap(hero_matrix: pd.DataFrame): fig, ax plt.subplots(figsize(10, 8)) im ax.imshow(hero_matrix, cmapYlGnBu) ax.set_xticks(range(len(hero_matrix.columns))) ax.set_xticklabels(hero_matrix.columns, rotation90) ax.set_yticks(range(len(hero_matrix.index))) ax.set_yticklabels(hero_matrix.index) plt.colorbar(im) plt.savefig(hero_heatmap.png, bbox_inchestight) plt.close()在矩阵中颜色越深表示这支队伍越频繁使用该英雄。如果两队都对某个英雄颜色很深说明这是双方都会关心的共用英雄如果只有一队颜色深则可能成为 BP 时针对的点。实际构造hero_matrix时需要先把hero_id映射成英雄名再对每支队伍做value_counts最后合并成 DataFrame。若某队没有使用过某个英雄对应位置用 0 填充。5.3 生成 HTML 报告当图表生成后可以用jinja2把图片路径和统计数据一起渲染成一份独立 HTML 报告方便直接打开或发给团队其他成员。from jinja2 import Template template Template( html headmeta charsetutf-8title{{ match_title }}/title/head body h1{{ match_title }}/h1 h2胜率对比/h2 img srcwin_rate.png altwin_rate / h2英雄使用热力图/h2 img srchero_heatmap.png althero_heatmap / h2统计摘要/h2 pre{{ summary }}/pre /body /html ) html template.render( match_titleSpirit vs Liquid, summary..., ) with open(report.html, w, encodingutf-8) as f: f.write(html)这样生成的报告不依赖 Jupyter 或其他在线服务在本地双击即可查看。如果是团队使用可以把报告目录挂到静态站点赛前自动刷新。6. 常见报错与排查链路6.1 请求超时和接口限流现象脚本执行到某个请求时长时间无响应或直接抛出Timeout异常。更常见的是请求后返回 HTTP 429。原因本地网络波动或接口同一时间段请求次数过多。排查方式查看响应状态码429 表示限流5xx 表示服务端问题。检查是否在循环里无间隔地请求大量比赛详情。检查是否有多个脚本同时访问同一个接口。处理建议所有请求必须设置timeout。对 429 和 5xx 做退避重试比如第一次等待 2 秒第二次 4 秒第三次 8 秒。批量请求前先取一条记录验证确认结构后再全量拉取。如果有可用的 API Token优先使用认证请求限流阈值会更高。6.2 字段缺失或结构变化现象代码执行到df[radiant_win]或df[hero_id]时抛出KeyError或者统计结果全部为 0。原因接口返回字段名与代码假设不一致或者某些比赛记录缺失字段。排查方式打印原始数据的某一条记录检查实际字段名。对比文档与代码中使用的字段。统计每个字段的非空数量确认哪些字段存在大量缺失。处理建议用df.get(col, None)代替直接索引避免第一行就崩溃。写一个兼容函数同时支持radiant_win和winner等写法。对缺失字段用fillna或直接过滤不要在不确定的情况下默认成 False 或 0。6.3 时区与日期过滤错误现象统计日期看起来少了一天或者过滤近七天数据时结果与预期不符。原因接口返回的start_time是 Unix 时间戳本身是 UTC 时间。如果直接取日期会得到 UTC 日期而不是本地日期。瑞士轮比赛时间以赛程当地时间为准简单加 8 小时也不一定正确因为部分赛区可能使用夏令时或其他时区。排查方式打印start_time转换后的 UTC 日期。确认赛程页面显示的是哪个时区。确认自己希望按哪个时区做日期窗口。处理建议统一先转成 UTC再转目标时区。示例df[start_time].dt.tz_convert(Asia/Shanghai)。做日期过滤时使用和边界不要用字符串包含判断。7. 学习环境与生产环境的分层建议7.1 个人分析脚本的快速运行方式学习阶段建议直接使用本地脚本或 Jupyter Notebook。整个流程可以分成两步第一步用load_sample_matches()跑通清洗、统计、可视化全流程。第二步切换到真实接口但只拉一支队伍、最多 20 场比赛验证字段后再扩展成两支队伍。个人脚本不需要考虑并发、监控、Token 加密但要养成好习惯把战队 ID、请求地址、统计窗口都放到配置变量或环境变量中而不是散落在代码里。7.2 团队定时任务与生产化补充如果要把这个脚本做成每天自动更新的赛前情报工具生产环境还需要补充以下内容使用环境变量或密钥管理服务保存 API Token不要提交到代码仓库。请求结果缓存到 SQLite避免每次打开页面都请求外部接口。定时任务增加锁防止上一个任务没有结束时下一个任务重复启动。采集任务和分析任务分离采集失败不能影响已有分析报告。对接口返回做日志记录字段变化时能第一时间发现问题。报表生成后做一次完整性校验例如检查报告文件和图片是否存在。异常报警至少包含任务名、错误类型、失败阶段和最近一次成功时间。生产级工具的重点不是代码多花哨而是失败时可定位、可恢复、可追踪。8. 最佳实践与扩展方向8.1 发布前检查清单无论个人使用还是团队使用每次发布脚本前建议按下面的清单检查一遍战队 ID 是否来自真实接口而不是写死的占位值。字段口径是否和接口返回一致尤其是radiant_win与team_win的转换。时间窗口是否按 UTC 处理日期过滤是否明确本地时区。是否过滤了未完结比赛和字段缺失严重的记录。API 请求是否设置了超时、退避重试和缓存。图表文件是否成功生成HTML 报告是否包含完整图片路径。Token 和密钥是否在仓库之外。脚本在断网或接口报错时是否能使用兜底数据继续运行。其中最后一条容易被忽略。很多脚本只在“网络正常、接口正常”的理想路径下测试一旦外部数据源不可用整个流程就会直接中断。加入本地示例数据兜底不只是为了开发方便也是给异常链路留一条应急通道。8.2 下一步可以扩展的方向当前实现已经覆盖从数据获取到报表输出的完整流程后续可以按需要扩展接入 Stratz 或赛事站点的更细粒度数据补充 ban/pick、经济曲线、视野得分。把历史比赛保存到数据库积累数据后做趋势分析和胜率预测。结合赛程接口做定时提醒在比赛开始前自动生成双方情报卡片。在 HTML 报告中加入 BP 模拟直观展示两队常用阵容和禁用偏好。对选手位置做更细拆分区分一号位、二号位、五号位状态而不仅仅是队伍整体数据。这套分析工具的核心价值不在预测胜负而在于把信息密度高的比赛数据整理成赛前可看、赛后可复盘的资料。实际项目里先把数据链路做稳定再慢慢增加统计维度比一开始就堆复杂模型更容易落地。