最近在做一个用户行为分析项目,数据量每天都在涨,从最初的两百万行很快飙升到了八百万行。一开始用的 Pandas 还能勉强应付,但内存占用轻松突破 8G,再加上一些分组聚合操作,笔记本风扇疯狂嘶吼,偶尔还会因为分配不出连续内存而直接报错。切换成 Polars 之后,内存在同样的操作下降低了将近一半,执行速度更是快了三四倍,体验确实有点超乎预期。本文就沿着这个真实场景,把迁移过程和关键用法梳理出来,希望能帮你少走些弯路。
为什么是Polars?
Polars 是用 Rust 实现的 DataFrame 库,底层基于 Apache Arrow 的内存模型。相比 Pandas,它有两个非常突出的特点:
- 零拷贝与列式存储:数据在内存中以 Arrow 格式长期存放,不同操作之间传递数据基本不需要额外复制,大幅减少内存开销。
- 懒执行与查询优化:可以像 Spark 那样先声明整个计算逻辑,然后一次性优化并执行。多余的全表扫描、中间列会被自动剪枝,避免不必要的计算。
最重要的是,Polars 的 API 设计非常接近 Pandas,但又消除了很多隐式拷贝和类型推断上的历史包袱,上手难度并不高。
环境准备与安装
Polars 同时提供 Rust 版本和 Python 绑定,我们只关心 Python 端就好。直接用 pip 安装:
pip install polars
如果你的数据存储在数据库中,还可以顺便安装连接器,例如读取 MySQL:
pip install polars[pandas, sqlalchemy] # pandas 用于互相转换,sqlalchemy 用于数据库读取
安装完成后,在脚本中导入,通常的约定是用 pl 作为别名:
import polars as pl
快速读取数据
假设我们有一个约 800 万行的 CSV 文件 user_actions.csv,包含字段:user_id, action, product_id, timestamp, amount。先用 Polars 读进来看看:
df = pl.read_csv("user_actions.csv")
print(df.head(3))
print(df.shape)
输出类似:
shape: (3, 5)
┌──────────┬───────────┬────────────┬─────────────────────┬────────┐
│ user_id ┆ action ┆ product_id ┆ timestamp ┆ amount │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i64 ┆ str ┆ f64 │
╞══════════╪═══════════╪════════════╪═════════════════════╪════════╡
│ 1001 ┆ view ┆ 501 ┆ 2025-01-01T12:01:05 ┆ 0.0 │
│ 1001 ┆ add_cart ┆ 501 ┆ 2025-01-01T12:02:03 ┆ 0.0 │
│ 1002 ┆ purchase ┆ 322 ┆ 2025-01-01T12:05:11 ┆ 299.99 │
└──────────┴───────────┴────────────┴─────────────────────┴────────┘
(8300000, 5)
默认情况下,read_csv 会尝试推断每列的数据类型,对于大文件,推断会消耗一些时间。如果你明确知道每列的类型,可以用 dtypes 参数显式指定,同时传入 infer_schema_length=0 关闭推断,读取速度会更快:
df = pl.read_csv(
"user_actions.csv",
dtypes={
"user_id": pl.Int64,
"action": pl.Utf8,
"product_id": pl.Int64,
"timestamp": pl.Utf8,
"amount": pl.Float64
},
infer_schema_length=0
)
当然,timestamp 列后续我们会转成真正的 datetime 类型。
用表达式(Expression)操作数据
Polars 的核心操作方式是通过表达式构建“操作管道”,而不是像 Pandas 那样频繁地取列赋值。最常见的就是 select、filter、with_columns 这几个方法。
列选择与类型转换
先把 timestamp 从字符串转为 datetime 类型,并且只保留我们关心的列:
df_clean = df.select(
pl.col("user_id"),
pl.col("action"),
pl.col("product_id").cast(pl.Int32), # 压缩存储
pl.col("timestamp").str.to_datetime("%Y-%m-%dT%H:%M:%S"),
pl.col("amount")
)
print(df_clean.dtypes)
这里的 str.to_datetime 是字符串命名空间下的方法,直接在表达式里完成转换,无需生成中间 Series。
过滤与条件组合
筛选出所有“购买”动作且金额大于 10 的记录:
purchases = df_clean.filter(
(pl.col("action") == "purchase") & (pl.col("amount") > 10.0)
)
print(purchases.shape)
条件里的 & 和 | 用来组合逻辑,和 Pandas 的按位运算符写法一致,但这里的表达式在 Polars 内部会被优化。
添加新列
假设我想给每行添加一个“星期几”字段,方便后续按天聚合:
df_clean = df_clean.with_columns(
pl.col("timestamp").dt.weekday().alias("day_of_week")
)
with_columns 只修改或添加指定列,其他列保持不变,内存开销极小。
分组聚合与窗口函数
一个很常见的需求:统计每位用户的总消费金额、购买次数,以及最近一次购买时间。用 Pandas 我通常会链式写 groupby 加 agg,Polars 的写法几乎一样:
user_stats = df_clean.filter(pl.col("action") == "purchase").group_by("user_id").agg(
pl.col("amount").sum().alias("total_spent"),
pl.col("action").count().alias("purchase_count"),
pl.col("timestamp").max().alias("last_purchase_time")
)
print(user_stats.head(5))
得到的 user_stats 是一个新的 DataFrame,只包含聚合结果。因为过滤和聚合都是惰性表达式,内部可以合并执行,实际速度比 Pandas 快了数倍。
另一个场景是计算每个用户在当日消费金额的排名,可以用窗口函数:
df_daily = df_clean.filter(pl.col("action") == "purchase").with_columns(
pl.col("timestamp").dt.date().alias("date")
)
df_ranked = df_daily.with_columns(
pl.col("amount").rank(method="dense", descending=True)
.over(["user_id", "date"])
.alias("rank_in_day")
)
print(df_ranked.select(["user_id", "date", "amount", "rank_in_day"]).head(8))
over() 指定了窗口分组,写法比 SQL 更紧凑。你不会看到临时表或子查询,所有逻辑都在表达式链条里完成。
懒执行(LazyFrame)与查询计划
平时练习我们可以用快速模式(即普通 DataFrame),但生产上强烈建议切换到 LazyFrame。它是 Polars 的性能核心,能够将一系列操作编译成一个优化的执行计划,只扫描必要的数据列,甚至在某些场景下只遍历数据一次。
把之前的清洗和聚合流程重构成懒加载版本:
qq = pl.scan_csv("user_actions.csv") # 创建一个 LazyFrame
lf_clean = qq.select([
pl.col("user_id"),
pl.col("action"),
pl.col("product_id").cast(pl.Int32),
pl.col("timestamp").str.to_datetime("%Y-%m-%dT%H:%M:%S"),
pl.col("amount")
]).with_columns(
pl.col("timestamp").dt.weekday().alias("day_of_week")
)
lf_purchase = lf_clean.filter(
(pl.col("action") == "purchase") & (pl.col("amount") > 10.0)
)
lf_stats = lf_purchase.group_by("user_id").agg(
pl.col("amount").sum().alias("total_spent"),
pl.col("action").count().alias("purchase_count"),
pl.col("timestamp").max().alias("last_purchase_time")
)
# 至此尚未执行任何计算
print(lf_stats.explain()) # 查看优化后的执行计划
result = lf_stats.collect() # 真正执行
print(result.head(5))
通过 explain() 可以直观看到查询优化器做了什么:它可能合并过滤器,提前进行列裁剪,甚至把某些聚合操作推到扫描阶段。这对于调试性能非常有用,也是我养成的习惯。
与Pandas无缝互转
团队中可能仍有部分成员习惯用 Pandas,或者某些可视化库只接受 Pandas DataFrame。Polars 提供了零拷贝互转:
# Polars -> Pandas
pdf = result.to_pandas()
# Pandas -> Polars (同样通过 Arrow 中转,高效)
pl_df = pl.from_pandas(pdf)
如果数据包含类别型或时间等特殊类型,可以设置 use_pyarrow_extension_array=True 以保留更精确的类型。
一个完整实战:找出高价值商品
最后我们串联一个较完整的例子:从原始 CSV 中读取用户操作数据,计算每个商品的总销售额、总购买次数、最后一次购买日期,并按销售额降序取前 10 件商品。完整脚本如下:
import polars as pl
# 懒加载 CSV
lf = pl.scan_csv("user_actions.csv")
# 转换时间列并过滤购买行为
purchases = (
lf.filter(pl.col("action") == "purchase")
.with_columns(
pl.col("timestamp").str.to_datetime("%Y-%m-%dT%H:%M:%S")
)
.filter(pl.col("amount") > 0)
)
# 按产品聚合
product_stats = (
purchases.group_by("product_id")
.agg([
pl.col("amount").sum().alias("total_revenue"),
pl.col("user_id").count().alias("purchase_count"),
pl.col("timestamp").max().alias("last_sale")
])
.sort("total_revenue", descending=True)
.limit(10)
)
# 执行并转 Pandas(可选)
top_products = product_stats.collect()
print(top_products)
# 如果要导出结果
top_products.write_csv("top_products.csv")
在我的那台 16GB 内存的 MacBook 上,处理 830 万行数据,整个流程不到 2 秒,内存峰值稳定在 1GB 左右。而之前用 Pandas 跑同样的逻辑,要花上近 10 秒,内存更是突破 3GB。这还只是单机情况,没有任何分布式加持。
常见坑与使用建议
- 类型推断:默认
scan_csv会读取前 100 行推断类型,如果某列前面都是空值,可能推断为 Null 类型,导致后续操作报错。建议用infer_schema_length调大或手动指定dtypes。 - 索引概念缺失:Polars 没有 Pandas 的 index 概念,所有数据都是列式存储。如果要按某列排序后用位置访问,直接
sort后加row或head。 - 表达式可组合:复杂逻辑尽量拆成多个变量,不要在一个
select里塞几十列,但也要注意不要因为拆得太散而多次触发collect。最好的方式就是全程使用 LazyFrame,最后一次性执行。 - 内存溢出:即使是 Polars,如果加载的数据集远超物理内存,还是要考虑用
scan_parquet分块读取,或者结合pl.read_csv_batched等方法做增量处理。
总结
从实际的迁移体验来看,Polars 并不是要完全推翻 Pandas 的生态,而是在处理较大规模数据时提供了一个更现代、更省资源的选项。学习成本不高,很多操作和 Pandas 一一对应,但又在性能和内存上拉开了明显差距。如果你日常工作中经常遇到数据量快速增长、内存吃紧的情况,很值得花一两个小时把常用操作迁移过来试一试。
目前 Polars 的绘图生态、第三方支持还没有 Pandas 那么丰富,不过这个问题可以通过 to_pandas() 轻松绕过。综合来看,它已经成为我日常数据分析的首选工具之一了。

