【Python数据科学实战之路】第15章 | PySpark大数据处理:分布式计算入门
环境声明
- Python版本:Python 3.12+(建议使用 3.10 以上版本)
- 开发工具:PyCharm 或 VS Code
- 操作系统:Windows / macOS / Linux(通用)
- PySpark版本:3.5+
学习目标
本章学习完成后,你将能够:
- 理解大数据和分布式计算的基本概念,掌握Spark的核心架构和工作原理
- 搭建PySpark开发环境,配置本地和集群运行模式
- 熟练使用RDD(弹性分布式数据集)进行数据转换和行动操作
- 掌握DataFrame API,使用Spark SQL进行结构化数据处理
- 了解Spark MLlib机器学习库,实现分布式机器学习任务
章节内容
1. 大数据与Spark概述
1.1 什么是大数据
大数据是指规模巨大、类型多样、处理速度要求高的数据集合。传统数据处理工具难以应对,因此需要分布式计算框架。
1.2 Spark的诞生与优势
Spark是由Apache软件基金会开发的快速通用集群计算系统,相比Hadoop MapReduce具有以下优势:
| 特性 | Spark | Hadoop MapReduce |
|---|---|---|
| 计算速度 | 内存计算,快10-100倍 | 磁盘IO频繁,速度较慢 |
| 易用性 | 支持Python、Scala、Java、R | 主要使用Java |
| 通用性 | 批处理、流处理、SQL、机器学习统一 | 主要面向批处理 |
| 容错性 | 基于RDD的血缘机制 | 基于任务重试 |
补充:Spark的创始人Matei Zaharia在UC Berkeley的AMPLab读博期间开发了Spark,2014年成为Apache顶级项目。
1.3 Spark核心架构
+---------------------+
| Driver Program | (驱动程序,运行main函数)
| (包含SparkContext) |
+----------+----------+
|
+----------v----------+
| Cluster Manager | (集群管理器:Standalone/YARN/Mesos)
+----------+----------+
|
+------+------+
| |
+---v---+ +---v---+
|Worker1| |Worker2| (工作节点,执行实际任务)
|Executor| |Executor|
+--------+ +--------+
2. PySpark环境搭建
2.1 安装PySpark
# 使用pip安装
pip install pyspark
# 或者使用conda安装
conda install -c conda-forge pyspark
2.2 验证安装
from pyspark.sql import SparkSession
# 创建SparkSession
spark = SparkSession.builder \
.appName("PySpark入门") \
.getOrCreate()
# 查看Spark版本
print(f"Spark版本: {spark.version}")
# 停止SparkSession
spark.stop()
3. RDD编程基础
3.1 什么是RDD
RDD(Resilient Distributed Dataset,弹性分布式数据集)是Spark最基本的抽象,代表一个不可变、可分区的数据集合。
一句话总结:RDD就像把一个大文件切成很多小块,分散存储在多台机器上,可以并行处理。
RDD的五大核心特性
| 特性 | 说明 | 作用 |
|---|---|---|
| 分区列表 | 数据被划分为多个分区 | 实现并行计算的基础 |
| 计算函数 | 每个分区的计算逻辑 | 定义如何从父RDD计算当前RDD |
| 依赖关系 | 记录RDD之间的血缘关系 | 实现容错和故障恢复 |
| 分区器 | 决定数据如何分布到分区 | 影响Shuffle性能 |
| 优先位置 | 数据本地性信息 | 优化任务调度,减少网络传输 |
RDD的不可变性
RDD一旦创建就不能修改,所有转换操作都会生成新的RDD。这种设计带来三个好处:
- 线程安全:多线程并发访问无需加锁
- 容错简单:通过重新计算即可恢复丢失数据
- 优化机会:Spark可以在执行前优化整个计算流程
from pyspark import SparkContext
sc = SparkContext.getOrCreate()
# RDD不可变性示例
rdd1 = sc.parallelize([1, 2, 3, 4, 5])
rdd2 = rdd1.map(lambda x: x * 2) # rdd1保持不变,返回新RDD
rdd3 = rdd1.filter(lambda x: x > 2) # rdd1仍然保持不变
print(f"rdd1: {rdd1.collect()}") # [1, 2, 3, 4, 5]
print(f"rdd2: {rdd2.collect()}") # [2, 4, 6, 8, 10]
print(f"rdd3: {rdd3.collect()}") # [3, 4, 5]
3.2 创建RDD
from pyspark import SparkContext
sc = SparkContext.getOrCreate()
# 从列表创建RDD
data = [1, 2, 3, 4, 5]
rdd = sc.parallelize(data)
# 指定分区数创建RDD(推荐大数据场景)
rdd_partitioned = sc.parallelize(data, numSlices=3)
print(f"分区数: {rdd_partitioned.getNumPartitions()}")
# 从文件创建RDD
rdd_from_file = sc.textFile("data.txt")
# 从文件创建RDD并指定最小分区数
rdd_from_file_partitioned = sc.textFile("data.txt", minPartitions=4)
# 从HDFS创建RDD
rdd_hdfs = sc.textFile("hdfs://namenode:9000/path/to/file.txt")
# 从多个文件创建RDD(通配符)
rdd_wildcard = sc.textFile("data/*.csv")
# 从数据库创建RDD(需要JDBC)
# rdd_jdbc = sc.jdbc("jdbc:mysql://localhost/db", "table", properties={"user": "root"})
# 从现有RDD创建新RDD
rdd_from_rdd = rdd.map(lambda x: x * 2)
3.3 RDD依赖关系与Shuffle机制
窄依赖 vs 宽依赖
RDD之间的依赖关系分为两类,这是理解Spark性能的关键:
窄依赖(Narrow Dependency)
+----------------+ +----------------+
| Parent RDD | | Parent RDD |
| Partition 1 | | Partition 2 |
+--------+-------+ +--------+-------+
| |
v v
+--------+-------+ +--------+-------+
| Child RDD | | Child RDD |
| Partition 1 | | Partition 2 |
+----------------+ +----------------+
特点:父RDD的一个分区对应子RDD的一个分区
操作:map, filter, flatMap, union
宽依赖(Wide Dependency / Shuffle Dependency)
+----------------+ +----------------+
| Parent RDD | | Parent RDD |
| Partition 1 | | Partition 2 |
+--------+-------+ +--------+-------+
| |
+----------+-----------+
|
+----------+-----------+
| |
v v
+--------+-------+ +--------+-------+
| Child RDD | | Child RDD |
| Partition 1 | | Partition 2 |
+----------------+ +----------------+
特点:父RDD的一个分区对应子RDD的多个分区
操作:reduceByKey, groupByKey, sortByKey, join
# 窄依赖示例 - 无需Shuffle
rdd = sc.parallelize([1, 2, 3, 4, 5, 6], 3)
# map是窄依赖,每个分区独立计算
mapped = rdd.map(lambda x: x * 2)
# filter也是窄依赖
filtered = mapped.filter(lambda x: x > 4)
print(f"窄依赖 lineage: {filtered.toDebugString().decode('utf-8')}")
# 宽依赖示例 - 需要Shuffle
pairs = sc.parallelize([
("a", 1), ("b", 2), ("a", 3), ("b", 4), ("c", 5)
], 2)
# reduceByKey是宽依赖,需要按Key重新分区
reduced = pairs.reduceByKey(lambda x, y: x + y)
print(f"宽依赖 lineage: {reduced.toDebugString().decode('utf-8')}")
Shuffle机制详解
Shuffle是Spark中最耗时的操作,理解其原理对性能优化至关重要:
Shuffle过程示意图
Map阶段(写入) Reduce阶段(读取)
+--------------+ +--------------+
| Partition 1 |---> [Sort] ---> | |
| Partition 2 |---> [Sort] ---> | Shuffle |---> [Merge] ---> Result
| Partition 3 |---> [Sort] ---> | Read |
+--------------+ +--------------+
| ^
| 磁盘文件 / 网络传输 |
+----------------------------------+
# Shuffle操作对比实验
import time
# 创建测试数据
data = [(i % 100, i) for i in range(100000)]
rdd = sc.parallelize(data, 4)
# 实验1: groupByKey(产生大量Shuffle数据)
start = time.time()
grouped = rdd.groupByKey().mapValues(list)
grouped.count() # 触发计算
groupby_time = time.time() - start
# 实验2: reduceByKey(在Map端预聚合,减少Shuffle)
start = time.time()
reduced = rdd.reduceByKey(lambda x, y: x + y)
reduced.count() # 触发计算
reduceby_time = time.time() - start
print(f"groupByKey耗时: {groupby_time:.3f}秒")
print(f"reduceByKey耗时: {reduceby_time:.3f}秒")
print(f"性能提升: {groupby_time / reduceby_time:.1f}倍")
# 实验3: aggregateByKey(更灵活的预聚合)
start = time.time()
# 参数: 初始值, 分区聚合函数, 全局聚合函数
aggregated = rdd.aggregateByKey(
(0, 0), # 初始值: (sum, count)
lambda acc, val: (acc[0] + val, acc[1] + 1), # 分区聚合
lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1]) # 全局聚合
)
avg_result = aggregated.mapValues(lambda x: x[0] / x[1])
avg_result.count()
aggregate_time = time.time() - start
print(f"aggregateByKey耗时: {aggregate_time:.3f}秒")
分区策略详解
# 1. Hash分区(默认)
# 根据Key的hashCode决定分区
hash_partitioned = pairs.partitionBy(numPartitions=4)
# 2. Range分区
# 根据Key的范围划分分区,适合有序数据
from pyspark import RangePartitioner
# pairs.sortByKey() 内部使用Range分区
# 3. 自定义分区器
from pyspark import Partitioner
class CustomPartitioner(Partitioner):
def __init__(self, num_partitions):
self.num_partitions = num_partitions
def numPartitions(self):
return self.num_partitions
def getPartition(self, key):
# 自定义分区逻辑:按Key的首字母分区
if isinstance(key, str) and len(key) > 0:
return ord(key[0].lower()) % self.num_partitions
return 0
# 使用自定义分区器
# custom_partitioned = pairs.partitionBy(CustomPartitioner(4))
# 4. 查看分区信息
print(f"分区数: {rdd.getNumPartitions()}")
print(f"各分区元素数: {rdd.glom().map(len).collect()}")
# 5. 重新分区
coalesced = rdd.coalesce(2) # 减少分区(窄依赖,无Shuffle)
repartitioned = rdd.repartition(6) # 增加分区(宽依赖,有Shuffle)
3.4 RDD转换操作(Transformations)
转换操作是惰性求值的,不会立即执行。
# map: 对每个元素应用函数
rdd = sc.parallelize([1, 2, 3, 4])
mapped_rdd = rdd.map(lambda x: x * 2) # [2, 4, 6, 8]
# filter: 过滤元素
filtered_rdd = rdd.filter(lambda x: x > 2) # [3, 4]
# flatMap: 扁平化映射
words_rdd = sc.parallelize(["hello world", "pyspark"])
flat_rdd = words_rdd.flatMap(lambda x: x.split(" ")) # ["hello", "world", "pyspark"]
# reduceByKey: 按键聚合
pairs = sc.parallelize([("a", 1), ("b", 2), ("a", 3)])
reduced = pairs.reduceByKey(lambda x, y: x + y) # [("a", 4), ("b", 2)]
3.4 RDD行动操作(Actions)
行动操作会触发实际计算。
# collect: 收集所有元素到驱动程序
result = rdd.collect()
# count: 统计元素个数
count = rdd.count()
# take: 取前N个元素
first_n = rdd.take(3)
# reduce: 聚合所有元素
total = rdd.reduce(lambda x, y: x + y)
# saveAsTextFile: 保存到文件
rdd.saveAsTextFile("output_path")
3.5 RDD高级转换操作
# combineByKey: 最灵活的聚合操作
# 参数: createCombiner, mergeValue, mergeCombiners
pairs = sc.parallelize([
("apple", 3), ("banana", 2), ("apple", 5),
("banana", 4), ("cherry", 1)
])
# 计算每个Key的平均值
avg_by_key = pairs.combineByKey(
lambda val: (val, 1), # 初始值转换: value -> (sum, count)
lambda acc, val: (acc[0] + val, acc[1] + 1), # 合并新值
lambda acc1, acc2: (acc1[0] + acc2[0], acc1[1] + acc2[1]) # 合并分区结果
).mapValues(lambda x: x[0] / x[1])
print(avg_by_key.collect())
# foldByKey: 带初始值的reduceByKey
folded = pairs.foldByKey(0, lambda x, y: x + y)
# sortByKey: 按键排序
sorted_pairs = pairs.sortByKey(ascending=True, numPartitions=2)
# cogroup: 多RDD分组(类似SQL的FULL OUTER JOIN)
rdd1 = sc.parallelize([("a", 1), ("b", 2)])
rdd2 = sc.parallelize([("a", "x"), ("a", "y"), ("b", "z")])
cogrouped = rdd1.cogroup(rdd2)
print(cogrouped.mapValues(lambda x: (list(x[0]), list(x[1]))).collect())
# join系列操作
rdd3 = sc.parallelize([("a", 10), ("c", 30)])
inner_joined = rdd1.join(rdd3) # 内连接
left_joined = rdd1.leftOuterJoin(rdd3) # 左外连接
full_joined = rdd1.fullOuterJoin(rdd3) # 全外连接
# subtract: 集合差集
rdd_a = sc.parallelize([1, 2, 3, 4])
rdd_b = sc.parallelize([3, 4, 5, 6])
diff = rdd_a.subtract(rdd_b) # [1, 2]
# intersection: 集合交集
intersect = rdd_a.intersection(rdd_b) # [3, 4]
# distinct: 去重
unique = sc.parallelize([1, 2, 2, 3, 3, 3]).distinct()
# cartesian: 笛卡尔积(谨慎使用,数据量爆炸)
cartesian = sc.parallelize([1, 2]).cartesian(sc.parallelize(["a", "b"]))
# [(1, "a"), (1, "b"), (2, "a"), (2, "b")]
# zip: 拉链操作(两个RDD分区数和元素数必须相同)
zipped = sc.parallelize([1, 2, 3]).zip(sc.parallelize(["a", "b", "c"]))
# zipWithIndex: 添加索引
with_index = sc.parallelize(["a", "b", "c"]).zipWithIndex()
# [("a", 0), ("b", 1), ("c", 2)]
3.6 RDD缓存与持久化
# 缓存策略对比
from pyspark import StorageLevel
rdd = sc.parallelize(range(1000000), 4)
mapped = rdd.map(lambda x: x * x * x)
# 1. MEMORY_ONLY(默认)
# 仅内存存储,速度快但占用内存
mapped.cache() # 等价于 persist(StorageLevel.MEMORY_ONLY)
# 2. MEMORY_AND_DISK
# 内存不足时溢写到磁盘
mapped.persist(StorageLevel.MEMORY_AND_DISK)
# 3. MEMORY_ONLY_SER
# 序列化存储(节省内存,但读取时需要反序列化)
mapped.persist(StorageLevel.MEMORY_ONLY_SER)
# 4. DISK_ONLY
# 仅磁盘存储
mapped.persist(StorageLevel.DISK_ONLY)
# 触发缓存
mapped.count()
# 再次使用时会从缓存读取
mapped.reduce(lambda x, y: x + y)
# 取消持久化
mapped.unpersist()
# 检查点(Checkpoint)- 切断血缘,适合迭代算法
sc.setCheckpointDir("/tmp/checkpoints")
long_lineage_rdd = sc.parallelize([1])
for i in range(100):
long_lineage_rdd = long_lineage_rdd.map(lambda x: x + 1)
if i % 10 == 0:
long_lineage_rdd.checkpoint() # 每10步切断一次血缘
避坑小贴士
- RDD的转换操作是惰性求值的,只有遇到行动操作才会真正执行
- collect()操作会将所有数据拉取到驱动程序,大数据量时慎用,可能导致内存溢出
- 频繁使用collect()会影响性能,尽量在RDD层面完成计算
- 避免使用groupByKey:优先使用reduceByKey或aggregateByKey进行预聚合
- 合理设置分区数:太少会导致任务过大,太多会增加调度开销,一般每个分区100MB-1GB
- 及时unpersist():缓存的RDD不再使用时及时释放内存
- 慎用cartesian:笛卡尔积会产生N*M的数据量,极易导致OOM
4. Spark性能优化专题
4.1 广播变量与累加器
广播变量(Broadcast Variables)
当需要在所有节点上使用同一份较大数据时,使用广播变量可以避免数据重复传输。
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("广播变量示例").getOrCreate()
sc = spark.sparkContext
# 场景:将小表广播到大表进行Join
# 大表(假设有1亿条记录)
large_data = [(i, f"user_{i}", i % 100) for i in range(10000)]
large_rdd = sc.parallelize(large_data, 10)
# 小表(假设只有100条记录)
small_lookup = {i: f"category_{i}" for i in range(100)}
# 不使用广播(每个任务都传输小表)
def join_without_broadcast(record):
user_id, name, cat_id = record
return (user_id, name, small_lookup.get(cat_id, "unknown"))
# 使用广播(只传输一次,所有任务共享)
broadcast_lookup = sc.broadcast(small_lookup)
def join_with_broadcast(record):
user_id, name, cat_id = record
return (user_id, name, broadcast_lookup.value.get(cat_id, "unknown"))
# 对比性能
import time
start = time.time()
result1 = large_rdd.map(join_without_broadcast).collect()
print(f"不使用广播耗时: {time.time() - start:.3f}秒")
start = time.time()
result2 = large_rdd.map(join_with_broadcast).collect()
print(f"使用广播耗时: {time.time() - start:.3f}秒")
# 释放广播变量
broadcast_lookup.unpersist()
累加器(Accumulators)
累加器用于在任务中进行全局计数或求和,只能在Driver端读取。
# 使用累加器统计异常数据
error_count = sc.accumulator(0)
total_count = sc.accumulator(0)
def process_with_counter(record):
global error_count, total_count
total_count += 1
try:
# 处理逻辑
result = int(record) * 2
return result
except:
error_count += 1
return None
data = sc.parallelize(["1", "2", "abc", "4", "5", "xyz"])
results = data.map(process_with_counter).filter(lambda x: x is not None).collect()
print(f"总记录数: {total_count.value}")
print(f"错误记录数: {error_count.value}")
print(f"错误率: {error_count.value / total_count.value:.2%}")
# 自定义累加器
from pyspark.accumulators import AccumulatorParam
class VectorAccumulatorParam(AccumulatorParam):
def zero(self, value):
return [0] * len(value)
def addInPlace(self, val1, val2):
for i in range(len(val1)):
val1[i] += val2[i]
return val1
vector_acc = sc.accumulator([0, 0, 0], VectorAccumulatorParam())
4.2 数据倾斜处理
数据倾斜是指某些Key的数据量远大于其他Key,导致部分任务执行时间过长。
# 识别数据倾斜
from pyspark.sql import functions as F
# 方法1: 查看Key分布
key_counts = df.groupBy("key_column").count().orderBy(F.desc("count"))
key_counts.show(20)
# 方法2: 计算倾斜度
total_count = df.count()
distinct_keys = df.select("key_column").distinct().count()
avg_per_key = total_count / distinct_keys
skewed_keys = df.groupBy("key_column") \
.count() \
.filter(F.col("count") > avg_per_key * 10) \
.select("key_column") \
.rdd.map(lambda x: x[0]) \
.collect()
print(f"倾斜的Key: {skewed_keys}")
解决方案1: 加盐(Salting)
import random
# 给倾斜的Key添加随机前缀
SALT_COUNT = 10
def add_salt(key):
if key in skewed_keys:
return f"{key}_{random.randint(0, SALT_COUNT - 1)}"
return key
# 两阶段聚合
# 第一阶段:局部聚合(加盐)
phase1 = rdd.map(lambda x: (add_salt(x[0]), x[1])) \
.reduceByKey(lambda x, y: x + y)
# 第二阶段:去除盐,全局聚合
phase2 = phase1.map(lambda x: (x[0].split("_")[0], x[1])) \
.reduceByKey(lambda x, y: x + y)
解决方案2: 拆分倾斜Key
# 将倾斜的Key单独处理
normal_keys_rdd = rdd.filter(lambda x: x[0] not in skewed_keys)
skewed_keys_rdd = rdd.filter(lambda x: x[0] in skewed_keys)
# 对倾斜Key使用mapJoin(广播小表)
broadcast_small = sc.broadcast(small_table_dict)
skewed_result = skewed_keys_rdd.map(lambda x: (x[0], (x[1], broadcast_small.value.get(x[0]))))
# 正常Key使用reduceByKey
normal_result = normal_keys_rdd.reduceByKey(lambda x, y: x + y)
# 合并结果
final_result = normal_result.union(skewed_result)
4.3 Spark UI监控与调优
# 配置Spark以启用历史服务器和详细日志
spark = SparkSession.builder \
.appName("监控示例") \
.config("spark.eventLog.enabled", "true") \
.config("spark.eventLog.dir", "/tmp/spark-events") \
.config("spark.history.fs.logDirectory", "/tmp/spark-events") \
.getOrCreate()
# 获取Spark UI链接
print(f"Spark UI: http://localhost:4040")
# 关键指标解读
"""
Jobs页面:
- 查看Job执行时间和状态
- 识别慢任务
Stages页面:
- 查看Stage的输入/输出数据量
- 识别Shuffle读写量
- 查看任务执行时间分布
Tasks页面:
- 查看每个任务的执行时间
- 识别数据倾斜(某些任务时间明显更长)
- 查看GC时间和Shuffle时间
Storage页面:
- 查看缓存的RDD/DataFrame
- 内存和磁盘使用情况
"""
# 代码中获取度量信息
sc.statusTracker().getExecutorInfos()
4.4 内存与序列化优化
# 配置序列化方式
spark = SparkSession.builder \
.appName("序列化优化") \
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer") \
.config("spark.kryo.registrationRequired", "false") \
.getOrCreate()
# Kryo序列化比Java序列化更快更紧凑
# 对于自定义类,需要注册
# conf.registerKryoClasses([MyClass.class])
# 内存配置
spark = SparkSession.builder \
.appName("内存优化") \
.config("spark.executor.memory", "4g") \
.config("spark.executor.memoryOverhead", "512m") \
.config("spark.driver.memory", "2g") \
.config("spark.memory.fraction", "0.8") \
.config("spark.memory.storageFraction", "0.3") \
.getOrCreate()
"""
内存配置说明:
- executor.memory: Executor堆内存
- executor.memoryOverhead: Executor非堆内存(用于PySpark等)
- memory.fraction: 用于执行和存储的内存比例(默认0.6)
- memory.storageFraction: 存储内存占fraction的比例(默认0.5)
"""
5. Spark Streaming流处理
5.1 DStream基础
from pyspark.streaming import StreamingContext
# 创建StreamingContext,批次间隔2秒
ssc = StreamingContext(sc, 2)
# 从TCP socket接收数据
lines = ssc.socketTextStream("localhost", 9999)
# 处理逻辑:词频统计
words = lines.flatMap(lambda line: line.split(" "))
pairs = words.map(lambda word: (word, 1))
word_counts = pairs.reduceByKey(lambda x, y: x + y)
# 输出结果
word_counts.pprint()
# 启动流处理
ssc.start()
ssc.awaitTermination()
5.2 窗口操作
# 创建带检查点的StreamingContext
ssc = StreamingContext(sc, 1)
ssc.checkpoint("/tmp/streaming-checkpoint")
# 从Kafka接收数据(需要spark-streaming-kafka包)
# from pyspark.streaming.kafka import KafkaUtils
# kafka_stream = KafkaUtils.createStream(ssc, "zookeeper:2181", "consumer-group", {"topic": 1})
lines = ssc.socketTextStream("localhost", 9999)
words = lines.flatMap(lambda line: line.split(" "))
pairs = words.map(lambda word: (word, 1))
# 滑动窗口操作
# 窗口大小30秒,滑动间隔10秒
windowed_counts = pairs.reduceByKeyAndWindow(
lambda x, y: x + y, # 聚合函数
lambda x, y: x - y, # 逆函数(用于增量计算)
windowDuration=30,
slideDuration=10
)
windowed_counts.pprint()
ssc.start()
ssc.awaitTermination()
5.3 Structured Streaming
from pyspark.sql import SparkSession
from pyspark.sql.functions import explode, split, window
spark = SparkSession.builder \
.appName("StructuredStreaming") \
.getOrCreate()
# 读取流数据(从socket)
lines = spark.readStream \
.format("socket") \
.option("host", "localhost") \
.option("port", 9999) \
.load()
# 处理逻辑
words = lines.select(
explode(split(lines.value, " ")).alias("word")
)
word_counts = words.groupBy("word").count()
# 输出到控制台
query = word_counts.writeStream \
.outputMode("complete") \
.format("console") \
.start()
query.awaitTermination()
Structured Streaming输出模式
# 1. Complete Mode:输出完整结果表(适用于聚合查询)
agg_query = word_counts.writeStream \
.outputMode("complete") \
.format("console") \
.start()
# 2. Append Mode:只输出新增行(适用于无聚合查询)
append_query = words.writeStream \
.outputMode("append") \
.format("console") \
.start()
# 3. Update Mode:只输出更新的行
update_query = word_counts.writeStream \
.outputMode("update") \
.format("console") \
.start()
流数据Join
# 流-静态Join
static_df = spark.read.parquet("/path/to/static/data")
stream_df = spark.readStream.format("kafka").option("subscribe", "topic").load()
joined = stream_df.join(static_df, "join_key")
# 流-流Join(需要水印)
from pyspark.sql.functions import expr
stream1 = spark.readStream.format("kafka").option("subscribe", "topic1").load()
stream2 = spark.readStream.format("kafka").option("subscribe", "topic2").load()
# 添加水印处理延迟数据
stream1_with_watermark = stream1.withWatermark("timestamp", "10 minutes")
stream2_with_watermark = stream2.withWatermark("timestamp", "10 minutes")
joined_streams = stream1_with_watermark.join(
stream2_with_watermark,
expr("""
join_key = join_key AND
timestamp1 >= timestamp2 - interval 5 minutes AND
timestamp1 <= timestamp2 + interval 5 minutes
"""),
"inner"
)
6. DataFrame与Spark SQL
4.1 DataFrame简介
DataFrame是Spark 2.0+推荐的编程接口,以命名列的方式组织分布式数据,类似于pandas的DataFrame,但支持分布式计算。
4.2 创建DataFrame
from pyspark.sql import SparkSession
from pyspark.sql import Row
spark = SparkSession.builder.appName("DataFrame示例").getOrCreate()
# 从RDD创建
rdd = spark.sparkContext.parallelize([
Row(name="张三", age=25, city="北京"),
Row(name="李四", age=30, city="上海"),
Row(name="王五", age=28, city="广州")
])
df = spark.createDataFrame(rdd)
# 从列表创建
data = [("张三", 25, "北京"), ("李四", 30, "上海")]
columns = ["name", "age", "city"]
df = spark.createDataFrame(data, columns)
# 从JSON/CSV文件创建
df_json = spark.read.json("data.json")
df_csv = spark.read.csv("data.csv", header=True, inferSchema=True)
4.3 DataFrame常用操作
# 显示数据
df.show()
# 查看Schema
df.printSchema()
# 选择列
df.select("name", "age").show()
# 过滤数据
df.filter(df.age > 25).show()
df.filter("age > 25").show()
# 分组聚合
df.groupBy("city").count().show()
df.groupBy("city").agg({"age": "avg"}).show()
# 排序
df.orderBy(df.age.desc()).show()
4.4 Spark SQL
# 注册临时视图
df.createOrReplaceTempView("people")
# 执行SQL查询
result = spark.sql("""
SELECT city, AVG(age) as avg_age
FROM people
GROUP BY city
ORDER BY avg_age DESC
""")
result.show()
5. Spark MLlib机器学习
5.1 MLlib简介
MLlib是Spark的可扩展机器学习库,提供常用的机器学习算法和工具。
5.2 特征工程
from pyspark.ml.feature import VectorAssembler, StandardScaler
# 特征向量化
assembler = VectorAssembler(
inputCols=["feature1", "feature2", "feature3"],
outputCol="features"
)
df_vector = assembler.transform(df)
# 标准化
scaler = StandardScaler(
inputCol="features",
outputCol="scaledFeatures"
)
scaler_model = scaler.fit(df_vector)
df_scaled = scaler_model.transform(df_vector)
5.3 机器学习示例(逻辑回归)
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import BinaryClassificationEvaluator
# 划分训练集和测试集
train_df, test_df = df_scaled.randomSplit([0.8, 0.2], seed=42)
# 创建模型
lr = LogisticRegression(
featuresCol="scaledFeatures",
labelCol="label",
maxIter=10
)
# 训练模型
model = lr.fit(train_df)
# 预测
predictions = model.transform(test_df)
# 评估
evaluator = BinaryClassificationEvaluator()
accuracy = evaluator.evaluate(predictions)
print(f"模型准确率: {accuracy}")
6. 实战项目:电商用户行为分析
本项目将使用PySpark完成一个完整的电商用户行为分析系统,涵盖数据清洗、用户画像、RFM分析和推荐系统。
6.1 项目概述与数据准备
项目结构
├── data/
│ ├── user_behavior.csv # 用户行为数据
│ ├── user_info.csv # 用户基本信息
│ └── product_info.csv # 商品信息
├── src/
│ ├── data_cleaning.py # 数据清洗
│ ├── user_profile.py # 用户画像
│ ├── rfm_analysis.py # RFM分析
│ └── recommendation.py # 推荐系统
└── output/ # 输出结果
数据生成脚本
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import random
# 设置随机种子
np.random.seed(42)
random.seed(42)
# 生成用户行为数据
def generate_user_behavior(n_records=100000):
"""生成用户行为日志"""
user_ids = np.random.randint(1000, 1100, n_records) # 100个用户
product_ids = np.random.randint(10000, 10500, n_records) # 500个商品
# 行为类型:浏览、收藏、加购、购买
behaviors = np.random.choice(
['view', 'fav', 'cart', 'buy'],
n_records,
p=[0.7, 0.1, 0.15, 0.05]
)
# 生成时间戳(最近30天)
base_time = datetime(2024, 1, 1)
timestamps = [
base_time + timedelta(
days=random.randint(0, 29),
hours=random.randint(0, 23),
minutes=random.randint(0, 59)
)
for _ in range(n_records)
]
# 生成金额(购买行为才有)
amounts = [
round(random.uniform(10, 1000), 2) if b == 'buy' else 0
for b in behaviors
]
df = pd.DataFrame({
'user_id': user_ids,
'product_id': product_ids,
'behavior': behaviors,
'timestamp': timestamps,
'amount': amounts
})
return df
# 生成用户基本信息
def generate_user_info():
"""生成用户基本信息"""
user_ids = range(1000, 1100)
ages = np.random.randint(18, 60, 100)
genders = np.random.choice(['M', 'F'], 100)
cities = np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], 100)
register_dates = [
datetime(2023, 1, 1) + timedelta(days=random.randint(0, 365))
for _ in range(100)
]
df = pd.DataFrame({
'user_id': user_ids,
'age': ages,
'gender': genders,
'city': cities,
'register_date': register_dates
})
return df
# 生成商品信息
def generate_product_info():
"""生成商品信息"""
product_ids = range(10000, 10500)
categories = np.random.choice(
['电子产品', '服装', '食品', '家居', '图书'],
500
)
prices = np.random.uniform(10, 1000, 500).round(2)
brands = np.random.choice(['品牌A', '品牌B', '品牌C', '品牌D', '品牌E'], 500)
df = pd.DataFrame({
'product_id': product_ids,
'category': categories,
'price': prices,
'brand': brands
})
return df
# 保存数据
if __name__ == "__main__":
behavior_df = generate_user_behavior(100000)
user_df = generate_user_info()
product_df = generate_product_info()
behavior_df.to_csv("data/user_behavior.csv", index=False)
user_df.to_csv("data/user_info.csv", index=False)
product_df.to_csv("data/product_info.csv", index=False)
print("数据生成完成!")
6.2 数据清洗与ETL
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
from pyspark.sql.types import *
# 初始化Spark
spark = SparkSession.builder \
.appName("电商用户行为分析") \
.config("spark.sql.adaptive.enabled", "true") \
.getOrCreate()
# 读取数据
behavior_df = spark.read.csv("data/user_behavior.csv", header=True, inferSchema=True)
user_df = spark.read.csv("data/user_info.csv", header=True, inferSchema=True)
product_df = spark.read.csv("data/product_info.csv", header=True, inferSchema=True)
print("原始数据统计:")
print(f"行为数据: {behavior_df.count()} 条")
print(f"用户数据: {user_df.count()} 条")
print(f"商品数据: {product_df.count()} 条")
# 数据清洗
class DataCleaner:
def __init__(self, df):
self.df = df
self.cleaning_log = []
def remove_duplicates(self):
"""去除重复数据"""
before_count = self.df.count()
self.df = self.df.dropDuplicates()
after_count = self.df.count()
self.cleaning_log.append(f"去除重复: {before_count - after_count} 条")
return self
def handle_missing_values(self):
"""处理缺失值"""
# 查看缺失值统计
missing_stats = []
for col_name in self.df.columns:
null_count = self.df.filter(F.col(col_name).isNull()).count()
if null_count > 0:
missing_stats.append(f"{col_name}: {null_count}")
if missing_stats:
self.cleaning_log.append(f"缺失值统计: {', '.join(missing_stats)}")
# 删除包含缺失值的行
self.df = self.df.dropna()
else:
self.cleaning_log.append("无缺失值")
return self
def filter_invalid_data(self):
"""过滤无效数据"""
before_count = self.df.count()
# 过滤金额为负的记录(购买行为)
self.df = self.df.filter(
~((F.col("behavior") == "buy") & (F.col("amount") <= 0))
)
after_count = self.df.count()
self.cleaning_log.append(f"过滤无效数据: {before_count - after_count} 条")
return self
def standardize_timestamp(self):
"""标准化时间戳"""
self.df = self.df.withColumn(
"timestamp",
F.to_timestamp(F.col("timestamp"))
).withColumn(
"date",
F.to_date(F.col("timestamp"))
).withColumn(
"hour",
F.hour(F.col("timestamp"))
)
self.cleaning_log.append("时间戳标准化完成")
return self
def get_clean_data(self):
return self.df
def get_log(self):
return self.cleaning_log
# 执行数据清洗
cleaner = DataCleaner(behavior_df)
cleaned_behavior = cleaner.remove_duplicates() \
.handle_missing_values() \
.filter_invalid_data() \
.standardize_timestamp() \
.get_clean_data()
print("\n数据清洗日志:")
for log in cleaner.get_log():
print(f" - {log}")
print(f"\n清洗后数据: {cleaned_behavior.count()} 条")
6.3 用户画像构建
class UserProfileBuilder:
def __init__(self, behavior_df, user_df, product_df):
self.behavior_df = behavior_df
self.user_df = user_df
self.product_df = product_df
def build_behavior_features(self):
"""构建行为特征"""
# 统计各类行为次数
behavior_pivot = self.behavior_df.groupBy("user_id").pivot("behavior").count().fillna(0)
behavior_pivot = behavior_pivot.select(
"user_id",
F.col("view").alias("view_count"),
F.col("fav").alias("fav_count"),
F.col("cart").alias("cart_count"),
F.col("buy").alias("buy_count")
)
# 计算转化率
behavior_features = behavior_pivot.withColumn(
"fav_rate", F.col("fav_count") / F.col("view_count")
).withColumn(
"cart_rate", F.col("cart_count") / F.col("view_count")
).withColumn(
"buy_rate", F.col("buy_count") / F.col("view_count")
).fillna(0)
return behavior_features
def build_consumption_features(self):
"""构建消费特征"""
consumption = self.behavior_df.filter(F.col("behavior") == "buy") \
.groupBy("user_id") \
.agg(
F.sum("amount").alias("total_amount"),
F.avg("amount").alias("avg_amount"),
F.count("*").alias("order_count"),
F.max("amount").alias("max_amount"),
F.min("amount").alias("min_amount")
).fillna(0)
return consumption
def build_preference_features(self):
"""构建偏好特征"""
# 关联商品信息
behavior_with_product = self.behavior_df.join(
self.product_df,
"product_id",
"left"
)
# 统计各类目偏好
category_pref = behavior_with_product.groupBy("user_id", "category") \
.count() \
.groupBy("user_id") \
.pivot("category") \
.sum("count") \
.fillna(0)
# 找出最偏好的类目
category_cols = [c for c in category_pref.columns if c != "user_id"]
category_pref = category_pref.withColumn(
"favorite_category",
F.greatest(*category_cols)
)
return category_pref
def build_activity_features(self):
"""构建活跃度特征"""
activity = self.behavior_df.groupBy("user_id").agg(
F.countDistinct("date").alias("active_days"),
F.min("date").alias("first_active"),
F.max("date").alias("last_active"),
F.count("*").alias("total_actions")
)
# 计算活跃天数占比(假设数据跨度30天)
activity = activity.withColumn(
"activity_rate", F.col("active_days") / 30
)
return activity
def build_full_profile(self):
"""构建完整用户画像"""
behavior_features = self.build_behavior_features()
consumption_features = self.build_consumption_features()
preference_features = self.build_preference_features()
activity_features = self.build_activity_features()
# 合并所有特征
profile = self.user_df \
.join(behavior_features, "user_id", "left") \
.join(consumption_features, "user_id", "left") \
.join(preference_features, "user_id", "left") \
.join(activity_features, "user_id", "left") \
.fillna(0)
return profile
# 构建用户画像
profile_builder = UserProfileBuilder(cleaned_behavior, user_df, product_df)
user_profile = profile_builder.build_full_profile()
print("用户画像字段:")
for col in user_profile.columns:
print(f" - {col}")
user_profile.show(5)
6.4 RFM模型分析
from pyspark.sql import Window
class RFMAnalysis:
def __init__(self, behavior_df, reference_date=None):
self.behavior_df = behavior_df
self.reference_date = reference_date or F.current_date()
def calculate_rfm(self):
"""计算RFM指标"""
# 只考虑购买行为
purchase_df = self.behavior_df.filter(F.col("behavior") == "buy")
# R: 最近一次购买距今天数
recency_df = purchase_df.groupBy("user_id").agg(
F.max("date").alias("last_purchase_date")
).withColumn(
"recency",
F.datediff(self.reference_date, F.col("last_purchase_date"))
)
# F: 购买频率
frequency_df = purchase_df.groupBy("user_id").count() \
.withColumnRenamed("count", "frequency")
# M: 购买金额
monetary_df = purchase_df.groupBy("user_id").agg(
F.sum("amount").alias("monetary")
)
# 合并RFM
rfm = recency_df.join(frequency_df, "user_id", "outer") \
.join(monetary_df, "user_id", "outer") \
.fillna(0)
return rfm
def score_rfm(self, rfm_df):
"""RFM评分(1-5分)"""
# 使用分位数进行评分
rfm_stats = rfm_df.select(
F.percentile_approx("recency", [0.2, 0.4, 0.6, 0.8]).alias("r_percentiles"),
F.percentile_approx("frequency", [0.2, 0.4, 0.6, 0.8]).alias("f_percentiles"),
F.percentile_approx("monetary", [0.2, 0.4, 0.6, 0.8]).alias("m_percentiles")
).collect()[0]
r_thresholds = rfm_stats["r_percentiles"]
f_thresholds = rfm_stats["f_percentiles"]
m_thresholds = rfm_stats["m_percentiles"]
# R评分(越小越好,所以反向)
rfm_scored = rfm_df.withColumn(
"r_score",
F.when(F.col("recency") <= r_thresholds[0], 5)
.when(F.col("recency") <= r_thresholds[1], 4)
.when(F.col("recency") <= r_thresholds[2], 3)
.when(F.col("recency") <= r_thresholds[3], 2)
.otherwise(1)
)
# F评分(越大越好)
rfm_scored = rfm_scored.withColumn(
"f_score",
F.when(F.col("frequency") >= f_thresholds[3], 5)
.when(F.col("frequency") >= f_thresholds[2], 4)
.when(F.col("frequency") >= f_thresholds[1], 3)
.when(F.col("frequency") >= f_thresholds[0], 2)
.otherwise(1)
)
# M评分(越大越好)
rfm_scored = rfm_scored.withColumn(
"m_score",
F.when(F.col("monetary") >= m_thresholds[3], 5)
.when(F.col("monetary") >= m_thresholds[2], 4)
.when(F.col("monetary") >= m_thresholds[1], 3)
.when(F.col("monetary") >= m_thresholds[0], 2)
.otherwise(1)
)
# 综合RFM得分
rfm_scored = rfm_scored.withColumn(
"rfm_score",
F.concat(F.col("r_score"), F.col("f_score"), F.col("m_score"))
)
return rfm_scored
def segment_users(self, rfm_scored):
"""用户分群"""
segments = rfm_scored.withColumn(
"segment",
F.when((F.col("r_score") >= 4) & (F.col("f_score") >= 4) & (F.col("m_score") >= 4), "重要价值客户")
.when((F.col("r_score") >= 4) & (F.col("f_score") >= 4) & (F.col("m_score") < 4), "一般价值客户")
.when((F.col("r_score") >= 4) & (F.col("f_score") < 4) & (F.col("m_score") >= 4), "重要发展客户")
.when((F.col("r_score") >= 4) & (F.col("f_score") < 4) & (F.col("m_score") < 4), "新客户")
.when((F.col("r_score") < 4) & (F.col("f_score") >= 4) & (F.col("m_score") >= 4), "重要保持客户")
.when((F.col("r_score") < 4) & (F.col("f_score") >= 4) & (F.col("m_score") < 4), "一般保持客户")
.when((F.col("r_score") < 4) & (F.col("f_score") < 4) & (F.col("m_score") >= 4), "重要挽留客户")
.otherwise("流失客户")
)
return segments
# 执行RFM分析
rfm_analyzer = RFMAnalysis(cleaned_behavior)
rfm = rfm_analyzer.calculate_rfm()
rfm_scored = rfm_analyzer.score_rfm(rfm)
rfm_segments = rfm_analyzer.segment_users(rfm_scored)
print("RFM分析结果:")
rfm_segments.select("user_id", "recency", "frequency", "monetary",
"r_score", "f_score", "m_score", "rfm_score", "segment").show(10)
# 分群统计
segment_stats = rfm_segments.groupBy("segment").agg(
F.count("*").alias("user_count"),
F.avg("monetary").alias("avg_monetary"),
F.avg("frequency").alias("avg_frequency")
).orderBy(F.desc("user_count"))
print("\n用户分群统计:")
segment_stats.show()
6.5 协同过滤推荐
from pyspark.ml.feature import StringIndexer
from pyspark.ml.recommendation import ALS
from pyspark.ml.evaluation import RegressionEvaluator
class RecommendationEngine:
def __init__(self, behavior_df):
self.behavior_df = behavior_df
self.model = None
def prepare_data(self):
"""准备评分数据"""
# 将行为转换为评分权重
behavior_weights = {
'view': 1,
'fav': 3,
'cart': 5,
'buy': 10
}
# 计算用户对商品的评分
ratings = self.behavior_df.groupBy("user_id", "product_id").agg(
F.sum(
F.when(F.col("behavior") == "view", behavior_weights['view'])
.when(F.col("behavior") == "fav", behavior_weights['fav'])
.when(F.col("behavior") == "cart", behavior_weights['cart'])
.when(F.col("behavior") == "buy", behavior_weights['buy'])
).alias("rating")
).filter(F.col("rating") > 0)
# 索引化用户ID和商品ID
user_indexer = StringIndexer(inputCol="user_id", outputCol="user_idx")
product_indexer = StringIndexer(inputCol="product_id", outputCol="product_idx")
ratings_indexed = user_indexer.fit(ratings).transform(ratings)
ratings_indexed = product_indexer.fit(ratings_indexed).transform(ratings_indexed)
return ratings_indexed.select(
F.col("user_idx").cast("int"),
F.col("product_idx").cast("int"),
F.col("rating").cast("float")
)
def train_model(self, ratings_df):
"""训练ALS模型"""
# 划分训练集和测试集
train, test = ratings_df.randomSplit([0.8, 0.2], seed=42)
# 创建ALS模型
als = ALS(
maxIter=10,
regParam=0.01,
userCol="user_idx",
itemCol="product_idx",
ratingCol="rating",
coldStartStrategy="drop"
)
# 训练模型
self.model = als.fit(train)
# 评估模型
predictions = self.model.transform(test)
evaluator = RegressionEvaluator(
metricName="rmse",
labelCol="rating",
predictionCol="prediction"
)
rmse = evaluator.evaluate(predictions)
print(f"模型RMSE: {rmse}")
return self.model
def get_recommendations(self, n_recommendations=10):
"""为所有用户生成推荐"""
if self.model is None:
raise ValueError("请先训练模型")
# 为每个用户推荐商品
user_recs = self.model.recommendForAllUsers(n_recommendations)
return user_recs
def get_similar_products(self, product_idx, n_similar=10):
"""获取相似商品"""
if self.model is None:
raise ValueError("请先训练模型")
# 获取商品特征向量
product_factors = self.model.itemFactors
# 计算余弦相似度
target_vector = product_factors.filter(F.col("id") == product_idx).collect()[0]["features"]
# 广播目标向量
broadcast_vector = spark.sparkContext.broadcast(target_vector)
def cosine_similarity(features):
import numpy as np
v1 = np.array(broadcast_vector.value)
v2 = np.array(features)
return float(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)))
from pyspark.sql.types import FloatType
similarity_udf = F.udf(cosine_similarity, FloatType())
similar_products = product_factors \
.filter(F.col("id") != product_idx) \
.withColumn("similarity", similarity_udf(F.col("features"))) \
.orderBy(F.desc("similarity")) \
.limit(n_similar)
return similar_products
# 训练推荐模型
rec_engine = RecommendationEngine(cleaned_behavior)
ratings = rec_engine.prepare_data()
rec_engine.train_model(ratings)
# 生成推荐
recommendations = rec_engine.get_recommendations(n_recommendations=5)
print("\n用户推荐结果(前5个用户):")
recommendations.show(5, truncate=False)
6.6 结果输出与可视化
# 保存分析结果
def save_results():
"""保存所有分析结果"""
# 用户画像
user_profile.write.parquet("output/user_profile.parquet")
# RFM分群结果
rfm_segments.write.parquet("output/rfm_segments.parquet")
# 推荐结果
recommendations.write.parquet("output/recommendations.parquet")
# 导出为CSV(小数据量)
segment_stats.toPandas().to_csv("output/segment_stats.csv", index=False)
print("结果已保存到output目录")
# 生成分析报告
def generate_report():
"""生成分析报告"""
report = []
report.append("=" * 50)
report.append("电商用户行为分析报告")
report.append("=" * 50)
# 数据概览
report.append(f"\n【数据概览】")
report.append(f" 总用户数: {user_df.count()}")
report.append(f" 总商品数: {product_df.count()}")
report.append(f" 总行为记录: {cleaned_behavior.count()}")
# 行为统计
behavior_stats = cleaned_behavior.groupBy("behavior").count().collect()
report.append(f"\n【行为分布】")
for row in behavior_stats:
report.append(f" {row['behavior']}: {row['count']}")
# RFM分群
report.append(f"\n【用户分群】")
for row in segment_stats.collect():
report.append(f" {row['segment']}: {row['user_count']}人")
report.append("\n" + "=" * 50)
return "\n".join(report)
# 打印报告
print(generate_report())
# 关闭Spark
spark.stop()
7. 本章小结
本章介绍了PySpark大数据处理的基础知识:
- Spark架构:理解Driver、Executor和Cluster Manager的关系
- RDD编程:掌握转换操作和行动操作的区别与使用
- DataFrame API:使用结构化数据进行高效处理
- Spark SQL:通过SQL语句查询数据
- MLlib:使用分布式机器学习算法
大数据处理的核心思想是分而治之——将大任务拆分成小任务,在多台机器上并行执行,最后汇总结果。PySpark让Python开发者能够轻松驾驭大数据处理。
本章内容编写中
本章正在持续完善中,将涵盖以下内容:
- PySpark基础概念与环境配置详解
- RDD高级操作与性能优化
- DataFrame API深入实践
- Spark SQL复杂查询案例
- 分布式机器学习实战项目
- Spark Streaming流处理入门
- 生产环境部署与调优技巧
敬请期待完整内容更新…
如果你觉得本章内容对你有帮助,欢迎点赞、收藏、评论交流!
更多推荐


所有评论(0)