Apache Spark Java 示例:数据清洗

Apache Spark Java 示例:数据清洗

本文将详细介绍如何使用 Apache Spark 进行高效的数据清洗。数据清洗是数据预处理的关键步骤,涉及处理缺失值、异常值、格式问题、重复数据等,为后续分析提供高质量数据。

电商订单数据清洗场景

我们将清洗一个包含问题的电商订单数据集,包含以下字段:

  • order_id:订单ID(重复值、格式问题)
  • customer_id:客户ID(缺失值)
  • product_id:产品ID(格式问题)
  • quantity:数量(异常值、负数)
  • price:价格(异常值、格式问题)
  • order_date:订单日期(格式问题、无效日期)
  • city:城市(拼写错误、大小写不一致)
  • payment_method:支付方式(类别值错误)

完整实现代码

import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.StructType;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.functions.*;

import java.util.Arrays;
import java.util.List;

public class DataCleaningExample {

    public static void main(String[] args) {
        // 1. 创建SparkSession
        SparkSession spark = SparkSession.builder()
                .appName("E-commerce Data Cleaning")
                .master("local[*]")
                .config("spark.sql.shuffle.partitions", "8")
                .getOrCreate();
        
        try {
            // 2. 创建模拟数据集(包含各种数据质量问题)
            Dataset<Row> rawData = createSampleData(spark);
            System.out.println("原始数据:");
            rawData.show(false);
            
            // 3. 数据质量分析
            analyzeDataQuality(rawData);
            
            // 4. 数据清洗流程
            Dataset<Row> cleanedData = cleanData(rawData);
            
            // 5. 清洗后数据验证
            System.out.println("\n清洗后的数据:");
            cleanedData.show(false);
            validateCleanedData(cleanedData);
            
            // 6. 保存清洗后的数据
            cleanedData.write()
                    .mode("overwrite")
                    .parquet("data/cleaned_orders");
            
            System.out.println("数据清洗完成并保存到 data/cleaned_orders");
            
        } catch (Exception e) {
            System.err.println("数据处理过程中出错: " + e.getMessage());
            e.printStackTrace();
        } finally {
            spark.stop();
        }
    }
    
    /**
     * 创建包含数据质量问题的模拟数据集
     */
    private static Dataset<Row> createSampleData(SparkSession spark) {
        List<Row> data = Arrays.asList(
            // 正常数据
            RowFactory.create("ORD-1001", "CUST-001", "PROD-100", 2, 49.99, "2023-01-15", "New York", "Credit Card"),
            // 缺失值
            RowFactory.create("ORD-1002", null, "PROD-101", 1, 129.99, "2023-01-16", "Los Angeles", "PayPal"),
            // 异常值
            RowFactory.create("ORD-1003", "CUST-003", "PROD-102", -1, 79.99, "2023-01-17", "Chicago", "Credit Card"),
            RowFactory.create("ORD-1004", "CUST-004", "PROD-103", 1000, 24.99, "2023-01-18", "Boston", "Invalid Payment"),
            // 格式问题
            RowFactory.create("ORD1005", "CUST-005", "PROD104", 3, "29.99", "01/19/2023", "san francisco", "Credit Card"),
            // 重复数据
            RowFactory.create("ORD-1001", "CUST-001", "PROD-100", 2, 49.99, "2023-01-15", "New York", "Credit Card"),
            // 日期问题
            RowFactory.create("ORD-1006", "CUST-006", "PROD-105", 1, 199.99, "2023-02-30", "Seattle", "PayPal"),
            // 城市拼写错误
            RowFactory.create("ORD-1007", "CUST-007", "PROD-106", 2, 39.99, "2023-01-20", "New Yrok", "Credit Card"),
            // 数量为字符串
            RowFactory.create("ORD-1008", "CUST-008", "PROD-107", "two", 59.99, "2023-01-21", "Austin", "Credit Card"),
            // 价格格式问题
            RowFactory.create("ORD-1009", "CUST-009", "PROD-108", 1, "$89.99", "2023-01-22", "Miami", "PayPal"),
            // 无效支付方式
            RowFactory.create("ORD-1010", "CUST-010", "PROD-109", 3, 19.99, "2023-01-23", "Portland", "Unknown")
        );
        
        StructType schema = new StructType()
            .add("order_id", DataTypes.StringType)
            .add("customer_id", DataTypes.StringType)
            .add("product_id", DataTypes.StringType)
            .add("quantity", DataTypes.StringType) // 故意设为String以演示转换
            .add("price", DataTypes.StringType)    // 故意设为String以演示转换
            .add("order_date", DataTypes.StringType)
            .add("city", DataTypes.StringType)
            .add("payment_method", DataTypes.StringType);
        
        return spark.createDataFrame(data, schema);
    }
    
    /**
     * 数据质量分析
     */
    private static void analyzeDataQuality(Dataset<Row> data) {
        System.out.println("\n================ 数据质量分析 ================");
        
        // 1. 缺失值分析
        System.out.println("缺失值统计:");
        data.select(
            count(when(col("order_id").isNull(), 1)).alias("order_id_missing"),
            count(when(col("customer_id").isNull(), 1)).alias("customer_id_missing"),
            count(when(col("product_id").isNull(), 1)).alias("product_id_missing"),
            count(when(col("quantity").isNull(), 1)).alias("quantity_missing"),
            count(when(col("price").isNull(), 1)).alias("price_missing"),
            count(when(col("order_date").isNull(), 1)).alias("order_date_missing"),
            count(when(col("city").isNull(), 1)).alias("city_missing"),
            count(when(col("payment_method").isNull(), 1)).alias("payment_method_missing")
        ).show();
        
        // 2. 重复数据检查
        long totalCount = data.count();
        long distinctCount = data.distinct().count();
        System.out.println("重复记录: " + (totalCount - distinctCount));
        
        // 3. 异常值分析
        System.out.println("\n数值字段统计:");
        data.select(
            min("quantity").alias("min_quantity"),
            max("quantity").alias("max_quantity"),
            min("price").alias("min_price"),
            max("price").alias("max_price")
        ).show();
        
        // 4. 唯一值分析
        System.out.println("\n类别字段唯一值:");
        System.out.println("城市: " + data.select("city").distinct().count());
        System.out.println("支付方式: " + data.select("payment_method").distinct().count());
    }
    
    /**
     * 数据清洗流程
     */
    private static Dataset<Row> cleanData(Dataset<Row> data) {
        System.out.println("\n================ 数据清洗流程 ================");
        
        // 1. 处理重复数据
        Dataset<Row> dedupedData = data.dropDuplicates("order_id");
        System.out.println("去重后记录数: " + dedupedData.count() + "/" + data.count());
        
        // 2. 格式标准化
        Dataset<Row> formattedData = dedupedData
            // 订单ID格式标准化 (ORD1005 -> ORD-1005)
            .withColumn("order_id", 
                when(col("order_id").rlike("^ORD\\d+$"), 
                    regexp_replace(col("order_id"), "ORD", "ORD-"))
                .otherwise(col("order_id"))
            )
            // 产品ID格式标准化 (PROD104 -> PROD-104)
            .withColumn("product_id", 
                when(col("product_id").rlike("^PROD\\d+$"), 
                    regexp_replace(col("product_id"), "PROD", "PROD-"))
                .otherwise(col("product_id"))
            )
            // 城市名称标准化 (首字母大写)
            .withColumn("city", initcap(trim(col("city"))));
        
        // 3. 数据类型转换
        Dataset<Row> typedData = formattedData
            // 数量转换为整数 (处理字符串和异常值)
            .withColumn("quantity_cleaned", 
                when(col("quantity").rlike("^\\d+$"), col("quantity").cast(DataTypes.IntegerType))
                .otherwise(lit(null))
            )
            // 价格转换为浮点数 (移除货币符号)
            .withColumn("price_cleaned", 
                regexp_replace(col("price"), "[^\\d.]", "").cast(DataTypes.DoubleType)
            )
            // 日期格式转换
            .withColumn("order_date_cleaned", 
                when(col("order_date").rlike("^\\d{2}/\\d{2}/\\d{4}$"),
                    to_date(col("order_date"), "MM/dd/yyyy"))
                .otherwise(to_date(col("order_date"), "yyyy-MM-dd"))
            );
        
        // 4. 处理缺失值
        Dataset<Row> filledData = typedData
            // 填充缺失的客户ID
            .withColumn("customer_id", 
                when(col("customer_id").isNull(), "UNKNOWN")
                .otherwise(col("customer_id"))
            )
            // 填充缺失的数量
            .withColumn("quantity_cleaned", 
                when(col("quantity_cleaned").isNull(), 1)
                .otherwise(col("quantity_cleaned"))
            );
        
        // 5. 处理异常值
        Dataset<Row> outlierHandledData = filledData
            // 数量范围限制 (1-100)
            .withColumn("quantity_cleaned", 
                when(col("quantity_cleaned").lt(1), 1)
                .when(col("quantity_cleaned").gt(100), 100)
                .otherwise(col("quantity_cleaned"))
            )
            // 价格范围限制 (0.01-10000)
            .withColumn("price_cleaned", 
                when(col("price_cleaned").lt(0.01), 0.01)
                .when(col("price_cleaned").gt(10000), 10000)
                .otherwise(col("price_cleaned"))
            )
            // 处理无效日期
            .withColumn("order_date_cleaned", 
                when(col("order_date_cleaned").isNull(), to_date(lit("2023-01-01")))
                .otherwise(col("order_date_cleaned"))
            );
        
        // 6. 标准化类别值
        Dataset<Row> standardizedData = outlierHandledData
            // 标准化城市名称 (修正拼写错误)
            .withColumn("city_cleaned", 
                when(col("city").equalTo("New Yrok"), "New York")
                .when(col("city").equalTo("san Francisco"), "San Francisco")
                .otherwise(col("city"))
            )
            // 标准化支付方式
            .withColumn("payment_method_cleaned", 
                when(col("payment_method").isin("Credit Card", "PayPal"), col("payment_method"))
                .otherwise("Other")
            );
        
        // 7. 选择并重命名清洗后的列
        Dataset<Row> cleanedData = standardizedData.select(
            col("order_id").alias("order_id"),
            col("customer_id").alias("customer_id"),
            col("product_id").alias("product_id"),
            col("quantity_cleaned").alias("quantity"),
            col("price_cleaned").alias("price"),
            col("order_date_cleaned").alias("order_date"),
            col("city_cleaned").alias("city"),
            col("payment_method_cleaned").alias("payment_method")
        );
        
        return cleanedData;
    }
    
    /**
     * 清洗后数据验证
     */
    private static void validateCleanedData(Dataset<Row> cleanedData) {
        System.out.println("\n================ 清洗后数据验证 ================");
        
        // 1. 检查缺失值
        System.out.println("缺失值统计:");
        cleanedData.select(
            count(when(col("order_id").isNull(), 1)).alias("order_id_missing"),
            count(when(col("customer_id").isNull(), 1)).alias("customer_id_missing"),
            count(when(col("product_id").isNull(), 1)).alias("product_id_missing"),
            count(when(col("quantity").isNull(), 1)).alias("quantity_missing"),
            count(when(col("price").isNull(), 1)).alias("price_missing"),
            count(when(col("order_date").isNull(), 1)).alias("order_date_missing"),
            count(when(col("city").isNull(), 1)).alias("city_missing"),
            count(when(col("payment_method").isNull(), 1)).alias("payment_method_missing")
        ).show();
        
        // 2. 检查异常值
        System.out.println("\n数值范围验证:");
        cleanedData.select(
            min("quantity").alias("min_quantity"),
            max("quantity").alias("max_quantity"),
            min("price").alias("min_price"),
            max("price").alias("max_price")
        ).show();
        
        // 3. 检查格式一致性
        System.out.println("\n格式验证:");
        System.out.println("订单ID格式: " + 
            cleanedData.filter(col("order_id").rlike("^ORD-\\d+$")).count() + 
            "/" + cleanedData.count());
        
        System.out.println("产品ID格式: " + 
            cleanedData.filter(col("product_id").rlike("^PROD-\\d+$")).count() + 
            "/" + cleanedData.count());
        
        // 4. 检查类别值
        System.out.println("\n支付方式分布:");
        cleanedData.groupBy("payment_method").count().show();
        
        System.out.println("城市分布:");
        cleanedData.groupBy("city").count().show();
    }
}

数据清洗关键技术详解

1. 缺失值处理策略

策略方法适用场景
删除记录.filter(col("column").isNotNull())缺失值比例低且随机
填充默认值.na().fill(value)类别变量或已知默认值
统计值填充.withColumn("col", when(col("col").isNull(), meanVal))数值变量
插值填充使用窗口函数插值时间序列数据
模型预测填充机器学习模型预测复杂数据关系

2. 异常值检测方法

// IQR方法检测异常值
double q1 = data.stat().approxQuantile("price", new double[]{0.25}, 0.01)[0];
double q3 = data.stat().approxQuantile("price", new double[]{0.75}, 0.01)[0];
double iqr = q3 - q1;
double lowerBound = q1 - 1.5 * iqr;
double upperBound = q3 + 1.5 * iqr;

Dataset<Row> outliers = data.filter(
    col("price").lt(lowerBound).or(col("price").gt(upperBound))
);

其他方法:

  • Z-score(value - mean) / stddev
  • 固定阈值:业务规则定义范围
  • 聚类检测:K-means等聚类方法
  • 隔离森林:无监督异常检测算法

3. 数据类型转换

// 安全类型转换
data.withColumn("quantity", 
    expr("case when quantity rlike '^\\d+$' then cast(quantity as int) else null end")
);

// 使用try_cast函数(Spark 3.0+)
data.withColumn("price", expr("try_cast(price as double)"));

常见转换场景:

  • 字符串转数值/日期
  • 数值转类别
  • 时间戳转换
  • 数组/Map类型转换

4. 字符串处理

// 常用字符串函数
data.withColumn("city", initcap(trim(col("city")))) // 首字母大写并去空格
   .withColumn("email", lower(col("email"))) // 转为小写
   .withColumn("phone", regexp_replace(col("phone"), "[^0-9]", "")) // 移除非数字字符
   .withColumn("name", split(col("full_name"), " ").getItem(0)) // 拆分字符串
   .withColumn("domain", substring_index(col("email"), "@", -1)) // 提取域名
   .withColumn("description", concat(col("product"), lit(" - "), col("category"))); // 拼接字符串

5. 日期处理

// 日期格式转换
data.withColumn("order_date", 
    to_date(col("order_date"), "yyyy-MM-dd") // 标准格式
);

// 处理多种日期格式
data.withColumn("date_parsed", 
    coalesce(
        to_date(col("date_str"), "yyyy-MM-dd"),
        to_date(col("date_str"), "MM/dd/yyyy"),
        to_date(col("date_str"), "dd-MMM-yyyy")
    )
);

// 提取日期部分
data.withColumn("order_year", year(col("order_date")))
   .withColumn("order_month", month(col("order_date")))
   .withColumn("order_day", dayofmonth(col("order_date")));

高级清洗技术

1. 使用UDF处理复杂逻辑

import org.apache.spark.sql.api.java.UDF1;

// 注册城市标准化UDF
spark.udf().register("standardizeCity", (UDF1<String, String>) city -> {
    if (city == null) return "Unknown";
    String cleanCity = city.trim().toLowerCase();
    switch (cleanCity) {
        case "nyc": case "new york": return "New York";
        case "la": case "los angeles": return "Los Angeles";
        case "sf": case "san fran": return "San Francisco";
        default: return city.substring(0, 1).toUpperCase() + city.substring(1).toLowerCase();
    }
}, DataTypes.StringType);

// 使用UDF
data.withColumn("city_clean", callUDF("standardizeCity", col("city")));

2. 基于窗口函数的填充

import org.apache.spark.sql.expressions.Window;

// 使用前一个有效值填充
WindowSpec window = Window.orderBy("date").rowsBetween(-1, -1);
data.withColumn("filled_value", 
    coalesce(
        col("value"),
        last(col("value"), true).over(window)
    )
);

3. 复杂数据类型处理

// 处理JSON数据
{"order_id": "ORD-1001", "items": [{"product": "A", "qty": 2}, {"product": "B", "qty": 1}]}
// 解析JSON
Dataset<Row> jsonData = spark.read().json("data/orders.json");

// 展开数组
Dataset<Row> exploded = jsonData.select(
    col("order_id"),
    explode(col("items")).alias("item")
);

// 提取嵌套字段
Dataset<Row> cleaned = exploded.select(
    col("order_id"),
    col("item.product").alias("product"),
    col("item.qty").alias("quantity")
);

4. 使用JOIN标准化值

// 创建标准化映射表
List<Row> cityMapping = Arrays.asList(
    RowFactory.create("New Yrok", "New York"),
    RowFactory.create("san francisco", "San Francisco"),
    RowFactory.create("LA", "Los Angeles")
);

Dataset<Row> mappingDF = spark.createDataFrame(cityMapping, 
    new StructType()
        .add("raw_city", DataTypes.StringType)
        .add("standard_city", DataTypes.StringType)
);

// 使用JOIN标准化
Dataset<Row> cleanedData = data.join(mappingDF, 
    data.col("city").equalTo(mappingDF.col("raw_city")), 
    "left_outer"
).select(
    data.col("order_id"),
    coalesce(mappingDF.col("standard_city"), data.col("city")).alias("city"),
    // 其他字段...
);

数据质量监控

1. 数据质量规则定义

import org.apache.spark.sql.DataFrame;

public class DataQualityRules {
    // 检查缺失值
    public static boolean checkMissingValues(DataFrame df, String column) {
        return df.filter(col(column).isNull()).count() == 0;
    }
    
    // 检查值范围
    public static boolean checkValueRange(DataFrame df, String column, double min, double max) {
        return df.filter(col(column).lt(min).or(col(column).gt(max))).count() == 0;
    }
    
    // 检查格式
    public static boolean checkFormat(DataFrame df, String column, String regex) {
        return df.filter(not(col(column).rlike(regex))).count() == 0;
    }
    
    // 检查唯一性
    public static boolean checkUniqueness(DataFrame df, String column) {
        return df.select(column).distinct().count() == df.count();
    }
}

2. 自动化质量检查

// 执行数据质量检查
public void runQualityChecks(Dataset<Row> data) {
    boolean passed = true;
    
    // 1. 检查缺失值
    if (!DataQualityRules.checkMissingValues(data, "customer_id")) {
        System.err.println("customer_id 存在缺失值");
        passed = false;
    }
    
    // 2. 检查价格范围
    if (!DataQualityRules.checkValueRange(data, "price", 0.01, 10000)) {
        System.err.println("price 存在超出范围的值");
        passed = false;
    }
    
    // 3. 检查订单ID格式
    if (!DataQualityRules.checkFormat(data, "order_id", "^ORD-\\d+$")) {
        System.err.println("order_id 格式不正确");
        passed = false;
    }
    
    // 4. 检查唯一性
    if (!DataQualityRules.checkUniqueness(data, "order_id")) {
        System.err.println("order_id 存在重复值");
        passed = false;
    }
    
    if (passed) {
        System.out.println("所有数据质量检查通过");
    } else {
        System.err.println("数据质量检查未通过");
    }
}

生产环境最佳实践

1. 清洗流程配置化

// 配置文件示例 (JSON)
{
  "rules": [
    {
      "name": "fill_missing_customer_id",
      "type": "fill_missing",
      "column": "customer_id",
      "value": "UNKNOWN"
    },
    {
      "name": "standardize_order_id",
      "type": "regex_replace",
      "column": "order_id",
      "pattern": "^ORD(\\d+)$",
      "replacement": "ORD-$1"
    }
  ]
}

// 加载配置并应用规则
public Dataset<Row> applyCleaningRules(Dataset<Row> data, List<CleaningRule> rules) {
    for (CleaningRule rule : rules) {
        switch (rule.getType()) {
            case "fill_missing":
                data = data.withColumn(rule.getColumn(), 
                    when(col(rule.getColumn()).isNull(), rule.getValue())
                    .otherwise(col(rule.getColumn())));
                break;
            case "regex_replace":
                data = data.withColumn(rule.getColumn(), 
                    regexp_replace(col(rule.getColumn()), rule.getPattern(), rule.getReplacement()));
                break;
            // 其他规则类型...
        }
    }
    return data;
}

2. 增量数据清洗

// 读取增量数据
Dataset<Row> newData = spark.read()
    .format("parquet")
    .load("data/incremental/orders");

// 应用清洗规则
Dataset<Row> cleanedNewData = cleanData(newData);

// 合并到主数据集
cleanedNewData.write()
    .mode("append")
    .parquet("data/cleaned_orders");

3. 数据血统追踪

// 添加清洗元数据
Dataset<Row> withMetadata = cleanedData
    .withColumn("cleaning_timestamp", current_timestamp())
    .withColumn("cleaning_version", lit("v1.2"))
    .withColumn("data_source", input_file_name());

4. 错误数据处理

// 分离错误数据
Dataset<Row> validData = cleanedData.filter(
    col("quantity").between(1, 100).and(col("price").between(0.01, 10000))
);

Dataset<Row> invalidData = cleanedData.except(validData);

// 保存错误数据
invalidData.write()
    .mode("append")
    .parquet("data/invalid_orders");

性能优化策略

1. 分区与分桶

// 按日期分区保存
cleanedData.write()
    .partitionBy("order_date")
    .parquet("data/cleaned_orders");

// 分桶加速JOIN
cleanedData.write()
    .bucketBy(16, "customer_id")
    .sortBy("order_date")
    .saveAsTable("cleaned_orders");

2. 广播小数据集

// 广播城市映射表
Dataset<Row> cityMapping = ... // 小数据集
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "10485760"); // 10MB

Dataset<Row> joined = data.join(
    broadcast(cityMapping),
    data.col("city").equalTo(cityMapping.col("raw_city")),
    "left_outer"
);

3. 并行处理

// 增加shuffle分区数
spark.conf.set("spark.sql.shuffle.partitions", "200");

// 启用自适应查询执行
spark.conf.set("spark.sql.adaptive.enabled", "true");

4. 内存管理

// 缓存中间结果
data.persist(StorageLevel.MEMORY_AND_DISK());

// 使用堆外内存
spark.conf.set("spark.memory.offHeap.enabled", "true");
spark.conf.set("spark.memory.offHeap.size", "2g");

总结

通过这个电商订单数据清洗示例,我们展示了Spark在数据清洗中的强大能力:

  1. 数据质量分析:识别缺失值、异常值、重复数据
  2. 格式标准化:统一ID格式、日期格式、字符串格式
  3. 类型转换:安全转换数据类型
  4. 缺失值处理:填充默认值或统计值
  5. 异常值处理:限制范围或替换
  6. 类别值标准化:修正拼写错误和大小写
  7. 数据验证:确保清洗后数据质量

Spark数据清洗的优势:

  • 分布式处理:处理大规模数据集
  • 丰富API:提供多种数据转换函数
  • 统一平台:与ETL、分析、机器学习集成
  • 容错机制:自动处理节点故障
  • 性能优化:多种技术提升处理效率

实际应用场景:

  • 数据仓库ETL流程
  • 机器学习数据预处理
  • 实时数据清洗管道
  • 数据迁移与整合
  • 数据质量监控系统

通过结合Spark的强大功能和合理的数据清洗策略,可以高效地将原始数据转化为高质量、可分析的数据资产,为数据驱动决策奠定坚实基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值