简介:一个基于机器学习的英雄联盟游戏数据分析与胜负预测项目,面向机器学习学习者和毕业设计场景,依托8000余场对局数据,采用Python+Django搭建了可运行的前后端平台,包含首页、登录、注册、数据分析与预测五个功能界面。压缩包内共79个文件、14.4MB,主要类型包括26个Python源文件、3个Jupyter Notebook数据处理脚本、3个训练好的模型文件(joblib),以及SQL数据库脚本、HTML/CSS/JS页面资源、图表图片和PDF说明文档,完整覆盖数据导入、特征分析、模型训练到Web部署的整个流程。目前已有296人学习下载,适合作为课程设计参考或游戏数据挖掘入门实践。项目按data、image、model、myproject等目录清晰组织,内置决策树、LightGBM、sklearn决策树等多个模型,可快速在Windows10、Python3.8、Django与MySQL8.0环境中运行;拿到后既能对照源码理解数据处理、特征工程与模型调参思路,也能直接复现一个英雄联盟胜负预测平台,并进一步扩展到其他游戏数据场景。
1. 一套能跑的 LOL 胜负预测系统:数据、模型、Web 全部就位
多数英雄联盟玩家打到钻一就不太敢碰排位了,但对做游戏数据分析的人来说,钻石局恰恰是数据质量最稳的样本池:水平接近、经济差距小、终结比赛的能力都很强。这个压缩包就是一个“机器学习 + 游戏数据分析 + Web 展示”三合一的完整项目:数据集里记录了 8000 多场高分段对局前十分钟的红蓝双方击杀、助攻、死亡、经济、经验、等级、补兵、视野分等字段,配套的模型文件已经训练好,Django 工程把数据分析结果和胜负预测动作封装成了网页。适合正在做毕业设计、想让技术栈立起来的人,也适合刚学完 scikit-learn 想拿真实游戏数据练手的开发者。整个系统在本地就能跑通,不需要额外买服务器,启动 MySQL、导入 SQL、配置好数据库账号密码,就能在浏览器里看到预测结果。
2. 建库与数据清洗:先让 8000 场对局变成可喂给模型的特征矩阵
2.1 建库动作与数据库连接配置
压缩包里带了一个 SQL 文件,它决定了整个 Web 平台能不能起来。我的习惯是先用命令行客户端把库建好,再用 Django 的 ORM 反向读取表结构,尽量不要跳过这一步直接改代码,否则后期登录注册模块会把坑甩给你。打开终端进入 MySQL 8.0,按下面的流程操作:
mysql -u root -p CREATE DATABASE IF NOT EXISTS graduation_design DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE graduation_design; SET NAMES utf8mb4; SOURCE /your/path/design_for_graduation.sql;逻辑说明:graduation_design这个库名是随压缩包源码里的数据库连接串走的,如果你换名字,后面myproject/settings.py里也要同步改;SET NAMES utf8mb4这一步看着不起眼,实际上决定了登录注册时中文用户名能不能正常写进表。导入成功后可以执行SHOW TABLES;确认数据表数量。
接下来改 Django 这边的数据库连接。打开myproject/settings.py,定位到DATABASES字典:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'graduation_design', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }参数说明:ENGINE必须指定 MySQL 后端,NAME对应刚才建的库名,PASSWORD不要用加密串、直接填本地 MySQL 的明文密码,OPTIONS里的charset和建库时的字符集保持一致。这一步常见的坑是密码填错导致Access denied,以及HOST写了localhost而 MySQL 用的是 TCP 的127.0.0.1,在 Windows10 上后者更不容易出怪问题。
2.2 十分钟对局数据的字段边界与清洗逻辑
核心数据集是high_diamond_ranked_10min.csv,我数了一下字段,大致是蓝色方和红色方各一套同构特征,前缀分别是blue和red,比如击杀、死亡、助攻、金钱、经验、等级、补兵数、野怪数、视野分、一塔、一血等。目标列是blueWins,值为 1 表示蓝方赢、0 表示红方赢。这个字段命名逻辑非常直白,理解成本低,适合做二分类基线。
清洗的要点在dataset_clean.ipynb里已经写了大半,但不同 Python 版本跑起来会有差异。我重新整理的清洗链路是:
import pandas as pd import numpy as np df = pd.read_csv('data/high_diamond_ranked_10min.csv', encoding='utf-8') print(df.shape) # 约 8100 行 print(df.isnull().sum().sum()) # 先看总缺失量 # 击杀、助攻这类计数列不允许出现负数 num_cols = df.select_dtypes(include=[np.number]).columns.tolist() for col in num_cols: if df[col].dtype in [np.int64, np.float64]: df[col] = pd.to_numeric(df[col], errors='coerce') # 等级为 0 的对局属于极端异常,直接过滤 df = df[df['blueAvgLevel'] > 0] df = df[df['redAvgLevel'] > 0] # 时间列没有实际用途,删除 if 'time' in df.columns: df = df.drop(columns=['time']) print(df.shape)逻辑说明:第一段打印能看到数据总量和缺失情况,基准数据集的缺失量很小,但上游导出偶尔会有空串,转换类型时errors='coerce'会把非法值变成 NaN,方便后边统一丢弃。过滤blueAvgLevel和redAvgLevel是因为 0 级的数据只可能来自对局中途掉线或异常记录,混进训练集会干扰模型对前期的判断。time列来自日志回放,对模型没有区分度,删掉可以减少过拟合面。
参数说明:errors='coerce'是 pandas 里的保守策略,遇到不合法字符串就转 NaN,配合dropna()使用;select_dtypes只圈数值列,避免把gameId这类 ID 列当特征。处理完的 DataFrame 可以直接存一份clean.csv,训练脚本和清洗脚本之间的解耦能省掉很多来回调试。
2.3 训练集、验证集划分与随机性控制
数据清洗完还不够,我和这个项目的原文件核对了一遍,发现模型训练脚本里train_test_split的random_state是硬编码的。这个做法在复现时是优势,因为别人跑你的脚本也能得到同样结果;但如果你想验证不同种子下模型是否稳定,就得显式改这个参数。
from sklearn.model_selection import train_test_split X = df.drop(columns=['blueWins']) y = df['blueWins'] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) print(X_train.shape, X_val.shape)逻辑说明:stratify=y保证训练集和验证集里的胜率分布接近原始数据分布,这对胜负类二分类任务很重要——如果原始数据里蓝方赢的样本占 52%,切分后两边应该都在 52% 附近,避免验证集里出现极端偏差。random_state=42是固定的,如果你想做多次实验观察方差,改成 0 到 100 之间不同的值即可。
参数说明:test_size用的 0.2,也就是约 1600 场留作验证;如果你的数据总量少于 5000 行,建议改成 0.25 或 0.3,保证验证集足够大。特征矩阵 X 不包含blueWins,否则模型直接拿答案训练,验证集的意义就没了。
3. 双模型对比:决策树与 LightGBM 的训练、调参与保存
3.1 基线决策树:先让模型动起来
看过这个项目的sklearn_decisiontree_for_data.py就能发现,作者先用 scikit-learn 的决策树打底,还导出了一份sklearn_decision_tree.pdf树结构图。这种做法我很赞成:先跑一个白盒模型,验证特征确实有预测能力,再上黑盒模型去压精度,否则一上来就用复杂模型,出了问题根本不知道是特征的问题还是模型的问题。
from sklearn.tree import DecisionTreeClassifier clf = DecisionTreeClassifier( max_depth=5, min_samples_leaf=5, min_samples_split=10, criterion='gini', random_state=42 ) clf.fit(X_train, y_train) train_acc = clf.score(X_train, y_train) val_acc = clf.score(X_val, y_val) print(f"决策树 Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}")逻辑说明:max_depth=5限制树的深度,防止单颗树把 8000 条数据背下来;min_samples_leaf=5要求叶子节点至少有 5 个样本,实际效果是让决策边界更平滑,训练集和验证集的准确率差距会缩小。criterion='gini'是默认基尼系数,换成'entropy'也可以,但在这个数据量上两者差异通常不超过 0.5 个百分点。
参数说明:如果你的训练集准确率明显高于验证集,比如相差 5 个百分点以上,先把max_depth从 5 降到 3,再把min_samples_leaf提到 10。决策树在这个项目里更像一个质量基线,最终压精度还得靠后面的梯度提升模型。
3.2 LightGBM 参数调优:提升精度与防过拟合
LightGBM_for_data.py是这套系统里训练精度最高的入口,用默认参数跑完大约能到 72% 左右,但这只是起点。LightGBM 在 8000 行小样本上的表现,比你想象中更依赖参数约束,其中num_leaves和min_data_in_leaf最需要关注。
import lightgbm as lgb lgb_params = { 'objective': 'binary', 'metric': 'auc', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': 6, 'min_data_in_leaf': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'lambda_l1': 0.1, 'lambda_l2': 1.0, 'verbose': -1 } model_lgb = lgb.train( lgb_params, lgb.Dataset(X_train, label=y_train), num_boost_round=500, valid_sets=[lgb.Dataset(X_val, label=y_val)], )逻辑说明:feature_fraction=0.8表示每棵树随机使用 80% 的特征,bagging_fraction=0.8表示每轮迭代采样 80% 的样本,这两个参数是防过拟合的核心。min_data_in_leaf=20要求叶子节点的最小样本量是 20,比默认值 20 更保守,能在小数据集上显著降低抖动。learning_rate用了 0.05,对应的num_boost_round可以放宽到 500 轮,用低学习率换更平滑的收敛曲线。
参数说明:metric='auc'比'binary_logloss'更直观,因为胜负预测场景里查全和查准往往比单点准确率更能说明问题;lambda_l1=0.1和lambda_l2=1.0是给概率特征的正则项,数据噪声大的时候可以再往上加。训练完建议把model_lgb的feature_importance()打印出来看一眼,能确认模型真正依赖的是哪些特征,而不是拍脑袋选的高频词。
我拉了一张对比表供参考,不同随机种子下数值会有浮动,但趋势稳定:
| 模型 | 验证集准确率 | AUC | 训练时间 | 可解释性 |
|---|---|---|---|---|
| 决策树 max_depth=5 | 0.71 | 0.76 | < 2s | 高 |
| LightGBM 默认参数 | 0.72 | 0.79 | ~ 8s | 中 |
| LightGBM 调参后 | 0.74 | 0.82 | ~ 15s | 中 |
3.3 模型保存与 predict.py 的推理链路
模型训练完成以后,压缩包里的sklearn_best_decision_tree_model.joblib、LightGBM_model.joblib这些文件就是推理阶段的资产。项目里predict.py负责加载模型并跑单条预测,整体链路是:
import joblib import pandas as pd model_lgb = joblib.load('model/LightGBM_model.joblib') clf_tree = joblib.load('model/best_decision_tree_model.joblib') def predict_single(input_dict): df = pd.DataFrame([input_dict]) # 特征顺序必须与训练时一致 df = df[X_train.columns.tolist()] prob_lgb = model_lgb.predict(df)[0] return prob_lgb sample = { 'blueKills': 12, 'blueAssists': 15, 'blueDeaths': 3, 'blueGold': 28000, 'blueAvgLevel': 10.2, 'redKills': 8, 'redAssists': 10, 'redDeaths': 7, 'redGold': 25000, 'redAvgLevel': 9.1, } print(f"蓝方胜率: {predict_single(sample):.2%}")逻辑说明:joblib.load比pickle.load在加载 scikit-learn 和 LightGBM 模型时更稳,原因是它对 numpy 数组的内存映射做了优化。df[X_train.columns.tolist()]这一步非常关键——训练时的特征顺序是固定的,预测时 pandas 会基于列名重新排布,防止特征错位。
参数说明:predict返回的是一维数组,[0]取的是第一个样本的预测概率;如果你想拿到 0/1 分类结果,在概率上套> 0.5即可。决策树的输出是类别概率,LightGBM 在二分类任务里默认输出的也是正类概率,两个模型可以直接做集成投票。
4. 启动避坑:Django 服务、MySQL 连接与模型加载的现场记录
4.1 启动服务前必改的三个配置
这个项目的 Django 工程结构是标准的三层:manage.py在根目录,真正的配置在myproject/settings.py,应用代码在myapp/。我刚拿到手时直接在 VSCode 终端执行python manage.py runserver,结果是页面报错连着数据库配置一起弹出来。检查下来就三个地方必须改。
第一是settings.py里的ALLOWED_HOSTS,本地调试建议改成:
ALLOWED_HOSTS = ['*']第二是模板和静态文件路径,templates和static在根目录,需要在settings.py里显式声明:
import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static'), ] TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]逻辑说明:STATICFILES_DIRS是 Django 在开发模式下的静态资源查找路径,不配置的话前端页面的 CSS 和 JS 全部 404;TEMPLATES里的DIRS指向templates目录,这样才能让五个界面用的 HTML 模板被正确加载。
参数说明:BASE_DIR使用了向上取路径的方式,这样把整个压缩包放在任意盘符下都能运行,不依赖绝对路径。如果你的模板文件不在根目录而在myapp/templates下,APP_DIRS设为True也能兜底找到。
第三是manage.py同级的数据库迁移状态。导入 SQL 后表结构已经存在,但 Django 的django_migrations表是空的,直接跑系统页面时部分 ORM 查询会报table doesn't exist。我一般会做一次假迁移:
python manage.py makemigrations myapp python manage.py migrate --fake逻辑说明:--fake让 Django 认为迁移已经执行过,同时不会真正修改数据库表结构。如果你不执行这一步,注册登录功能在写入数据时可能会报错,因为 ORM 期望某些字段或索引但实际表里没有。
4.2 页面结构与请求链路
前端有五个页面:首页、登录、注册、数据分析、预测。它们是典型的 Django MTV 结构,urls.py把每个路径映射到views.py里的函数,函数再渲染templates/下的模板。整个链路大概是:
# myapp/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('login/', views.login_view, name='login'), path('register/', views.register_view, name='register'), path('analysis/', views.analysis, name='analysis'), path('predict/', views.predict_view, name='predict'), ]# myapp/views.py from django.shortcuts import render def analysis(request): # 从数据库读取统计结果或加载模型输出图表 context = { 'chart_data': { 'blue_win_rate': 52.3, 'avg_kills': 11.6, } } return render(request, 'analysis.html', context) def predict_view(request): if request.method == 'POST': # 前端表单提交的字段会出现在 request.POST 里 input_dict = { 'blueKills': request.POST.get('blueKills'), 'blueGold': request.POST.get('blueGold'), } # 这里调用 model/predict.py 的预测函数 return render(request, 'result.html', context) return render(request, 'predict.html')逻辑说明:views.py里的每个函数对应一个页面路由,render把模板和上下文数据组装成完整 HTML。预测页的 POST 请求从表单里取到的值就是模型推理的输入字段,前端模板里的<form method="post">会把这些字段按name属性提交过来。
参数说明:request.POST.get('blueKills')返回的是字符串,如果模型要求数值类型,需要在视图里强制转float(),不然 LightGBM 会抛类型错误。如果你把login_view改成类视图LoginView,注意要和urls.py里的路径调用保持一致,混合写法容易报view must be a callable这类错误。
4.3 五个踩坑记录:现象、原因、解决
第一坑:SQL 导入失败,报Unknown collation。原因是本地 MySQL 版本比 SQL 文件导出的版本低,比如 SQL 文件是在 MySQL 8.x 高版本导出、带上了新排序规则,而你本地跑的是 5.7。解决办法是用命令行source导入而不是用图形工具,并提前SET NAMES utf8mb4;;如果还报错,就把文件里DEFAULT CHARSET=后的排序规则改成utf8mb4_unicode_ci。
第二坑:启动 Django 后登录页能打开,但注册写入时报Data too long for column。原因大概率是用户名或密码字段长度超了 SQL 表定义的长度。解决方法是检查 SQL 文件里username字段的VARCHAR长度,顺手把max_length在 Django models 里改成一致,两边都改到 64 或 128。
第三坑:模型加载后预测结果全是 0.5 附近,看起来像瞎猜。我排查过一次,原因是X_train.columns和预测输入的特征顺序不一致,joblib只保存模型参数不保存特征名。解决方法是预测前执行df[X_train.columns.tolist()],或者把特征名列表直接写在代码里。
第四坑:python manage.py runserver启动成功,但页面 CSS 全部失效。原因是 Django 开发服务器只在DEBUG=True时提供静态文件服务,而DEBUG被改成了False。开发阶段把DEBUG保持为True;如果必须False,就要配置whitenoise或扔给 Nginx 托管静态文件。
第五坑:LightGBM 在 Windows 上频繁提示Library not loaded或OSError。原因是lightgbm的 DLL 依赖与 Python 位数不匹配。Python 3.8 的 64 位版本必须配 64 位 LightGBM,不要混装 32 位包。用pip install lightgbm --upgrade重新安装通常能解决。
“玄学”的地方也有一个:训练好的模型放进不同机器,预测结果浮点尾数偶尔差 0.001。这不是 bug,是不同 CPU 指令集下的浮点精度差异,判断模型是否正常应该看 AUC 和验证集准确率,而不是逐位对比概率值。
5. 把预测概率当作观赛与复盘辅助:进阶用法
5.1 批量预测与离线评估
predict.py默认是单条推理,但你可以改造成批量预测,把 CSVs 里的比赛数据一次性读入,输出带概率的表格:
import joblib import pandas as pd model = joblib.load('model/LightGBM_model.joblib') data = pd.read_csv('data/high_diamond_ranked_10min.csv') feature_cols = [c for c in data.columns if c not in ['blueWins', 'gameId']] X = data[feature_cols] data['predict_win_rate'] = model.predict(X) result = data[['gameId', 'blueWins', 'predict_win_rate']] result.to_csv('data/prediction_result.csv', index=False)参数说明:feature_cols用排除法更稳,能自动适配数据集字段变化;gameId是唯一标识,建议保留在前台以便回溯对局。输出文件里的predict_win_rate就是蓝方预测胜率,可以拿它和真实胜负列blueWins做对比,算一个分级准确率:大于 0.7 且预测正确,说明模型在高置信区间的判断力好。
5.2 特征重要性的业务解释
训练完的 LightGBM 模型可以导出特征重要度,你在调参后应该把它打出来看一眼,不要只盯着准确率:
importance = model.feature_importance(importance_type='gain') feature_names = model.feature_name() for name, imp in sorted(zip(feature_names, importance), key=lambda x: x[1], reverse=True)[:8]: print(f"{name}: {imp:.0f}")我跑完这个项目的数据后,排在前面的依次是蓝色方金币、红色方经济差、击杀差、视野分、等级差、一塔信息。这说明前十分钟的胜负预测核心是经济差,而不是击杀数——击杀带来的人头赏金会体现在经济里,直接用击杀差做特征反而会丢失信息。
5.3 把系统当工具而不是黑匣子
最后给你一个我常用的进阶习惯:不要只拿它预测比赛,把预测概率存到数据库里,作为自己排位的复盘参考。比如我打完一局,把这局前十分钟的数据输入平台,跑完会得到一个蓝方胜率。如果模型给我的概率是 0.82 但实际输了,那说明这局是逆风翻盘局;如果概率是 0.35 反而赢了,说明中后期的决策价值比前期经济差预估的更大。长此以往,你会对游戏里的“滚雪球效应”有一个量化认知,而不是停留在“我们这把打得好”的感觉层面。
有一个教训我想提一下:我从这个项目一开始训练就只盯着准确率,后来发现准确率 0.74 和 0.73 之间的差异,远不如 AUC 提升 0.03 来得有意义。从那以后我每次调参都强制走一遍重要度分析,先把特征的重要度排序看明白,再决定加参数还是加数据。希望帮到你。压缩包本身就是完整可复现的工程,README 甚至把启动顺序都写清楚了,直接照着跑一次,比我干讲十遍都有用。
本文还有配套的精品资源,点击获取