3. 使用Spark SQL进行探索性数据分析

拿到一份零售销售数据集后,真正决定后续建模质量的工作,往往不是立刻写聚合报表,而是先做一轮系统性的探索性数据分析(EDA)。EDA 的目标不是“把数据看一遍”,而是回答四个具体问题:数据能不能用、分布长什么样、缺失和异常在哪里、哪些字段值得进入特征工程。在 Databricks 中,Spark SQL 与 DataFrame API 可以混用,既能用 SQL 做描述性统计,也能用 PySpark 做缺失值填充和异常检测,最后把结果落到 Delta 表或临时视图供可视化使用。

数据加载与初步体检

假设零售销售数据以 CSV 形式存放在 Unity Catalog 卷或云存储路径中,字段包括订单号、订单日期、门店、商品类别、数量、单价、折扣、销售额、客户编号。加载时不要直接 SELECT *,而应显式声明 schema,避免 Spark 把日期和金额推断成字符串。示例代码如下:

from pyspark.sql.types import *
schema = StructType([
    StructField("order_id", StringType(), False),
    StructField("order_date", DateType(), True),
    StructField("store_id", StringType(), True),
    StructField("category", StringType(), True),
    StructField("qty", IntegerType(), True),
    StructField("unit_price", DecimalType(10,2), True),
    StructField("discount", DecimalType(4,2), True),
    StructField("sales_amount", DecimalType(12,2), True),
    StructField("customer_id", StringType(), True)
])
df = (spark.read.option("header", True)
      .schema(schema)
      .csv("/Volumes/retail/sales/raw/sales_2024.csv"))
df.createOrReplaceTempView("sales_raw")

加载后先做“体检”:统计行数、列数、各列非空比例,并查看前几行。用 SQL 可以这样写:

SELECT COUNT(*) AS total_rows,
       COUNT(DISTINCT order_id) AS distinct_orders,
       MIN(order_date) AS min_date,
       MAX(order_date) AS max_date
FROM sales_raw;

如果 total_rows 与 distinct_orders 差距很大,说明订单号可能重复,需要进一步按订单号分组检查。日期范围则帮助判断数据是否覆盖完整销售周期,避免把季节性波动误判为异常。

描述性统计与分布观察

对数值字段,用 summary() 或 SQL 的 percentile_approx 查看均值、标准差、分位数。零售场景中,销售额和数量通常呈右偏分布,均值会被大额订单拉高,因此中位数和 P95 比均值更有参考价值。例如:

SELECT category,
       COUNT(*) AS cnt,
       ROUND(AVG(sales_amount),2) AS avg_sales,
       ROUND(PERCENTILE_APPROX(sales_amount, 0.5),2) AS p50_sales,
       ROUND(PERCENTILE_APPROX(sales_amount, 0.95),2) AS p95_sales,
       ROUND(STDDEV(sales_amount),2) AS std_sales
FROM sales_raw
GROUP BY category
ORDER BY avg_sales DESC;

对类别字段,用 GROUP BY 统计频次,观察是否存在长尾类别或拼写不一致(如“Electronics”与“electronics”)。这一步常能发现数据录入问题,比后续建模时再处理更省成本。

缺失值处理

缺失值要先区分“真缺失”和“业务上无意义”。例如 customer_id 为空可能代表匿名购买,不应随意填充;而 discount 为空通常意味着无折扣,可填 0。用 SQL 统计各列空值比例:

SELECT
  SUM(CASE WHEN customer_id IS NULL THEN 1 ELSE 0 END) / COUNT(*) AS null_customer,
  SUM(CASE WHEN discount IS NULL THEN 1 ELSE 0 END) / COUNT(*) AS null_discount,
  SUM(CASE WHEN sales_amount IS NULL THEN 1 ELSE 0 END) / COUNT(*) AS null_sales
FROM sales_raw;

处理策略上,discount 用 COALESCE(discount, 0) 填充;sales_amount 若缺失,可用 qty * unit_price * (1 - discount) 回算,但必须记录回算比例,避免污染后续分析。对于缺失比例超过 40% 且无法回算的字段,建议先标记为“待确认”,不要强行填充。

异常检测

零售数据的异常通常有三类:数量为负、单价为 0、销售额与数量单价严重不符。用 IQR 方法可以快速定位数值异常:计算 Q1 和 Q3,定义下界 Q1 - 1.5*IQR、上界 Q3 + 1.5*IQR,超出范围的记录标记为疑似异常。SQL 实现如下:

WITH stats AS (
  SELECT PERCENTILE_APPROX(sales_amount, 0.25) AS q1,
         PERCENTILE_APPROX(sales_amount, 0.75) AS q3
  FROM sales_raw
)
SELECT s.*, st.q1, st.q3,
       st.q1 - 1.5*(st.q3-st.q1) AS lower_bound,
       st.q3 + 1.5*(st.q3-st.q1) AS upper_bound
FROM sales_raw s CROSS JOIN stats st
WHERE s.sales_amount < st.q1 - 1.5*(st.q3-st.q1)
   OR s.sales_amount > st.q3 + 1.5*(st.q3-st.q1);

除了统计异常,还要做业务规则校验:qty <= 0、unit_price <= 0、discount > 1、sales_amount < 0。这些记录应单独落到 sales_rejected 表,并附上拒绝原因,而不是直接删除。保留拒绝记录有助于回溯数据源问题。

可视化准备

EDA 的最后一步是把清洗后的数据整理成适合可视化的宽表。Databricks 中常用做法是创建一个聚合视图,按日期、门店、类别汇总销售额和订单数,供后续 Dashboard 或 Notebook 图表直接读取:

CREATE OR REPLACE TEMP VIEW sales_daily AS
SELECT order_date, store_id, category,
       SUM(sales_amount) AS total_sales,
       COUNT(DISTINCT order_id) AS order_cnt,
       SUM(qty) AS total_qty
FROM sales_clean
GROUP BY order_date, store_id, category;

这样,可视化层不需要再关心缺失值和异常值,只需关注趋势、对比和分布。下面这张图概括了从原始数据到可视化准备的完整处理链,以及每个环节的关键判断点。

零售销售数据 EDA 处理链 ① 数据加载 显式 Schema CSV → 临时视图 ② 描述性统计 均值 / 中位数 / P95 类别频次分布 ③ 缺失值处理 COALESCE 填充 回算 + 标记 ④ 异常检测 IQR 上下界 业务规则校验 ⑤ 可视化准备 sales_daily 聚合视图 日期 × 门店 × 类别 异常记录分支 sales_rejected 表 保留拒绝原因,不直接删除 异常记录 关键判断点 行数与订单号去重数差异大 → 检查订单重复 销售额右偏 → 用中位数和 P95 代替均值描述 缺失比例 > 40% 且无法回算 → 标记待确认,不强行填充 IQR 越界或业务规则不通过 → 落入 rejected 表并记录原因

初步分析报告应包含什么

完成上述步骤后,一份可交付的初步分析报告至少应包含:数据规模与时间范围、各字段缺失比例、数值字段的分位数与异常比例、类别字段的 Top 分布、以及清洗后可用于可视化的聚合视图说明。报告中的数字应来自实际执行的查询结果,而不是估算。若某些字段缺失严重或异常比例过高,应在报告中明确标注“需业务确认”,并给出后续处理建议,例如补充数据源、调整采集规则或引入人工复核。

在 Databricks 中,建议把 EDA 过程写成可重复执行的 Notebook,每一步用 display() 或 display(spark.sql(...)) 查看中间结果,并把关键统计量写入 Delta 表,便于后续对比不同批次数据的变化。

EDA 不是一次性任务。随着新数据批次进入,缺失模式和异常比例可能变化,因此建议把上述统计查询封装成参数化脚本,按日期分区定期运行,形成数据质量监控的基线。这样,后续的特征工程和建模才能建立在可信的数据基础上。