pd.concat() 千万行爆内存因默认全载入内存、重索引、列对齐及dtype隐式升格;应统一dtypes、设ignore_index=true和copy=false、分批拼接、启用dtype_backend="pyarrow",仍oom则换polars或dask。

为什么 pd.concat() 在千万行时会爆内存?
因为默认情况下 pd.concat() 会先将所有输入 DataFrame 全部载入内存,再统一重索引、对齐列、合并数据——哪怕你只拼接两个 500 万行的 DataFrame,中间可能临时占用 3–4 倍原始内存。尤其当列类型不一致(比如某列在 A 中是 object,在 B 中是 string 或 category),Pandas 2.0 会强制升格为最宽泛类型,进一步放大内存开销。
实操建议:
- 提前统一各 DataFrame 的列名、列顺序、dtypes,用
astype()显式转换,避免隐式推断 - 禁用自动索引重置:
ignore_index=True是安全的,但ignore_index=False(默认)会尝试保留原索引,触发冗余排序和去重逻辑,关掉它 - 若拼接后不依赖原始索引,强制加
ignore_index=True,并设copy=False(Pandas 2.0+ 支持)减少副本
用 pd.concat() 分批 + dtype_backend="pyarrow" 能省多少内存?
Pandas 2.0 引入了 dtype_backend="pyarrow" 模式,对字符串、数值列有显著压缩率和零拷贝优势,但不是所有操作都兼容——concat 是少数原生支持它的核心操作之一。
实操建议:
- 读取阶段就启用:例如
pd.read_csv(..., dtype_backend="pyarrow"),后续所有concat都继承该 backend - 分批拼接比一次性拼更稳:把 10 个 100 万行的 DataFrame 分成每 3 个一组
concat,再合起来,能规避单次大对象分配失败 - 注意 PyArrow backend 下
None和pd.NA行为一致,但np.nan可能被转为 null,需提前清洗
当 concat 仍卡死,该换 dask.dataframe 还是 polars?
如果已确认 dtype 统一、分批、PyArrow 启用后仍 OOM 或耗时超 10 分钟,说明单机 Pandas 架构已达瓶颈。此时别硬调参数,直接换引擎。
实操建议:
-
dask.dataframe适合你已有大量 Pandas 代码、想最小改动迁移:用dask.delayed包装读取 +dask.dataframe.from_delayed构建,.compute()前不会真执行;但 shuffle 类操作(如按列 merge)仍慢 -
polars更适合纯拼接场景:其pl.concat([df1, df2, ...], how="vertical")默认零拷贝、列式内存布局,千万行拼接常在秒级;缺点是 API 不兼容 Pandas,且需用pl.read_csv()等原生读取,不能直接喂 Pandas DataFrame - 别混用:不要用 Pandas 读完再转 Polars,那一步
.to_pandas()就可能 OOM;应从文件直读
拼接后立刻 to_parquet 而不是 to_csv 的真实原因
千万行 DataFrame 拼完后,如果马上 to_csv,Pandas 会逐行序列化为文本,触发大量字符串分配和 IO 缓冲,CPU 和磁盘都容易打满;而 to_parquet 利用列式压缩(默认 snappy)、字典编码、null bitmap,体积常只有 CSV 的 1/5–1/3,写入也更快。
实操建议:
- 写 Parquet 时加
use_dictionary=True(默认开启)和compression="zstd"(比 snappy 压缩率更高,CPU 开销略增) - 避免写单一大文件:用
partition_cols=["date"]或chunk_size=500_000分块,方便后续用 Dask/Polars 并行读 - 别等拼完再写:边 concat 边写临时 Parquet 片段,最后用
pyarrow.dataset.dataset()统一读取,彻底绕过全量内存驻留
df.dtypes 并强制统一。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











