Polars 速度快、语法现代、表达力强,但很多人刚上手就把它当 Pandas 用,结果性能优势全都浪费了。
下面是新手最容易犯的 10 个错误,以及对应的解决思路。
1、直接 read_csv而不用 scan_*
新手拿到一个大 CSV,上来就这么写:
df=pl.read_csv("events.csv")
这会把整个文件一口气塞进内存。文件一旦上了 GB 级别,内存直接爆掉,性能也跟着完蛋。正确做法是用惰性扫描:
lf=pl.scan_csv("events.csv")
所有操作保持惰性状态,直到最后调用
.collect()
。
这样做的好处是优化器可以把过滤和投影操作下推到扫描阶段,I/O 和内存占用都会大幅下降。
2、还在用 Python 循环或 .apply()
想给数据加个新列,很多人会写成这样:
df=df.with_columns(
pl.col("price").apply(lambdax: x*1.19)
)
这种写法强迫 Python 逐行处理,完全没有向量化可言,慢得离谱。换成原生表达式:
df=df.with_columns(
(pl.col("price") *1.19).alias("price_with_vat")
)
这样操作会跑在 Rust 层面,有 SIMD 加速,还能融合进查询计划里。性能差距就变得很大了
3、collect() 调用太早、太频繁
新手经常写出这种流水线:
df1=lf.filter(...).collect()
df2=df1.with_columns(...).collect()
每调一次
.collect()
,整个数据集就要完整物化一遍。应该把所有操作串起来,最后只 collect 一次:
result= (
lf.filter(...)
.with_columns(...)
.groupby(...)
.agg(...)
)
df=result.collect()
单次
.collect()
让优化器有机会做全局优化,计算量能省下一大截。
4、不做列裁剪(投影下推)
比如加载了一张 200 多列的宽表,实际只用到 4 列——但整张表还是全读进来了。正确做法是是尽早筛选列:
lf=lf.select(["user_id", "country", "revenue", "event_time"])
Polars 会把投影下推到扫描层,从磁盘上读取时只读这几列。配合 Parquet 格式效果更明显,速度提升非常可观。
5、太早转成 Pandas
有人习惯这么干:
pd_df=lf.collect().to_pandas()
还没过滤、没分组、没聚合,就先转成 Pandas 了,结果几千万行数据全在 Pandas 里慢慢磨。合理的做法是先在 Polars 里把重活干完:
cleaned=lf.filter(...).groupby(...).agg(...)
pdf=cleaned.collect().to_pandas()
Polars 是计算引擎,Pandas 只是展示层,搞反了性能优势就没有了。
6、搞混 DataFrame、LazyFrame 和 Expr
新手容易写出这种代码:
lf.groupby("user_id").sum()
或者:
df.with_columns(lf.col("price"))
原因是没搞清楚三种核心类型的区别。
要记住:DataFrame 是已经物化的数据;LazyFrame 是查询计划;Expr 是列表达式。
lf=pl.scan_csv("file.csv") # LazyFrame
df=lf.collect() # DataFrame
expr=pl.col("amount") # Expr
模型清晰了,才能避开各种隐蔽 bug也才能让优化器真正发挥作用。
7、以为 .unique()和 Pandas 一样
有些人期望
.unique()
返回排序后的结果,但 Polars 默认保留原始顺序:
lf.select(pl.col("country").unique())
这跟 Pandas 的行为是不一样,所以很容易出逻辑错误。如果需要排序就显式加上:
lf.select(pl.col("country").unique().sort())
显式排序能避免跨框架时的隐性差异。
8、不管数据类型
CSV 里的数据经常乱七八糟:
"19.99", "20", "error", ""
Pandas 碰到这种情况会默默建个 object 列,而Polars 会尝试推断类型,但新手往往不验证。
这时在扫描时直接指定类型更靠谱:
lf=pl.scan_csv(
"orders.csv",
dtypes={"price": pl.Float64}
)
或者读完再转:
df=df.with_columns(pl.col("price").cast(pl.Float64))
类型明确的管道更稳定、更可预测,跑起来也更快。
9、大数据聚合不开流式模式
几十亿行数据做 groupby:
lf.groupby("user_id").agg(...)
内存肯定撑不住,程序就直接崩掉了。这时要开启流式模式:
result= (
lf.groupby("user_id")
.agg(pl.col("amount").sum())
.collect(streaming=True)
)
流式处理会分块执行特别适合 ETL 场景和日志分析管道。
10、多次 with_columns而不是合并表达式
新手容易这么写:
df=df.with_columns(pl.col("a") +pl.col("b"))
df=df.with_columns(pl.col("c") -pl.col("d"))
df=df.with_columns(pl.col("e") *1.19)
三次调用,三个独立步骤,没法融合优化。可以将他们合并到一个表达式块里:
df=df.with_columns([
(pl.col("a") +pl.col("b")).alias("ab"),
(pl.col("c") -pl.col("d")).alias("cd"),
(pl.col("e") *1.19).alias("e_vat")
])
Polars 会把这些表达式融合成一个优化后的操作。步骤少了自然就快了。
总结
从 Pandas 转过来的人,很容易带着旧习惯写 Polars 代码,结果性能优势全没了。上面这些点总结下来就是:惰性优先、表达式为主、最后才 collect、别用 Python 循环、列要有明确类型、多用 LazyFrame、善用投影下推和谓词下推、大数据开流式处理。
养成这些习惯,Polars 的性能才能真正释放出来。
作者:Brent Fischer