尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Dota2数据分析实战:从Steam Web API到英雄胜率与阵容熵值

发布时间:2026/9/12 3:00:47

资讯中心
01
ARTICLE

Dota2数据分析实战:从Steam Web API到英雄胜率与阵容熵值

Dota2数据分析实战:从Steam Web API到英雄胜率与阵容熵值
简介面向计算机科学、人工智能、大数据、电子信息等专业学生及游戏数据分析爱好者提供一份Dota2游戏数据分析的完整源码项目。该项目紧密贴合课程设计、期末大作业与毕业设计场景代码经严格调试下载后即可运行适合已有一定Python和数据分析基础的学习者参考。项目围绕比赛详情数据展开包含数据读取、预处理、特征统计与结果输出的完整流程涉及JSON格式数据解析以及胜率、击杀死亡助攻比等关键指标计算可帮助读者理解真实游戏数据项目的组织方式。资源共包含八个文件涵盖Python主脚本、JSON比赛详情数据、XML工程配置、IML模块说明与项目说明文档压缩包仅920KB体量紧凑便于导入开发环境。当前已有两百零六人浏览学习说明该项目具有实践参照意义。通过细致阅读源码与数据读者可掌握游戏数据解析、指标计算与结果展示等技能并将通用方法迁移至其他游戏或数据集为课程设计或毕业设计提供可直接运行的蓝本。1. 从一场碾压局说起Dota2 数据里到底藏着什么上周复盘一场天梯局敌方中单火女 22 分钟超神我方节奏全无。可拉出赛后数据一看火女全场的补刀只比我方宙斯多 12 刀团队经济领先也不过 4K真正致命的是前 15 分钟我方劣势路的 5 次阵亡每一次都精准撞在对方开雾抓人的时间点上。这种从“我觉得输了”到“数据告诉我为什么输”的视角切换正是 Dota2 游戏数据分析的价值所在。围绕它做一套完整的源码项目不是把比赛录像下载下来看两遍而是通过 Steam Web API 批量拉取比赛记录、清洗 JSON 字段、按英雄、玩家、对局时长、经济经验曲线等维度做聚合分析最终形成可复用的分析工程。适合正在学 Python 数据分析、想拿真实游戏数据练手或者对 MOBA 平衡性、玩家行为感兴趣的人。2. 数据从哪来Steam Web API 的调用与文件落盘2.1 拉取 Dota2 比赛记录的最小可行代码Dota2 的比赛数据主要来自 Valve 官方的 Steam Web API不需要抓包爬网页只要申请一个 API Key 就能用。整个项目的数据源头是我在使用中验证过的一条链路先用GetMatchHistory按条件筛比赛列表再用GetMatchDetails逐场拉详情。前者负责批量发现 match_id后者负责把一场比赛的完整数据结构化。import requests import json import time import csv API_KEY 你的_Steam_API_Key BASE_URL https://api.steampowered.com def fetch_match_history(hero_idNone, date_minNone, matches_requested10): params { key: API_KEY, game_mode: 22, matches_requested: matches_requested } if hero_id: params[hero_id] hero_id if date_min: params[date_min] date_min url f{BASE_URL}/IDOTA2Match_570/GetMatchHistory/v1/ resp requests.get(url, paramsparams, timeout15) resp.raise_for_status() data resp.json().get(result, {}) return data.get(matches, []) def fetch_match_details(match_id): url f{BASE_URL}/IDOTA2Match_570/GetMatchDetails/v1/ params {key: API_KEY, match_id: match_id} resp requests.get(url, paramsparams, timeout15) resp.raise_for_status() return resp.json().get(result, {})2.1.1 参数调优的边界与陷阱game_mode22对应的是全英雄选择这是天梯路人局最常见的模式用来分析胜率和英雄节奏最有代表性。matches_requested单次上限是 100超过会被静默截断所以批量拉取时要用循环配合time.sleep(1)控制频率。Steam 对 API 的限流是 1 秒 1 个请求超出会返回 429我在实际跑批时踩过这个坑推荐在请求头里加User-Agent并在每次请求后至少等待 1.2 秒。date_min是 Unix 时间戳用来设定赛事时间下限。这里有个细节Game Mode 参数如果传错比如填了0即未知模式返回的可能是人机局或活动模式对分析结果影响很大。所以我一般会先在返回里按lobby_type过滤只保留 0公开匹配和 7普通匹配两种。fetch_match_history返回的只是比赛摘要里面没有每个玩家的完整出装和技能加点必须再调fetch_match_details。这意味着 100 场比赛就要发 101 个请求如果做全英雄分析建议先落盘再慢慢跑。2.2 把 JSON 响应改造成适合 pandas 读入的 CSV拿到GetMatchDetails的 JSON 后不能直接扔给 pandas因为players字段是一个数组每个元素里又有几十个键值且不同位置如player_slot小于 128 为天辉大于等于 128 为夜魇含义不同。我习惯先做一层扁平化把玩家级数据展开成行再把比赛级数据作为公共列拼接回去。def flatten_match(match): rows [] for p in match.get(players, []): row { match_id: match[match_id], game_mode: match.get(game_mode), duration: match.get(duration), radiant_win: match.get(radiant_win), player_slot: p[player_slot], hero_id: p[hero_id], kills: p.get(kills, 0), deaths: p.get(deaths, 0), assists: p.get(assists, 0), gpm: p.get(gold_per_min, 0), xpm: p.get(xp_per_min, 0), gold: p.get(net_worth, 0), last_hits: p.get(last_hits, 0), denies: p.get(denies, 0), level: p.get(level, 0) } row[team] radiant if p[player_slot] 128 else dire rows.append(row) return rows def save_matches_to_csv(match_ids, filenamedota_matches.csv): all_rows [] for mid in match_ids: detail fetch_match_details(mid) if detail and detail.get(match_id): all_rows.extend(flatten_match(detail)) time.sleep(1.2) with open(filename, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesall_rows[0].keys()) writer.writeheader() writer.writerows(all_rows) print(f已保存 {len(all_rows)} 行数据到 {filename})2.2.1 为什么用 CSV 而不是直接入库对于单机分析CSV 比 SQLite 更直观调试时能用 Excel 直接打开看而且后续换数据集时不需要迁移 schema。但注意字段不要随便命名gpm和xpm是 Dota2 社区通用缩写在合并外部数据集比如从 OpenDota 拉的补充字段时减小对齐成本。如果项目想做成长期复跑的工程我建议在save_matches_to_csv里追加参数appendTrue用os.path.exists判断文件头部避免重复拉取相同比赛。3. 数据清洗从脏数据到可分析的结构化表格3.1 处理缺失值和异常游戏模式真实 API 返回的 JSON 里经常有缺失字段比如玩家中途退出时leaver_status可能非 0某些极早期对局没有net_worth。直接dropna会损失大量样本我一般按列处理数值列缺失填 0比赛级字段缺失且会影响胜率统计的直接丢弃整行。import pandas as pd df pd.read_csv(dota_matches.csv) df df[df[game_mode] 22].copy() df df[df[duration] 600].copy() df[kills] df[kills].fillna(0).astype(int) df[deaths] df[deaths].fillna(0).astype(int) df[assists] df[assists].fillna(0).astype(int) df[gold] df[gold].fillna(0).astype(int) df[hero_id] df[hero_id].astype(int) df[is_radiant] df[team] radiant df[player_won] df.apply( lambda r: r[radiant_win] if r[is_radiant] else not r[radiant_win], axis1 ) df[kd_ratio] df.apply( lambda r: r[kills] / r[deaths] if r[deaths] 0 else r[kills], axis1 )3.1.1 清洗逻辑的取舍说明过滤掉duration 600是对局是因为 10 分钟内的局大部分是一方秒退或掉线数据噪声远大于信号。player_won这个字段是玩家维度的胜败标记从radiant_win推导而来很多新手会直接拿团队胜负当个人胜负这是经典的逻辑错误。kd_ratio没有直接用kills/deaths是因为死亡数为 0 时会除零常见做法是死亡为 0 时直接用击杀数或者加一个极小值 epsilon。这个字段是后续分析中性价比最高的一个维度。3.2 英雄 ID 映射与中文名称转换API 返回的hero_id是整数编号比如 1 是敌法师2 是斧王。硬记编号不现实项目源码里需要一个 hero 映射表。常见的做法是加载 Valve 官方的heroes.json或者用社区维护的映射字典。hero_map { 1: Anti-Mage, 2: Axe, 3: Bane, 4: Bloodseeker, 5: Crystal Maiden, 6: Drow Ranger, 7: Earthshaker, 8: Juggernaut, 9: Mirana, 10: Morphling, 11: Shadow Fiend, 12: Phantom Lancer, 13: Puck, 14: Pudge, 15: Razor, 16: Sand King, 17: Storm Spirit, 18: Sven, 19: Tiny, 20: Vengeful Spirit, 21: Windranger, 22: Zeus, 23: Kunkka, 25: Lina, 26: Lion, 27: Shadow Shaman, 28: Slardar, 29: Tidehunter, 30: Witch Doctor, 31: Lich, 32: Riki, 33: Enigma, 34: Tinker, 35: Sniper, 36: Necrophos, 37: Warlock, 38: Beastmaster, 39: Queen of Pain, 40: Venomancer, 41: Faceless Void, 42: Wraith King, 43: Death Prophet, 44: Phantom Assassin, 45: Pugna, 46: Templar Assassin, 47: Viper, 48: Luna, 49: Dragon Knight, 50: Dazzle, 51: Clockwerk, 52: Leshrac, 53: Natures Prophet, 54: Lifestealer, 55: Dark Seer, 56: Bounty Hunter, 57: Doom, 58: Ancient Apparition, 59: Spectre, 60: Chen, 61: Bristleback, 62: Alchemist, 63: Invoker, 64: Silencer, 65: Outworld Devourer, 66: Lycan, 67: Lone Druid, 68: Chaos Knight, 69: Meepo, 70: Treant Protector, 71: Ogre Magi, 72: Undying, 73: Rubick, 74: Disruptor, 75: Nyx Assassin, 76: Naga Siren, 77: Keeper of the Light, 78: Io, 79: Visage, 80: Slark, 81: Medusa, 82: Troll Warlord, 83: Centaur Warrunner, 84: Magnus, 85: Timbersaw, 86: Brewmaster, 87: Dark Willow, 88: Pangolier, 89: Grimstroke, 90: Hoodwink, 91: Void Spirit, 92: Snapfire, 93: Mars, 94: Dawnbreaker, 95: Marci, 96: Primal Beast, 97: Muerta, 98: Kez } df[hero_name] df[hero_id].map(hero_map).fillna(Unknown)3.2.1 映射表的实用建议如果只是做胜率分析hero 名称不是必须的但一旦要为英雄分组输出热力图或排行榜没有名称会没法读图。fillna(Unknown)是为了防止后续groupby时把新英雄直接丢弃。Dota2 每年都会加新英雄比如 97 和 98 就是近两个版本的新面孔如果拉到的对局里出现映射表外的 ID需要回头补表。4. 核心分析维度英雄胜率、经济效率与击杀参与4.1 英雄胜率与 pick 率的置信区间英雄胜率是整个项目里最直观也最容易误导人的指标。直接用df.groupby(hero_name)[player_won].mean()得到的是点估计样本量小的英雄比如冷门辅助波动极大。我一般会同时算 95% 置信区间用scipy.stats的beta分布或 Wilson 区间。from scipy.stats import beta def wilson_interval(wins, n, z1.96): if n 0: return (0, 0) p wins / n denom 1 z**2 / n center (p z**2 / (2*n)) / denom margin z * ((p * (1-p) / n z**2 / (4*n**2)) ** 0.5) / denom return (center - margin, center margin) hero_stats df.groupby(hero_name).agg( pick_count(player_won, count), win_count(player_won, sum) ).reset_index() hero_stats[win_rate] hero_stats[win_count] / hero_stats[pick_count] hero_stats[ci_low], hero_stats[ci_high] zip(*hero_stats.apply( lambda r: wilson_interval(r[win_count], r[pick_count]), axis1 )) hero_stats hero_stats[hero_stats[pick_count] 50].sort_values(win_rate, ascendingFalse) print(hero_stats.head(10).to_string(indexFalse))4.1.1 为什么 pick 数必须设阈值50 场以下的英雄胜率波动能超过 10 个百分点排名没有任何参考价值。Wilson 区间比简单的p ± 1.96*sqrt(p(1-p)/n)在 p 接近 0 或 1 时更稳健是处理二项分布比率的标准做法。输出结果里ci_low和ci_high的跨度能直观告诉你好数据与坏数据之间的边界。4.2 GPM/XPM 的分段分析与英雄定位规律经济效率指标用均值汇总会抹平英雄定位差异。中单炼金和五号位冰女的 GPM 没有可比性对比分组内分布才有意义。我习惯按英雄分组后计算 GPM 的 25/50/75 分位数再结合胜负做差异检验。gpm_stats df.groupby(hero_name)[gpm].describe(percentiles[0.25, 0.5, 0.75]) gpm_stats.columns [count, mean, std, p25, p50, p75, min, max] win_gpm df[df[player_won]].groupby(hero_name)[gpm].mean().rename(win_gpm) lose_gpm df[~df[player_won]].groupby(hero_name)[gpm].mean().rename(lose_gpm) gpm_impact pd.concat([gpm_stats, win_gpm, lose_gpm], axis1) gpm_impact[gpm_diff] gpm_impact[win_gpm] - gpm_impact[lose_gpm] gpm_impact gpm_impact[gpm_impact[count] 50].sort_values(gpm_diff, ascendingFalse) print(gpm_impact[[count, p50, win_gpm, lose_gpm, gpm_diff]].head(10))4.2.1 看 gpm_diff 的正确姿势gpm_diff是同一英雄胜局平均 GPM 减去负局平均 GPM。如果这个值很大说明该英雄对经济领先的依赖极强经济崩了基本没用如果接近 0 甚至为负说明该英雄是节奏型或团战型吃等级和技能而非纯刷钱。拿这个维度去和duration交叉还能看出英雄的强势期是前期还是大后期。5. 对局维度挖掘比赛时长、阵容组合与翻盘规律5.1 时长区间与胜率的关系把比赛时长离散化成区间再统计不同区间里天辉/夜魇胜率是分析地图平衡性的便宜方案。Dota2 的天辉和夜魇在视觉、地形、Roshan 位置上存在天然差异定期的胜率偏移是个有讨论度的话题。bins [0, 15, 25, 35, 45, 60, 120] labels [0-15min, 15-25min, 25-35min, 35-45min, 45-60min, 60min] df[duration_bin] pd.cut(df[duration] / 60, binsbins, labelslabels) duration_stats df.groupby(duration_bin, observedTrue).agg( matches(match_id, nunique), radiant_win_rate(radiant_win, mean) ).reset_index() duration_stats[dire_win_rate] 1 - duration_stats[radiant_win_rate] print(duration_stats)5.1.1 分组观察的细节pd.cut左开右闭所以 15 分钟整的对局被归入 15-25 区间。observedTrue是 pandas 2.x 的要求否则 CATEGORICAL 类型的分组会保留空组警告。radiant_win_rate用mean是因为radiant_win是布尔值均值即胜率。类似的分析思路可以扩展到first_blood_time一血时间和first_blood_team拿一血的阵营是否存在相关性但GetMatchDetails只提供玩家人头数据不直接提供一血时间需要从first_blood_player_slot推断。这个字段在部分旧对局里是空的筛选时要注意。5.2 阵容熵值衡量阵容多样性的量化指标一个经常被忽视的维度是阵容多样性。用香农熵计算每场比赛 10 个英雄的分布情况熵值高说明阵容百花齐放熵值低说明大家都在抢版本强势英雄。from collections import Counter import math def shannon_entropy(hero_ids): counter Counter(hero_ids) total len(hero_ids) entropy 0 for count in counter.values(): p count / total entropy - p * math.log2(p) return entropy match_heroes df.groupby(match_id)[hero_id].apply(list) match_entropy match_heroes.apply(shannon_entropy).rename(entropy) df_with_entropy df.merge(match_entropy, onmatch_id) entropy_by_bin df_with_entropy.groupby(duration_bin, observedTrue)[entropy].mean() print(entropy_by_bin)5.2.1 熵值结果的解读路径如果某段时间版本更新后熵值骤降基本可以断定存在超模英雄这比单纯看胜率更快。反过来如果熵值长期偏高但整体胜率接近 50%说明当前版本的平衡性在可接受范围内。另外注意熵值的上限是 log2(10)约等于 3.32实际值能到 3 以上就算非常多样了。6. 本地验证与进阶玩法用随机抽样检验分析脚本的可靠性把分析脚本跑通只是第一步真正要确认的是结果不是偶发噪声。这里给一个固定做法从原始数据里做 bootstrap 抽样重新计算英雄胜率看指标波动幅度。这能同时验证 API 拉数脚本有没有系统性偏置以及样本量是否够用。import numpy as np def bootstrap_winrate(df_subset, n_bootstrap1000, seed42): rng np.random.default_rng(seed) rates [] for _ in range(n_bootstrap): sample df_subset.sample(nlen(df_subset), replaceTrue, random_staterng) rates.append(sample[player_won].mean()) return np.percentile(rates, [2.5, 50, 97.5]) sample_hero Shadow Fiend hero_df df[df[hero_name] sample_hero] ci bootstrap_winrate(hero_df) print(f{sample_hero} 胜率 95% 置信区间: {ci[0]*100:.1f}% - {ci[2]*100:.1f}%) variant_df hero_df[hero_df[duration_bin].isin([25-35min, 35-45min])] variant_ci bootstrap_winrate(variant_df) print(f过滤时长后置信区间: {variant_ci[0]*100:.1f}% - {variant_ci[2]*100:.1f}%)replaceTrue表示有放回抽样这是 bootstrap 的核心random_state固定种子保证结果可复现。如果两次不同时长过滤后置信区间完全不重叠说明时长对英雄胜率的影响在统计上显著。如果重叠严重就需要增大样本量或换更细的拆分维度。进阶方向可以考虑把同一套清洗和聚合逻辑封装成 Python 包输入 API Key 和参数输出 CSV 和图表。把flatten_match、wilson_interval、shannon_entropy放进独立的.py模块主脚本只做编排。这样后续加入新数据源时比如从 OpenDota 的镜像接口拉取技能加点数据或者在本地 SQLite 里维护增量更新都不需要改动分析逻辑。最后给一个实用验证技巧把分析脚本输出和 Valve 官方客户端的内置赛后总结页做交叉对比抽查 10 场比赛的 KDA、GPM、胜负是否一致。如果全部一致整个链路的可信度就有了锚点如果出现偏差优先检查player_slot的阵营判断逻辑这是最常见的错位来源。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。