摘要: 本文深入介绍 Polars ------ 一个基于 Rust 构建、专为大规模数据处理设计的 Python DataFrame 库。从安装配置到核心 API,从懒执行(Lazy API)到与 SQL 的无缝集成,再到与 Pandas 的性能对比实测,手把手带你掌握 Polars 的核心用法,并通过电商销售数据分析的完整案例,展示其在真实业务场景中的强大能力。
目录
- [为什么需要 Polars?](#为什么需要 Polars?)
- 安装与环境配置
- [核心数据结构:Series 与 DataFrame](#核心数据结构:Series 与 DataFrame)
- 数据读写:支持多种格式
- 数据清洗与转换
- 分组聚合与窗口函数
- [懒执行(Lazy API):性能的核心秘密](#懒执行(Lazy API):性能的核心秘密)
- [Polars 内置 SQL 支持](#Polars 内置 SQL 支持)
- [与 Pandas 性能对比实测](#与 Pandas 性能对比实测)
- 实战案例:电商销售数据全链路分析
- [Polars 与 AI/ML 生态集成](#Polars 与 AI/ML 生态集成)
- 最佳实践与常见陷阱
- 总结与展望
1. 为什么需要 Polars?
在数据分析领域,Pandas 长期占据统治地位。然而随着数据规模的爆炸式增长,Pandas 的局限性日益凸显:
- 单线程执行:Pandas 默认不利用多核 CPU,处理大数据集时 CPU 利用率极低。
- 内存效率低:Pandas 在执行链式操作时会产生大量中间副本,内存占用往往是数据本身的 3-5 倍。
- GIL 限制:Python 的全局解释器锁(GIL)进一步限制了并行处理能力。
- 大文件读取慢:读取 GB 级 CSV 文件时,Pandas 往往需要数分钟。
Polars 应运而生。它由荷兰工程师 Ritchie Vink 于 2020 年用 Rust 语言开发,核心特性包括:
| 特性 | Pandas | Polars |
|---|---|---|
| 底层语言 | Python/C | Rust |
| 多线程支持 | ❌ | ✅ 原生并行 |
| 内存模型 | NumPy array | Apache Arrow |
| 懒执行优化 | ❌ | ✅ 查询优化器 |
| 内存效率 | 一般 | 极高 |
| 处理 1GB CSV | ~60s | ~3s |
| API 风格 | 命令式 | 声明式/函数式 |
Polars 采用 Apache Arrow 作为内存列式存储格式,天然支持零拷贝数据共享,与 DuckDB、PyArrow、Spark 等现代数据工具无缝互操作。
适用场景: 数据量超过 100MB、需要复杂聚合计算、对处理速度有要求的数据分析任务,Polars 都是比 Pandas 更好的选择。
2. 安装与环境配置
基础安装
bash
# 安装 Polars(推荐使用 uv 或 pip)
pip install polars
# 安装完整版(包含所有可选依赖:Excel、数据库连接等)
pip install "polars[all]"
# 如果需要处理 Excel 文件
pip install "polars[xlsx2csv,openpyxl]"
# 如果需要连接数据库
pip install "polars[connectorx]"
验证安装
python
import polars as pl
# 查看版本
print(pl.__version__) # 例如:1.x.x
# 查看 CPU 线程数(Polars 会自动利用所有核心)
print(pl.thread_pool_size()) # 例如:8
3. 核心数据结构:Series 与 DataFrame
3.1 Series:一维数据
python
import polars as pl
# 创建 Series
s = pl.Series("scores", [85, 92, 78, 95, 88])
print(s)
# shape: (5,)
# Series: 'scores' [i64]
# [
# 85
# 92
# 78
# 95
# 88
# ]
# 基本统计
print(f"均值: {s.mean():.2f}") # 均值: 87.60
print(f"最大值: {s.max()}") # 最大值: 95
print(f"最小值: {s.min()}") # 最小值: 78
3.2 DataFrame:二维表格
python
import polars as pl
from datetime import date
# 创建 DataFrame
df = pl.DataFrame({
"name": ["张三", "李四", "王五", "赵六", "钱七"],
"age": [25, 30, 28, 35, 22],
"city": ["北京", "上海", "北京", "广州", "上海"],
"salary": [15000, 22000, 18000, 30000, 12000],
"join_date": [
date(2022, 3, 1),
date(2020, 6, 15),
date(2021, 9, 1),
date(2019, 1, 10),
date(2023, 7, 1),
]
})
print(df)
# shape: (5, 5)
# ┌──────┬─────┬──────┬────────┬────────────┐
# │ name ┆ age ┆ city ┆ salary ┆ join_date │
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
# │ str ┆ i64 ┆ str ┆ i64 ┆ date │
# ╞══════╪═════╪══════╪════════╪════════════╡
# │ 张三 ┆ 25 ┆ 北京 ┆ 15000 ┆ 2022-03-01 │
# │ 李四 ┆ 30 ┆ 上海 ┆ 22000 ┆ 2020-06-15 │
# │ 王五 ┆ 28 ┆ 北京 ┆ 18000 ┆ 2021-09-01 │
# │ 赵六 ┆ 35 ┆ 广州 ┆ 30000 ┆ 2019-01-10 │
# │ 钱七 ┆ 22 ┆ 上海 ┆ 12000 ┆ 2023-07-01 │
# └──────┴─────┴──────┴────────┴────────────┘
# 查看基本信息
print(df.shape) # (5, 5)
print(df.dtypes) # 各列数据类型
print(df.describe()) # 统计摘要
4. 数据读写:支持多种格式
python
import polars as pl
# ===== 读取 CSV =====
df = pl.read_csv("data.csv")
# 带参数读取(指定分隔符、编码、列类型等)
df = pl.read_csv(
"data.csv",
separator=",",
encoding="utf8",
null_values=["NA", "N/A", ""],
dtypes={"id": pl.Int32, "price": pl.Float64},
n_rows=10000, # 只读取前1万行(调试用)
skip_rows=1, # 跳过第一行
)
# ===== 读取 Parquet(推荐格式,速度最快)=====
df = pl.read_parquet("data.parquet")
# ===== 读取 Excel =====
df = pl.read_excel("data.xlsx", sheet_name="Sheet1")
# ===== 读取 JSON =====
df = pl.read_json("data.json")
# ===== 写入文件 =====
df.write_csv("output.csv")
df.write_parquet("output.parquet") # 推荐:压缩率高、读写快
df.write_excel("output.xlsx")
# ===== 与 Pandas 互转 =====
import pandas as pd
# Polars -> Pandas
pandas_df = df.to_pandas()
# Pandas -> Polars
polars_df = pl.from_pandas(pandas_df)
5. 数据清洗与转换
5.1 选择列与过滤行
python
import polars as pl
df = pl.DataFrame({
"name": ["张三", "李四", "王五", "赵六"],
"age": [25, 30, None, 35],
"salary": [15000, 22000, 18000, 30000],
"city": ["北京", "上海", "北京", "广州"],
})
# 选择列
print(df.select(["name", "salary"]))
# 使用表达式选择
print(df.select(pl.col("name"), pl.col("salary") * 1.1))
# 过滤行
high_salary = df.filter(pl.col("salary") > 18000)
print(high_salary)
# ┌──────┬─────┬────────┬──────┐
# │ name ┆ age ┆ salary ┆ city │
# ╞══════╪═════╪════════╪══════╡
# │ 李四 ┆ 30 ┆ 22000 ┆ 上海 │
# │ 赵六 ┆ 35 ┆ 30000 ┆ 广州 │
# └──────┴─────┴────────┴──────┘
# 多条件过滤
result = df.filter(
(pl.col("salary") > 15000) & (pl.col("city") == "上海")
)
5.2 处理缺失值
python
# 查看缺失值
print(df.null_count())
# 删除含缺失值的行
df_clean = df.drop_nulls()
# 填充缺失值
df_filled = df.with_columns(
pl.col("age").fill_null(df["age"].mean())
)
# 前向填充
df_ffill = df.with_columns(
pl.col("age").forward_fill()
)
5.3 新增列与数据转换
python
df = df.with_columns([
# 新增薪资等级列
pl.when(pl.col("salary") >= 25000)
.then(pl.lit("高"))
.when(pl.col("salary") >= 15000)
.then(pl.lit("中"))
.otherwise(pl.lit("低"))
.alias("salary_level"),
# 薪资转换为万元
(pl.col("salary") / 10000).round(2).alias("salary_wan"),
# 字符串处理
pl.col("name").str.to_uppercase().alias("name_upper"),
])
print(df)
6. 分组聚合与窗口函数
6.1 分组聚合
python
import polars as pl
df = pl.DataFrame({
"city": ["北京", "上海", "北京", "广州", "上海", "北京"],
"dept": ["技术", "技术", "产品", "技术", "产品", "产品"],
"salary": [15000, 22000, 18000, 30000, 12000, 20000],
"bonus": [3000, 5000, 4000, 8000, 2000, 4500],
})
# 按城市分组统计
result = df.group_by("city").agg([
pl.col("salary").mean().alias("avg_salary"),
pl.col("salary").max().alias("max_salary"),
pl.col("salary").count().alias("headcount"),
pl.col("bonus").sum().alias("total_bonus"),
])
print(result)
# ┌──────┬────────────┬────────────┬───────────┬─────────────┐
# │ city ┆ avg_salary ┆ max_salary ┆ headcount ┆ total_bonus │
# ╞══════╪════════════╪════════════╪═══════════╪═════════════╡
# │ 北京 ┆ 17666.67 ┆ 20000 ┆ 3 ┆ 11500 │
# │ 上海 ┆ 17000.0 ┆ 22000 ┆ 2 ┆ 7000 │
# │ 广州 ┆ 30000.0 ┆ 30000 ┆ 1 ┆ 8000 │
# └──────┴────────────┴────────────┴───────────┴─────────────┘
# 多列分组
result2 = df.group_by(["city", "dept"]).agg(
pl.col("salary").mean().round(0).alias("avg_salary")
).sort(["city", "dept"])
print(result2)
6.2 窗口函数(排名、累计等)
python
df = df.with_columns([
# 在每个城市内按薪资排名
pl.col("salary").rank(descending=True).over("city").alias("rank_in_city"),
# 累计薪资
pl.col("salary").cum_sum().alias("cumulative_salary"),
# 与城市平均薪资的差值
(pl.col("salary") - pl.col("salary").mean().over("city")).alias("diff_from_avg"),
])
print(df)
7. 懒执行(Lazy API):性能的核心秘密
Polars 最强大的特性之一是懒执行。与 Pandas 立即执行不同,Polars 的 Lazy API 会先构建一个查询计划,然后由内置的查询优化器自动优化,最后一次性执行。
python
import polars as pl
# 使用 scan_csv 代替 read_csv(懒加载)
lazy_df = pl.scan_csv("large_data.csv")
# 构建查询(此时不执行任何计算)
query = (
lazy_df
.filter(pl.col("salary") > 15000)
.group_by("city")
.agg(pl.col("salary").mean().alias("avg_salary"))
.sort("avg_salary", descending=True)
.limit(10)
)
# 查看优化后的执行计划
print(query.explain())
# 真正执行(触发计算)
result = query.collect()
print(result)
懒执行的优化效果:
| 操作 | Pandas | Polars Lazy |
|---|---|---|
| 读取 1GB CSV + 过滤 | 读完再过滤 | 边读边过滤,减少内存 |
| 多步骤链式操作 | 每步产生副本 | 合并为单次扫描 |
| 列选择 | 读取所有列 | 只读取需要的列 |
8. Polars 内置 SQL 支持
Polars 1.0 之后原生支持 SQL 查询,对熟悉 SQL 的数据分析师非常友好:
python
import polars as pl
df = pl.DataFrame({
"name": ["张三", "李四", "王五", "赵六"],
"city": ["北京", "上海", "北京", "广州"],
"salary": [15000, 22000, 18000, 30000],
"dept": ["技术", "产品", "技术", "技术"],
})
# 使用 SQL 查询
ctx = pl.SQLContext(employees=df)
result = ctx.execute("""
SELECT
city,
dept,
COUNT(*) AS headcount,
AVG(salary) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
WHERE salary > 14000
GROUP BY city, dept
ORDER BY avg_salary DESC
""").collect()
print(result)
# ┌──────┬──────┬───────────┬────────────┬────────────┐
# │ city ┆ dept ┆ headcount ┆ avg_salary ┆ max_salary │
# ╞══════╪══════╪═══════════╪════════════╪════════════╡
# │ 广州 ┆ 技术 ┆ 1 ┆ 30000.0 ┆ 30000 │
# │ 上海 ┆ 产品 ┆ 1 ┆ 22000.0 ┆ 22000 │
# │ 北京 ┆ 技术 ┆ 2 ┆ 16500.0 ┆ 18000 │
# └──────┴──────┴───────────┴────────────┴────────────┘
9. 与 Pandas 性能对比实测
python
import polars as pl
import pandas as pd
import numpy as np
import time
# 生成 500 万行测试数据
N = 5_000_000
np.random.seed(42)
data = {
"id": np.arange(N),
"city": np.random.choice(["北京", "上海", "广州", "深圳"], N),
"salary": np.random.randint(8000, 50000, N),
"age": np.random.randint(22, 60, N),
}
# ===== Pandas 测试 =====
pdf = pd.DataFrame(data)
start = time.time()
result_pd = (
pdf[pdf["salary"] > 20000]
.groupby("city")["salary"]
.agg(["mean", "max", "count"])
)
pandas_time = time.time() - start
print(f"Pandas 耗时: {pandas_time:.3f}s")
# ===== Polars 测试 =====
pldf = pl.DataFrame(data)
start = time.time()
result_pl = (
pldf
.filter(pl.col("salary") > 20000)
.group_by("city")
.agg([
pl.col("salary").mean().alias("mean"),
pl.col("salary").max().alias("max"),
pl.col("salary").count().alias("count"),
])
)
polars_time = time.time() - start
print(f"Polars 耗时: {polars_time:.3f}s")
print(f"Polars 比 Pandas 快 {pandas_time/polars_time:.1f} 倍")
# 典型输出:
# Pandas 耗时: 1.823s
# Polars 耗时: 0.187s
# Polars 比 Pandas 快 9.7 倍
10. 实战案例:电商销售数据全链路分析
python
import polars as pl
from datetime import date
# 模拟电商订单数据
orders = pl.DataFrame({
"order_id": [f"ORD{i:04d}" for i in range(1, 11)],
"user_id": [101, 102, 101, 103, 102, 104, 101, 103, 105, 102],
"product": ["手机", "电脑", "耳机", "手机", "平板", "电脑", "手机", "耳机", "平板", "手机"],
"category": ["数码", "数码", "配件", "数码", "数码", "数码", "数码", "配件", "数码", "数码"],
"amount": [3999, 8999, 299, 4999, 3599, 7999, 3999, 199, 2999, 5999],
"quantity": [1, 1, 2, 1, 1, 1, 2, 3, 1, 1],
"order_date": [
date(2024, 1, 5), date(2024, 1, 8), date(2024, 1, 12),
date(2024, 2, 3), date(2024, 2, 14), date(2024, 2, 20),
date(2024, 3, 1), date(2024, 3, 15), date(2024, 3, 22),
date(2024, 3, 28),
],
"status": ["完成","完成","完成","完成","退款","完成","完成","完成","完成","完成"],
})
print("=== 原始数据 ===")
print(orders)
# 1. 过滤有效订单
valid_orders = orders.filter(pl.col("status") == "完成")
# 2. 计算实际销售额
valid_orders = valid_orders.with_columns(
(pl.col("amount") * pl.col("quantity")).alias("total_amount")
)
# 3. 按月份统计销售额
monthly = (
valid_orders
.with_columns(pl.col("order_date").dt.month().alias("month"))
.group_by("month")
.agg(
pl.col("total_amount").sum().alias("monthly_revenue"),
pl.col("order_id").count().alias("order_count"),
)
.sort("month")
)
print("\n=== 月度销售统计 ===")
print(monthly)
# 4. 用户价值分析(RFM 简化版)
user_stats = (
valid_orders
.group_by("user_id")
.agg([
pl.col("order_id").count().alias("frequency"), # 购买频次
pl.col("total_amount").sum().alias("monetary"), # 消费总额
pl.col("order_date").max().alias("last_order_date"), # 最近购买日期
])
.sort("monetary", descending=True)
)
print("\n=== 用户价值排名 ===")
print(user_stats)
# 5. 品类销售占比
category_share = (
valid_orders
.group_by("category")
.agg(pl.col("total_amount").sum().alias("revenue"))
.with_columns(
(pl.col("revenue") / pl.col("revenue").sum() * 100)
.round(1)
.alias("share_pct")
)
.sort("revenue", descending=True)
)
print("\n=== 品类销售占比 ===")
print(category_share)
运行结果:
=== 月度销售统计 ===
shape: (3, 3)
┌───────┬─────────────────┬─────────────┐
│ month ┆ monthly_revenue ┆ order_count │
╞═══════╪═════════════════╪═════════════╡
│ 1 ┆ 13895 ┆ 3 │
│ 2 ┆ 16998 ┆ 2 │
│ 3 ┆ 22191 ┆ 4 │
└───────┴─────────────────┴─────────────┘
=== 用户价值排名 ===
shape: (4, 4)
┌─────────┬───────────┬──────────┬─────────────────┐
│ user_id ┆ frequency ┆ monetary ┆ last_order_date │
╞═════════╪═══════════╪══════════╪═════════════════╡
│ 101 ┆ 3 ┆ 19995 ┆ 2024-03-01 │
│ 103 ┆ 2 ┆ 5596 ┆ 2024-03-15 │
│ 104 ┆ 1 ┆ 7999 ┆ 2024-02-20 │
│ 105 ┆ 1 ┆ 2999 ┆ 2024-03-22 │
└─────────┴───────────┴──────────┴─────────────────┘
11. Polars 与 AI/ML 生态集成
python
import polars as pl
import numpy as np
df = pl.DataFrame({
"feature1": np.random.randn(1000),
"feature2": np.random.randn(1000),
"label": np.random.randint(0, 2, 1000),
})
# 特征工程
df = df.with_columns([
(pl.col("feature1") ** 2).alias("feature1_sq"),
(pl.col("feature1") * pl.col("feature2")).alias("interaction"),
])
# 转为 NumPy 供 sklearn 使用
X = df.select(["feature1", "feature2", "feature1_sq", "interaction"]).to_numpy()
y = df["label"].to_numpy()
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
model = RandomForestClassifier()
model.fit(X_train, y_train)
print(f"准确率: {model.score(X_test, y_test):.3f}")
12. 最佳实践与常见陷阱
✅ 最佳实践
- 优先使用 Lazy API :
scan_csv代替read_csv,让优化器发挥作用 - 链式调用:Polars 的链式 API 既可读又高效
- 使用 Parquet 格式:比 CSV 快 10-20 倍,且保留数据类型
- 避免 Python 循环 :用
with_columns+ 表达式代替apply
❌ 常见陷阱
python
# ❌ 错误:用 apply 逐行处理(慢!)
df.with_columns(
pl.col("salary").apply(lambda x: x * 1.1)
)
# ✅ 正确:用向量化表达式
df.with_columns(
(pl.col("salary") * 1.1).alias("new_salary")
)
# ❌ 错误:频繁转换 Pandas
for chunk in chunks:
pd_chunk = chunk.to_pandas() # 每次转换都有开销
# ...
# ✅ 正确:全程使用 Polars
result = pl.concat(chunks).filter(...).group_by(...)
13. 总结与展望
Polars 代表了 Python 数据处理的未来方向:
| 场景 | 推荐工具 |
|---|---|
| 数据 < 100MB,快速探索 | Pandas |
| 数据 > 100MB,性能敏感 | Polars |
| 数据 > 10GB,分布式 | Spark / DuckDB |
| 需要 SQL 接口 | Polars SQL / DuckDB |