在数据科学领域,Pandas 长期占据着核心地位,但随着数据集规模不断膨胀,其单核执行和内存消耗的瓶颈日益明显。近年来,一个基于 Rust 内核的 Python Polars 库迅速崛起,凭借惰性计算、向量化执行和零拷贝内存管理,在处理数百万行乃至数亿行数据时展现出数十倍的性能优势。本文将带你从零开始掌握 Polars,通过完整的实战案例,完成从 Pandas 到 Polars 的高效迁移。
一、Polars 的核心优势
Polars 被设计为一个真正并行化的 DataFrame 库,与 Pandas 的根本区别在于:
- Apache Arrow 内存模型:数据以列式格式存储,支持零拷贝共享,减少序列化开销。
- 惰性执行:构建查询计划而非立即执行,优化器自动合并操作,减少中间结果内存分配。
- 全核并行:几乎所有操作都利用多线程,充分利用现代 CPU。
- 表达式系统:使用链式表达式而非隐式索引,代码更加清晰且不易出错。
这些特性使得 Polars 在数据清洗、特征工程、日志分析等场景下,比 Pandas 快 5 到 50 倍,且内存占用更低。
二、安装与快速入门
使用 pip 安装 Polars:
pip install polars
同时推荐安装 pyarrow 以获得更好的文件读写支持。我们来创建一个简单的 DataFrame 并查看其结构:
import polars as pl
# 从字典创建 DataFrame
df = pl.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'age': [25, 30, 35, 40],
'salary': [70000, 80000, 120000, 95000]
})
print(df)
# 输出:
# shape: (4, 3)
# ┌─────────┬─────┬────────┐
# │ name ┆ age ┆ salary │
# │ --- ┆ --- ┆ --- │
# │ str ┆ i64 ┆ i64 │
# ╞═════════╪═════╪════════╡
# │ Alice ┆ 25 ┆ 70000 │
# │ Bob ┆ 30 ┆ 80000 │
# │ Charlie ┆ 35 ┆ 120000 │
# │ Diana ┆ 40 ┆ 95000 │
# └─────────┴─────┴────────┘
与 Pandas 不同,Polars 在打印时会显示列的类型(str, i64 等)和形状。这种清晰的类型展示对编写类型安全的查询非常有帮助。
三、核心操作:选择、过滤与排序
Polars 的操作基于表达式 pl.col(),避免了 Pandas 中常见的链式索引问题。我们用经典的 Titanic 数据集示范。
# 读取 CSV 文件(Polars 自动推断类型)
df = pl.read_csv('titanic.csv')
print(df.head())
# 选择若干列
df.select([
pl.col('Survived'),
pl.col('Pclass'),
pl.col('Name')
])
# 过滤:找出票价大于100的乘客
df.filter(pl.col('Fare') > 100)
# 多条件过滤:一等舱且年龄小于18的乘客
df.filter((pl.col('Pclass') == 1) & (pl.col('Age') < 18))
# 排序:按年龄降序,缺失值放最后
df.sort('Age', descending=True, nulls_last=True)
表达式可以组合,并支持 is_not_null()、is_in()、str.contains() 等方法,覆盖绝大多数数据筛选需求。
四、数据清洗与列操作
Polars 的列操作通过 with_columns 添加或修改列,结合表达式可以完成复杂清洗。
# 添加新列:将票价转换为欧元(假设汇率0.93)
df_clean = df.with_columns(
(pl.col('Fare') * 0.93).alias('Fare_EUR')
)
# 修改列类型:Age 转为浮点
df_clean = df_clean.with_columns(
pl.col('Age').cast(pl.Float64)
)
# 填充缺失值:年龄用中位数填充,港口用'S'填充
df_filled = df.with_columns(
pl.col('Age').fill_null(df['Age'].median()),
pl.col('Embarked').fill_null('S')
)
# 字符串操作:提取姓氏(Name 中逗号前的部分)
df_names = df.with_columns(
pl.col('Name').str.split(',').list.first().alias('Surname')
)
这些操作在执行时非常高效,因为每一个 with_columns 都是一次计划中的步骤,可以合并优化。
五、分组聚合与窗口函数
分组聚合是数据分析的核心。Polars 的 group_by 配合 agg 方法提供了丰富的聚合能力。
# 按船舱等级分组,计算生存率和平均票价
agg_df = df.group_by('Pclass').agg([
pl.col('Survived').mean().alias('SurvivalRate'),
pl.col('Fare').mean().alias('AvgFare'),
pl.col('PassengerId').count().alias('Count')
])
print(agg_df)
窗口函数(Window Functions)允许在不破坏行数的前提下添加聚合列:
# 为每个乘客添加所在船舱等级的平均票价
df_with_avg = df.with_columns(
pl.col('Fare').mean().over('Pclass').alias('AvgFareByClass')
)
更复杂的分组排序后取前N行也可以轻松实现:
# 每个船舱等级中票价最高的2名乘客
top_fare_per_class = df.sort('Fare', descending=True).group_by('Pclass').head(2)
六、惰性执行与查询优化
Polars 的杀手锏是惰性执行。使用 lazy() 方法将 DataFrame 转为 LazyFrame,后续操作构建查询计划,直到调用 collect() 才真正执行。优化器会自动调整操作顺序、合并过滤,极大减少内存分配。
# 惰性模式示例
lazy_df = df.lazy()
result = (lazy_df
.filter(pl.col('Age') > 18)
.group_by('Pclass')
.agg(pl.col('Survived').mean().alias('AdultSurvival'))
.sort('Pclass')
)
# 此时仍无实际计算,执行 collect() 触发
final = result.collect()
print(final)
使用 explain() 可以查看优化后的执行计划:
print(result.explain()) # 输出类似SQL的查询计划
对于百万行以上的数据集,惰性执行通常能将计算时间缩短一半甚至更多,且内存峰值显著下降。
七、与 Pandas 的性能对比实战
我们用一段代码生成 1000 万行数据,并分别用 Pandas 和 Polars 进行分组聚合,直观感受速度差距。
import numpy as np
import pandas as pd
import polars as pl
import time
# 生成测试数据:1000万行,4列
n = 10_000_000
np.random.seed(42)
data = {
'category': np.random.choice(['A','B','C','D'], n),
'value1': np.random.randn(n),
'value2': np.random.randn(n),
'value3': np.random.randn(n)
}
# Pandas 测试
pdf = pd.DataFrame(data)
start = time.time()
pdf.groupby('category').agg({'value1':'mean','value2':'sum','value3':'std'})
pandas_time = time.time() - start
# Polars 测试(惰性执行)
pl_df = pl.DataFrame(data).lazy()
start = time.time()
(pl_df.group_by('category')
.agg([pl.col('value1').mean(),pl.col('value2').sum(),pl.col('value3').std()])
.collect())
polars_time = time.time() - start
print(f"Pandas 耗时: {pandas_time:.3f}秒")
print(f"Polars 耗时: {polars_time:.3f}秒")
print(f"加速比: {pandas_time/polars_time:.1f}x")
典型输出:Pandas 需要 3.2 秒,Polars 仅需 0.2 秒,加速超过 15 倍。在多核服务器上,加速比甚至可达 30 倍以上。
八、从 Pandas 迁移的实用对照表
常见 Pandas 操作对应的 Polars 写法:
# 读取 CSV
# Pandas: pd.read_csv('file.csv')
# Polars: pl.read_csv('file.csv')
# 选取列
# Pandas: df[['col1','col2']]
# Polars: df.select(['col1','col2'])
# 过滤行
# Pandas: df[df['age'] > 30]
# Polars: df.filter(pl.col('age') > 30)
# 分组聚合
# Pandas: df.groupby('key').agg({'val':'mean'})
# Polars: df.group_by('key').agg(pl.col('val').mean())
# 排序
# Pandas: df.sort_values('col')
# Polars: df.sort('col')
# 添加列
# Pandas: df['new'] = df['a'] + df['b']
# Polars: df.with_columns((pl.col('a') + pl.col('b')).alias('new'))
尽管语法略有不同,但 Polars 的表达式系统更加一致,避免了 Pandas 中 loc、iloc、链式赋值等容易引发警告的陷阱。
九、文件读写与数据源连接
Polars 支持 CSV、JSON、Parquet、IPC、Delta Lake 等多种格式,并可直接从数据库、S3 等读取。以 Parquet 为例:
# 写入 Parquet(极高的压缩比和读取速度)
df.write_parquet('titanic.parquet', compression='zstd')
# 读取 Parquet
df_parquet = pl.read_parquet('titanic.parquet')
连接 MySQL 或 PostgreSQL 可通过 pl.read_database() 实现:
import polars as pl
conn_str = "postgresql://user:pass@localhost:5432/mydb"
df_db = pl.read_database("SELECT * FROM users", conn_str)
这使得 Polars 能够直接嵌入 ETL 管道,无需先导出为中间文件。
十、最佳实践与总结
- 惰性优先:始终将 DataFrame 转为 LazyFrame 后再进行复杂操作,最后
collect()。 - 链式表达式:使用
select、with_columns、filter等链式调用,保持代码可读性。 - 列操作采用表达式:避免使用行级循环,充分利用列式并行。
- 适当设置类型:在创建 DataFrame 时指定 schema,可进一步提升性能。
- 与 Pandas 互通:通过
df.to_pandas()和pl.from_pandas()在两者间转换,便于逐步迁移。
Polars 不是要完全取代 Pandas,而是在大数据量和高性能场景下提供更优选择。随着其生态系统日益成熟(支持 GPU、流式数据等),掌握 Polars 已成为 Python 数据工程师的核心竞争力。将本文的实战案例应用到你的日常工作中,你会感受到数据处理速度的质变。

