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在数据清洗中的强大能力:
- 数据质量分析:识别缺失值、异常值、重复数据
- 格式标准化:统一ID格式、日期格式、字符串格式
- 类型转换:安全转换数据类型
- 缺失值处理:填充默认值或统计值
- 异常值处理:限制范围或替换
- 类别值标准化:修正拼写错误和大小写
- 数据验证:确保清洗后数据质量
Spark数据清洗的优势:
- 分布式处理:处理大规模数据集
- 丰富API:提供多种数据转换函数
- 统一平台:与ETL、分析、机器学习集成
- 容错机制:自动处理节点故障
- 性能优化:多种技术提升处理效率
实际应用场景:
- 数据仓库ETL流程
- 机器学习数据预处理
- 实时数据清洗管道
- 数据迁移与整合
- 数据质量监控系统
通过结合Spark的强大功能和合理的数据清洗策略,可以高效地将原始数据转化为高质量、可分析的数据资产,为数据驱动决策奠定坚实基础。
1323

被折叠的 条评论
为什么被折叠?



