📅 2026-05-26 👤 王芳 🏷️ Pandas 2.0 · PyArrow · 数据工程

Python 数据处理:Pandas 2.0 与 PyArrow 集成深度解析

深入解析 Pandas 2.0 的 PyArrow 后端实现、Copy-on-write 机制、Nullable 类型系统、字符串性能优化,以及从旧版本迁移的最佳实践。

1. PyArrow 后端

1.1 从 NumPy 到 Arrow 的架构转变

Pandas 传统上基于 NumPy 的 ndarray 结构,每列必须是同质数据类型,且不支持缺失值(NaN 是 float 独有的 hack)。PyArrow 作为 Apache Arrow 的 Python 实现,提供了列式内存布局、原生 NULL 支持和跨语言互操作性。

Pandas 2.0 启用 PyArrow 后端
import pandas as pd

# Pandas 2.0+ 启用 Arrow -backed DataFrame
pd.set_option("dataframe.default_dtype", "pyarrow")

# 或在读龅时指定
df = pd.read_parquet("data.parquet", dtype_backend="pyarrow")

# 或使用 pyarrow-backed CSV 读取
df = pd.read_csv("data.csv", dtype_backend="pyarrow")

# 查看内存布局
df.memory_usage(deep=True)
# vs NumPy-backed: 内存占用通常减少 30-50%

1.2 Arrow 内存模型

Arrow 采用列式存储(Column-oriented),每列数据连续内存存放,配合 SIMD 指令可实现批量扫描的高效处理。相比行式存储,OLAP 查询性能提升显著。

Arrow vs NumPy 内存布局对比
# NumPy 行式存储 (每行连续)
# [row0: a, b, c, d] [row1: e, f, g, h] [row2: i, j, k, l]
# 访问一行高效,访问一列需要跳步访问

# Arrow 列式存储 (每列连续)
# [col0: a, e, i] [col1: b, f, j] [col2: c, g, k] [col3: d, h, l]
# 访问一列高效,支持 SIMD 批量处理

# Arrow 还包含 metadata(null bitmap, offset array)
# null_count: [0, 1, 0] 表示第2行有 NULL
# offsets: [0, 3, 6, 9] 表示变长字符串的边界

1.3 与 Parquet 的集成

Pandas 2.0 的 read_parquetto_parquet 原生集成 PyArrow,直接读写 Arrow IPC 格式,无需转换。

Pandas + Parquet 读写
# 读取 Parquet(自动使用 PyArrow)
df = pd.read_parquet("sales.parquet")
# df.dtypes 会显示 pyarrow 类型

# 写入 Parquet
df.to_parquet("output.parquet", engine="pyarrow", compression="zstd")

# 读取远程 Parquet (S3)
import pyarrow.parquet as pq
dataset = pq.ParquetDataset("s3://bucket/data/")
df = dataset.read().to_pandas()

# 分片读取大文件
import pyarrow.parquet as pq
pf = pq.ParquetFile("large.parquet")
for batch in pf.iter_batches(batch_size=100_000):
    process(batch.to_pandas())

2. Copy-on-write

2.1 机制原理

Copy-on-write(CoW)是 Pandas 2.0 的默认行为。当 DataFrame 被赋值给另一个变量时,不立即复制数据,而是共享底层 Arrow 数组。只有在写入操作(write)发生时,才触发真正的数据复制。这大幅减少了内存占用和操作开销。

Copy-on-write 行为演示
# Pandas 2.0 默认启用 CoW
df = pd.DataFrame({"a": [1, 2, 3]})
df_v2 = df  # 不复制,共享 Arrow 数组

# 写入操作触发复制
df_v2.iloc[0, 0] = 99  # 此时才复制

# 如果只是读取,不会复制
df_readonly = df
print(df_readonly.head())  # 无复制开销

# 强制复制(当确认需要独立副本时)
df_copy = df.copy()  # 或 df.copy()
# 使用浅拷贝
df_shallow = df.__copy__()

2.2 CoW 性能收益

操作 Pandas 1.x (非 CoW) Pandas 2.0 (CoW) 提升
变量赋值 O(n) 全量复制 O(1) 引用共享 100x+
链式操作 O(n*m) 多重复制 O(n) 单次复制 ~m 倍
内存峰值 2x DataFrame 大小 ~1x(延迟复制) 50%+
groupby + transform 多次中间复制 单次优化复制 2-3x

2.3 CoW 与操作链

CoW 的最大受益场景是链式操作——在 Pandas 1.x 中,df.filter().groupby().transform() 会产生多个中间副本,而 CoW 优化为只在最终写入时复制一次。

链式操作优化
# Pandas 2.0: 链式操作只在最终写入时复制
result = (
    df.query("category == 'electronics'")
    .groupby("region")
    .agg("revenue": "sum")
    .sort_values("revenue", ascending=False)
)

# 旧版本 Pandas 1.x 行为:
# step1 = df.query()     -> 复制 1
# step2 = step1.groupby() -> 复制 2
# step3 = step2.agg()    -> 复制 3
# result = step3.sort()  -> 复制 4
# 内存峰值 = 4x df size

# Pandas 2.0 CoW 行为:
# 所有步骤共享 Arrow 数组引用
# 仅在 sort_values 写入时复制一次
# 内存峰值 = ~1.5x df size
💡 关键洞察

CoW 并不意味着所有操作都不复制——只有当操作链最终产生一个视图(view)而非写入时,才能享受零复制优化。如果操作结果需要落盘或导出,仍然会发生复制。

3. Nullable 类型

3.1 Nullable 类型的演进

Pandas 传统上使用 np.nan 表示缺失值,但 NaN 只能用于 float 类型,整数列遇到缺失值会被强制转为 float。Pandas 2.0 通过 PyArrow 的 ExtensionType 实现了真正的 nullable 类型。

Nullable 类型 vs 传统类型
# 传统方式:整数 + NaN = float(类型丢失)
s = pd.Series([1, 2, 3, np.nan])
s.dtype  # dtype('float64') - 整数信息丢失

# Pandas 2.0 nullable: 保留整数类型
s_nullable = pd.Series([1, 2, 3, None], dtype="Int64")
s_nullable.dtype  # Int64(带大写 I,与 Python int 区分)

# 支持的 nullable 类型
pd.Series([1, 2, None], dtype="Int8")   # Int8/16/32/64
pd.Series([1.0, 2.0, None], dtype="Float64")  # Float32/64
pd.Series([True, False, None], dtype="boolean") # boolean(带小写)
pd.Series(["a", "b", None], dtype="string")      # string 类型

3.2 与 Arrow 类型映射

Arrow 类型与 Pandas nullable 类型有一一映射关系,这使得 PyArrow 后端可以无损地保留类型信息。

Arrow 类型映射表
# Arrow 类型 <-> Pandas dtype 映射
pyarrow.int8()   <-> pd.Int8Dtype()
pyarrow.int16()  <-> pd.Int16Dtype()
pyarrow.int32()  <-> pd.Int32Dtype()
pyarrow.int64()  <-> pd.Int64Dtype()
pyarrow.float()  <-> pd.Float32Dtype()
pyarrow.double() <-> pd.Float64Dtype()
pyarrow.bool_() <-> pd.BooleanDtype()
pyarrow.string() <-> pd.StringDtype()

# 验证类型一致性
import pyarrow as pa
arr = pa.array([1, 2, None], type=pa.int64())
s = pd.Series(arr.to_pandas())
print(s.dtype)  # Int64

3.3 字符串类型的性能差异

pd.StringDtype() 在 PyArrow 后端下使用 pyarrow.string(),内存占用比 object 类型减少 60%+,且避免了 Python 对象封套的 overhead。

字符串类型性能对比
# 内存占用对比 (1M 行字符串)
# object dtype: ~120 MB(Python object + pointer  overhead)
# StringDtype (PyArrow): ~45 MB(连续 Arrow 字符串)
# StringDtype (NumPy): ~80 MB(NumPy 字符串数组)

import sys
s_object = pd.Series(["hello"]*1_000_000, dtype="object")
s_string = pd.Series(["hello"]*1_000_000, dtype="string")

sys.getsizeof(s_object)    # ~80 MB
sys.getsizeof(s_string)    # ~8 MB(字符串数据在 Arrow 堆外)
s_string.memory_usage()    # ~45 MB(包含 metadata)

4. 字符串优化

4.1 StringDtype 实现机制

PyArrow-backed StringDtype 将字符串存储为 offset + data 的紧凑格式,避免了 Python object 的指针间接寻址。offset 数组记录每个字符串的起止位置,数据区域连续存放。

Arrow 字符串内部结构
# Arrow StringArray 内部结构
# offsets: [0, 5, 10, 16, 22, 28]
# data: "hellohelloworldpythopytho"
#         ^^^^^  ^^^^^ ^^^^^ ^^^^^ ^^^^^
#         str0   str1   str2   str3   str4

# null bitmap (如果有 NULL)
# nulls: [0, 1, 0, 0, 1] -> str1 和 str4 是 NULL

import pyarrow as pa
arr = pa.array(["hello", "world", "python"])
print(arr.type)        # string
print(arr.buffers())   # [null_bitmap, offset, data]

4.2 向量化字符串操作

PyArrow 提供了一批高效的向量化字符串函数,Pandas 2.0 通过 dt 访问器暴露这些操作。

向量化字符串操作
# PyArrow-backed 字符串操作(自动使用向量化实现)
df = pd.DataFrame({"email": ["alice@example.com", "bob@company.org"]})
df["domain"] = df["email"].str.split("@").str[1]
df["is_company"] = df["domain"].str.endswith(".org")

# 使用 .str accessor 的性能
# str.contains / str.replace / str.split 等操作
# 在 PyArrow 后端下使用 pyarrow.compute 引擎
# 性能比纯 Python 实现快 5-10x

# 直接使用 pyarrow.compute (极限优化)
import pyarrow.compute as pc
domains = pc.split_pattern(df["email"].array, "@")
# domains 是 Arrow ChunkedArray,直接操作,无需转换

4.3 正则表达式优化

Pandas 2.0 的字符串操作支持正则表达式,并通过 PyArrow 的正则引擎实现高效执行。相比 Python 的 re 模块,避免了每次操作都编译正则的开销。

正则表达式优化
# Pandas 2.0 支持正则表达式的向量化操作
s = pd.Series(["user123@example.com", "test@domain.org", "invalid"])

# 提取邮箱用户名部分
s.str.extract(r"^([^@]+)@")

# 判断是否是有效邮箱
s.str.match(r"^[a-zA-Z0-9_.]+@[a-zA-Z0-9]+\.[a-zA-Z]{2,}$")

# 批量替换
s.str.replace(r"[0-9]+", "#", regex=True)

# 使用 re2 正则引擎(可选)
# pip install google-re2 后自动启用,内存更安全

5. 迁移指南

5.1 从 Pandas 1.x 迁移步骤

✅ 推荐迁移步骤

  • 1. 升级到 Pandas 2.0+,安装 pyarrow
  • 2. 运行测试套件检查兼容性
  • 3. 启用 PyArrow 后端:pd.set_option("dataframe.default_dtype", "pyarrow")
  • 4. 替换 df.astype(object) 为 PyArrow 兼容方式
  • 5. 验证数值精度和类型一致性

❌ 常见兼容性问题

  • df.applymap → 改为 df.map
  • pd.api.types.is_integer_dtype → 检查 isinstance(dtype, pd.Int64Dtype)
  • 依赖 np.nan 的逻辑需改用 pd.NA
  • Categorical 类型行为有变化

5.2 关键 API 变更

旧 API (Pandas 1.x) 新 API (Pandas 2.0) 说明
df.applymap(fn) df.map(fn) 方法重命名
df.astype("category") df.astype("string") 推荐使用 string 而非 object
np.nan pd.NA Nullable 类型使用 pd.NA
pd.Int64Dtype() pd.Int64Dtype() 保持兼容,但行为更严格

5.3 性能验证

迁移后应进行性能基准测试,重点关注:内存占用、数据加载速度、字符串操作性能。

性能基准测试模板
import pandas as pd
import time
import tracemalloc

# 启用 PyArrow 后端
pd.set_option("dataframe.default_dtype", "pyarrow")

# 内存基准
tracemalloc.start()
df = pd.read_csv("large_file.csv", dtype_backend="pyarrow")
current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current/1024/1024:.1f}MB, Peak: {peak/1024/1024:.1f}MB")
tracemalloc.stop()

# 操作性能基准
start = time.perf_counter()
result = df.groupby("category").agg("value": "sum")
elapsed = time.perf_counter() - start
print(f"GroupBy: {elapsed*1000:.2f}ms")
⚠️ 注意事项

Pandas 2.0 的 PyArrow 后端仍在快速发展中,部分第三方库(如 matplotlib、seaborn)可能尚未完全兼容。在迁移生产代码前,务必在真实数据规模下进行完整的功能和性能验证。