简介这是一份基于Python的英雄联盟胜率预测项目源码包面向对机器学习与电竞数据分析感兴趣的开发者、数据科学初学者及竞赛研究者。项目围绕监督学习分类模型展开涵盖数据处理、数值计算、模型构建等核心环节完整演示了从数据清洗、特征工程如战队战绩、经济状态到模型选择、交叉验证与参数调优再到训练评估和部署更新的全流程。资源共24个文件以22个Python脚本为主包括主程序、分类器、游戏状态与数字分类器测试、训练脚本、配置文件等另附1个数据说明txt和1个gitignore压缩包仅31KB结构紧凑便于快速阅读和二次开发。目前已有235人学习下载。对于希望上手比赛预测或了解机器学习项目组织方式的读者这份代码提供了直观的模块划分、测试用例和可运行示范能帮助理解胜率预测应用的完整落地思路。1. LeaguePredictor英雄联盟胜负预测先识别那些虚高的准确率我拆过一个很有意思的Python项目叫LeaguePredictor目标是预测英雄联盟单局比赛的获胜概率。市面上这类项目很多但大多数演示准确率都在90%以上你拿过去一跑发现训练集上准得吓人换到新比赛上立刻原形毕露。这个项目的价值不在“预测”本身而在于它逼你把特征工程和数据泄漏的问题完整走一遍。LeaguePredictor适合两类人一类是想用Python做游戏数据分析的从业者想找一个真实、脏、有时间属性的数据集练手另一类是排位玩家想搞清楚BP阶段到底哪个位置、哪个英雄组合真正影响胜率。接下来我从数据拉取、特征构建、模型训练到概率校准完整拆一遍这个项目包括那些让准确率虚高的坑。2. 数据底座从Riot API到干净训练集2.1 数据源选型为什么锁定match-v5接口LeaguePredictor的核心数据来自Riot Games官方API。做这个项目之前我对比过几个数据来源第三方网站爬虫、玩家对局导出的JSON、以及官方API。最后锁定了官方match-v5接口。理由有三个第一数据结构规范字段命名稳定省去大量清洗工作第二可以按puuid拉取某个玩家的历史比赛也能按比赛ID拉取完整对局两种拉法配合起来能拼出连续时间段的比赛池第三官方接口有明确的限流规则虽然麻烦但至少不会突然改版。一个容易混淆的点是账号标识。Riot API里有三个ID概念游戏内名gameName、账户IDaccountId、玩家通用IDpuuid。拉取比赛记录必须用puuid不是游戏内名。LeaguePredictor的代码里专门封装了一层summoner到puuid的转换函数我建议你拿到项目后先去读这部分不要跳过否则后面匹配比赛记录时会出现大量空数据。2.2 拉取与清洗并发限流和脏数据剔除拉数据这一步我一般写一个简单的Python脚本用requests库直接调接口。LeaguePredictor项目里提供了一个类似的实现核心逻辑是按时间范围分页拉取比赛ID再逐一获取比赛详情。这里有几个参数需要注意matchlist接口的startTime和endTime是Unix时间戳按秒计算默认只能拉到最近90天的比赛count参数单次最多100条如果调大接口直接报400。代码示例如下import requests import time API_KEY RGAPI-xxxx-xxxx REGION asia # 处理KR服务器数据 def fetch_match_ids(puuid, start_time, end_time, count100): url fhttps://{REGION}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids headers {X-Riot-Token: API_KEY} params { startTime: start_time, endTime: end_time, start: 0, count: count } resp requests.get(url, headersheaders, paramsparams) resp.raise_for_status() return resp.json() def fetch_match_detail(match_id, retry3): url fhttps://{REGION}.api.riotgames.com/lol/match/v5/matches/{match_id} headers {X-Riot-Token: API_KEY} for attempt in range(retry): try: resp requests.get(url, headersheaders) if resp.status_code 429: wait int(resp.headers.get(Retry-After, 20)) time.sleep(wait) continue resp.raise_for_status() return resp.json() except requests.exceptions.RequestException: time.sleep(2 ** attempt) return None这段代码里我最想强调的是429处理。Riot API的限流不是简单的每秒请求数限制而是按“时间段窗口”统计响应头里的Retry-After字段会明确告诉你需要等多久。很多人在这一步翻车写成固定sleep(2)短时间还行拉几万场比赛后必被拉黑。LeaguePredictor项目里用的也是类似的指数退避策略我建议保留这个逻辑。拿到原始JSON后清洗工作比想象中麻烦。最常见的脏数据来源包括重开对局remake、自定义房间、极短对局。重开对局的gameDuration通常在90秒以内数据完整但没有预测意义自定义房间的玩家段位极不均匀会严重干扰模型。LeaguePredictor在数据预处理阶段会过滤掉gameDuration小于240秒的对局同时要求比赛模式等于CLASSIC匹配或排位。另外有些对局会出现participants字段少于10人的情况这种直接丢弃不要试图补全。洗完后需要做的是冗余字段裁剪。一个match详情JSON至少有200个字段LeaguePredictor会压缩成一个包含对局时间、双方英雄ID、召唤师技能、段位、击杀、经济、等级、视野分数的精简结构。这里有个经验把原始JSON保存一份备份再裁剪因为你后面做特征工程时往往会发现某个字段当时觉得没用后来却需要回溯补齐。2.3 可复现的数据管线设计数据管线的设计直接决定了项目能不能在不同机器上复现。LeaguePredictor的做法是把“拉取原始数据”和“构建训练数据”分成两个独立阶段。第一阶段只做拉取和备份存成parquet格式文件第二阶段从parquet读取做清洗、特征工程、划分数据集。这样做的最大好处是你不必每次调参都重新拉一遍API也避免了网络不稳定导致的实验不可复现。具体到文件组织我建议按比赛日期分目录存储例如raw_data/2024-01-15/。每个目录里放match.json和match_ids.json。另外拉数据的时间范围要记录一个manifest文件字段包括start_time、end_time、拉取日期、API版本。LeaguePredictor项目自带了一个简单的manifest记录脚本虽然简陋但足够用。清洗阶段有一个非常隐蔽的坑官方API的gameVersion字段。不同版本之间英雄数值、地图机制可能变化剧烈如果训练集横跨多个大版本模型学到的是版本差异而不是阵容差异。LeaguePredictor的解决方式是只保留最近两个版本的比赛或者把gameVersion做成一个类别特征。我倾向于前者简单直接模型稳定性更好。3. 特征工程把对局时间轴压成特征向量3.1 英雄选择特征从One-Hot到共现矩阵英雄选择是BP阶段唯一可见的信息也是LeaguePredictor最重要的特征源。LOL目前有一百多个英雄如果直接对每个英雄做One-Hot编码会产生一百多维的稀疏特征模型能学到信息但效率低。LeaguePredictor项目采用的是“英雄ID嵌入”的思路先用One-Hot表达双方各五个英雄再通过一个嵌入层压缩成低维向量。不过在实际训练中我发现嵌入层会增加Pipeline的复杂度如果你的目标是先跑通流程可以直接用One-Hot。我在做这个项目时先用One-Hot跑出一个基线结果再尝试嵌入版本看是否有提升。具体特征构建代码如下import pandas as pd import numpy as np def build_hero_features(match_df, hero_id_to_idx): hero_feat np.zeros((len(match_df), len(hero_id_to_idx))) banned_feat np.zeros((len(match_df), len(hero_id_to_idx))) for i, row in match_df.iterrows(): blue_heros row[blue_team_heroes] # 列表5个英雄ID red_heros row[red_team_heroes] for hid in blue_heros: hero_feat[i, hero_id_to_idx[hid]] 1 for hid in red_heros: hero_feat[i, hero_id_to_idx[hid]] -1 # 红方用负值区分阵营 for bid in row[bans]: if bid in hero_id_to_idx: banned_feat[i, hero_id_to_idx[bid]] 1 feat_df pd.DataFrame( hero_feat, columns[fhero_{idx} for idx in range(len(hero_id_to_idx))] ) ban_df pd.DataFrame( banned_feat, columns[fban_{idx} for idx in range(len(hero_id_to_idx))] ) return pd.concat([feat_df, ban_df], axis1)这里有个关键设计蓝方英雄编码为1红方编码为-1。这样模型可以直接学到“某个英雄在蓝方胜率高”还是“在红方胜率高”的非对称信息。如果你只用0/1编码模型天然认为红蓝方对称但实际上召唤师峡谷的红蓝方胜率确实存在小幅差异优先让模型自己学出来。Ban位特征我专门提一下。LeaguePredictor把Ban位做成了独立特征而不是和Pick位混在一起。多数教程会忽略Ban位但BP阶段Ban位同样承载了强信息——比如某个版本T0英雄被Ban相当于双方策略空间被压缩。把Ban位和Pick分开编码模型能分别学到“Ban掉哪个英雄对胜率影响大”。3.2 时间序列压缩经济、等级、视野的聚合方式如果你做的不是BP阶段预测而是前15分钟预测获胜概率那么时间轴数据才是主角。LeaguePredictor支持两种预测场景默认是前15分钟预测因为玩家通常想知道“这局15分钟能不能赢”。时间序列特征需要压缩成固定长度向量常见做法是在固定时间点采样。我选择了5个时间点3分钟、6分钟、9分钟、12分钟、15分钟。每个时间点提取7个指标双方经济差、经验差、击杀差、塔差、小龙数差、峡谷先锋数差、视野分数差。然后按时间维度拼接形成35维特征。这种做法的好处是模型能学到“经济差是持续拉大还是波动”这种趋势信息。LeaguePredictor里用的是等宽采样也就是每一分钟都提取一次最后拼接成高维矩阵。两种方式各有优势等宽采样的信息更完整但容易过拟合。视野分数差这个特征经常被忽略。LOL里的视野控制排眼、插眼其实是低段位和高段位差距最大的地方。我做了个简单实验把视野相关特征移除后模型准确率几乎不下降但概率排名有明显变化。这说明视野特征对排序有用但对分类的边际贡献不如经济差直接。构建这部分特征的代码大致思路是按时间点groupby后求差值然后pivot成宽表。关键是处理缺失值——如果某场比赛15分钟就已经结束那么12分钟和15分钟的数据存在但后续时间点为空。我的处理方式是对提前结束的比赛将缺失时间点的值用最后已知值填充并额外加一个“比赛是否提前结束”的mask特征。这样模型就能学到“这局可能是碾压局”的信号。3.3 标签构建与样本平衡标签是“蓝方是否获胜”从match详情里的teams字段中提取。注意games字段里只有一个teams数组不要用participants里的win字段因为participants的描述有歧义不同人读起来容易把阵营搞反。样本平衡问题在LOL预测中不算严重因为蓝红双方胜率大约是50.5%比49.5%天然接近均衡。真正不平衡的是段位分布。如果拉数据的玩家是高段位那么训练集里高段位对局密度远高于低段位。用这个模型去预测青铜局的BP阵容准确率会明显下滑。LeaguePredictor没有做段位分层采样但我在复现时自己加了一个约束训练集内部按段位分层确保每个段位区间占比不超过30%。段位特征本身我建议先离散化不要直接喂数字。青铜、白银、黄金、铂金、钻石、大师、宗师、王者这八个档位之间存在质的差异而不是均匀的数值差异。LeaguePredictor的做法是映射成0到7的整数编码再用LightGBM直接处理。如果你用神经网络建议做Embedding。逻辑回归的话还是分桶做成哑变量更稳。另外预测时机也会影响标签的分布。BP阶段预测的标签是整场比赛的最终结果前15分钟预测的标签也是最终结果但两者的难度完全不同。LeaguePredictor默认训练的是BP阶段模型因为数据构造最简单——只需要英雄选择和段位。我建议你先跑通BP版本再加入时间轴特征做增强版本。4. 模型选型与训练从逻辑回归到LightGBM4.1 先跑逻辑回归的原因我见过的LOL预测项目里有一半以上直接上XGBoost或LightGBM然后报告一个极高的准确率。这里有个常见的认知偏差LOL胜负预测的训练集是时间相关的如果不对训练集时间窗口做严格切分几乎所有模型都能达到70%的准确率因为相邻比赛的阵容和版本是相似的。先跑逻辑回归有三个理由第一逻辑回归对特征重要性有清晰的解释能直观看到哪些特征驱动预测第二逻辑回归的线性边界被称为“强基线”如果LightGBM在这个基础上提升不超过5个百分点说明特征工程有问题不是模型不够强第三逻辑回归收敛极快可以在一分钟内验证特征构建是否正确。LeaguePredictor的默认模型配置是LightGBM但我强烈建议你把项目里跑LightGBM的代码先改成逻辑回归试一遍。改法很简单from sklearn.linear_model import LogisticRegression model LogisticRegression( max_iter500, C0.1, solverlbfgs, class_weightbalanced )逻辑回归在LOL预测数据上有一个天生的短板无法处理特征的交互效应。比如“金克丝配露露”的增益在逻辑回归看来只是两个独立特征的线性相加而LightGBM可以学到这对组合的非线性协同。但逻辑回归输出的概率天然校准良好不会像树模型那样给出一堆集中在0.3到0.7之间的保守估计。4.2 LightGBM关键参数与调参经验从逻辑回归切换到LightGBM重点不是提升准确率而是提升排序质量。LightGBM的叶子生长策略和直方图算法在表格型数据上通常显著优于Gradient Boosting。LeaguePredictor里封装了一套参数我复现时发现效果不错但需要针对数据规模微调。import lightgbm as lgb params { objective: binary, metric: binary_logloss, boosting_type: gbdt, num_leaves: 31, max_depth: -1, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, n_jobs: 8, min_data_in_leaf: 50 } dtrain lgb.Dataset(X_train, labely_train) dvalid lgb.Dataset(X_valid, labely_valid) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dvalid], callbacks[lgb.early_stopping(stopping_rounds100)] )我逐个解释关键参数的取值逻辑。num_leaves设为31这是默认值适合中小规模数据叶子节点太多容易过拟合feature_fraction设为0.8意思是每棵树训练时只用80%的特征提供随机性降低过拟合bagging_fraction同样0.8与bagging_freq配合每5轮做一次行采样min_data_in_leaf设为50强制每个叶子节点至少50个样本防止叶子过于碎片化。关于learning_rate0.05是安全和效果平衡比较好的起点。你可以试试0.01和0.1在早停100轮的前提下如果0.01的验证集loss更低说明数据量足够大可以增加一轮数。我这里特别强调early_stopping的重要性。LOL预测数据集的样本量通常不超过5万条训练2000轮很容易在第1500轮开始过拟合。训练结束后一定要看特征重要性。LightGBM的gain重要性特别有用它等于“该特征在所有树中被用于分裂带来的平均增益总和”。我在实验里发现经济差在12分钟和15分钟两个时间点的增益值占据了绝对优势而英雄选择特征的增益排在第二梯队这说明LOL的前期对线结果确实主导了胜率。4.3 验证策略按时间切分不要随机切分这是LeaguePredictor项目里最重要的一处设计也是最容易被忽视的。如果你随机把数据集切成80%训练和20%验证那么训练集和验证集会包含同一时间段内的比赛阵容、版本、英雄强度高度相似验证集准确率会虚高5个百分点以上。正确的做法是按时间切分比如取前70%时间的比赛作为训练集后30%时间的比赛作为验证集。LeaguePredictor代码库里提供了按game_creation字段排序后切分的工具函数我没有改它直接复用了。cutoff_time X_train_full[game_creation].quantile(0.7) train_idx X_train_full[game_creation] cutoff_time valid_idx X_train_full[game_creation] cutoff_time X_train X_train_full[train_idx].drop(columns[game_creation]) X_valid X_train_full[valid_idx].drop(columns[game_creation]) y_train y_train_full[train_idx] y_valid y_train_full[valid_idx]用这个切分方式后模型的验证集准确率通常会比随机切分低3到6个百分点这是正常现象。如果你看到哪个LOL预测项目的验证准确率高于90%先检查它的验证集是不是随机切分的。时间窗的跨度也要注意。LeaguePredictor在README里提到它训练的数据跨度为三个月但版本更新频繁超过一个多月的数据在版本大幅变更后可能失效。我的经验是做BP阶段预测时只保留最近两个月的数据就够了。时间跨度过长模型会把旧版本的英雄强度当成新版本的规律。5. 避坑与排查复现LeaguePredictor时踩过的五个坑5.1 因果泄漏十杀率特征让准确率虚高到96%我在第一次复现时按照教程加了一堆统计特征包括战队过去十场的十杀率、一血率、小龙控制率。训练集准确率一度达到96%我一度以为自己找到了最强模型。后来换上按时间切分验证集准确率暴跌到67%。原因很典型这些统计特征是“后视镜像”——它们的值本身是由历史比赛结果计算出来的而未来比赛的结果会反向影响这些统计特征的更新。如果训练集和验证集时间窗口重叠模型相当于在拿答案抄题。解决方法是彻底放弃这类历史战绩聚合特征只保留英雄选择、段位、Ban位、以及比赛自身的时间轴数据。LeaguePredictor的默认特征集里没有加入任何战队历史统计我一开始觉得它“简陋”后来才明白这是刻意避开了因果泄漏。从那以后我每次做预测类项目都会先列一个“哪些特征里隐藏了答案”的清单。5.2 段位分布失衡导致预测偏向高分段早期模型在铂金以下对局中预测极其不准我检查特征重要性时发现段位特征增益值很低说明模型几乎没有利用段位信息。原因是训练数据集中在钻石以上段位低段位样本占比不到10%LightGBM在训练时会把低段位当成噪声。解决方法是按段位分层重采样保证每个段位区间在训练集中占比不低于10%。5.3 重开和掉线数据造成标签噪声有一批比赛实际只打了4分钟就因为中单掉线被系统判定重开但比赛记录里gameDuration和参与者的KDA数据都被完整写入。如果把这类比赛当成正常对局喂给模型模型会学到“4分钟巨大经济差必胜”这种错误规律。解决方法是严格过滤gameDuration小于300秒的比赛同时删除任意参与者存在dc标记的对局。5.4 国内网络环境下API访问失败率高这个项目在拉取Riot API时依赖海外服务器访问。在国内网络环境下直连Riot API经常出现连接超时。常见方案是配置HTTP代理。这个踩坑点比较敏感不做展开。总之确保你的执行环境能稳定访问Riot的域名即可。5.5 模型输出的概率与实际胜率脱节LightGBM输出的概率值不能直接当作真实胜率使用它会存在系统性偏差。我在一次复盘中发现模型预测某阵容胜率70%但实际该阵容胜率只有62%。这不是模型坏了而是GBDT类模型在概率校准上的固有问题。下一章我会详细说明怎么用Platt Scaling完成概率校准。6. 概率校准与评估把输出变成可解释的胜率6.1 用Platt Scaling修正概率偏移LightGBM输出的分数经过sigmoid转换后整体分布会偏向中间值0.35到0.65而真实胜率应该覆盖从5%到95%的完整区间。解决方案是做Platt Scaling。操作逻辑很清晰from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV( estimatormodel, methodsigmoid, cv3 ) calibrated.fit(X_valid, y_valid) prob calibrated.predict_proba(X_valid)[:, 1]注意要用独立验证集来拟合校准器不能用训练集否则校准器会学习到训练集的过拟合偏向。CalibratedClassifierCV内部会用交叉验证的方式做所以可以直接吃训练集。我这里是把训练好的LightGBM作为estimator传入然后Fit到验证集上。校准完成后模型输出0.6胜率意味着在100场类似阵容下应该能赢60场。之后你再拿这个概率去BP阶段辅助决策才会有一个合理的依据。6.2 用Brier Score和Log Loss评估综合表现准确率只能回答分类正误但胜率预测是一个回归问题。LeaguePredictor自带的评估脚本里同时输出了准确率、Log Loss和Brier Score三个指标。Log Loss对“过度自信的错误预测”惩罚极大。如果你预测A阵容胜率95%结果A输了Log Loss惩罚远高于预测60%的失败。模型准确率Log LossBrier Score逻辑回归基线63.2%0.6230.211LightGBM未校准66.8%0.5870.204LightGBMPlatt66.5%0.5560.189从上表可以看到Platt校准后准确率几乎不变但Log Loss和Brier Score都有明显下降。这验证了一个正确直觉校准提升的是概率质量不是分类边界。6.3 把模型封装成BP辅助评分工具最后一件事是将训练完成的模型封装成一个小工具输入双方阵容、Ban位、双方段位输出蓝方胜率并可视化每个位置对胜率的贡献。LeaguePredictor这段封装我直接用了逻辑是以一人一方阵容频繁互换位置后计算胜率变化。从那以后我每次拿排位对局验证模型时都会强制做一次概率校准的完整流程。预测得准不准是模型能力问题概率可不可信是工程问题两个问题不能混在一起。希望帮到你。本文还有配套的精品资源点击获取