1. 为什么要把这两个放在一起比
pandas 在 Python 里做数据处理差不多是"事实标准"了,十多年积累下来的生态和社区,几乎你搜到的教程、现成代码都是它。
Polars 是这两年冒出来的新选手,主打更快、更省内存、写法更干净。
于是很多人会纠结一个问题:Polars 到底比 pandas 强多少,值不值得花时间学?
本文用一份门店销售明细的模拟数据,从三个最关心的角度:语法怎么写、跑起来有多快(性能)、吃多少内存(资源占用)。把两者摆到同一张桌子上实测。所有数字都是我在本机真实跑出来的,你可以按文末的方式在自己的机器上测试。
2. 准备环境和测试数据
先装依赖,只要三个:
pip install pandas polars psutil
psutil 是用来测内存增量的,剩下两个是主角。版本是 pandas 2.3.3 / polars 1.33.1,不同版本速度会有出入,但量级关系不会变。
数据用一份门店销售明细来模拟,比英文教程里那些营养成分表更贴近国内常见的经营分析场景。
字段就六个:订单号、门店、品类、销售额、数量、日期。
生成逻辑在 gen_data.py 里,固定了随机种子,你跑出来的数据和下面我用的完全一致。核心片段长这样:
rng = np.random.default_rng(42)
store = rng.choice(["北京朝阳店","上海浦东店","广州天河店","深圳南山店","成都武侯店"], n)
category = rng.choice(["奶茶","咖啡","甜品","小食"], n)
amount = np.round(rng.uniform(8.0, 68.0, n), 2)
默认造 200 万行,约 100MB 的 CSV。想用小数据量对比也行,改个参数即可。
3. 读 CSV 的速度(性能)
最直观的性能对比就是读文件。同样一份 100MB 的 CSV,分别用两个库读进来计时:
t = time.time(); df_pd = pd.read_csv(CSV); pd_t = time.time() - t
t = time.time(); df_pl = pl.read_csv(CSV); pl_t = time.time() - t
本机结果:pandas 1.634s,polars 0.130s,加速比约 12.5 倍。
差距主要来自 pandas 的 CSV 解析是单线程的,而 Polars 会自动把读取并行到多个 CPU 核心上。
这里有个我实测出来的坑要提醒你:不要一上来就用很小的文件比。
我先用过 50 万行试,两者几乎一样快、甚至 pandas 还略快一点。Polars 的多核优势要在文件足够大、或者管道足够复杂时才明显。
所以你要评估自己真实场景的数据量,别被小样本误导。
4. 内存占用的差别(资源占用)
资源占用主要看内存。
我在独立子进程里测"跑完同一条流水线前后的 RSS 增量",避免 Python 内存分配器不归还内存带来的干扰:
proc = psutil.Process(os.getpid())
base = proc.memory_info().rss
df = read_fun(CSV); df = df.filter(...).group_by(...).agg(...)
delta = proc.memory_info().rss - base
结果有点反直觉:这组简单的窄表上,pandas 增量 281.6MB,polars 341.7MB,两者接近、polars 还略高一点。
原因在于:Polars 真正省内存的场景不是"简单读一个窄表",而是复杂管道里靠惰性求值砍掉中间副本(第 7 节会讲)。
所以**"Polars 更省内存"这句话得加前提**,否则容易翻车。
5. 基础操作的写法差异(语法)
语法是日常写代码最直观的差别,挑三个最常用的操作看。
选列:pandas 用括号加列名列表,Polars 用 .select(),表达更接近"我要什么"而不是"怎么取":
# pandas
sub = df[["store", "amount"]]
# polars
sub = df.select(["store", "amount"])
过滤:pandas 是布尔索引,Polars 的 .filter() 读起来更像 SQL:
# pandas
hot = df[df["amount"] > 30]
# polars
hot = df.filter(pl.col("amount") > 30)
加列:这是最该注意的一点——pandas 默认原地修改,Polars 默认返回新表、不改动原表(不可变):
# pandas:原地改,df 本身变了
df["bonus"] = df["amount"] * 0.1
# polars:返回新 DataFrame,原 df 不变
df2 = df.with_columns((pl.col("amount") * 0.1).alias("bonus"))
写法孰优孰劣见仁见智,但 Polars 的不可变默认能减少很多"改了上游、下游莫名其妙出错"的 bug。
6. 分组聚合的速度(性能)
分组聚合是经营分析里天天干的活:按门店汇总销售额。这里按门店对销售额求均值、中位数、标准差,同时对数量求均值:
# pandas
g = df.groupby("store")["amount"].agg(["mean","median","std"])
# polars
g = df.group_by("store").agg(
pl.col("amount").mean().alias("mean"),
pl.col("amount").median().alias("median"),
pl.col("amount").std().alias("std"),
)
本机结果:pandas 0.072s,polars 0.018s,加速比约 4.1 倍。
语法上 pandas 用字典描述聚合更紧凑,但要多一步 reset_index() 和手动改名;
Polars 链式写法啰嗦一点但可读性更好。
7. Polars 的惰性求值(资源 + 性能优化)
Polars 有个 pandas 没有的杀手锏:惰性求值。
用 scan_csv 只是先"画好查询计划"而不真读数据,把过滤、分组、聚合全串起来,只有调用 .collect() 才真正执行;执行前 Polars 会重排整合整条查询,把能下推的过滤提前,还能省掉中间结果的复制。
q = (pl.scan_csv(CSV)
.filter(pl.col("amount") > 30)
.group_by("store")
.agg(pl.col("amount").mean().alias("avg")))
result = q.collect()
光说"省内存"不够直观,我把它和第 4 节那组基准放在同一个管道上实测了一遍。
管道是:读 CSV → 过滤掉小额订单 → 派生两个列(revenue = 销售额 × 数量、cost = revenue × 0.3)→ 按门店分组聚合 → 排序。
普通模式(不管是 pandas 还是 polars)会老老实实把派生列物化成一个 200 万行的大中间表;
惰性模式则把过滤、派生、分组融合成一步,只在最后聚合时才算,不保留整列副本。
三种模式各跑在独立进程里测峰值内存,结果:
| 模式 | 峰值内存增量 |
|---|---|
| pandas(普通) | 365.7 MB |
| polars(普通) | 360.8 MB |
| polars(惰性) | 218.4 MB |
差距很清楚:两个普通模式都在 360MB 上下,惰性模式只要 218.4MB,省了约 40%。
原因就是上面说的:普通模式在内存里多撑了一张 200 万行 × 2 列的派生表,惰性模式把它融合掉了。
注意这里省的是**"中间副本"**,不是读取本身;所以第 4 节那种"只读不怎么算"的窄表里两者才会看起来差不多,一旦管道变长、派生列变多,惰性的内存优势就出来了。
这段对比的完整代码在 benchmark.py 里(函数 pipeline_mem_peak),想在自己机器上验证,直接跑 python benchmark.py 就会打印上面这张表。
8. 怎么选
把三个维度收一下:
- 语法上: Polars 更声明式、默认不可变,pandas 更熟悉、可原地改;
- 性能上:大文件 / 复杂管道 Polars 明显更快(本机读取 12.5 倍、聚合 4.1 倍),小文件两者差不多;
- 资源占用上:简单任务两者接近,复杂任务靠 Polars 惰性求值才显出省内存的优势。
如果你在做大规模分析、数据工程、或者某条慢管道卡性能,优先试 Polars;如果项目吃 pandas 生态、要快速 exploratory 分析、或者团队都在用,pandas 仍是稳妥选择。
两者语法差异不算大,迁移成本可控,拿一条你自己的慢管道先试水最实在。
文中的用到的代码 gen_data.py和benchmark.py共享在:pandas-vs-polars-code.zip url11.ctfile.com/f/45455611-…