
简介电商平台积累的海量行为数据催生了精准用户画像的强烈需求。基于Spark的电商用户画像数据挖掘项目源码正是面向大数据开发工程师、数据分析师及电商技术学习者借助Scala和Java混合编程依托分布式计算框架完整覆盖从行为数据清洗、特征提取、建模训练到画像标签生成的工程链路可支撑个性化推荐与精准营销等实际业务。资源包共四百六十二个文件压缩后约十三点四五兆主要包含二百九十六个Scala类文件和七十个Scala源文件辅以二十个Java源文件以及属性、XML、JSON等配置资源并带JavaScript、CSS和HTML前端展示组件模块命名tags-etl、tags-ml、tags-model、tags-web依次对应数据接入、算法训练、标签模型与可视化展示的层次划分。当前已有三百三十九人学习下载。研读源码可掌握RFM、USG等标签模型的Scala实现理解标签数据在HBase等存储与模型服务之间的流转细节也可参考前端呈现方式是一套贴近生产环境的电商画像实战资料。1. 电商用户画像跑在 Spark 上为什么说代码量只占三成做电商用户画像第一反应往往是“我要写多少挖掘代码”。实际把一个基于 Spark 的电商用户画像数据挖掘项目源码从拿到手到跑通你会发现真正花时间的不是模型训练而是数据管道、特征加工和参数调优。这个标题指向的是一整套可落地的工程方案从订单表、行为日志里清洗出用户维度的特征再用 Spark 做批量聚合和挖掘最终产出一张能支撑运营、推荐、营销的画像宽表。适合谁适合已有电商数仓基础、想把用户标签工程化落地的数据工程师也适合准备做 Spark 方向面试项目、需要一套完整源码逻辑做底子的学习者。我经手过几个类似的画像项目一个共同点是单机跑不动Hive 写起来又太长Spark 的 DataFrame API 和 SQL 刚好卡在“开发效率”和“分布式能力”的平衡点上。这篇文章不绕弯子直接从选型理由、数据管道、特征工程、源码实现、集群调优到验证方法把整个链路拆开讲。2. 为什么电商画像押在 Spark 上从选型逻辑到数据管道设计2.1 Spark 不是唯一选择但它是离线和准实时场景的平衡点电商用户画像的数据来源通常是三张表用户信息表、订单事实表、行为日志表。订单表日增几十万到几百万行行为日志日增几亿条。这个量级用 Pandas 处理直接内存溢出用 Hive 写 MR 任务跑得慢且调试麻烦用 Flink 做实时画像成本又高——绝大多数中小电商团队根本撑不起一套完整的实时链路。Spark 落在这个中间地带离线批处理能力成熟Spark SQL 上手快DataFrame 的广播变量、分区剪裁、动态资源分配都有现成机制。还有一个容易被忽略的点画像模型的迭代速度。电商业务标签比如“高价值用户”“流失预警”往往每周甚至每天都得重算。Spark 的 lineage 机制让重复计算可以复用 checkpoint配合持久化策略重跑一次全量画像的时间能压到小时级。这和“数据挖掘”的诉求是吻合的——挖掘不是一次性的事是持续滚动的事。2.2 数据管道的第一公里JSON 日志解析与脏数据清洗拿到订单和日志数据的第一件事不是算特征而是把数据管道打通。实际场景里行为日志最常见的是 JSON 格式而且嵌套层级不浅。Spark 读取 JSON 有两个坑一是 schema 推断在数据量大的时候会额外扫一遍文件拖慢任务二是脏数据会直接导致解析失败。常见做法是提前定义 schema用from_json搭配unresolved处理异常行。import org.apache.spark.sql.types._ import org.apache.spark.sql.functions._ val jsonSchema StructType(Array( StructField(user_id, StringType, true), StructField(event, StringType, true), StructField(timestamp, LongType, true), StructField(page, StringType, true), StructField(item_id, StringType, true), StructField(duration, IntegerType, true) )) val rawLog spark.read .option(multiLine, false) .schema(jsonSchema) .json(hdfs:///data/logs/20240101/) val cleanedLog rawLog .filter(col(user_id).isNotNull col(user_id) ! ) .withColumn(date, from_unixtime(col(timestamp) / 1000, yyyy-MM-dd))这段代码的逻辑是先显式声明 schema避免 Spark 反复推断然后过滤掉 user_id 为空的行最后从时间戳里提取日期字段方便后续按天分区聚合。timestamp / 1000是因为很多前端埋点给的是毫秒级时间戳不除以 1000 会导致日期全是 1970 年——这个坑我踩过不止一次。2.3 分区分桶设计直接决定任务能不能跑完数据管道里最容易忽略的是底层存储布局。Spark 读取 Hive 表时如果分区字段选错比如用date字符串分区但实际按hour粒度写入会产生大量小文件拖垮后续所有 Job。电商画像场景我一般建议按date做一级分区、user_id的哈希值做二级分桶。分桶数取集群核数的 2 到 3 倍比如 100 个 core 就设 200 到 300 个桶。这样后续 join 时能触发 bucket join避免 shuffle 带来的网络开销。CREATE TABLE dwd_user_behavior ( user_id STRING, event STRING, timestamp BIGINT, page STRING, item_id STRING, duration INT ) PARTITIONED BY (date STRING) CLUSTERED BY (user_id) INTO 256 BUCKETS STORED AS PARQUET;逻辑说明CLUSTERED BY让相同 user_id 的数据落到同一个桶里后续按用户维度的聚合、join 都能在本地完成。Parquet 列式存储对这类“按列读取、按用户聚合”的负载非常友好比 ORC 在这类场景下表现更稳定。分桶数 256 是经验值需要根据集群规模调整过少会出现单个桶数据量过大过多则小文件问题复发。3. 画像特征工程三件套RFM、行为偏好与商品类目交叉3.1 RFM 不是公式是三个可落地的时间窗口聚合用户画像最核心的标签是 RFM——Recency最近一次购买距今天数、Frequency一定周期内购买次数、Monetary一定周期内消费金额。很多人直接照搬教科书公式算出来却没法用原因是没有结合电商业务定义窗口。日活型电商和复购型电商的窗口周期完全不同。我一般把窗口设为 30 天、90 天、180 天三档分别计算留给下游建模去选择。SELECT user_id, DATEDIFF(CURRENT_DATE, MAX(order_date)) AS recency_30d, COUNT(DISTINCT order_id) AS freq_30d, SUM(amount) AS monetary_30d FROM dwd_orders WHERE order_date DATE_SUB(CURRENT_DATE, 30) GROUP BY user_id;这段 SQL 的核心是DATEDIFF(CURRENT_DATE, MAX(order_date))——它算的是“最近一次购买距今天数”注意方向别搞反。COUNT(DISTINCT order_id)而不是COUNT(*)因为一个订单可能拆成多行明细。SUM(amount)算的是支付金额不是订单金额这两个字段在电商订单表里经常同时存在用错会导致金额虚高。3.2 行为偏好特征从浏览到加购的转化权重设计购买行为只是结果行为日志里才有过程。电商画像的第二个特征族是“用户行为偏好”浏览了什么品类、搜索了哪些关键词、在哪个页面停留最久。这里需要做的是把行为事件赋予权重比如“购买”权重 5、“加购”权重 3、“收藏”权重 2、“浏览”权重 1然后按品类聚合出偏好得分。preference_sql SELECT user_id, category_id, SUM(CASE WHEN event purchase THEN 5 WHEN event cart THEN 3 WHEN event favor THEN 2 WHEN event view THEN 1 ELSE 0 END) AS preference_score FROM dwd_user_behavior WHERE date BETWEEN 2024-01-01 AND 2024-03-31 GROUP BY user_id, category_id 这段 SQL 的关键是CASE WHEN权重映射。权重比例不是拍脑袋定的一般基于业务转化漏斗来校准如果浏览到购买的转化率是 5%那么购买的权重就应该是浏览的 20 倍但实际经验是 5 倍左右效果更好因为权重太大反而忽略了潜在兴趣用户。这个度需要用后验 A/B 测试来调。3.3 商品类目交叉把“人”和“货”连起来的关键一步单独的 RFM 只能回答“这个用户值多少钱”回答不了“这个用户喜欢什么”。类目偏好需要把用户行为表和商品维度表 join再聚合成用户-类目得分矩阵。这一步数据量最大因为是多对多关系——一个用户看多个类目一个类目被多个用户看。性能优化上要广播商品维度表避免大 shuffle。商品表通常只有几十万行完全可以广播到每个 executor 内存里。val productDF spark.read.parquet(hdfs:///dwd/dim_product) .select(item_id, category_id, category_name) val behaviorWithCategory cleanedLog .join(broadcast(productDF), Seq(item_id), left_outer) .filter(col(category_id).isNotNull)broadcast()是这里的核心。没有它Spark 会对几亿行行为数据和几十万行商品表做SortMergeJoin产生大量 shuffle 数据。广播后每个 executor 把商品表加载进内存join 完全在本地完成。注意left_outer的处理如果行为日志里 item_id 在维度表里不存在join 后会是 null必须在后续过滤掉否则聚合出的偏好得分全部偏低。4. 画像宽表生成与挖掘模型Spark SQL 与 Scala 的完整落地4.1 画像宽表的核心 Schema 与构建策略所有特征最终要落成一张“宽表”——一行一个用户一列一个特征。宽表的 schema 设计直接决定下游使用方推荐、运营、客服好不好取数。我常用的分层是基础人口属性、消费能力层、行为偏好层、生命周期层、预测分位层。每层用不同的表结构存储最终在读取层做视图合并而不是物理地拼成一张几百列的大宽表——那样太脆任何一期数据延迟会导致整张表不可用。但源码里通常给的是一个物理宽表的构建过程原因是简单直接、适合中小团队。构建逻辑是先算好各特征子表最后统一JOIN。这里的关键是各子表的粒度必须一致都按user_id的唯一粒度输出否则JOIN后行数爆炸。4.2 用 Spark SQL 完成宽表合并与空值兜底当特征工程产出多张子表后最常踩的坑是某个用户在 30 天内没有购买记录RFM 子表里根本没有这一行。直接JOIN会导致该用户的所有画像特征为 null。电商用户画像里沉默用户恰恰是最需要打标的群体不能丢。解决方案是使用LEFT OUTER加COALESCE兜底。CREATE TABLE dws_user_profile_full AS SELECT u.user_id, COALESCE(r.recency_30d, 999) AS recency_30d, COALESCE(r.freq_30d, 0) AS freq_30d, COALESCE(r.monetary_30d, 0) AS monetary_30d, COALESCE(p.category_id, unknown) AS top_category, CASE WHEN r.monetary_30d 5000 THEN high_value WHEN r.monetary_30d 1000 THEN mid_value WHEN r.monetary_30d 0 THEN low_value ELSE silent END AS value_segment FROM dwd_user_base u LEFT OUTER JOIN rfm_30d r ON u.user_id r.user_id LEFT OUTER JOIN top_category p ON u.user_id p.user_id;逻辑说明COALESCE(r.recency_30d, 999)表示无购买记录的用户最近购买天数设为 999这比设 0 科学——0 会被下游模型误解为“今天刚买过”。value_segment的分层阈值 5000/1000 只是起步值具体要根据客单价分布来定常见做法是取全体用户消费金额的 80 分位和 50 分位。LEFT OUTER JOIN的顺序也有讲究用户基表必须放最左边保证所有用户都出现在结果里。4.3 挖掘模型部分不是算法炫技而是输出可解释的用户分群标题里有“数据挖掘”很多初学者以为要上 XGBoost、深度学习。电商画像的项目源码里最常见的挖掘任务其实是分群。KMeans 是出场率最高的算法因为它无监督、可解释、实现成本低。特征输入不能直接扔 RFM 原始值因为金额和天数的量纲差异太大距离计算会被金额主导。import org.apache.spark.ml.feature.VectorAssembler import org.apache.spark.ml.clustering.KMeans import org.apache.spark.ml.evaluation.ClusteringEvaluator val featureCols Array(recency_30d, freq_30d, monetary_30d) val assembler new VectorAssembler() .setInputCols(featureCols) .setOutputCol(features) val assembledDF assembler.transform(profileDF) val kmeans new KMeans() .setK(4) .setSeed(42L) .setMaxIter(20) .setFeaturesCol(features) .setPredictionCol(segment) val model kmeans.fit(assembledDF) val segmentedDF model.transform(assembledDF)这段代码的关键是VectorAssembler将三个数值列组装成向量KMeans的setK(4)表示分成 4 类。类别数怎么定常见做法是跑 2 到 8 组 K用ClusteringEvaluator算轮廓系数取下降拐点对应的 K。setSeed(42L)很重要不设种子每次运行结果不一样下游拿数的人会来投诉“你们画像变了吗”。KMeans 对特征缩放敏感如果发现分群结果总是按金额大小排序说明没做标准化要加StandardScaler。5. 集群调优与任务排查跑不动、数据倾斜、指标对不上的 5 个实战坑5.1 数据倾斜导致某个 Executor OOM现象整个 Spark 任务大部分 Stage 几秒跑完唯独最后一个 Stage 卡死日志里看到某个 Executor 的 GC 时间特别长最后报 OOM。原因按 user_id 聚合时少数超级用户比如机器人账号、刷单号的数据量是普通用户的几千倍HashPartitioner 把海量数据打进同一个分区。解决过滤极端用户 加盐打散。val saltedDF rawDF .filter(col(user_id) ! robot_001) .withColumn(salt, (rand * 10).cast(int)) .withColumn(salted_id, concat(col(user_id), lit(_), col(salt)))逻辑说明先确认哪些 user_id 是异常热点直接过滤掉过滤不掉的情况下给 user_id 拼一个 0 到 9 的随机后缀把原本集中在一个分区的数据打散到 10 个分区。注意salted_id只用于聚合计算最终结果还是要group by user_id还原。副作用也很明显加盐后原本能触发的 bucket join 失效了所以只对倾斜严重的 Stage 使用。5.2 小文件过多导致 NameNode 压力剧增现象任务跑完查看输出目录发现生成了几万个几十 KB 的小文件下次读取时 Spark 直接卡在“Listing files”阶段。原因repartition(1000)或者动态资源分配把并行度拉得太高每个 partition 写出的数据量太少。解决合并输出分区。df.coalesce(64) .write .mode(overwrite) .partitionBy(date) .parquet(hdfs:///dws/user_profile/)coalesce(64)和repartition(64)有本质区别前者只减少分区数不产生 shuffle后者会产生全量 shuffle。输出文件压缩和合并建议采用coalesce但前提是原分区数据量不悬殊。分区数 64 的设定依据是 HDFS 单文件建议在 128MB 以上如果你这天的数据量是 8GB64 个分区刚好平均 128MB。5.3 动态分区写 Hive 表时分区数超过上限现象任务跑了一半报错Number of dynamic partitions exceeded。原因partitionBy(date, user_id)这种写法导致每个 user_id 一个分区Hive 默认限制是 1000 个动态分区。解决检查是不是把用户维度放进了分区字段。画像结果表的分区粒度最多到date和segmentuser_id 只能作为普通字段存在。5.4 时间窗口回看数据导致重复计算现象周一跑完画像周二发现上周的数据管道补了一批历史数据重新全量跑一次要 5 小时。原因没有做增量回刷设计。解决把宽表计算分成“全量初始化”和“增量更新”两条链路。增量更新用MERGE INTO按主键 upsert只重算受影响的分区。注意如果画像里的“累计消费金额”不能分段累加需要额外记录断点全量重算兜底。5.5 Spark 任务挂掉后重试总是失败在同一个位置现象重跑任务报错信息一模一样比如某个 JSON 文件解析失败或者某个分区数据写入权限不足。原因有坏文件或脏数据被持久化到了源数据目录。解决先把报错的路径ls出来检查是脏数据就过滤掉或补上 schema是权限问题就检查 HDFS 目录属主。重试前务必先看完整堆栈不要盲目加大资源重跑治标不治本。6. 画像跑完怎么验证分布对比、抽样回溯与迭代节奏新画像表上线第一件事不是接推荐系统而是验证正确性。我的验证三连是总量核对、分布对比、抽样回溯。总量核对是指SELECT COUNT(DISTINCT user_id)和数仓里的用户维度表对比偏差超过 2% 就要查 join 是否丢数据。分布对比是指把新画像的价值分层比例和上一版对比比如“高价值用户占比从 15% 变成 3%”大概率是 monetary 字段口径变了。抽样回溯最费时但最可靠随机挑 20 个用户人工进后台系统核对他们的订单记录和行为日志看画像标签是否符合直觉。迭代节奏上画像特征一个月加一版就够每次只加一个特征族比如这月加“价格敏感度”下月加“品牌偏好”。一次加太多特征出了问题没法定位是哪个特征干扰了模型。跑完画像也别急着删中间结果保存 2 到 3 天的 checkpoint方便回看和排查。最后说一个习惯每次跑完任务把 Stage 的耗时和 shuffle 数据量记录下来超过上次的 20% 就主动看代码——这比等下游投诉后再排查要省太多时间。希望帮到你。本文还有配套的精品资源点击获取