☰
基于Hadoop与Spark的大数据实战:英雄联盟排位赛阵容分析平台搭建
2026/10/6 13:58:45 网站建设 项目流程

做这个hadoop+Spark+基于Python的英雄联盟排位赛阵容分析平台,起因其实很简单:某段时间我排位连跪,复盘时翻了几十场对局记录,发现每场只能看到零散数据,完全看不出"阵容"层面到底输在哪。当时正好在啃大数据生态,就想着不如把排位对局数据全量落盘,用Hadoop做底层存储、Spark做批量分析、Python做清洗和可视化大屏,把"这套阵容为什么赢、为什么输"真正量化出来。

这个平台做完之后,效果超出预期:不仅能算单套阵容的综合胜率,还能拆出经济曲线、控制链、视野、前期节奏等十几个维度,把阵容画像直接投到屏幕上。整个项目从环境搭建到数据分析再到可视化展示,链路完整,非常适合拿来做大数据技术栈的课程设计、毕设或工程练手。下面我把整个搭建和调试过程拆开讲,能直接照着复现。

1. 先想清楚:排位阵容分析到底要解决什么问题

1.1 为什么以"阵容"作为核心分析对象

英雄联盟是一个5v5对抗游戏,单场的胜负往往被解读成"某个选手操作好"或"某波团战失误",但站在大数据视角,单场是噪声,阵容才是可聚合的样本。同一套阵容在100场对局里表现如何,哪些英雄组合在一起会显著拉高或拉低胜率,这些规律只有通过海量对局才能显现。这个平台的出发点,就是不做"上帝视角"的赛后复盘,而是做"统计学视角"的阵容体检。

我最终确定的分析对象是:每场排位赛中的两个阵营,按位置拆出五个英雄,再关联对局时长、击杀、经济、防御塔、视野得分、小龙/大龙/先锋等客观指标。这样每一场对局就变成了一条结构化记录,几十场作为样本不够看,但几千场、上万分比赛聚合起来之后,阵容的"强弱画像"就非常清晰了。

1.2 想从数据里回答的问题清单

在设计分析逻辑之前,我先列了一张问题清单,后续所有报表维度都围绕这些问题展开:

  • 当前版本里,哪些阵容组合的胜率明显高于整体均值?
  • 某两个英雄同时出场时,化学反应是正向还是负向?
  • 阵容在前中期(0-15分钟)、中后期(15-25分钟)、后期(25分钟以后)的优劣势如何分布?
  • 赢下对局的阵容在控制、开团、消耗、分带等标签上有没有共性?
  • 输出位英雄的装备走向与团队经济分配是否存在可量化的规律?

这些问题表面上是游戏理解,落到工程上就是一组聚合指标:阵容胜率、经济效率、击杀贡献、控制链覆盖率、资源控制率。每一类指标都能用Hadoop生态的批处理链路串起来,不需要实时计算,T+1跑批就够用。

2. 平台架构与数据流转:从原始对局到可视化大屏的一条链路

2.1 技术选型:每个组件只干它最擅长的事

整个平台的技术栈看起来"重",但真拆开之后其实很清晰:没有哪个组件在抢别人的活,全是按数据流分工。

链路环节选型职责
数据采集与清洗Python解析原始对局数据,做字段抽取、类型转换、异常过滤
分布式存储Hadoop HDFS存放清洗前后的半结构化数据,按目录分区
批量分析与聚合Spark读取清洗数据,跑SQL/DataFrame作业,生成统计结果
结果存储MySQL存放Spark产出的聚合结果,供可视化层查询
可视化呈现Pyecharts + Flask + ECharts提供大屏页面与数据接口

这里有一个容易被忽略的点:为什么不在清洗阶段直接做完整分析,非要引入Hadoop和Spark?

我的理由是:采集端Python跑的单机脚本,处理几百MB没问题,但一旦数据量到了几十GB、上百GB,单机Pandas会直接内存爆炸。HDFS负责把大文件分布到多个节点,Spark则把计算任务切成小任务并行执行。换句话说,这套架构不是为了炫技,而是为了"数据量涨上去之后不用推翻重来"。

2.2 数据流转与目录约定

我这次的数据链路是这样设计的:

  1. Python采集脚本拿到原始JSON,经过第一层清洗后,写入HDFS的/lol/raw目录,按赛季和日期分区。
  2. 清洗脚本再次读取raw目录,做字段规范化、缺失值处理,输出为Parquet格式,落到/lol/clean目录。
  3. Spark作业读取clean目录,执行阵容聚合、英雄组合分析,把结果写入MySQL的几张结果表。
  4. Flask后端提供/api/comp_stats、/api/hero_pair等接口,大屏通过Ajax轮询拉数据。

这套链路最舒服的地方在于:每一层的数据都是"物化"过的,中途任何一步挂了,只需要重跑那一层,不用从采集端重新拉一遍原始数据。

3. Hadoop 环境搭建:伪分布式快速起步,集群模式按需切换

3.1 伪分布式起步:一台机器也能跑通全流程

很多初学者一上来就在VMware里开三台虚拟机搭Hadoop集群,结果网络配置搞了两天,SSH免密没配好,还没开始分析就先放弃了。我的建议是:纯学习阶段先用伪分布式,把全链路跑通,再考虑要不要扩成集群。

伪分布式就是在一台Linux机器上同时启动NameNode、DataNode、ResourceManager、NodeManager等进程。我用的环境是Ubuntu 20.04 + JDK 8 + Hadoop 3.3.x。需要提前做三件事:配置JAVA_HOME、配置SSH localhost免密登录、保证/etc/hosts里主机名解析正常。

ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys

免密登录的重要性在于:Hadoop启动脚本需要通过SSH在节点上拉起进程,如果每次都要输密码,脚本会卡住。这个问题虽然不起眼,但确实是我第一次搭建时卡得最久的地方。

3.2 配置文件的坑与启动检查

Hadoop的配置集中在$HADOOP_HOME/etc/hadoop/目录下,核心是五个文件:core-site.xml、hdfs-site.xml、yarn-site.xml、mapred-site.xml和workers。伪分布式模式下,我只改了前三个。

core-site.xml最关键的是fs.defaultFS,它决定了HDFS的访问地址。我使用hdfs://localhost:9000:

<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/home/hadoop/tmp</value> </property> </configuration>

hdfs-site.xml里,伪分布式必须把副本数dfs.replication设为1,否则三份副本会占满磁盘。同时指定NameNode和DataNode的本地目录:

<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.namenode.name.dir</name> <value>/home/hadoop/hdfs/name</value> </property> <property> <name>dfs.datanode.data.dir</name> <value>/home/hadoop/hdfs/data</value> </property> </configuration>

启动前记得格式化NameNode,否则会出现"NameNode not formatted"错误。格式化命令是hdfs namenode -format。这里有个小坑:如果之前启动过Hadoop,或者目录路径换过,格式化的目录必须清空,否则DataNode进程可能反复退出。

启动和检查命令如下:

start-dfs.sh start-yarn.sh jps

jps输出里应该能看到NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager这五个进程。如果少了任何一个,优先查看$HADOOP_HOME/logs/下的日志,不要瞎猜。

3.3 从伪分布式到HA集群:Zookeeper要管的事

平台开发阶段,单节点完全够用。但如果后面要接到真实大规模场景,就绕不开高可用。Hadoop高可用集群里,NameNode不能单点故障,所以核心思路是让两个NameNode组成主备,Zookeeper负责检测主节点心跳、触发故障转移。

我最初理解Zookeeper时绕了一个弯:以为ZK是给HDFS存数据的,后来才理清楚,ZK只是当一个"协调者",存的是集群元数据的锁和状态信息。要做HA,必须引入JournalNode来同步两个NameNode的元数据,Zookeeper则确保同时只有一个Active节点对外服务。这一步是"hadoop和zookeeper整合实战"中最容易混的部分,建议对照官方文档做两遍,第一遍照抄,第二遍自己画一遍状态流转图。

不过回到这个平台本身,伪分布式模式下不需要部署ZK,先把Spark分析跑通,再考虑集群化是更务实的路径。

4. 数据采集与预处理:脏数据怎么变成可用特征

4.1 数据来源与原始JSON结构

数据是一切分析的基础,这里要强调合规性。平台使用的数据来源包括:官方赛事公开数据、个人排位对局记录导出,以及公开数据集。所有数据都只用于本地学习研究,不涉及非公开接口或绕过限制的采集方式。

原始对局数据通常是JSON结构,核心字段大致是这样的:

{ "game_id": "202502280001", "game_duration": 1820, "teams": [ { "team_id": 100, "win": true, "players": [ { "champion_id": 64, "position": "top", "kills": 3, "deaths": 5, "assists": 12, "gold_earned": 12500, "vision_score": 42, "total_damage": 52100 } ] } ] }

这类JSON有几个通病:字段命名五花八门、英雄ID对应关系分散在不同文件里、部分场次数据缺失。如果不做清洗直接丢给Spark,后面聚合时会出现大量脏数据,所以这一步必须放在HDFS和Spark之前。

4.2 清洗规则与特征提取

清洗脚本我用Python写,核心逻辑是三层:

第一层是结构展开。把嵌套的JSON拍平成一张宽表,一行代表一个玩家在一场比赛中的表现,同时把队伍胜负、对局时长等场次级字段冗余带过来。

第二层是字段规范化。英雄ID要转换成英雄名称;装备ID要映射成装备名;位置字段统一成top/jungle/mid/bottom/support五种写法;时间单位统一为秒或分钟。

第三层是异常过滤。对局时长小于5分钟的局基本是挂机和秒退,直接过滤;KDA中死亡数为0的情况要特殊处理,不能直接除0;金币、伤害为负数的记录属于脏数据,删掉。

我还顺手做了一步"期切分":按对局时间把数据切到early、mid、late三个时期,对应0-15分钟、15-25分钟、25分钟以后。这样后期做阵容强弱趋势分析时,不用在Spark里再算时间窗口。

清洗示例代码如下:

import json import pandas as pd def parse_match(raw: dict) -> list: rows = [] duration = raw["game_duration"] for team in raw["teams"]: for player in team["players"]: rows.append({ "game_id": raw["game_id"], "duration": duration, "win": 1 if team["win"] else 0, "champion": champion_map.get(player["champion_id"], "unknown"), "position": player["position"], "kills": player["kills"], "deaths": player["deaths"], "assists": player["assists"], "gold": player["gold_earned"], "vision": player["vision_score"], "damage": player["total_damage"], "phase": cut_phase(duration) }) return rows

清洗后的结果直接以Parquet格式写入HDFS,因为Parquet是列式存储,对Spark后续按列过滤和聚合非常友好,比CSV小很多,读取速度也快。这一步是很多教程不会强调的细节,我实际对比过:同样一批数据,CSV路径下Spark SQL跑一个聚合要40秒,Parquet路径只要12秒左右。

4.3 HDFS分区与存储估算

HDFS目录我设计成三层分区:

/lol/raw/{season}/{date}/ /lol/clean/{season}/{date}/ /lol/result/{analysis_type}/

raw和clean按赛季、日期分区,是因为排位数据天然带有时间属性,按天分区后面做增量处理非常方便。result目录按分析类型分区,比如comp_stats存放阵容统计,hero_pair存放英雄组合分析。

存储量方面可以估算一下:一场排位对局的JSON大约几十KB,清洗后的Parquet每行玩家级记录大约几百字节。按一个赛季5000场来算,clean目录大概不到1GB,伪分布式完全扛得住。如果按日增量接入多赛季数据,再到HDFS扩容的时机,集群化才有真正的必要性。

5. Spark 分析核心:用 SQL 把"阵容"变成可比较的画像

5.1 阵容画像:从对局明细到可比较的数值

数据清洗完之后,摆在Spark面前的就是一张宽表:每行是一个玩家在一场比赛中的数据。但"阵容"是个团队概念,需要从"单个玩家表现"提升到"5人组合表现"。

我在平台上定义了四类核心画像指标:

  • 基础胜率:某个5人组合或某个英雄组合的胜场数/总场数
  • 资源控制率:小龙/大龙/峡谷先锋的获取比例
  • 经济效率:每金币投入转化为伤害的能力,用总伤害/总金币衡量
  • 节奏强度:通过首塔、一血、前期经济差来判断阵容是前期阵容还是后期阵容

这些指标的共性在于全部基于"比率"或"均值",而不是绝对值。因为英雄联盟每个版本的节奏完全不同,只有把数据归一化成比率,才能在不同版本之间做横向比较。

5.2 Spark SQL 实现:单场聚合与英雄联动

Spark 分析我直接用PySpark SQL写,开发体验很接近写普通SQL,但底层是分布式执行。核心任务有两个:阵容胜率统计和英雄组合关联分析。

阵容胜率统计相对直接,按5个英雄组合分组,统计胜负。由于数据是玩家级别的,需要先用窗口函数把同队的5个英雄拼起来:

from pyspark.sql import SparkSession from pyspark.sql.window import Window from pyspark.sql import functions as F spark = SparkSession.builder \ .appName("lol_comp_analysis") \ .master("yarn") \ .getOrCreate() df = spark.read.parquet("hdfs://localhost:9000/lol/clean/*.parquet") # 用窗口函数给每个队伍内的玩家按位置排序,拼出阵容ID w = Window.partitionBy("game_id", "win").orderBy("position") comp_df = df.withColumn( "comp_id", F.concat_ws("-", F.collect_list("champion").over(w)) ) comp_stats = comp_df.groupBy("comp_id", "win").count() \ .groupBy("comp_id") \ .agg( F.sum("count").alias("total"), F.sum(F.when(F.col("win") == 1, F.col("count")).otherwise(0)).alias("wins") ) \ .withColumn("win_rate", F.col("wins") / F.col("total"))

英雄组合关联分析的逻辑更绕一点。要算"英雄A和英雄B同时出场时的胜率",就不能只按队伍聚合,需要把同队英雄两两配对。这一步我用DataFrame自关联,把同一场、同一队、不同位置的英雄拆成两两组合:

pair_df = df.alias("a").join( df.alias("b"), (F.col("a.game_id") == F.col("b.game_id")) & (F.col("a.win") == F.col("b.win")) & (F.col("a.position") < F.col("b.position")), "inner" ).select( F.col("a.game_id"), F.col("a.win"), F.col("a.champion").alias("hero_a"), F.col("b.champion").alias("hero_b") ) pair_stats = pair_df.groupBy("hero_a", "hero_b", "win").count() \ .groupBy("hero_a", "hero_b") \ .agg( F.sum("count").alias("total"), F.sum(F.when(F.col("win") == 1, F.col("count")).otherwise(0)).alias("wins") ) \ .withColumn("win_rate", F.col("wins") / F.col("total")) \ .filter(F.col("total") >= 30) # 样本量太少没有统计意义

最后这一步total >= 30的过滤非常重要。如果组合只出场了3次,胜率是100%也没有参考价值,必须设置最低样本门槛。

5.3 输出表结构与执行参数

Spark作业产出的结果写入MySQL的三张核心表:

  • tbl_comp_stats:5英雄组合的出场次数、胜率、平均对局时长
  • tbl_hero_pair:英雄两两组合的联动胜率与场次
  • tbl_phase_stats:不同时期(early/mid/late)的各阵容关键指标均值

当Spark作业任务较重时,Executors的资源配置也很关键。我第一次在YARN上跑聚合时,默认参数直接把集群内存打爆了,后来改成按数据量估算:

spark-submit \ --master yarn \ --deploy-mode cluster \ --num-executors 4 \ --executor-memory 4g \ --executor-cores 2 \ analysis_comp.py

对于几GB的数据,4个4G内存的Executor通常够用。如果数据量更大,优先加Executor数量而不是单Executor内存,因为单Executor内存过大会导致GC压力。

6. 可视化大屏:把分析结果摆上前台

6.1 大屏布局:业务指标怎么摆才有逻辑

大数据项目最后如果没有一个"看得见"的呈现,很容易被当成纯后台作业。可视化大屏的作用,是把Spark跑出来的结果,以最小理解成本展示给非技术背景的人。

布局我采用了经典的三栏式:

  • 顶部是一条KPI指标带,展示总对局数、平均胜率、最具统治力阵容、当前版本登场英雄数。
  • 左侧是一个柱状图,展示登场率Top10的英雄;下面接一个表格,展示阵容胜率Top10。
  • 中间核心区是一张雷达图,画当前选中阵容在输出、控制、经济、视野、防御等维度上的得分,再往下是英雄组合联动热力图。
  • 右侧放一个折线图,展示不同时期(前期/中期/后期)阵容胜率的变化趋势。

真正把大屏从"好看"变"有用"的关键,是给图表之间加联动。比如点击左侧的英雄柱状图,中间的雷达图就切换成包含该英雄的阵容画像。这个联动效果用的是ECharts的事件回调,前端代码量不大,但演示效果提升非常明显。

6.2 Flask + Pyecharts 的接法

我用Pyecharts在Python侧直接生成图表配置,然后由Flask提供数据接口,前端负责请求和渲染。有一个常见的坑是Pyecharts生成的是HTML页面,如果直接把它塞进大屏框架,样式会很乱。我采用的方式是:后端只返回ECharts需要的JSON配置,前端拿到配置后自己初始化图表实例。

后端接口示例:

from flask import Flask, jsonify from pyspark.sql import SparkSession app = Flask(__name__) @app.route("/api/hero_pair") def hero_pair(): spark = SparkSession.builder.getOrCreate() df = spark.read.jdbc( url="jdbc:mysql://localhost:3306/lol_analysis", table="tbl_hero_pair", properties={"user": "root", "password": "***"} ) rows = df.limit(50).toPandas().to_dict(orient="records") return jsonify({"code": 0, "data": rows})

前端每5秒轮询一次这个接口,拉到新数据后更新图表。由于结果表是T+1更新的,轮询频率不需要太高,否则反而是资源浪费。

6.3 缓存、刷新与部署细节

大屏上线后遇到一个比较影响体验的问题:每次刷新页面,所有图表要重新请求后端,盯着屏幕看的人会看到一片白屏。解决办法很简单,后端加一层缓存,Spark结果写入MySQL后,用Redis缓存接口响应,缓存有效期设为10分钟。

部署方面,我用Nginx托管静态HTML页面,Flask作为后端服务跑在5000端口,通过Nginx反向代理把/api/转发到Flask。这样前端静态资源和API请求都是同一域名,避免跨域问题。实际踩过的坑是:如果直接用Flask托管静态页面,大屏的图表资源加载会很慢,用Nginx之后明显顺畅了。

7. 调试复盘:这套平台最容易踩的坑

7.1 资源调度与Spark作业稳定性

整个平台跑下来,问题最集中的阶段就是Spark作业在YARN上的资源管理。我遇到过两类典型错误。

第一类是ExecutorLostFailure:日志里能看到某个Executor被NodeManager杀掉,通常是单Executor内存超限。排查思路是先看Spark UI上的Event Timeline,确认是哪一步触发了内存飙升。我在做英雄组合自关联时,中间有一个巨大的Shuffle,如果不做df.repartition(partitions)控制并行度,很容易在Reduce阶段炸掉。

第二类是Container ... running beyond virtual memory limits。YARN默认会把物理内存和虚拟内存都纳入限制,Python的PySpark进程有时虚拟内存很高,需要在yarn-site.xml里调整:

<property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property>

不过这只是绕过限制,根本解法还是减小单案并行任务的内存压力。

7.2 数据质量与版本兼容问题

对局数据里藏着很多"版本陷阱"。比如英雄联盟版本更新后,某个英雄的重做会导致ID变化,有些英雄会被禁用或者删除,如果清洗脚本里维护的映射表不及时更新,分析结果就会串。我的解决方案是把英雄映射表放在HDFS上的一个版本化目录里,每天跑批前先检查映射表版本,如果版本对不上就触发一次映射更新流程。

还有一个很琐碎但很致命的坑:个别字段可能是null,在Spark里做concat_ws拼阵容ID时,null值会被拼成空串,导致两场完全不同的对局共用同一个comp_id。后来我统一在清洗阶段把所有null都设置为"unknown",才彻底解决这个问题。

中文乱码也遇到过:Pyecharts生成图表默认字体对中文支持没问题,但Linux服务器本地缺中文字体时,ECharts的标题会显示成方块。解决办法是往服务器装fonts-wqy-microhei,然后重新生成缓存。

7.3 大屏呈现的适配与交互细节

大屏往往要在不同分辨率的显示器上投放,如果写死像素宽度,换台设备就乱套。我采用的办法是:先按1920×1080设计,再用CSS的transform: scale()根据屏幕实际宽高做等比缩放。这样字体和图表的相对位置能保持一致。

另一个交互细节是轮询超时。如果Spark正在重跑结果表,MySQL里的数据可能被锁,后端接口响应变慢,前端如果默认超时时间太短,会频繁报错。我在前端把请求超时时间调到了15秒,并且做了错误重试:连续失败3次才提示"数据加载失败",避免用户看一眼大屏就刷出满屏报错。

这套平台做完之后,我个人最大的体会是:把游戏数据和分布式计算放在一起,恰恰是理解大数据实战链路成本最低的方式。因为游戏数据的规模和复杂性都很"亲民",不需要企业级数据量就能把HDFS、Spark、可视化大屏完整串起来。而在这个过程中踩过的每一个坑——从Hadoop的进程起不来,到Spark的Executor爆炸,再到ECharts的中文乱码——都变成了后续做其他项目时可以快速调用的经验。如果你也想做一个能跑通全链路的大数据项目,不妨就从这个阵容分析平台开始,走一遍,比看十遍教程都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询