☰
基于Hadoop的电影网站用户性别预测:从日志到标签的工程链路
2026/10/12 15:08:34 网站建设 项目流程

简介:这份资源是基于KNN算法的Hadoop电影网站用户性别预测实现程序,面向具备一定Java与Hadoop基础、希望上手大数据分类实战的学习者与开发者,可用于理解数据预处理、特征拆分与KNN分类建模的完整流程。压缩包共78个文件,约5.86MB,包含28个java源码、30个class编译文件、5个jar包及properties、dat、readme等配置与数据文件,覆盖数据预处理、KNN数据计算与模型评价等模块,其中预处理阶段可打包成jar在Hadoop上运行,数据目录需上传至运行目录,KNN计算阶段改为本地目录即可直接执行。由于数据量较大,完整运行耗时较长,作者实测约2小时。目前已有3038人学习下载,适合作为大数据分类项目的参考案例,帮助读者掌握从数据准备到模型验证的落地思路与排错方法。

1. 电影网站用户性别预测:从日志到标签的工程链路

电影网站后台每天沉淀大量行为日志,用户注册时未必填性别,但推荐系统、广告投放、内容运营都想知道“这个账号背后大概率是男是女”。基于 Hadoop 的电影网站用户性别预测实现程序,本质是把原始点击、评分、搜索、收藏等埋点数据,经过 HDFS 存储、MapReduce 或 Spark 清洗、特征工程,再训练一个二分类模型,最后把预测结果写回 Hive 表供业务查询。它解决的不是“算法多先进”,而是“数据量上来之后,离线链路怎么稳定跑通”。适合有 Java 或 Scala 基础、正在做用户画像、推荐冷启动、日志数仓的工程师。下面按真实落地顺序拆开讲,不堆概念,只讲能复现的步骤和参数。

2. 数据准备与 Hadoop 环境:日志怎么进 HDFS 才不翻车

2.1 电影网站日志的典型字段与采样策略

电影网站的用户行为日志一般来自 Nginx 访问日志和前端埋点上报,常见字段包括用户 ID、电影 ID、行为类型(点击、评分、收藏、搜索)、时间戳、评分值、搜索关键词、设备类型。性别预测不能直接用原始文本,需要先做用户粒度聚合。我一般先在 Hive 里建一张外部表指向 HDFS 上的原始日志目录,按天分区,避免全量扫描。

-- 建原始日志外部表,按天分区,方便增量导入 CREATE EXTERNAL TABLE IF NOT EXISTS ods_movie_user_log ( user_id STRING COMMENT '用户唯一标识', movie_id STRING COMMENT '电影ID', action_type STRING COMMENT '行为类型: click/rate/favorite/search', action_time BIGINT COMMENT '行为时间戳(秒)', rating DOUBLE COMMENT '评分值, 非评分行为为NULL', search_kw STRING COMMENT '搜索关键词, 非搜索行为为NULL', device_type STRING COMMENT '设备类型: android/ios/web' ) PARTITIONED BY (dt STRING COMMENT '日期分区 yyyyMMdd') ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS TEXTFILE LOCATION '/warehouse/ods/movie_user_log';

这段建表语句的关键点:外部表不移动数据,删表不丢文件;分区字段 dt 放在最后,导入时用ALTER TABLE ... ADD PARTITION或MSCK REPAIR TABLE挂载。字段用 STRING 存 user_id 是常见做法,因为很多站点用户 ID 是雪花算法生成的 19 位长整型,用 BIGINT 在 Hive 里做 join 时容易和字符串类型对不上。评分值用 DOUBLE,搜索关键词允许 NULL,后续特征工程里会区分“有搜索行为”和“无搜索行为”。

采样策略上,如果全量用户几千万,直接跑全量特征工程会拖垮集群。我一般按 user_id 哈希取模采样 10% 到 20% 做训练集,验证集单独按时间切分,比如用前 20 天训练、后 5 天验证,避免用未来数据预测过去造成特征穿越。这一步不做,后面模型离线 AUC 虚高,上线就翻车。

2.2 Hadoop 集群最小可用配置与提交命令

做这个程序不一定非要几十台机器,单机伪分布式或三节点就能跑通全流程。常见做法是 NameNode 和 ResourceManager 分开部署,DataNode 和 NodeManager 同机。关键参数我一般会调这几个:yarn.nodemanager.resource.memory-mb设成物理内存的 70%,yarn.scheduler.maximum-allocation-mb不超过单容器上限,mapreduce.map.memory.mb和mapreduce.reduce.memory.mb根据特征维度设 2048 到 4096。

# 把本地清洗好的用户行为文件上传到 HDFS 对应分区目录 hdfs dfs -mkdir -p /warehouse/ods/movie_user_log/dt=20240101 hdfs dfs -put ./user_log_20240101.tsv /warehouse/ods/movie_user_log/dt=20240101/ # 挂载分区,让 Hive 能查到数据 hive -e "ALTER TABLE ods_movie_user_log ADD IF NOT EXISTS PARTITION (dt='20240101') LOCATION '/warehouse/ods/movie_user_log/dt=20240101';" # 提交一个 MapReduce 作业做基础清洗,输出到 dwd 层 hadoop jar /opt/jars/log_clean.jar com.demo.clean.LogCleanDriver \ -D mapreduce.job.queuename=root.default \ -D mapreduce.map.memory.mb=2048 \ -D mapreduce.reduce.memory.mb=4096 \ /warehouse/ods/movie_user_log/dt=20240101 \ /warehouse/dwd/user_behavior_clean/dt=20240101

上传命令里-mkdir -p先建目录再 put,避免路径不存在报错。ALTER TABLE挂载分区后,Hive 元数据里才有这条分区记录,否则SELECT查不到数据。提交作业时用-D传参覆盖默认配置,队列名按实际 YARN 队列改,内存参数别超过yarn.scheduler.maximum-allocation-mb,否则容器起不来,日志里会看到Container is running beyond physical memory limits。清洗逻辑一般包括:过滤 user_id 为空的脏数据、把时间戳转成日期、把评分值限制在 0 到 10 之间、去掉爬虫 UA 对应的记录。

2.3 用 MapReduce 做用户粒度聚合的代码骨架

原始日志是行为粒度,一个用户多条记录,性别预测需要用户粒度特征。用 MapReduce 做聚合是 Hadoop 生态里最稳的方式,虽然 Spark 更快,但很多老集群只有 MR 环境。

// Mapper: 以 user_id 为 key,输出行为统计的中间结果 public class UserAggMapper extends Mapper<LongWritable, Text, Text, Text> { private Text outKey = new Text(); private Text outVal = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] cols = value.toString().split("\t"); if (cols.length < 7 || cols[0].isEmpty()) { return; // 脏数据直接丢弃 } String userId = cols[0]; String actionType = cols[2]; double rating = "NULL".equals(cols[4]) ? -1.0 : Double.parseDouble(cols[4]); outKey.set(userId); // 输出格式: 行为类型|评分值|设备类型 outVal.set(actionType + "|" + rating + "|" + cols[6]); context.write(outKey, outVal); } } // Reducer: 按用户聚合点击次数、评分数、平均分、设备偏好 public class UserAggReducer extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { int clickCnt = 0, rateCnt = 0, favCnt = 0, searchCnt = 0; double ratingSum = 0.0; Map<String, Integer> deviceMap = new HashMap<>(); for (Text v : values) { String[] parts = v.toString().split("\\|"); String action = parts[0]; double rating = Double.parseDouble(parts[1]); String device = parts[2]; deviceMap.merge(device, 1, Integer::sum); switch (action) { case "click": clickCnt++; break; case "rate": rateCnt++; if (rating >= 0) ratingSum += rating; break; case "favorite": favCnt++; break; case "search": searchCnt++; break; default: break; } } double avgRating = rateCnt > 0 ? ratingSum / rateCnt : 0.0; String mainDevice = deviceMap.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey).orElse("unknown"); // 输出: 点击|评分|收藏|搜索|平均分|主设备 String result = clickCnt + "|" + rateCnt + "|" + favCnt + "|" + searchCnt + "|" + String.format("%.2f", avgRating) + "|" + mainDevice; context.write(key, new Text(result)); } }

Mapper 里对脏数据做了前置过滤,cols.length < 7和user_id为空直接 return,避免后续解析异常。评分值用 -1 表示 NULL,因为 Java 里 double 不能存 null,用哨兵值在 Reducer 里判断。Reducer 用 HashMap 统计设备分布,取出现次数最多的设备作为主设备。输出用竖线分隔,方便后续 Hive 建表用ROW FORMAT DELIMITED FIELDS TERMINATED BY '|'加载。这个骨架跑通后,特征维度就是 6 个基础统计量,后面可以再加时间窗口特征、电影类型偏好特征。

3. 特征工程与性别标签构建:把行为变成模型能吃的向量

3.1 性别标签从哪里来:正负样本的确定与清洗

电影网站里性别标签通常来自用户注册资料或第三方登录返回,但存在大量缺失和错误。我一般只取“明确填写且后续行为一致”的用户作为有标签样本。具体做法是:从用户资料表里取 gender 字段,只保留 male/female 两种取值,去掉 unknown 和空值;再和用户行为聚合表 join,要求该用户至少有 5 条行为记录,否则行为特征太稀疏,模型学不到东西。

-- 构建有标签训练样本,要求行为数 >= 5 SELECT u.user_id, CASE WHEN u.gender = 'male' THEN 1 ELSE 0 END AS label, b.click_cnt, b.rate_cnt, b.fav_cnt, b.search_cnt, b.avg_rating, b.main_device FROM dwd_user_profile u JOIN dwd_user_behavior_agg b ON u.user_id = b.user_id WHERE u.gender IN ('male', 'female') AND b.total_actions >= 5 AND u.dt = '20240101';

label 用 1 表示男性、0 表示女性,这是二分类的常见编码。total_actions >= 5这个阈值不是拍脑袋,我试过 3 条和 10 条,3 条时噪声太大,10 条时样本量掉一半,5 条是折中。join 之后还要做一次去重,因为用户资料表可能有多条历史记录,按 user_id 取最新一条。这一步做完,正负样本比例如果超过 3:1,需要在训练时设scale_pos_weight或做欠采样,否则模型会偏向多数类。

3.2 行为特征编码:类别特征与数值特征的处理差异

上一步得到的特征里,main_device是类别特征,click_cnt等是数值特征。性别预测里设备类型其实有信号,比如某些设备型号男性占比更高,但不能直接塞字符串进模型。常见做法是独热编码或标签编码,我一般用独热编码,因为设备类型取值不多(android/ios/web/unknown),维度可控。

# 用 PySpark 做特征编码和向量组装 from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("gender_feature_engineering") \ .config("spark.sql.shuffle.partitions", "200") \ .enableHiveSupport() \ .getOrCreate() df = spark.sql("SELECT * FROM dwd_gender_train_sample") # 设备类型做独热编码 indexer = StringIndexer(inputCol="main_device", outputCol="device_index", handleInvalid="keep") encoder = OneHotEncoder(inputCol="device_index", outputCol="device_vec") df = encoder.transform(indexer.fit(df).transform(df)) # 数值特征做标准化,避免量纲差异影响逻辑回归 from pyspark.ml.feature import StandardScaler num_cols = ["click_cnt", "rate_cnt", "fav_cnt", "search_cnt", "avg_rating"] assembler_num = VectorAssembler(inputCols=num_cols, outputCol="num_vec") scaler = StandardScaler(inputCol="num_vec", outputCol="num_scaled", withMean=True, withStd=True) df = scaler.fit(assembler_num.transform(df)).transform(assembler_num.transform(df)) # 合并数值和类别特征 assembler_all = VectorAssembler(inputCols=["num_scaled", "device_vec"], outputCol="features") final_df = assembler_all.transform(df).select("user_id", "label", "features") final_df.write.mode("overwrite").parquet("/warehouse/dws/gender_features")

StringIndexer的handleInvalid="keep"很关键,线上预测时可能遇到训练集没见过的设备类型,keep 会把它归到额外类别而不是报错。StandardScaler的withMean=True和withStd=True同时做中心化和缩放,逻辑回归、SVM 这类对量纲敏感的模型必须做,树模型可以跳过。spark.sql.shuffle.partitions设 200 是经验值,数据量在千万级时比较均衡,太小会导致单分区数据倾斜,太大产生大量小文件。最后输出 parquet 格式,比 CSV 省空间且保留 schema。

3.3 训练集与验证集切分:时间切分比随机切分更靠谱

性别预测模型上线后要预测未来用户,如果用随机切分,同一用户的行为可能同时出现在训练集和验证集,造成标签泄露,离线 AUC 虚高。我一般按时间切分:用前 20 天有标签用户做训练,后 5 天做验证。如果用户行为跨天,以最后一次行为时间归属。

# 按时间切分,避免同一用户跨集合 train_df = spark.sql(""" SELECT * FROM dws_gender_features WHERE dt BETWEEN '20240101' AND '20240120' """) valid_df = spark.sql(""" SELECT * FROM dws_gender_features WHERE dt BETWEEN '20240121' AND '20240125' """) # 检查两个集合用户是否重叠 overlap = train_df.select("user_id").intersect(valid_df.select("user_id")).count() print("用户重叠数:", overlap) # 应为 0

如果 overlap 不为 0,说明有用户跨期活跃,需要按 user_id 去重,只保留最后一次行为所在日期。这个检查我每次跑流程都会做,曾经因为忽略它,离线 AUC 0.85 上线后掉到 0.62,血泪经验。切分后还要看两个集合的标签分布是否一致,如果验证集男性占比和训练集差超过 5 个百分点,说明时间切分引入了分布偏移,需要调整切分点或做分层采样。

4. 模型训练与 Hadoop 集成:逻辑回归和 GBDT 怎么选

4.1 逻辑回归作为基线:参数少、可解释、上线快

性别预测不是图像识别那种复杂任务,行为特征和性别的关系相对线性,逻辑回归往往能拿到不差的 AUC,而且系数可以直接解释——比如“搜索次数”系数为正,说明搜索越多越可能是男性。我一般先跑逻辑回归做基线,再试 GBDT 看提升空间。

from pyspark.ml.classification import LogisticRegression from pyspark.ml.evaluation import BinaryClassificationEvaluator lr = LogisticRegression( featuresCol="features", labelCol="label", maxIter=100, # 迭代次数,100 通常够收敛 regParam=0.01, # L2 正则,防止过拟合 elasticNetParam=0.0, # 0 表示纯 L2 threshold=0.5 # 分类阈值,后续可按业务调 ) model = lr.fit(train_df) pred = model.transform(valid_df) evaluator = BinaryClassificationEvaluator( labelCol="label", rawPredictionCol="rawPrediction", metricName="areaUnderROC" ) auc = evaluator.evaluate(pred) print("验证集 AUC:", auc)

maxIter=100是逻辑回归的常见上限,如果 100 次还没收敛,日志里会警告,可以加到 200,但通常说明特征有问题。regParam=0.01是 L2 正则系数,越大惩罚越强,我试过 0.001 到 0.1,0.01 在千万级样本上比较稳。threshold=0.5是默认分类阈值,如果业务更怕把男性误判成女性,可以调低到 0.4,让更多样本判为男性。AUC 在 0.7 到 0.8 之间算正常,低于 0.65 说明特征信号太弱,需要加特征。

4.2 GBDT 提升非线性拟合:参数怎么设不翻车

GBDT 能自动做特征交叉,对非线性关系拟合更好,但参数多,调不好容易过拟合。我一般用 Spark ML 的 GBTClassifier,树数量设 50 到 100,深度设 5 到 8,学习率设 0.05 到 0.1。

from pyspark.ml.classification import GBTClassifier gbt = GBTClassifier( featuresCol="features", labelCol="label", maxIter=80, # 树的数量 maxDepth=6, # 单棵树深度 stepSize=0.08, # 学习率 subsamplingRate=0.8, # 行采样比例 featureSubsetStrategy="auto" ) gbt_model = gbt.fit(train_df) gbt_pred = gbt_model.transform(valid_df) gbt_auc = evaluator.evaluate(gbt_pred) print("GBDT 验证集 AUC:", gbt_auc)

maxIter=80配合stepSize=0.08,总迭代量约 6.4,和逻辑回归的 100 次迭代量级接近。maxDepth=6是经验值,超过 8 容易过拟合,低于 4 欠拟合。subsamplingRate=0.8做行采样,增加模型多样性,降低方差。如果 GBDT 比逻辑回归 AUC 高不到 0.02,我一般选逻辑回归,因为训练快、可解释、上线依赖少。GBDT 模型文件大,加载慢,在 Hadoop 集群上做批量预测时资源消耗更高。

4.3 把模型预测结果写回 Hive 表供业务查询

模型训练完,要对全量用户做预测,结果写回 Hive 表。这一步用 Spark 批量推理,输出到 HDFS,再挂 Hive 分区。

# 对全量用户特征做预测 all_users = spark.sql("SELECT user_id, features FROM dws_gender_features_all") predictions = gbt_model.transform(all_users) # 提取 user_id、预测性别、概率 result = predictions.selectExpr( "user_id", "CAST(prediction AS INT) AS gender_pred", "CAST(probability[1] AS DOUBLE) AS male_prob" ) result.write.mode("overwrite").parquet("/warehouse/dws/gender_prediction") # 建 Hive 表指向预测结果 spark.sql(""" CREATE EXTERNAL TABLE IF NOT EXISTS dws_gender_prediction ( user_id STRING, gender_pred INT, male_prob DOUBLE ) STORED AS PARQUET LOCATION '/warehouse/dws/gender_prediction' """)

probability[1]是男性概率,Spark ML 的 probability 列是向量,索引 1 对应 label=1。gender_pred存 0/1,业务方可以按male_prob > 0.7取高置信用户做定向运营。写 parquet 后建外部表,不复制数据,业务方用 Hive SQL 或 Presto 直接查。注意预测结果要按天分区,否则全量覆盖后历史结果丢失,没法做效果回溯。

5. 避坑与排查:性别预测链路里最容易翻车的 5 个点

5.1 现象:离线 AUC 很高,上线后效果暴跌

原因通常是特征穿越或标签泄露。比如用用户注册时间做特征,但注册时间在预测时不可得;或者训练集和验证集有同一用户。解决方法是严格按时间切分,检查特征在预测时刻是否可得,所有 join 操作确认没有用到未来数据。我一般会在特征表里加一列feature_date,预测时只取feature_date <= 预测日期的记录。

5.2 现象:MapReduce 作业卡在 reduce 阶段 99% 不动

原因是数据倾斜,某个 user_id 行为记录特别多,或者某个设备类型占比过高。解决方法是给 key 加随机前缀做两阶段聚合,或者在 Reducer 里对热点 key 单独处理。我一般先看 Counter 里各 reduce 的输入记录数,差 10 倍以上就确认倾斜。加前缀的代价是多一次 shuffle,但能跑完。

5.3 现象:Hive 查询报Vertex failed, vertexName=Map 1

原因是 YARN 容器内存不足,常见于特征维度高、单条记录大。解决方法是调大mapreduce.map.memory.mb和mapreduce.map.java.opts,后者一般是前者的 80%。同时检查是否有笛卡尔积 join,比如用户表和电影表没加关联条件,导致数据量爆炸。

5.4 现象:模型预测结果全是男性或全是女性

原因是正负样本严重不均衡,或者特征标准化时用了全量数据导致验证集信息泄露。解决方法是训练时设weightCol做类别加权,或者对多数类欠采样。标准化参数必须只在训练集上 fit,然后 transform 验证集和预测集,不能全量 fit。

5.5 现象:Spark 作业报Container killed by YARN for exceeding memory limits

原因是spark.executor.memory设太小,或者spark.sql.shuffle.partitions太大导致每个分区数据量不均。解决方法是 executor 内存设 4G 到 8G,shuffle 分区数按数据量除以 128MB 估算。另外spark.memory.fraction默认 0.6,可以调到 0.7 给执行内存更多空间。

6. 进阶技巧:用特征重要性反推业务洞察

模型跑通只是第一步,真正有价值的是从特征重要性里读出业务信号。逻辑回归看系数绝对值,GBDT 看featureImportances。我一般会把重要性排前 10 的特征打印出来,对照业务解释。

# GBDT 特征重要性 import pandas as pd importances = gbt_model.featureImportances.toArray() feature_names = ["click_cnt", "rate_cnt", "fav_cnt", "search_cnt", "avg_rating", "device_android", "device_ios", "device_web", "device_unknown"] imp_df = pd.DataFrame({"feature": feature_names, "importance": importances}) imp_df = imp_df.sort_values("importance", ascending=False) print(imp_df.head(10))

跑出来如果search_cnt重要性最高,说明搜索行为性别区分度大,运营可以针对搜索用户做差异化推荐。如果avg_rating重要性低,说明评分高低和性别关系弱,后续可以少花精力在这类特征上。这个反推过程我一般会做成表格给业务方看,比单纯报 AUC 更有说服力。

特征名重要性业务解读
search_cnt0.23搜索越频繁越偏男性
fav_cnt0.18收藏行为男性略多
device_ios0.15iOS 设备女性占比高
avg_rating0.09评分高低与性别弱相关
click_cnt0.07点击量区分度一般

另一个进阶方向是模型融合,把逻辑回归和 GBDT 的预测概率加权平均,权重按验证集 AUC 分配。我试过 0.4 逻辑回归加 0.6 GBDT,AUC 比单模型高 0.01 到 0.02,但推理耗时翻倍,是否值得看业务对延迟的容忍度。如果每天只跑一次离线预测,融合完全可行;如果要实时预测,单模型更稳。

最后说一个我踩过的坑:模型上线后不要只看 AUC,要看业务指标。曾经有个版本 AUC 0.78,但男性用户召回率只有 0.5,原因是阈值 0.5 对男性不友好。后来把阈值调到 0.4,召回率上到 0.72,精确率只掉 3 个点,业务方更满意。所以模型评估一定要结合混淆矩阵和业务场景,别被单一指标忽悠。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询