十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DOTA2赛事数据可视化分析实战:从OpenDota API到Python图表

DOTA2赛事数据可视化分析实战:从OpenDota API到Python图表 从 TI15 小组赛 VG 让一追二说起DOTA2 赛事数据可视化分析实战教程TI15 国际邀请赛小组赛阶段VG 对阵 GL 的这场 BO3 打得相当过瘾。VG 首局失利后没有崩心态第二局开始调整节奏最终让一追二成功晋级 2-1 组。尤其是 XM 操刀的滚滚石鳞剑士几波游走节奏直接把 GL 的阵容拉扯得支离破碎这种“游龙”级别的表现赛后自然成为弹幕和论坛的讨论焦点。不过作为一名关注比赛的开发老哥除了看比赛、刷集锦之外我更好奇一个事能不能把比赛里的关键数据抓下来做成可视化分析看看 XM 的滚滚到底“游”在哪一路、“滚”出了多少团队收益这篇文章就围绕“DOTA2 比赛数据可视化分析”这个主题展开。我会结合 VG vs GL 这场 TI15 小组赛的真实背景带大家从比赛 ID 获取、数据分析、关键事件提取到可视化呈现完整走一遍流程。无论你是刚接触 DOTA2 API 的新手还是想写一个赛事复盘小工具的进阶玩家这篇文章都能给你一个可落地的思路。1. 比赛数据分析能做什么1.1 数据回放的价值观赛体验通常停留在“看个热闹”的层面比如谁拿了人头、谁推了塔、哪波团战赢了。但比赛数据能告诉我们更多XM 的滚滚在游戏时间 12 分钟到 20 分钟之间做了多少次有效游走每次游走是否成功逼出对方核心的 BKB 或者传送门VG 让一追二的关键转折点在数据层面有什么异常波动团队经济差、经验差的曲线在哪一分钟出现“剪刀差”这些问题的答案都藏在比赛的详细数据里。通过赛后数据分析我们可以把一场 BO3 拆解成时间轴 地图事件 经济曲线的组合从而更客观地还原比赛进程。1.2 应用场景比赛数据可视化分析并不仅限于职业比赛复盘。它可以延伸到自己开黑后的录像分析看看哪个时间点掉点最严重自定义地图或练习模式的效率评估构建战队数据库长期追踪选手英雄池与胜率关系做赛事内容运营时快速生成数据截图和动态图表。与其在各种数据网站上等别人做好图不如自己动手写一套流程想分析哪场就分析哪场。2. 环境准备与工具链2.1 本教程使用的核心工具为了完成这次分析我建议按下面的工具链来搭建环境Python 3.9用于数据获取、清洗与分析。Requests调用 DOTA2 数据接口。Pandas处理表格化数据尤其是英雄经济和经验曲线。Matplotlib / Seaborn绘制经济曲线、击杀时间轴等静态图。Plotly如果需要交互式的图表可以做 HTML 版本。版本方面不需要太纠结Python 3.8 以上都能跑通。如果你用的是较新的 Python 3.11 或 3.12只要 Pandas 和 Matplotlib 正常安装即可。2.2 数据来源说明DOTA2 比赛数据主要通过 Valve 的官方 API 和第三方数据平台暴露。常用的有两个OpenDota API开源能查比赛详情、选手信息、英雄数据实际开发中用得非常多。STRATZ API基于 GraphQL数据更丰富需要 Token。本文以 OpenDota 的公开接口为例来写代码不需要 Token 就能查询大部分公开比赛数据入门门槛较低。如果你准备长期大量抓取数据建议注册一个 OpenDota 账号并申请 API Key速率限制会更宽松。2.3 目录结构建议先创建一个干净的工程目录后面所有脚本都放在里面dota2-analysis/ ├── main.py # 主流程入口 ├── data/ │ ├── match_data.json # 原始比赛数据 │ └── processed.csv # 处理后的分析数据 └── output/ └── analysis_report/ ├── economy_curve.png ├── event_timeline.png └── xm_roll_track.png在动手写代码之前先确认一下你的 Python 环境里已经装好依赖pip install requests pandas matplotlib plotly装完以后就可以开始写数据获取脚本了。3. DOTA2 比赛数据接口的调用方法3.1 OpenDota API 基本结构OpenDota 提供很多接口但我们的分析主要依赖两个https://api.opendota.com/api/matches/{match_id}这个接口会返回一场比赛的完整数据包括players每个选手的详细数据objectives建筑物摧毁事件teamfights团战数据radiant_win天辉是否获胜duration比赛时长秒match_id 在比赛结束后可在客户端、网页端或者各种数据站中找到。对于已经打过的 TI15 小组赛你可以通过 OpenDota 的搜索接口找到 VG 和 GL 的交手记录。3.2 调用示例先看一个最简单的请求代码用来测试网络连通性和接口返回格式。import requests import json def fetch_match(match_id: int): url fhttps://api.opendota.com/api/matches/{match_id} headers { User-Agent: Mozilla/5.0 (compatible; dota2-analysis/1.0) } resp requests.get(url, headersheaders, timeout30) resp.raise_for_status() return resp.json() if __name__ __main__: # 这里替换成你要分析的比赛 ID test_id 1234567890 data fetch_match(test_id) print(比赛时长:, data.get(duration), 秒) print(天辉胜利:, data.get(radiant_win)) print(json.dumps(data, ensure_asciiFalse)[:500])这里有一个重要提醒比赛 ID 不要乱猜请用真实存在的比赛 ID。你可以先在 OpenDota 官网搜索 VG 或 GL 的战队名找到对应比赛后再填入。3.3 搜索战队比赛记录如果你不知道 VG 和 GL 在 TI15 的具体比赛 ID可以先通过战队搜索接口进行查询。OpenDota 的战队信息接口如下https://api.opendota.com/api/teams/{team_id}/matches不过战队 ID 也需要现查。更直接的办法是用“选手比赛记录”接口https://api.opendota.com/api/players/{account_id}/recentMatches这个接口会返回某位选手最近的比赛记录里面包含 match_id。你可以先找到 XM 的账号 ID然后拉取他的近期比赛过滤出当天对阵 GL 的那一场。import requests def get_recent_matches(account_id: int, limit: int 5): url fhttps://api.opendota.com/api/players/{account_id}/recentMatches params {limit: limit} resp requests.get(url, paramsparams, timeout30) resp.raise_for_status() return resp.json() if __name__ __main__: # 示例账号 ID请替换成 XM 的实际数字 ID account_id 123456789 matches get_recent_matches(account_id) for m in matches[:3]: print(m[match_id], m[hero_id], m.get(kills), m.get(deaths), m.get(assists))这里要注意如果接口返回 429 状态码说明请求频率超过了限制。这时需要增加 sleep 间隔或使用 API Key。4. 比赛关键数据提取与处理4.1 选手数据解构拿到比赛 JSON 后第一步是把 players 数组里的数据提取成结构化表格。OpenDota 的每个玩家对象包含几十个字段我们重点关注以下内容account_id玩家 IDhero_id使用的英雄 IDkills / deaths / assistsKDAgold_per_min每分钟金钱xp_per_min每分钟经验hero_damage英雄伤害tower_damage建筑伤害last_hits补刀数denies反补数item_0 到 item_5装备槽位如果我们要分析 XM 的滚滚表现可以这样处理import pandas as pd def extract_players(match_data): players [] for player in match_data.get(players, []): row { account_id: player.get(account_id), hero_id: player.get(hero_id), kills: player.get(kills), deaths: player.get(deaths), assists: player.get(assists), gold_per_min: player.get(gold_per_min), xp_per_min: player.get(xp_per_min), hero_damage: player.get(hero_damage), tower_damage: player.get(tower_damage), last_hits: player.get(last_hits), denies: player.get(denies), } rows.append(row) return pd.DataFrame(rows) df_players extract_players(match_data) print(df_players)处理后你会看到一个 10 行左右的表格每个英雄一行后面的分析就可以基于这个结构进行。4.2 经济经验曲线提取经济曲线是复盘 BO3 最直观的数据维度。OpenDota 的比赛详情中有一个字段叫“radiant_gold_adv”它记录的是“天辉经济领先值”。如果这个值大于 0说明天辉领先小于 0则夜魇领先。我们可以用这个字段画出全场比赛的经济差走势。import matplotlib.pyplot as plt def plot_gold_advantage(match_data): gold_adv match_data.get(radiant_gold_adv, []) xp_adv match_data.get(radiant_xp_adv, []) minutes [i / 60 for i in range(len(gold_adv))] plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.plot(minutes, gold_adv, label经济领先, color#d62728) plt.axhline(0, colorgray, linestyle--, linewidth0.8) plt.xlabel(分钟) plt.ylabel(天辉 - 夜魇 经济差) plt.title(团队经济走势) plt.grid(True, alpha0.3) plt.subplot(1, 2, 2) plt.plot(minutes, xp_adv, label经验领先, color#1f77b4) plt.axhline(0, colorgray, linestyle--, linewidth0.8) plt.xlabel(分钟) plt.ylabel(天辉 - 夜魇 经验差) plt.title(团队经验走势) plt.grid(True, alpha0.3) plt.tight_layout() plt.savefig(output/economy_curve.png, dpi160) plt.show()这段代码会自动生成两张子图。如果 VG 是在后期完成翻盘你会在经济曲线上看到一个明显的 V 形反转那是判断节奏拐点的核心依据。4.3 事件时间轴提取另一个有价值的数据字段是 objectives它记录了推塔、打肉山等关键事件。我们可以把这些事件投影到时间轴上标注出团队节奏变化。def extract_objectives(match_data): objectives [] for obj in match_data.get(objectives, []): objectives.append({ time: obj.get(time), type: obj.get(type), key: obj.get(key), team: obj.get(team) # 0 或 1对应天辉/夜魇 }) return pd.DataFrame(objectives) df_obj extract_objectives(match_data) print(df_obj.head(20))这里要注意objectives 并不包含击杀事件只包含建筑和 Roshan 事件。如果你想看击杀时间点需要解析每个玩家的 kill 事件列表这部分数据相对复杂后面会介绍一个更简单的方法。5. 可视化呈现把数据变成“剪辑版”5.1 击杀时间线图比赛里的击杀事件可以通过遍历玩家数据中的“kills_log”字段来获取。每个玩家的 kills_log 都记录了击杀时间、被杀目标、是否助攻等信息。把这些数据聚合起来就能得到全场比赛的击杀时间轴。from collections import defaultdict def build_kill_timeline(match_data): kill_events defaultdict(list) for player in match_data.get(players, []): is_radiant player.get(player_slot) 128 for kill in player.get(kills_log, []): minute int(kill[time] // 60) kill_events[minute].append({ team: 天辉 if is_radiant else 夜魇, killer: player.get(name) or player.get(account_id), target: kill.get(target), time: kill[time] }) return kill_events timeline build_kill_timeline(match_data) for minute in sorted(timeline.keys()): print(f第{minute}分钟: {len(timeline[minute])}个击杀)这里有一个关键细节player_slot 小于 128 表示天辉大于等于 128 表示夜魇。这个判断标准是 OpenDota 的固定规则不要搞反。5.2 绘制团队击杀对比柱状图我们可以把击杀时间轴画成按分钟分组的柱状图直观对比天辉和夜魇的节奏。def plot_kill_comparison(timeline, duration_minutes): radiant_kills [0] * (duration_minutes 1) dire_kills [0] * (duration_minutes 1) for minute, events in timeline.items(): for ev in events: if ev[team] 天辉: radiant_kills[minute] 1 else: dire_kills[minute] 1 minutes list(range(duration_minutes 1)) plt.figure(figsize(14, 5)) plt.bar(minutes, radiant_kills, label天辉击杀, color#2ca02c, alpha0.7) plt.bar(minutes, dire_kills, label夜魇击杀, color#d62728, alpha0.7, bottomradiant_kills) plt.xlabel(分钟) plt.ylabel(击杀数) plt.title(按分钟击杀分布) plt.legend() plt.grid(True, axisy, alpha0.3) plt.savefig(output/kill_timeline.png, dpi160) plt.show()这种方式虽然简单但在复盘时非常实用。比如 XM 的滚滚在 12 分钟、16 分钟、22 分钟各带了一波节奏你会在柱状图上看到对应分钟区间内天辉击杀柱突然变高。5.3 选手游走路径还原基础版要真正做“游走路线”的可视化需要用到比赛内的 position 数据也就是玩家在地图上的坐标。OpenDota 接口不直接提供完整的逐秒坐标但有一部分字段可以通过match_data[players][i][times]和[positions]拿到。这些字段不一定在每次接口调用中都返回如果缺失我们可以用团战数据来近似还原 XM 的主要活动区域。def extract_tf_positions(match_data, account_id): positions [] for tf in match_data.get(teamfights, []): for player in tf.get(players, []): if player.get(account_id) account_id: positions.append({ time: tf.get(start), x: player.get(pos_x), y: player.get(pos_y), deaths: player.get(deaths), kills: player.get(kills) }) return pd.DataFrame(positions)拿到这些位置后就可以在 DOTA2 地图底图上叠加散点分析 XM 在关键团战发生时的站位。如果你有更强的算法基础还可以进一步做热力图分析他的高频活动区域但作为一篇入门实战教程先做到散点标注已经足够。6. 完整实战VG vs GL 比赛分析脚本6.1 准备比赛 ID假设你已经从 OpenDota 上找到了 VG 对阵 GL 的那场小组赛 match_id。下面我们写一个综合脚本把前面几个功能整合成一条命令执行。完整的代码结构如下import requests import pandas as pd import matplotlib.pyplot as plt from collections import defaultdict # ---------- 数据获取 ---------- def fetch_match(match_id): url fhttps://api.opendota.com/api/matches/{match_id} resp requests.get(url, timeout30) resp.raise_for_status() return resp.json() # ---------- 玩家数据提取 ---------- def extract_players(match_data): players [] for p in match_data.get(players, []): players.append({ 玩家ID: p.get(account_id), 英雄ID: p.get(hero_id), 击杀: p.get(kills), 死亡: p.get(deaths), 助攻: p.get(assists), GPM: p.get(gold_per_min), XPM: p.get(xp_per_min), 英雄伤害: p.get(hero_damage), 建筑伤害: p.get(tower_damage), 补刀: p.get(last_hits), 反补: p.get(denies), }) return pd.DataFrame(players) # ---------- 经济曲线 ---------- def plot_economy(match_data): gold_adv match_data.get(radiant_gold_adv, []) xp_adv match_data.get(radiant_xp_adv, []) minute [i / 60 for i in range(len(gold_adv))] fig, axes plt.subplots(2, 1, figsize(12, 8)) axes[0].plot(minute, gold_adv, color#d62728) axes[0].axhline(0, colorgray, linestyle--, linewidth0.8) axes[0].set_title(经济领先走势) axes[0].set_ylabel(经济差) axes[0].grid(True, alpha0.3) axes[1].plot(minute, xp_adv, color#1f77b4) axes[1].axhline(0, colorgray, linestyle--, linewidth0.8) axes[1].set_title(经验领先走势) axes[1].set_ylabel(经验差) axes[1].set_xlabel(比赛分钟) axes[1].grid(True, alpha0.3) plt.tight_layout() plt.savefig(output/economy_curve.png, dpi160) print(经济曲线图已保存到 output/economy_curve.png) # ---------- 击杀时间轴 ---------- def build_timeline(match_data): timeline defaultdict(list) for p in match_data.get(players, []): team 天辉 if p.get(player_slot) 128 else 夜魇 for kill in p.get(kills_log, []): minute int(kill[time] // 60) timeline[minute].append({team: team, time: kill[time]}) return timeline # ---------- 主流程 ---------- if __name__ __main__: MATCH_ID 1234567890 # 替换成真实比赛 ID match_data fetch_match(MATCH_ID) df_players extract_players(match_data) print( 选手数据 ) print(df_players) plot_economy(match_data) timeline build_timeline(match_data) print( 按分钟击杀统计前10分钟 ) for minute in sorted(timeline.keys())[:10]: print(f第{minute}分钟: {len(timeline[minute])}个击杀)6.2 运行验证在命令行运行python main.py如果网络正常你会看到按玩家列出的 KDA、GPM、XPM 表格同时在 output 目录下生成经济曲线图。整体运行时间取决于接口响应速度和比赛数据量一般 10 秒到半分钟不等。6.3 预期输出说明虽然每场比赛的数据不一样但结构是固定的。以 VG 让一追二那场为例如果第二局是翻盘局你会看到经济差曲线从 20 分钟开始持续走低30 分钟后突然拉升最终在 40 分钟附近由负转正。这说明 VG 在中后期通过几波关键团战打了回来。如果你同时对比 XM 的 kills_log发现他在经济曲线拐点前后出现了多次击杀参与那就基本可以判断滚滚在中期带动了团队节奏是翻盘的重要支点。7. 常见问题与排查思路7.1 接口请求报 403 或 429这是 OpenDota 最典型的限流问题。403 通常是因为没有带 User-Agent 或者触发了防护策略429 则明确表示请求过于频繁。解决思路是降低请求频率或者申请 API Key。建议在代码里加上简单的重试机制。import time def fetch_match_with_retry(match_id, retries3): for i in range(retries): try: return fetch_match(match_id) except requests.exceptions.HTTPError as e: if e.response.status_code 429: wait_time 5 * (i 1) print(f触发限流等待 {wait_time}s 后重试) time.sleep(wait_time) else: raise7.2 比赛数据中没有 players 字段通常是因为比赛类型问题。OpenDota 对部分非排位或 bot 比赛的数据覆盖不完整。建议优先选择完整的职业比赛因为职业比赛的详情解析度最高。7.3 坐标数据为空正如前面所说position 数据不一定每次都能拿到。如果坐标为空不要硬画路线图可以退化为团队经济和击杀时间轴分析效果同样有说服力。7.4 比赛 ID 无效比赛 ID 是 DOTA2 客户端的展示 ID注意不要从网页源码中直接复制拼接特殊字符。建议通过 OpenDota 的搜索接口确认后再使用。问题现象常见原因解决思路403 Forbidden缺少 User-Agent 或被防火墙拦截添加请求头降低请求频率429 Too Many Requests无 API Key请求频率过高申请 API Key 或增加 sleep数据缺少 positions该比赛未保存完整坐标改用经济差 击杀时间轴比赛 ID 找不到ID 输入错误或比赛类型不支持用选手 recentMatches 接口验证绘图中文乱码Matplotlib 默认字体不支持中文配置中文字体例如 SimHei8. 工程化与最佳实践8.1 把比赛 ID 做成参数不要每次改代码里的 match_id。建议用命令行参数或配置文件来管理。python main.py --match-id 1234567890 --output ./output这样做的好处是你可以把同一个脚本复用到所有比赛分析中不必保存多个副本。8.2 数据缓存OpenDota 的接口虽然免费但请求量过大会影响可用性。建议把每次请求到的原始 JSON 保存到本地后续分析直接读取本地文件。import json import os def save_raw_data(match_data, pathdata/match_data.json): os.makedirs(data, exist_okTrue) with open(path, w, encodingutf-8) as f: json.dump(match_data, f, ensure_asciiFalse)后续如果代码调试出错只需读取文件不需要再次请求接口。8.3 代码分层把“数据获取”“数据处理”“可视化”拆成独立模块是工程化分析脚本的关键。这样即使将来换用 STRATZ API也只需要替换数据获取层不需要重写可视化逻辑。8.4 注意合理使用与版权边界分析公开比赛数据用于学习交流没有问题但如果要发布商业化报告请注意数据平台的条款要求。同时不要在程序里加入任何自动获取账号权限、绕过限制的逻辑。9. 总结与下一步拓展本文从 TI15 小组赛 VG 让一追二 GL 的比赛切入完整展示了基于 OpenDota API 的 DOTA2 比赛数据可视化分析流程。包括比赛数据获取、KDA 与 GPM 等指标提取、团队经济差走势绘制、按分钟击杀时间轴可视化等内容。你可以把这套思路扩展到任意一场比赛不一定是 TI 小组赛也可以是路人局的录像复盘。只要拿到 match_id脚本就能直接复跑。接下来可以考虑几个方向把多场比赛数据整合到 SQLite建立战队长期数据库引入 STRATZ GraphQL 接口获取更精细的插眼、游走、补刀差异数据使用 Plotly 生成交互式 HTML 报告方便在浏览器里点选查看每一分钟的事件加上选手历史英雄池分析观察对手在特定英雄上的胜率变化。建议你先拿 VG 对阵 GL 的比赛跑通一次再找另一场 BO3 验证脚本的通用性。数据分析和看比赛一样多复盘几场才能发现真正有意思的规律。
返回列表