pandas在千万级数据上变慢主因是内存设计缺陷:全量加载、隐式拷贝、单线程执行及中间结果膨胀;polars通过列式存储、延迟执行和类型优化可大幅提升性能。

为什么Pandas在千万级数据上会变慢
Pandas默认把整个DataFrame加载进内存,做计算时还常触发隐式拷贝(比如df[condition]、df.groupby().apply()),一旦数据量超过物理内存的60%,就会频繁触发系统swap,IO飙升,CPU利用率反而掉下去。这不是代码写得不好,是设计使然——Pandas面向的是“分析友好”,不是“规模友好”。
- 千万行×百列的CSV,用
pandas.read_csv()加载后实际内存占用往往是原始文件的3–5倍(字符串列未转category、数值列未设dtype、索引冗余) -
groupby().agg()这类操作在Pandas里是单线程执行,哪怕你机器有32核,它也只用1个 - 链式操作如
df.query().sort_values().drop_duplicates()会产生多个中间DataFrame,内存峰值可能达最终结果的4倍
用Polars替代Pandas:不改逻辑,只换引擎
Polars是Rust写的列式引擎,API高度兼容Pandas(尤其pl.DataFrame和pl.Expr),迁移成本极低。关键不是“重写”,而是“重读+微调”。
- 读取阶段就降内存:用
pl.read_csv("data.csv", dtypes={"user_id": pl.UInt32, "amount": pl.Float32}),比Pandas省40%+内存 - 延迟执行(
.lazy())必须加:所有操作先构图,.collect()才真正执行,避免中间结果落地 - 字符串处理别用
.str.contains()暴力匹配,优先用.str.extract()或.str.split().list.get(0),后者在Polars里是向量化实现,快5–10倍 - 避免
.to_pandas()回退——除非下游库(如scikit-learn)强制要求,否则全程用Polars;真要导出,用.write_parquet()比.write_csv()快10倍且体积小70%
示例对比(同数据,同逻辑):
# Pandas(耗时约8.2s,内存峰值4.1GB)
df = pd.read_csv("orders.csv")
result = df[df["status"] == "paid"].groupby("user_id")["amount"].sum().reset_index()
<h1>Polars(耗时约1.3s,内存峰值1.2GB)</h1><p>df = pl.read_csv("orders.csv").lazy()
result = df.filter(pl.col("status") == "paid").groupby("user_id").agg(pl.sum("amount")).collect()</p>
Dask DataFrame适合什么场景?别硬套
Dask不是“Pandas多线程版”,它是任务调度器+分块引擎。如果你的数据能切分成独立子集(比如按日期分区的订单表),且计算逻辑支持分而治之(groupby、join、map_partitions),Dask才真正发挥价值。
- 别用Dask加载单个大CSV:用
dask.dataframe.read_csv("data.csv")会自动按行切块,但若块内无法独立计算(如全局排序、累计求和),后续shuffle开销极大 -
compute()前务必检查ddf.npartitions:太少(1000)导致调度延迟压倒计算收益;建议初始设为CPU核心数×2~4 - join操作小心
how="inner"以外的类型:Dask对left/outerjoin需广播小表,若小表也超百万行,直接OOM - 真需要混用Pandas生态时,用
dask.delayed包装函数比dask.dataframe更可控,尤其涉及第三方库调用
迁移时最容易被忽略的3个细节
-
pd.Timestamp和pl.Datetime的时区行为不一致:pl.from_pandas()不会自动保留tz-aware信息,必须显式加time_unit="us"和time_zone="UTC"
- Polars的
is_null()返回布尔Series,而Pandas的isna()在标量上下文(如if df["x"].isna().all())会报错,得改用df["x"].is_null().all()
- Dask的
repartition()不是免费的:它触发全量重分布,等价于一次shuffle;如果只是想减少分区数,用ddf.repartition(npartitions=8).persist()比链式repartition().repartition()安全得多
pd.Timestamp和pl.Datetime的时区行为不一致:pl.from_pandas()不会自动保留tz-aware信息,必须显式加time_unit="us"和time_zone="UTC"
is_null()返回布尔Series,而Pandas的isna()在标量上下文(如if df["x"].isna().all())会报错,得改用df["x"].is_null().all()
repartition()不是免费的:它触发全量重分布,等价于一次shuffle;如果只是想减少分区数,用ddf.repartition(npartitions=8).persist()比链式repartition().repartition()安全得多真实项目里,90%的“千万级慢”问题靠Polars + .lazy() + 类型精简就能解决;剩下10%涉及跨分区依赖或复杂状态,才需要Dask甚至转向Spark。别一上来就铺分布式,先让单机跑得飞起。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











