基于赛前阵容数据的英雄联盟胜率预测:从特征工程到模型部署
简介LeaguePredictor 是一套基于 Python 的英雄联盟胜率预测项目源码面向具备一定机器学习基础、希望将分类模型落地到电子竞技数据分析的开发者与数据科学学习者。项目围绕历史比赛数据展开涵盖数据预处理、特征工程、模型选择、训练评估与部署更新等完整流程涉及 Pandas、Numpy、Scikit-learn 等常用库并尝试逻辑回归、随机森林、支持向量机等多种分类算法可用于战队战术分析、赛事结果预测等场景。资源包共 24 个文件以 22 个 py 脚本为核心辅以 gitignore 与 txt 说明文件整体约 31KB代码按特征构建、分类器训练与测试等模块组织结构清晰便于按需查阅。目前已有 235 人学习下载适合作为机器学习分类实战的参考案例帮助读者理解从原始数据到胜率输出的完整建模思路与工程组织方式。1. 从 Ban/Pick 结束那一刻预测英雄联盟的获胜几率排位赛里最让人上头的一幕是阵容刚锁完队友已经在聊天框里敲出「这把没了」。可真没了还是能打往往要等二十分钟才见分晓。LeaguePredictor 想干的事就是把这个判断提前到加载界面之前只拿双方阵容、位置、段位这些赛前就能拿到的信息输出一个胜率数字。它解决的不是「谁更强」这种玄学问题而是给复盘、BP 辅助、数据看板提供一个可量化、可复现的基线。适合两类人一类是想拿真实对局数据练手特征工程和分类模型的算法同学另一类是做电竞数据产品、需要给用户一个「阵容强度参考」的工程师。这篇笔记按我实际搭过的一套流程讲从数据字段怎么定到模型怎么训、怎么验、怎么避开那些让准确率虚高的坑尽量让你照着就能跑通一版。2. 先想清楚预测什么标签、特征与数据来源2.1 标签怎么定决定模型上限预测胜率本质是个二分类问题标签就是「这局蓝方赢没赢」取值 0 或 1。听起来简单但坑在数据来源。如果你用的是对局结束后的完整战绩那里面有击杀、经济、推塔数这些是结果不是原因拿它们当特征等于作弊模型准确率能到 95% 以上但上线后毫无用处因为赛前你根本拿不到。所以第一件事是把特征严格限制在「BP 结束、游戏加载前」这个时间点能观测到的信息。常见做法是只保留这些字段双方五个位置的英雄 ID、每个玩家的历史段位或隐藏分、双方整体平均段位、英雄的版本位置上单/打野/中单/下路/辅助、以及可选的红蓝方。红蓝方在职业比赛里有统计意义上的差异路人局里影响小但保留它成本很低让模型自己决定要不要用。提示如果你拿不到玩家段位只用英雄阵容也能做只是准确率会明显下降通常落在 55% 到 60% 区间别期待太高。2.2 特征工程把十个英雄变成模型能吃的数字英雄 ID 是类别变量不能直接当数值喂进去。最直接的做法是 one-hot但英雄池有一百多个十个位置 one-hot 之后维度爆炸且稀疏。我一般用两种方案一是把英雄映射到「定位标签」再 one-hot比如坦克、刺客、法师、射手、辅助维度降到个位数二是用 embedding把每个英雄学成一个低维向量让模型自己捕捉英雄之间的相似性。下面这段是把原始对局记录转成特征矩阵的核心逻辑用 pandas 处理import pandas as pd import numpy as np # df 每行是一局字段blue_top, blue_jg, ..., red_sup, blue_win HERO_COLS [fblue_{p} for p in [top,jg,mid,adc,sup]] \ [fred_{p} for p in [top,jg,mid,adc,sup]] def build_features(df, hero2idx): n len(df) # 每个位置一个 one-hot 块维度 英雄总数 X np.zeros((n, len(HERO_COLS) * len(hero2idx)), dtypenp.float32) for row_i, (_, row) in enumerate(df.iterrows()): for col_i, col in enumerate(HERO_COLS): hid hero2idx.get(row[col], 0) # 未知英雄归到 0 X[row_i, col_i * len(hero2idx) hid] 1.0 y df[blue_win].values.astype(np.float32) return X, y hero2idx {h: i 1 for i, h in enumerate(sorted(set( df[HERO_COLS].values.ravel())))} X, y build_features(df, hero2idx) print(X.shape, y.mean()) # 维度 10 * 英雄数y.mean() 看正负样本比例逻辑说明每个位置单独占一个 one-hot 块而不是十个位置共用一个块这样模型能区分「上单是亚索」和「中单是亚索」语义完全不同。参数上hero2idx从 1 开始编号0 留给未知英雄避免训练时出现没见过的 ID 直接报错。y.mean()打印的是蓝方胜率正常应该接近 0.5如果偏离太多说明数据采样有偏比如只爬了某一方的视角。2.3 数据从哪来规模要多少公开的对局数据接口能拿到脱敏后的比赛记录字段通常包含双方英雄、段位区间、胜负。我一般按版本切分同一个大版本内训模型跨版本验证因为英雄强度会随补丁变化。数据量上单版本五万局起步比较稳低于一万局时模型很容易过拟合到某几个热门英雄。切分比例用 8:1:1 做训练、验证、测试注意按时间切而不是随机切否则同一时间段的相似对局会同时出现在训练和测试里准确率虚高。3. 模型选型与训练从逻辑回归到梯度提升3.1 为什么先跑逻辑回归当基线很多人一上来就上深度模型结果调了两周还不如一个逻辑回归。我的习惯是先跑逻辑回归它训练快、可解释能告诉你哪些英雄组合对胜率影响大。用 scikit-learn 几行就能出结果from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, accuracy_score clf LogisticRegression(max_iter1000, C1.0) clf.fit(X_train, y_train) prob clf.predict_proba(X_test)[:, 1] print(AUC:, roc_auc_score(y_test, prob)) print(ACC:, accuracy_score(y_test, (prob 0.5).astype(int)))参数说明C是正则强度越小正则越强特征维度高的时候把它调到 0.1 到 0.5 之间能压住过拟合max_iter调大是因为 one-hot 特征多默认迭代次数常常不收敛。评价指标别只看准确率正负样本接近 1:1 时准确率还行但 AUC 更能反映模型排序能力赛前预测胜率本质是排序问题。3.2 梯度提升树当前性价比最高的选择逻辑回归跑通后换梯度提升树通常能再涨两三个点。LightGBM 或 XGBoost 都行我一般用 LightGBM训练快、对稀疏特征友好。关键参数不多但每个都要调import lightgbm as lgb params { objective: binary, metric: auc, learning_rate: 0.05, # 学习率0.05 起步太小训练慢 num_leaves: 63, # 叶子数控制模型复杂度 min_data_in_leaf: 100, # 叶子最小样本防过拟合 feature_fraction: 0.8, # 每棵树随机用 80% 特征 bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, } dtrain lgb.Dataset(X_train, y_train) dvalid lgb.Dataset(X_valid, y_valid, referencedtrain) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dvalid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)], )逻辑说明early_stopping(100)表示验证集 AUC 连续 100 轮不提升就停避免无脑堆树导致过拟合。num_leaves和min_data_in_leaf是一对叶子越多模型越复杂配合较大的min_data_in_leaf才能稳住。feature_fraction和bagging_fraction是行、列采样稀疏高维特征下这两个值调低一点通常更稳。训练完记得看特征重要性如果某个英雄的权重异常高多半是数据泄漏比如那个英雄只出现在某个时间段的样本里。3.3 类别不平衡与概率校准路人局蓝方胜率一般在 0.48 到 0.52 之间不算严重不平衡但如果你的数据里某一方明显偏多就要处理。简单做法是设scale_pos_weight或者对少数类过采样。更值得注意的是概率校准树模型输出的概率往往偏极端预测 0.9 的局实际胜率可能只有 0.75。如果你要把胜率直接展示给用户最好用 Platt scaling 或 isotonic 回归校准一下from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV(model, methodisotonic, cvprefit) calibrated.fit(X_valid, y_valid) prob_cal calibrated.predict_proba(X_test)[:, 1]参数说明method选isotonic适合样本量大的情况样本少时用sigmoid即 Platt scaling更稳cvprefit表示模型已经训好只做校准不再重新训练。校准后 AUC 基本不变但概率的绝对值更可信这对「展示胜率」这个场景很重要。4. 避坑与排查那些让准确率虚高的陷阱4.1 现象离线 AUC 0.85上线后用户说没用原因特征里混进了赛后信息最常见的是「队伍总经济」「击杀数」这类字段被误当成赛前特征。另一个隐蔽来源是按随机切分数据同一局的不同视角记录被分到训练和测试两边。解决写一个特征白名单只允许赛前可观测字段进入模型切分严格按时间或按对局 ID 去重后再切。上线前拿最近一周的新数据做一次盲测AUC 掉超过 5 个点就说明有泄漏。4.2 现象模型对热门英雄过拟合冷门英雄预测全错原因训练数据里热门英雄出现几千次冷门英雄只有几十次one-hot 后冷门英雄的权重几乎没被学到。解决对英雄做最小出现次数过滤出现少于 200 次的英雄归到「其他」类或者改用 embedding让相似英雄共享统计强度。也可以在损失函数里给冷门英雄样本更高权重。4.3 现象换了个版本准确率断崖下跌原因英雄强度随补丁变化上个版本强势的英雄这个版本被削模型学到的权重失效。解决按版本分别训模型或者把版本号作为特征加进去让模型自己学版本交互。更实际的做法是每次大版本更新后用新数据增量训练别指望一个模型吃一年。4.4 现象预测概率全是 0.5 附近区分度差原因特征信息量不足比如只用了英雄 ID 没用到段位或者模型正则过强把权重压得太狠。解决先看特征重要性如果所有特征权重都接近零说明正则太强调小C或调大num_leaves如果是特征本身太弱补充段位、英雄熟练度、近期战绩等维度。别急着换模型先确认输入信息够不够。4.5 现象训练时 AUC 很高验证集波动大原因数据量太小或者验证集划分不均匀某一折里恰好全是某个段位的对局。解决用 K 折交叉验证看方差方差大就加数据。验证集至少留五千局且按段位分层采样保证各段位比例和整体一致。5. 把胜率用起来从单点预测到 BP 辅助模型跑通只是第一步真正有价值的是把它嵌进 BP 流程。我一般会做一个「反事实预测」的小工具给定当前已选的英雄枚举下一个位置选哪个英雄能让己方胜率提升最多。实现上就是固定其他位置把候选英雄逐个填进去跑一遍模型按胜率排序。def suggest_pick(model, base_row, candidate_heroes, hero2idx, slot_col): results [] for h in candidate_heroes: row base_row.copy() row[slot_col] h X_cand, _ build_features(pd.DataFrame([row]), hero2idx) p model.predict(X_cand)[0] results.append((h, p)) return sorted(results, keylambda x: -x[1])[:5]逻辑说明base_row是当前已确定的九个位置slot_col是待选位置candidate_heroes是该位置的候选池。每次只改一个字段重建特征跑一次前向。参数上候选池别放全英雄按位置过滤后通常二三十个一次枚举几十次前向在毫秒级交互体验没问题。验证这套东西是否靠谱我习惯做两件事一是离线看校准曲线把预测概率分桶看每个桶里的实际胜率是否贴近预测值二是在真实 BP 里记录「模型推荐但人没选」的局事后看这些局的实际胜率如果模型推荐的英雄确实胜率更高说明它学到了东西。这一步没有捷径只能靠积累对局慢慢验。最后说个我踩过的坑别把胜率当成真理展示给用户。模型输出 0.62 不代表这局稳赢它只是说在历史相似阵容里蓝方赢了 62%。我现在的习惯是展示时加一句「基于历史对局统计」并且把概率限制在 0.35 到 0.65 之间避免极端数字误导人。做预测模型敬畏数据、留好后悔药比追求那零点几个点的 AUC 更重要。希望帮到你。本文还有配套的精品资源点击获取