Skip to content

项目9 广告检测的流量作弊识别——Spark综合实战 ​

本项目是 Spark 课程的收官实战,把前面学过的 Spark SQL、Hive、MLlib、集群提交全部串起来,完成一个完整的"数据分析 → 特征工程 → 建模评估 → 模型上线"业务闭环。


目录 ​


一、项目背景:虚假流量是什么 ​

1.1 业务问题 ​

互联网虚假流量:通过特殊方式(如设置程序每分钟访问一次网站主页)模仿人类浏览行为生成的访问流量。

为什么有人要作弊? 浏览量的增加一般能促进销售量增加;同等条件下,流量大的网站收取的广告费用更高。部分网站受利益驱使,通过作弊方式产生虚假流量。

结果:网站凭虚假流量收了更高广告费,却没促进真实浏览量和销量,直接损害广告主利益。

💡 一句话理解:广告主按"曝光/点击"付钱,作弊者用机器冒充真人刷量骗广告费——本项目就是用 Spark 把机器刷出来的流量识别出来。

1.2 常见的流量作弊方式(4种) ​

作弊方式原理数据层面的特征表现
脚本刷量设定程序,让电脑按规则访问目标网站同一 ip + 同一 cookie 高频出现
控制肉鸡访问利用受病毒感染的电脑(肉鸡)访问网站大量不同 ip 短时间集中访问
页面代码修改在媒体网站插入隐藏代码,加载指向目标网站的不可见小页面特定渠道的异常流量
DNS劫持篡改 DNS 数据,强制把用户访问改到目标网站大量用户被"劫持"到目标

本项目重点识别前两种衍生出的三类行为:脚本刷新、定期清除 cookie 刷新、ADSL 重新拨号刷新(见 4.4 节)。


二、需求分析:6步识别流程 ​

本项目要完成 3 大需求:

  1. 探索分析广告流量数据
  2. 预处理数据并构建特征
  3. 构建与评估分类模型

对应到完整实现流程(6 步):

① 数据抽取与划分
   (历史流量数据 → case_data_new.csv,7天数据)
        ↓
② 数据探索分析
   (缺失值、冗余字段基础探索 + 作弊行为特征业务探索)
        ↓
③ 数据预处理 + 特征构建
   (清洗规则 → 构建 N/N1/N2/N3 特征 → 建模样本数据)
        ↓
④ 建立识别模型并评估对比
   (逻辑回归 vs 随机森林,对比准确率)
        ↓
⑤ 保存效果较好的模型
        ↓
⑥ 模拟新数据,加载模型应用
   (LoadData 模拟新流量 → 模型预测 → 评估)

💡 项目定位:这不是一个"纯算法"项目,而是一个数据工程 + 机器学习的综合项目。特征怎么构造(业务理解)比模型调参更重要——这也是生产环境大数据工程师的真实工作方式。


三、数据说明:22个字段的数据字典 ​

数据文件:case_data_new.csv,共 7 天流量数据、1704154 条记录、22 个字段。

3.1 字段完整字典 ​

字段说明类型
rank记录序号数值型
dt相对日期(1~7,1 表示第一天)数值型
cookiecookie 值字符型
ipIP 地址(已脱敏)字符型
idfa用于识别 iOS 用户字符型
imei用于识别 Android 用户字符型
android用于识别 Android 用户字符型
openudid用于识别 iOS 用户字符型
mac识别不同硬件设备字符型
timestamps时间戳数值型
camp项目 ID数值型
creativeid创意 ID数值型
mobile_os设备 OS 版本(原始值)数值型
mobile_type设备型号数值型
app_key_md5应用 key(MD5)字符型
app_name_md5应用名(MD5)字符型
placementid广告位 ID字符型
useragent用户代理字符串字符型
mediaid媒体 ID数值型
os_type操作系统类型字符型
born_time记录生成时间数值型
label标签(0=正常,1=作弊)数值型

3.2 数据类型分布 ​

类型字段用途
字符型(12个)cookie、ip、idfa、imei、android、openudid、mac、app_key_md5、app_name_md5、placementid、useragent、os_type大部分不能直接进数值模型,需要编码或构造特征
数值型(10个)rank、dt、timestamps、camp、creativeid、mobile_os、mobile_type、mediaid、born_time、label部分可直接使用或作为特征来源

⚠️ 关键认知:大部分变量是字符型,不适用于直接运用在需要数值类型的分类模型中——这就是后面要做特征工程的根本原因。


四、任务9.1 探索分析广告流量数据 ​

本任务采用本地模式(master("local[*]"))对数据进行基础探索,根据探索结果确定清洗规则。

4.1 创建 SparkSession(代码9-1) ​

scala
import org.apache.spark.sql.SparkSession

object Explore {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("Explore").master("local[*]")
      .enableHiveSupport().getOrCreate()
    spark.sparkContext.setLogLevel("WARN")
  }
}
代码含义
.appName("Explore")应用名称,UI 和日志中显示
.master("local[*]")本地模式,* = 用满本机所有核
.enableHiveSupport()开启 Hive 支持(后续要读写 Hive 表)
.setLogLevel("WARN")只输出 WARN 及以上日志,减少刷屏

⚠️ 探索阶段用本地模式,建模阶段要改成集群模式(spark://master:7077)——代码9-18 会看到区别。

4.2 基础探索:记录数、日流量(代码9-2、9-3) ​

scala
// 读取数据(option 设置首行为列名)
val rawData = spark.read.option("header", "true").csv("E:\\data\\case_data_new.csv")
println("原始数据集行数为:" + rawData.count())

// 统计每天数据量
rawData.groupBy("dt").count().selectExpr("dt","count as dayCount").sort("dt").show()

结果:

  • 总记录数:1704154 条
  • 日流量:7 天差异不大 → 数据环境相对稳定,不存在数据倾斜问题 ✅

💡 selectExpr("dt","count as dayCount") = SQL 写法 SELECT dt, count AS dayCount,把 count() 的默认列名改成易读的 dayCount。

4.3 缺失值探索(代码9-4、9-5) ​

自定义缺失率统计方法 ​

scala
def MissingCount(data: DataFrame, columnName: String): Unit = {
  var missingRate = 0.0
  if (columnName != "creativeid") {
    missingRate = 1 - (data.select(columnName).na.drop().count().toDouble / data.count())
  } else {
    // creativeid 的"缺失"特殊处理:值为 0 视为缺失
    missingRate = data.select(columnName).filter("creativeid == 0").count() / data.count().toDouble
  }
  if (missingRate > 0) {
    println(columnName + " 缺少值比率:" + missingRate * 100 + "%")
  }
}
scala
val columnName = rawData.columns.toList
for (i <- columnName) {
  MissingCount(rawData, i)
}
代码含义
na.drop()删除该列含 null 的行
.count().toDouble / data.count()非缺失数 ÷ 总数 → 完整率;1 - 完整率 = 缺失率
creativeid == 0creativeid 的特殊逻辑:大量值为 0 不正常,0 视为缺失

缺失值结论 ​

22 个字段中 12 个字段存在缺失;useragent 缺失率较少,creativeid 高达 98.39%,且是数值型无法插补 → 后续删除缺失率过高的字段。

4.4 业务探索:三类作弊行为的特征(重点 ⭐) ​

这是本项目的灵魂:先理解作弊行为在数据里长什么样,才能构造出识别它们的特征。三种作弊方式 → 三个特征探索方向。

作弊一:脚本刷新网页(代码9-6、9-7) ​

行为特征:cookie 与 ip 不变,且存在多条记录。

scala
// 统计 ip+cookie 相同的记录数
val cookie_ip_distribute = rawData.groupBy("ip", "cookie")
  .count().withColumn("ip_cookie_count_precent",
  col("count") / rawData.count() * 100).orderBy(desc("count"))
cookie_ip_distribute.show(false)

// 过滤出同 ip、cookie 出现超过 100 次的记录数
val click_gt_100 = cookie_ip_distribute.filter("count > 100").count()
println("同ip、cookie出现超过100次以上的记录数:" + click_gt_100)

结果:7 天中同 ip+cookie 浏览超过 100 次的记录有 104 个,叠加起来占据大量流量——若不识别,广告主损失很大。

行为特征:ip 不变,多条不同 cookie 记录(作弊者清 cookie 制造"新用户")。

scala
val ip_distribute = rawData.groupBy("ip")
  .agg(countDistinct("cookie") as "ip_count")
  .groupBy("ip_count").agg(count("ip_count") as "ip_count_count",
  count("ip_count") / rawData.count() * 100 as "ip_count_count_precent")
  .orderBy(desc("ip_count"))
ip_distribute.show(false)

💡 两层 groupBy:第一层统计"每个 ip 对应多少个不同 cookie",第二层再统计"这些不同 cookie 数的分布"——看是否存在同一 ip 挂大量 cookie 的情况。

作弊三:ADSL 重新拨号刷新(代码9-9、9-10) ​

行为特征:某一时间段内,多条记录 ip 来自同一区域 → ip 前两段或前三段相同。

scala
// ip 前两段
val ip_two = rawData.withColumn("ip_two", substring_index(col("ip"), ".", 2))
  .groupBy("ip_two").agg(count("ip_two") as "ip_two_count").orderBy(desc("ip_two_count"))
ip_two.show(false)

// ip 前三段
val ip_three = rawData.withColumn("ip_three", substring_index(col("ip"), ".", 3))
  .groupBy("ip_three").agg(count("ip_three") as "ip_three_count").orderBy(desc("ip_three_count"))
ip_three.show(false)

结果:ip 前两段/前三段相同的记录上万条的情况较多 → 记录数过于庞大可大致判定为虚假流量。

三种作弊行为 → 特征对照表 ​

作弊方式数据特征表现将构建的特征
脚本刷新同 ip+cookie 高频出现N(ip+cookie 相同次数)
定期清 cookie同 ip 多 cookieN1(同 ip 不同 cookie 数)
ADSL 拨号ip 前两段相同N2(ip 前两段相同次数)
ADSL 拨号(更细)ip 前三段相同N3(ip 前三段相同次数)

💡 特征 = 业务规律的量化:探索阶段发现"作弊会造成什么数据现象",特征构建阶段就把这个现象"计数"成数值特征。先有业务洞察,才有特征工程。

4.5 探索结论汇总(清洗规则依据) ​

结论数据处理
记录数 170 万+1704154直接使用
日流量稳定7 天差异小无倾斜,不需要特殊处理
12 字段有缺失高缺失集中在字符型无法插补 → 删除高缺失字段
creativeid 缺失极高98.39%删除
冗余字段idfa/imei/android/openudid4 个字段均为"识别手机系统类型",可合并降缺失;暂不处理(后续特征构建不确定是否使用)
三类作弊特征明显ip/cookie 分布异常构建 N/N1/N2/N3 特征

五、任务9.2 预处理数据并构建特征 ​

5.1 删除缺失率过高的字段(代码9-11) ​

scala
val data_new = rawData.drop("mac").drop("creativeid").drop("mobile_os")
  .drop("mobile_type").drop("app_key_md5").drop("app_name_md5").drop("os_type")

删除 7 个字段的原因:缺失率高且多为字符型,无法插补,删除以减小对模型的影响。

删除字段备注
mac缺失率高
creativeid缺失率 98.39%,0 视为缺失
mobile_os / mobile_type缺失率高
app_key_md5 / app_name_md5缺失率高
os_type缺失率高

⚠️ 保留 idfa/imei/android/openudid:4 个字段含义相似(手机系统类型),后续特征构建时不确定是否使用,先不处理。

5.2 保存到 Hive(代码9-12) ​

scala
data_new.write.mode("overwrite").saveAsTable("ad_traffic.AdData")

前置准备:

  1. 依次启动 Hadoop 集群和 Hive 元数据服务
  2. Hive CLI 创建数据库:create database ad_traffic;
  3. 将 hive-site.xml 复制到项目工程的 resources 目录
知识点说明
saveAsTable保存为 Hive 表
mode("overwrite")覆盖保存(表存在则覆盖)
ad_traffic.AdData库名.表名

5.3 构建特征 N/N1/N2/N3(代码9-13、9-14)⭐ 本项目最难 ​

特征定义 ​

特征构建方法业务含义
N5 小时内,ip 与 cookie 相同的记录出现次数ip+cookie 不变 → 脚本刷新嫌疑
N15 小时内,同一个 ip 产生的不同 cookie 记录条数ip 不变多 cookie → 清 cookie 嫌疑
N25 小时内,ip 前两段相同的记录出现次数ADSL 拨号嫌疑
N35 小时内,ip 前三段相同的记录出现次数ADSL 拨号嫌疑(更细粒度)

为什么选 5 小时(18000 秒)? 根据广告点击周期的频率进行划分得出。

整体思路框架 ​

① 取 timestamps 最大值、最小值作为界限
② 以 18000 秒为间隔,用 List.range() 切分时间区间
③ for 循环遍历每个区间:
    - filter 出区间内数据
    - 构建 N/N1/N2/N3 四个特征
    - join 回原数据(按 rank 关联)
    - append 写入 Hive 表 TimeFeatures

时间区间划分(代码9-13) ​

scala
val max_min_timestamp = data.select(max(col(timestamps).cast(DataTypes.IntegerType)) as "max_ts",
      min(col(timestamps).cast(DataTypes.IntegerType)) as "min_ts").rdd.collect()
val max_ts = max_min_timestamp(0).getInt(0)
val min_ts = max_min_timestamp(0).getInt(1)
val times = List.range(min_ts, max_ts, 18000)
println("时间分割点:" + times)
代码含义
.cast(DataTypes.IntegerType)timestamps 转成整数(防止被当字符串比较)
.rdd.collect()取回 Driver 端,便于取值
List.range(min, max, 18000)从 min 到 max 每 18000 秒切一个点

特征构建(代码9-14) ​

scala
for (i <- 0 to 4) {
  // 取出该时间区间内的数据
  val data_sub = data.filter("timestamps>=" + times(i) + " and timestamps<" + times(i + 1))

  // 特征 N:ip+cookie 相同次数
  val data_N_sub = data_sub.groupBy(cookie, ip).agg(count(ip) as N)
    .join(data_sub, Seq(cookie, ip), "inner").select(ranks, N)

  // 特征 N1:同 ip 的不同 cookie 数
  val data_N1_sub = data_sub.groupBy(ip).agg(countDistinct(cookie) as N1)
    .join(data_sub, Seq(ip), "inner").select(ranks, N1)

  // 特征 N2:ip 前两段相同次数
  val data_ip_two = data_sub.withColumn("ip_two", substring_index(col(ip), ".", 2))
  val data_N2_sub = data_ip_two.groupBy("ip_two").agg(count("ip_two") as "N2")
    .join(data_ip_two, Seq("ip_two"), "inner").select(ranks, N2)

  // 特征 N3:ip 前三段相同次数
  val data_ip_three = data_sub.withColumn("ip_three", substring_index(col(ip), ".", 3))
  val data_N3_sub = data_ip_three.groupBy("ip_three").agg(count("ip_three") as "N3")
    .join(data_ip_three, Seq("ip_three"), "inner").select(ranks, N3)

  // 4 个特征按 rank 合并
  val data_model_N = data_N_sub.join(data_N1_sub, ranks).join(data_N2_sub, ranks).join(data_N3_sub, ranks)
  data_model_N.write.mode("append").saveAsTable("ad_traffic.TimeFeatures")
}
代码含义
groupBy(cookie, ip).agg(count(ip) as N)按 ip+cookie 分组计数 → 每个组合出现几次
.join(data_sub, Seq(cookie, ip), "inner")把计数回填到每条原始记录(关联键是 cookie+ip)
.select(ranks, N)只留 rank(用于最后合并)和特征值
countDistinct(cookie)同 ip 下不同 cookie 的个数
substring_index(col("ip"), ".", 2)取 ip 前两段(a.b.c.d → a.b)
mode("append")追加写入,循环 5 次把 5 个区间数据都写进同一张表

⚠️ Scala for 循环的经典坑(PPT 明确提醒): Scala 中 for 循环内部构建的变量无法传出循环之外,所以无法在代码中完成各区间特征数据集的拼接 → 解决思路:在循环内直接 append 写入 Hive 表,循环结束时所有区间数据都已在同一张表 TimeFeatures 中。

⚠️ 为什么只取前 25 小时? 数据量非常大,7 天全量特征构建对硬件要求极高、耗费大量时间 → 只取前 25 小时(5 个 5 小时区间,for (i <- 0 to 4))作为构建特征前的数据集。

5.4 合并标签构建 FeaturesData(代码9-15) ​

scala
val FeaturesData = spark.read.table("ad_traffic.TimeFeatures")
  .join(data, ranks).select(col(ranks), col("dt"), col("N"), col("N1"),
  col("N2"), col("N3"), col("label").cast("double"))
FeaturesData.write.mode("overwrite").saveAsTable("ad_traffic.FeaturesData")

💡 为什么要再合并一次? TimeFeatures 只有 rank+N/N1/N2/N3 四个特征,没有 label 标签;label 在完整数据集 AdData 中。用 rank 关联,取特征 + dt + label,组成建模需要的 FeaturesData。

⚠️ label.cast("double"):标签转成 Double,满足 MLlib 对标签列的类型要求。


六、任务9.3 特征标准化与数据划分 ​

6.1 为什么要标准化 ​

特征 N、N1、N2、N3 的数值量级差异很大(有的几百、有的几千),如果直接进模型,量级大的特征会对预测结果产生更大且不合理的影响 → 需要标准化。

本项目使用最小值-最大值归一化(MinMaxScaler):把每个特征压缩到 [0,1] 区间。

6.2 代码实现(代码9-16、9-17) ​

scala
import org.apache.spark.ml.feature.{MinMaxScaler, VectorAssembler}

// 1. 特征向量化:把 4 列特征合并成一个向量列
val VectorData = new VectorAssembler()
  .setInputCols(Array("N", "N1", "N2", "N3"))
  .setOutputCol("VectorFeatures")
  .transform(data)

// 2. 最小值——最大值归一化
val MaxMin = new MinMaxScaler()
  .setInputCol("VectorFeatures")
  .setOutputCol("features")
  .fit(VectorData)
val dataScaler = MaxMin.transform(VectorData)

// 3. 按 7:3 划分数据集
val Array(modelData, loadData) = dataScaler.randomSplit(Array(0.7, 0.3))
modelData.write.mode("overwrite").saveAsTable("ad_traffic.ModelData")
loadData.write.mode("overwrite").saveAsTable("ad_traffic.LoadData")
println("数据集总数据量:" + dataScaler.count())
println("模型构建数据量:" + modelData.count())
println("模型加载数据量:" + loadData.count())
代码含义
VectorAssembler特征向量化:4 个数值列 → 1 个向量列
MinMaxScaler.fit()学习每个特征的最小值/最大值(训练阶段)
MinMaxScaler.transform()用学到的参数做归一化(应用阶段)
输出列名 featuresMLlib 约定俗成的特征列名,算法默认找它
randomSplit(Array(0.7, 0.3))随机按 7:3 划分

结果:

数据集数据量用途
总数据量702135—
modelData(70%)491698模型构建与评估
loadData(30%)210437模拟真实模型应用阶段

💡 划分的深意:loadData 特意命名为"模型加载数据"而非"测试数据"——它在任务9.5 中模拟"真实场景中新产生的流量",用于检验"训练好的模型部署后是否有效"。


七、任务9.4 构建与评估分类模型 ​

广告流量作弊识别是二分类问题(label:0=正常,1=作弊)。本任务不再用本地模式,而是编译打包后用 **spark-submit 提交到集群(Standalone 模式)**运行。

7.1 逻辑回归模型(代码9-18~9-22) ​

SparkSession 集群模式设置(代码9-18)⭐ 与任务9.1 的区别 ​

scala
val spark = SparkSession.builder()
  .appName("Logistic").master("spark://master:7077")   // 集群模式
  .enableHiveSupport().getOrCreate()
val inputTable = args(0)   // 输入表名,从命令行参数传入
val output = args(1)       // 模型保存路径,从命令行参数传入
变化本地模式(任务9.1)集群模式(任务9.4)
masterlocal[*]spark://master:7077
数据源写死本地 CSV 路径args(0) 命令行参数(Hive 表名)
模型路径无args(1) 命令行参数(HDFS 路径)

💡 生产习惯:集群提交的代码不写死路径,用 args 接收参数,同一个 jar 可以灵活切换输入输出。

模型构建(代码9-19) ​

scala
val ModelData = spark.read.table(inputTable)
val Array(train, test) = ModelData.randomSplit(Array(0.7, 0.3))

val model = new LogisticRegression()
  .setElasticNetParam(0.03)   // 正则化系数
  .setMaxIter(15)             // 最大迭代次数
  .fit(train)

参数调优结论:经过调优,setMaxIter(15) + setElasticNetParam(0.03) + 其余默认值时模型效果较好。

💡 逻辑回归的输入是 LabeledPoint 格式,但 spark.ml 的 LogisticRegression 会自动搜寻 features 特征列和 label 标签列——无需手动设置特征/标签参数(这就是 Scaler 里输出列名必须叫 features 的原因)。

模型评估(代码9-20) ​

scala
val pre = model.transform(test)
pre.select("label", "prediction").show()
val evaluator = new MulticlassClassificationEvaluator()
  .setLabelCol("label")
  .setPredictionCol("prediction")
  .setMetricName("accuracy")
println("Logistic Model Accuracy:" + evaluator.evaluate(pre))
代码含义
model.transform(test)用模型对测试集做预测,输出带 prediction 列
MulticlassClassificationEvaluator多分类评估器(二分类是其特例)
.setMetricName("accuracy")用准确率评估(最直观)

模型保存与提交(代码9-21、9-22) ​

scala
model.write.overwrite().save(output)
bash
spark-submit --master spark://master:7077 \
--class Logistic /opt/data/model.jar ad_traffic.ModelData /tipdm/data/model/Logistic
spark-submit 参数值含义
--masterspark://master:7077Standalone 集群模式
--classLogistic主类
第一个参数/opt/data/model.jar打包好的 jar 包
第二个参数ad_traffic.ModelData传给 args(0):输入表
第三个参数/tipdm/data/model/Logistic传给 args(1):模型保存 HDFS 路径

逻辑回归结果:准确率 85.8% —— 对大部分作弊访问记录都可以识别。

7.2 随机森林模型(代码9-23~9-25) ​

scala
val rfModel = new RandomForestClassifier()
  .setFeaturesCol("features")
  .setLabelCol("label")
  .setNumTrees(5)     // 5 棵决策树
  .fit(train)
bash
spark-submit --master spark://master:7077 \
--class RandomForest /opt/data/model.jar ad_traffic.ModelData /tipdm/data/model/RandomForest

随机森林结果:准确率 91.1% —— 比逻辑回归提高近 5%。

7.3 两个模型深度对比 ​

维度逻辑回归随机森林
类型线性模型 + sigmoid集成学习(多棵决策树投票)
实现复杂度简单较复杂
准确率85.8%91.1%(高约 5%)
特征列设置自动识别 features/label需显式 setFeaturesCol/setLabelCol
保存类LogisticRegressionRandomForestClassifier
核心参数ElasticNetParam=0.03、MaxIter=15NumTrees=5
为什么差 5%本质是线性回归加 sigmoid,预测时易出现梯度消失;对非线性关系拟合能力有限数据足够时,多棵树集成的分类准确率更高,能捕捉非线性模式

💡 随机森林为什么更好? 逻辑回归是线性决策边界,遇到"特征与标签呈非线性关系"时力不从心(且深层网络才容易梯度消失,这里更多是线性模型的表达力限制);随机森林通过多棵决策树投票,天然适合非线性、特征量纲不一致的场景。


八、任务9.5 模型加载与应用 ​

8.1 场景理解 ​

真实生产:模型训练好后部署,新产生的流量数据没有 label,需要加载模型实时判断是否作弊。本项目用 LoadData(30% 数据)模拟新产生的数据(为便于评估,保留了 label)。

8.2 代码实现(代码9-26) ​

scala
import org.apache.spark.ml.classification.{LogisticRegressionModel, RandomForestClassificationModel}

object LoadModel {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("LoadModel").master("local[*]")
      .enableHiveSupport().getOrCreate()
    spark.sparkContext.setLogLevel("WARN")

    // 读取模拟的新数据
    val LoadData = spark.read.table("ad_traffic.LoadData")

    // 加载逻辑回归模型
    val LogisticModel = LogisticRegressionModel
      .load("hdfs://master:8020/tipdm/data/model/Logistic")
    val LogisticPre = LogisticModel.transform(LoadData)
    val LogisticAcc = new MulticlassClassificationEvaluator()
      .setLabelCol("label").setPredictionCol("prediction")
      .setMetricName("accuracy").evaluate(LogisticPre)
    println("逻辑回归模型后期数据准确率:" + LogisticAcc)

    // 加载随机森林模型
    val RandomForest = RandomForestClassificationModel
      .load("hdfs://master:8020/tipdm/data/model/RandomForest")
    val RandomForestPre = RandomForest.transform(LoadData)
    val RandomForestAcc = new MulticlassClassificationEvaluator()
      .setLabelCol("label").setPredictionCol("prediction")
      .setMetricName("accuracy").evaluate(RandomForestPre)
    println("随机森林模型后期数据准确率:" + RandomForestAcc)
  }
}
代码含义
LogisticRegressionModel.load(路径)加载训练时保存的逻辑回归模型
RandomForestClassificationModel.load(路径)加载随机森林模型
.transform(LoadData)对新数据做预测,输出 prediction 列
评估器由于模拟数据保留了 label,可计算准确率验证模型效果

⚠️ 训练用算法类,加载用模型类(经典易错点):

  • 训练:LogisticRegression / RandomForestClassifier
  • 加载:LogisticRegressionModel / RandomForestClassificationModel
  • 加载路径必须与保存路径一致(HDFS 上的 /tipdm/data/model/...)

结论:模型保存后再加载,不会损失模型效果——加载后的准确率与训练时一致,说明模型部署可用。


九、排错指南 ​

报错/问题原因解决
hive-site.xml not found / 无法读 Hive 表缺少 Hive 配置文件把 hive-site.xml 复制到项目 resources 目录
Table not found: ad_traffic.xxxHive 库没建或没连上create database ad_traffic;,确认 SparkSession 开启 enableHiveSupport
本地模式能跑、集群模式报 Failed to connect to mastermaster 地址不对 / 集群没启动确认 spark://master:7077、hosts 配置、Master 进程
IllegalArgumentException: Column 'features' does not exist特征列名不是 featuresMinMaxScaler 输出列名必须设为 features
label 类型不对报错label 是 Stringcol("label").cast("double")
提交报 ClassNotFound: Logistic--class 主类名不对 / jar 没打全确认主类全名、重新打包
模型加载报 Path does not existHDFS 保存/加载路径不一致核对 spark-submit 的 output 参数和 Load 路径
Python worker failed / 中文乱码环境问题集群环境用 Scala 代码为主(本项目全部是 Scala)
特征全为 0 或异常时间区间 filter 逻辑错检查 times 区间是否正确、timestamps 是否转成整数
for 循环外取不到变量Scala 作用域限制循环内直接写 Hive(append),不要试图循环外拼接
数据量太大跑不动全量特征构建硬件扛不住只取前 25 小时数据(本项目做法)

十、面试高频考点 ​

概念类 ​

Q1:什么是虚假流量?常见的作弊方式有哪些? A:虚假流量是通过程序等方式模仿人类浏览行为生成的访问流量。常见作弊方式:脚本刷量(同 ip+cookie 高频访问)、控制肉鸡访问、页面代码修改(隐藏 iframe)、DNS 劫持。本项目重点识别脚本刷新、定期清 cookie、ADSL 拨号三类。

Q2:数据标准化有哪几种?为什么本项目用 MinMaxScaler? A:常用有 MinMaxScaler(最小-最大归一化,缩放到 [0,1])和 StandardScaler(Z-score 标准化,均值为 0 方差为 1)。本项目特征间差值较大,MinMaxScaler 实现简单、不改变分布形态,适合本场景;StandardScaler 对存在离群点的数据更稳健。

Q3:逻辑回归和随机森林各自的特点? A:逻辑回归是线性分类模型,简单、可解释性强、训练快,但决策边界是线性的,对复杂非线性关系拟合有限;随机森林是集成学习(多棵决策树投票),能捕捉非线性关系、对量纲不敏感、准确率通常更高,但可解释性弱、训练更慢。本项目随机森林准确率(91.1%)比逻辑回归(85.8%)高约 5%。

原理类 ​

Q4:为什么要做特征工程?本项目 4 个特征是怎么来的? A:原始字段多为字符型无法直接建模,且量纲差异大。特征工程把业务规律量化为数值特征。本项目根据三种作弊行为的数据特征,构建:N(5 小时内同 ip+cookie 出现次数,识别脚本刷新)、N1(同 ip 不同 cookie 数,识别清 cookie)、N2/N3(ip 前两段/前三段相同次数,识别 ADSL 拨号)。

Q5:randomSplit(Array(0.7, 0.3)) 划分的数据有什么特点? A:随机划分,不保证每类样本比例与总体完全一致(分层抽样需 StratifiedSplit);划分结果不可复现(除非设置随机种子)。本项目把 30% 作为 LoadData 模拟"新产生的数据",用于模型部署验证。

Q6:为什么要按 5 小时窗口构建特征,而不是按全天? A:5 小时根据广告点击周期频率划分。时间窗口太短,同 ip+cookie 的正常访问也会被计为高频(误报);窗口太长则作弊特征被稀释。按窗口切分还能保留流量随时间变化的信息。

实操类 ​

Q7:Spark 程序如何在集群上运行?完整流程? A:① IDEA 中开发、测试(本地模式)→ ② 打包成 jar(本项目 model.jar)→ ③ 上传到 Linux(/opt/data/)→ ④ spark-submit --master spark://master:7077 --class 主类 jar 参数... 提交。注意:集群提交时 master 要写 spark://master:7077,输入输出用 args 接收。

Q8:模型保存和加载怎么写? A:保存:model.write.overwrite().save(path);加载:LogisticRegressionModel.load(path) / RandomForestClassificationModel.load(path)。训练用算法类(LogisticRegression),加载用模型类(LogisticRegressionModel)。

Q9:怎么评估分类模型? A:用 MulticlassClassificationEvaluator 设置 labelCol、predictionCol、metricName(accuracy 最直观,也可用 precision/recall/f1)。先 model.transform(test) 得到 prediction 列,再 evaluate。


十一、项目小结 ​

11.1 本项目学到什么 ​

环节技术要点
业务理解虚假流量 4 种作弊方式 → 数据特征表现
数据探索count/groupBy/agg/na.drop 统计缺失率、识别冗余
数据预处理drop 高缺失字段、保留可合并字段
特征工程时间窗口切分(List.range + 18000s)+ N/N1/N2/N3 构建 + join 回填
特征标准化VectorAssembler + MinMaxScaler → features 列
模型构建逻辑回归 vs 随机森林(同评估器对比)
模型部署spark-submit 集群提交 + 模型保存/加载 + 新数据预测

11.2 项目思考题(来自教案,可用于课堂) ​

  1. 引导性:什么是流量?什么是虚假流量?常见的分类算法有哪些?分别适用于哪些场景?
  2. 探究性:对广告流量数据的基础探索可以从哪些方面进行?不同的作弊方式对应的数据特征是什么?用哪些分类算法可能取得较好效果,为什么?
  3. 拓展性:尝试优化随机森林参数(如调整 numTrees、maxDepth)使模型取得更好效果;思考 5 小时窗口改成其他值会怎样?

11.3 本项目的"生产环境对照" ​

⚠️ 教材说明:本项目是简化版案例,实际业务环境会更完善复杂。生产环境通常会:

  • 用流处理(Spark Streaming/Flink)实时识别,而不是离线 T+1
  • 特征更多、窗口更精细、加入更多业务规则
  • 模型上线后有监控与定期重训机制
  • 数据量是全量(本项目只用了前 25 小时构建特征)

附录A:Hive 表结构清单 ​

表名内容字段
ad_traffic.AdData删除 7 个高缺失字段后的原始数据15 个字段
ad_traffic.TimeFeatures各时间区间构建的 N/N1/N2/N3 特征rank、N、N1、N2、N3
ad_traffic.FeaturesData特征 + 标签合并rank、dt、N、N1、N2、N3、label
ad_traffic.ModelData70% 建模数据含 features 向量 + label
ad_traffic.LoadData30% 模拟新数据含 features 向量 + label

附录B:代码文件清单(配套 code 目录) ​

文件对应任务模式
Explore.scala任务9.1 数据探索local[*]
Features.scala任务9.2 特征构建local[*]
Scaler.scala任务9.3 标准化+划分local[*]
Logistic.scala任务9.4 逻辑回归spark://master:7077
RandomForest.scala任务9.4 随机森林spark://master:7077
LoadModel.scala任务9.5 模型加载应用local[*]

配套数据:data/case_data_new.csv(7 天流量、1704154 条、22 字段)


本笔记基于教材配套 PPT、教案及代码整理,适用于教学与复习。

基于 Vite 强力驱动 | 纯静态轻量托管