1. 什么是 PySpark DataFrame?
PySpark DataFrame 是一种分布式的、带结构信息的数据集。
你可以把它理解成:
一张运行在 Spark 上的"超级数据表"。
它和数据库中的表很像,也和 Pandas DataFrame 很像,因为它同样有:
- 列名
- 数据类型
- 行数据
- 表结构
但它和 Pandas 最大的区别在于:
PySpark DataFrame 是分布式的,数据并不一定都放在一台机器里,而是可以由 Spark 调度到多个节点一起计算。
例如,一个简单的 DataFrame 看起来像这样:
text
+---+---+-------+----------+-------------------+
| a| b| c| d| e|
+---+---+-------+----------+-------------------+
| 1|2.0|string1|2000-01-01|2000-01-01 12:00:00|
| 2|3.0|string2|2000-02-01|2000-01-02 12:00:00|
| 3|4.0|string3|2000-03-01|2000-01-03 12:00:00|
+---+---+-------+----------+-------------------+
这种结构化形式,让数据处理比直接操作原始 RDD 更直观,也更贴近业务开发。
2. DataFrame 为什么这么重要?
在 Spark 生态里,DataFrame 之所以成为主流,不是偶然的。
它至少有 3 个非常明显的优势。
2.1 结构化更强
DataFrame 自带 schema,知道每一列叫什么、是什么类型,这让程序更可读、更容易维护。
2.2 性能更好
DataFrame 底层会交给 Spark 的优化器去生成执行计划,很多操作会自动优化,不需要开发者手工做太多底层控制。
2.3 更符合日常开发习惯
它既可以像表一样查询,也可以像链式 API 一样写代码,还能无缝切换到 SQL,所以非常适合日常数据开发。
简单说:
RDD 更底层,DataFrame 更实用。
3. DataFrame 最核心的特点:惰性执行
这是 PySpark DataFrame 最重要的概念之一。
所谓惰性执行(Lazy Evaluation),意思是:
当你对 DataFrame 做转换操作时,Spark 不会立刻执行,而是先把这些操作记录下来,等真正需要结果时再统一计算。
比如这句代码:
python
df.select("a")
它不会马上计算,只是告诉 Spark:
后面我要取 a 这一列,你先记住。
而下面这些操作,才会真正触发计算:
show()collect()count()take()toPandas()
为什么 Spark 要这么设计?
因为这样可以:
- 合并多个操作
- 减少中间结果
- 自动优化执行计划
- 提高整体性能
你可以把它理解成:
平时先记账,最后统一结算。
这也是为什么有时候你写了很多行 DataFrame 代码,控制台却没动静,因为 Spark 还没开始真正执行。
4. PySpark 的入口:SparkSession
几乎所有 PySpark 程序,都是从 SparkSession 开始的。
python
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
这个 spark 可以理解成整个 PySpark 程序的总入口。
后面很多操作都依赖它:
- 创建 DataFrame
- 读取文件
- 执行 SQL
- 修改配置
- 注册视图
如果你是用 pyspark 命令进入交互式环境,系统一般会自动帮你创建好 spark 变量。
所以很多示例代码都可以直接写:
python
df = spark.createDataFrame(...)
5. DataFrame 怎么创建?
PySpark DataFrame 的创建方式很多,常见包括:
- 从列表创建
- 从元组创建
- 从字典创建
- 从
Row创建 - 从 Pandas DataFrame 创建
- 从 RDD 创建
下面看几种最常见的写法。
5.1 从 Row 列表创建
python
from datetime import datetime, date
from pyspark.sql import Row
df = spark.createDataFrame([
Row(a=1, b=2., c='string1', d=date(2000, 1, 1), e=datetime(2000, 1, 1, 12, 0)),
Row(a=2, b=3., c='string2', d=date(2000, 2, 1), e=datetime(2000, 1, 2, 12, 0)),
Row(a=3, b=4., c='string3', d=date(2000, 3, 1), e=datetime(2000, 1, 3, 12, 0))
])
这种写法字段名明确,可读性很好,适合教程和小规模示例。
5.2 显式指定 schema
python
from datetime import datetime, date
df = spark.createDataFrame([
(1, 2., 'string1', date(2000, 1, 1), datetime(2000, 1, 1, 12, 0)),
(2, 3., 'string2', date(2000, 2, 1), datetime(2000, 1, 2, 12, 0)),
(3, 4., 'string3', date(2000, 3, 1), datetime(2000, 1, 3, 12, 0))
], schema='a long, b double, c string, d date, e timestamp')
这种方式更推荐在工程场景使用,因为类型完全由你控制,避免自动推断出错。
5.3 从 Pandas DataFrame 创建
python
import pandas as pd
from datetime import datetime, date
pandas_df = pd.DataFrame({
'a': [1, 2, 3],
'b': [2., 3., 4.],
'c': ['string1', 'string2', 'string3'],
'd': [date(2000, 1, 1), date(2000, 2, 1), date(2000, 3, 1)],
'e': [datetime(2000, 1, 1, 12, 0), datetime(2000, 1, 2, 12, 0), datetime(2000, 1, 3, 12, 0)]
})
df = spark.createDataFrame(pandas_df)
这对于熟悉 Pandas 的同学非常友好,适合本地测试和快速验证。
5.4 自动推断 schema 的优缺点
如果不写 schema,PySpark 会自动推断字段类型。
优点是省事。
缺点是:
- 小样本推断可能不准
- 空值可能影响类型判断
- 生产环境可控性较差
所以建议是:
学习时可省略,实战中尽量显式指定 schema。
6. 如何查看 DataFrame 数据?
创建完 DataFrame 后,第一件事通常就是先看看数据。
6.1 使用 show()
python
df.show()
默认显示前 20 行。
只看 1 行可以这样写:
python
df.show(1)
6.2 垂直显示
当字段很多、单行特别长时,可以改成垂直显示:
python
df.show(1, vertical=True)
输出会更清晰:
text
-RECORD 0------------------
a | 1
b | 2.0
c | string1
d | 2000-01-01
e | 2000-01-01 12:00:00
6.3 查看列名
python
df.columns
6.4 查看 schema
python
df.printSchema()
输出示例:
text
root
|-- a: long (nullable = true)
|-- b: double (nullable = true)
|-- c: string (nullable = true)
|-- d: date (nullable = true)
|-- e: timestamp (nullable = true)
6.5 查看摘要统计
python
df.select("a", "b", "c").describe().show()
这一步很适合快速看字段的:
- count
- mean
- stddev
- min
- max
7. 为什么 collect() 要慎用?
这是一个非常典型的新手坑。
7.1 collect() 的作用
python
df.collect()
它会把整个 DataFrame 的数据全部拉回 Driver 端,变成 Python 本地对象。
看起来很方便,但问题也非常明显:
数据量一大,Driver 内存很可能直接爆掉。
因为它的本质就是:
- 从各个 Executor 收集数据
- 拉回 Driver
- 放进本地 Python 进程里
7.2 更安全的替代方式
如果你只是想取几条数据,建议用:
python
df.take(1)
或者:
python
df.tail(1)
它们只取少量数据,更安全。
7.3 toPandas() 也要小心
python
df.toPandas()
这同样会把全部数据拉回 Driver,再转成 Pandas DataFrame。
所以要记住一句话:
collect() 和 toPandas() 在小数据时很好用,在大数据时很危险。
8. DataFrame 中的列到底是什么?
这是理解 PySpark 的一个关键点。
很多初学者以为:
python
df.a
拿到的是 a 列的数据。
其实不是。
它返回的是一个 Column 对象,也就是"列的表达式"。
python
df.a
# Column<'a'>
这意味着什么?
意味着在 PySpark 里,很多列操作本质上都不是在立刻算值,而是在构造表达式。
例如:
python
from pyspark.sql.functions import upper
upper(df.c)
df.c.isNull()
这些操作返回的仍然是 Column。
你可以把它理解成:
Column 不是值本身,而是后续计算的描述。
9. 如何选择列、添加列、过滤数据?
这部分是日常开发的高频操作。
9.1 选择列
python
df.select(df.c).show()
9.2 添加新列
python
from pyspark.sql.functions import upper
df.withColumn("upper_c", upper(df.c)).show()
这会新增一列 upper_c,值是 c 列的大写版本。
withColumn() 特别常用,常见场景包括:
- 新增派生字段
- 类型转换
- 数据清洗
- 字符串、时间处理
9.3 过滤行
python
df.filter(df.a == 1).show()
这和 SQL 里的 WHERE 非常像。
10. 如何在 PySpark 中应用 Python 函数?
PySpark 支持多种让你使用 Python 原生逻辑的方式,其中比较常见的是:
pandas_udfmapInPandasapplyInPandas
10.1 pandas_udf
python
import pandas as pd
from pyspark.sql.functions import pandas_udf
@pandas_udf("long")
def pandas_plus_one(series: pd.Series) -> pd.Series:
return series + 1
df.select(pandas_plus_one(df.a)).show()
这个函数适合列级处理。
你可以把它理解为:
Spark 把某一列分批交给 Pandas Series 处理,再把结果拼回来。
10.2 mapInPandas
python
def pandas_filter_func(iterator):
for pandas_df in iterator:
yield pandas_df[pandas_df.a == 1]
df.mapInPandas(pandas_filter_func, schema=df.schema).show()
mapInPandas() 更灵活,因为它处理的是 Pandas DataFrame,而不是单列 Series。
适合:
- 多列联合处理
- 输出行数不固定
- 复杂转换逻辑
11. DataFrame 如何分组聚合?
分组聚合是数据分析的核心场景之一。
先构造一份示例数据:
python
df = spark.createDataFrame([
['red', 'banana', 1, 10],
['blue', 'banana', 2, 20],
['red', 'carrot', 3, 30],
['blue', 'grape', 4, 40],
['red', 'carrot', 5, 50],
['black', 'carrot', 6, 60],
['red', 'banana', 7, 70],
['red', 'grape', 8, 80]
], schema=['color', 'fruit', 'v1', 'v2'])
11.1 groupBy 聚合
python
df.groupby("color").avg().show()
输出类似:
text
+-----+-------+-------+
|color|avg(v1)|avg(v2)|
+-----+-------+-------+
| red| 4.8| 48.0|
|black| 6.0| 60.0|
| blue| 3.0| 30.0|
+-----+-------+-------+
这就是典型的 split-apply-combine 思路。
11.2 applyInPandas 对每组做处理
python
def plus_mean(pandas_df):
return pandas_df.assign(v1=pandas_df.v1 - pandas_df.v1.mean())
df.groupby("color").applyInPandas(plus_mean, schema=df.schema).show()
这表示每个颜色分组内部,把 v1 减去该组平均值。
这种方式在特征工程、归一化、复杂统计处理里很有用。
12. DataFrame 如何读写数据?
Spark 的一个核心用途,就是把数据读进来、处理完、再写出去。
常见格式有:
- CSV
- Parquet
- ORC
12.1 CSV
写出:
python
df.write.csv("foo.csv", header=True)
读入:
python
spark.read.csv("foo.csv", header=True).show()
CSV 的优点是:
- 通用
- 可读
- 易交换
缺点是:
- 存储效率低
- 读取性能一般
- 类型信息不够强
12.2 Parquet
写出:
python
df.write.parquet("bar.parquet")
读入:
python
spark.read.parquet("bar.parquet").show()
Parquet 是 Spark 生态里非常常见的列式存储格式,优点包括:
- 压缩率高
- 读取快
- 更适合分析型查询
12.3 ORC
写出:
python
df.write.orc("zoo.orc")
读入:
python
spark.read.orc("zoo.orc").show()
ORC 和 Parquet 类似,也是一种高效列式存储格式。
13. DataFrame 与 SQL 为什么能无缝切换?
这是 Spark 非常强的一点。
因为 DataFrame 和 Spark SQL 共享同一套执行引擎,所以你既可以用 API,也可以写 SQL,而且二者可以自由切换。
13.1 注册临时视图
python
df.createOrReplaceTempView("tableA")
13.2 执行 SQL
python
spark.sql("SELECT count(*) FROM tableA").show()
所以你完全可以:
- 先用 DataFrame 做清洗
- 再用 SQL 做聚合
- 或者反过来
这在复杂业务场景里特别实用。
13.3 UDF 也能注册到 SQL 中
python
import pandas as pd
from pyspark.sql.functions import pandas_udf
@pandas_udf("integer")
def add_one(s: pd.Series) -> pd.Series:
return s + 1
spark.udf.register("add_one", add_one)
spark.sql("SELECT add_one(v1) FROM tableA").show()
这说明 Spark 里的 API、SQL、UDF 并不是三套割裂体系,而是彼此互通的。
13.4 SQL 表达式也能直接给 DataFrame 用
python
from pyspark.sql.functions import expr
df.selectExpr("add_one(v1)").show()
df.select(expr("count(*)") > 0).show()
这意味着你写 PySpark 时,可以非常灵活地混合使用 API 和 SQL 风格。
14. 初学者必须记住的 6 个关键点
学完这篇之后,建议你把下面 6 句话记住。
14.1 DataFrame 是结构化分布式数据集
它像数据库表,也像 Pandas,但底层是 Spark 的分布式计算能力。
14.2 DataFrame 是惰性执行的
很多转换操作不会立刻计算,只有 Action 才会真正触发执行。
14.3 SparkSession 是入口
没有 spark,后面的 DataFrame 操作几乎无从开始。
14.4 collect() 和 toPandas() 要慎用
它们会把数据拉回 Driver,大数据场景很容易 OOM。
14.5 Column 不是值,而是表达式
df.a 返回的是列对象,而不是某一列的实际值集合。
14.6 DataFrame 和 SQL 是互通的
它们共用执行引擎,可以自由切换、混合使用。
15. 一个完整的入门示例
下面给你一个很适合新手练手的完整示例:
python
from pyspark.sql import SparkSession
from pyspark.sql.functions import upper
spark = SparkSession.builder.appName("DataFrameQuickstart").getOrCreate()
data = [
(1, "alice", 18),
(2, "bob", 20),
(3, "cindy", 22)
]
df = spark.createDataFrame(data, ["id", "name", "age"])
print("=== 原始数据 ===")
df.show()
print("=== Schema ===")
df.printSchema()
print("=== 选择 name 列 ===")
df.select("name").show()
print("=== 新增大写列 ===")
df.withColumn("name_upper", upper(df.name)).show()
print("=== 过滤 age >= 20 ===")
df.filter(df.age >= 20).show()
print("=== 注册临时视图并执行 SQL ===")
df.createOrReplaceTempView("users")
spark.sql("SELECT id, name FROM users WHERE age >= 20").show()
spark.stop()
这个例子串起了最常见的一套流程:
- 创建 DataFrame
- 查看数据
- 查看 schema
- 选择列
- 新增列
- 过滤数据
- 注册视图
- 执行 SQL
新手把这段代码跑通,基本就摸到 PySpark DataFrame 的门了。