
如果你用过 Pandas,一定写过这样的代码:
df[df["age"] > 18]["name"]一行代码,做了两件事:先过滤,再选列。简单直观,但背后隐藏着巨大的性能代价 —— Pandas 会为每一步创建中间 DataFrame,数据被反复复制。
Polars 的设计哲学完全不同。它引入了一个核心概念:表达式(Expression,简称 Expr)。
🧠 把表达式想象成一张配方卡:你不需要立刻下厨做菜,而是先把所有步骤写在卡片上。等卡片写完了,厨师(Polars 的查询优化器)会审视整张卡片,去掉多余步骤、调整烹饪顺序,然后一次性高效地完成所有操作。
这就是 惰性求值(Lazy Evaluation) 的精髓。表达式不是"立刻执行的操作",而是"对操作的描述"。Polars 的优化器可以:
一句话总结:表达式是 Polars 最强大的武器,掌握它,你就掌握了 Polars 的灵魂。
col("name") —— 列选择器col() 是最基础的表达式,它告诉 Polars:"我要操作这一列"。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 创建示例 DataFrame
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana"],
"age" => [25, 30, 35, 28],
"salary" => [5000, 8000, 6000, 9000],
]?;
// col("name") 创建一个指向 "name" 列的表达式
let result = df
.clone()
.lazy()
.select([col("name")])
.collect()?;
println!("{}", result);
Ok(())
}输出:
shape: (4, 1)
┌─────────┐
│ name │
│ --- │
│ str │
╞═════════╡
│ "Alice" │
│ "Bob" │
│ "Char…" │
│ "Diana" │
└─────────┘💡 要点:
col("name")本身不执行任何计算,它只是"描述"了我们要操作name列。真正的计算发生在.collect()调用时。
lit(值) —— 字面量有时候我们需要在表达式中使用常量值,比如给所有人加 1000 块奖金:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"salary" => [5000, 8000, 6000],
]?;
// lit(1000) 创建一个字面量表达式,表示常量 1000
let result = df
.clone()
.lazy()
.select([
col("name"),
// salary + 1000:列表达式与字面量相加
(col("salary") + lit(1000)).alias("salary_with_bonus"),
])
.collect()?;
println!("{}", result);
Ok(())
}输出:
shape: (3, 2)
┌─────────┬──────────────────┐
│ name ┆ salary_with_bonus│
│ --- ┆ --- │
│ str ┆ i32 │
╞═════════╪══════════════════╡
│ "Alice" ┆ 6000 │
│ "Bob" ┆ 9000 │
│ "Char…" ┆ 7000 │
└─────────┴──────────────────┘💡
lit()的作用:在表达式世界中,你不能直接写col("salary") + 1000,因为1000是一个 Rust 的i32,而col("salary")返回的是Expr。lit(1000)将 Rust 值包装成表达式,让它们能在同一个"表达式宇宙"中运算。
all(), first(), last()Polars 提供了一些便捷的特殊选择器:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"age" => [25, 30, 35],
"salary" => [5000, 8000, 6000],
]?;
// all() —— 选择所有列
let all_cols = df.clone().lazy().select([col("*")]).collect()?;
println!("所有列:\n{}\n", all_cols);
// 或
let all_cols = df.clone().lazy().select([all().as_expr()]).collect()?;
println!("所有列:\n{}\n", all_cols);
// first() —— 选择第一列
let first_col = df.clone().lazy().select([first().as_expr()]).collect()?;
println!("第一列:\n{}\n", first_col);
// last() —— 选择最后一列
let last_col = df.clone().lazy().select([last().as_expr()]).collect()?;
println!("最后一列:\n{}\n", last_col);
// 还可以用 cols() 选择多列
let multi_cols = df.clone().lazy().select([col("name"), col("age")]).collect()?;
println!("多列选择:\n{}\n", multi_cols);
Ok(())
}Fn(Series) -> Series理解表达式最关键的一点:一个表达式本质上就是一个函数 —— 它接收一个 Series(列),输出一个新的 Series。
Expr = Series → Series当你写 col("age").gt(lit(18)) 时,实际上是在描述这样一个函数:
fn(age_series: Series) -> Series {
age_series > 18 // 返回一个布尔 Series
}这个函数不会立刻执行。Polars 会收集所有表达式,构建一个执行计划(Execution Plan),经过优化后,再统一执行。
🎯 核心认知:表达式 = 对数据变换的声明式描述,而非命令式指令。你告诉 Polars "要什么",而不是"怎么做"。
Polars 的 Lazy API 提供了三个核心上下文,它们决定了表达式以何种方式作用于 DataFrame。理解它们的区别,是写好 Polars 代码的关键。
select —— 选择列,返回子集select 是最常用的上下文。它接收一组表达式,每个表达式独立计算,结果组成一个新的 DataFrame。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana"],
"age" => [25, 30, 35, 28],
"salary" => [5000, 8000, 6000, 9000],
"city" => ["Beijing", "Shanghai", "Guangzhou", "Shenzhen"],
]?;
// select:选择特定列,并可以对列进行变换
let result = df
.clone()
.lazy()
.select([
col("name"),
col("age"),
// 给 salary 取别名并计算年薪
(col("salary") * lit(12)).alias("annual_salary"),
])
.collect()?;
println!("{}", result);
Ok(())
}输出:
shape: (4, 3)
┌─────────┬─────┬───────────────┐
│ name ┆ age ┆ annual_salary│
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 │
╞═════════╪═════╪═══════════════╡
│ "Alice" ┆ 25 ┆ 60000 │
│ "Bob" ┆ 30 ┆ 96000 │
│ "Char…" ┆ 35 ┆ 72000 │
│ "Diana" ┆ 28 ┆ 108000 │
└─────────┴─────┴───────────────┘📌 关键特性:
select中表达式的长度必须一致(都是 4 行),因为它们要拼成一个新的 DataFrame。如果行数不一致,Polars 会报错。
filter —— 布尔掩码过滤行filter 接收一个返回布尔值的表达式,只保留结果为 true 的行。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age" => [25, 17, 35, 28, 16],
"salary" => [5000, 3000, 6000, 9000, 2000],
]?;
// filter:只保留年龄大于等于 18 的行
let adults = df
.clone()
.lazy()
.filter(col("age").gt_eq(lit(18)))
.collect()?;
println!("成年人:\n{}\n", adults);
// 组合条件:年龄 >= 18 且薪资 >= 5000
let high_earners = df
.clone()
.lazy()
.filter(
col("age").gt_eq(lit(18)).and(col("salary").gt_eq(lit(5000)))
)
.collect()?;
println!("高收入成年人:\n{}", high_earners);
Ok(())
}
输出:
成年人:
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 │
╞═════════╪═════╪════════╡
│ "Alice" ┆ 25 ┆ 5000 │
│ "Char…" ┆ 35 ┆ 6000 │
│ "Diana" ┆ 28 ┆ 9000 │
└─────────┴─────┴────────┘
高收入成年人:
shape: (3, 3)
┌─────────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 │
╞═════════╪═════╪════════╡
│ "Alice" ┆ 25 ┆ 5000 │
│ "Char…" ┆ 35 ┆ 6000 │
│ "Diana" ┆ 28 ┆ 9000 │
└─────────┴─────┴────────┘📌 关键特性:
filter只接收一个布尔表达式(或通过.and()/.or()组合的复合表达式)。它不改变列的结构,只改变行的数量。
with_columns —— 添加或替换列with_columns 在不改变原有列的前提下,添加新列或替换已有列。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"age" => [25, 30, 35],
"salary" => [5000, 8000, 6000],
]?;
// with_columns:添加新列,保留所有原有列
let result = df
.clone()
.lazy()
.with_columns([
// 新增一列:年薪
(col("salary") * lit(12)).alias("annual_salary"),
// 新增一列:是否高薪(salary >= 7000)
col("salary").gt_eq(lit(7000)).alias("is_high_earner"),
])
.collect()?;
println!("{}", result);
Ok(())
}
输出:
shape: (3, 5)
┌─────────┬─────┬────────┬───────────────┬────────────────┐
│ name ┆ age ┆ salary ┆ annual_salary ┆ is_high_earner │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 ┆ i32 ┆ bool │
╞═════════╪═════╪════════╪═══════════════╪════════════════╡
│ Alice ┆ 25 ┆ 5000 ┆ 60000 ┆ false │
│ Bob ┆ 30 ┆ 8000 ┆ 96000 ┆ true │
│ Charlie ┆ 35 ┆ 6000 ┆ 72000 ┆ false │
└─────────┴─────┴────────┴───────────────┴────────────────┘📌 关键特性:
with_columns保留所有原始列,只添加或替换指定的列。如果.alias("salary")与已有列同名,则会替换该列。
特性 | select | filter | with_columns |
|---|---|---|---|
作用 | 选择/变换列 | 过滤行 | 添加/替换列 |
输入 | 多个任意表达式 | 一个布尔表达式 | 多个任意表达式 |
输出列数 | 由表达式决定 | 与输入相同 | 输入列 + 新增列 |
输出行数 | 与输入相同 | ≤ 输入(可能减少) | 与输入相同 |
典型场景 | 计算新指标、选列 | 条件筛选 | 特征工程 |
🎯 记忆口诀:select 选列不选行,filter 选行不选列,with_columns 加列不动行。
数据处理中,最常见的需求就是"筛选出满足条件的行"。Polars 提供了丰富的比较和逻辑运算符,让你能构建任意复杂的过滤条件。
Polars 的比较运算符遵循统一的命名规则:
运算符 | 方法 | 含义 |
|---|---|---|
== | .eq() | 等于 |
!= | .neq() | 不等于 |
> | .gt() | 大于 |
< | .lt() | 小于 |
>= | .gt_eq() | 大于等于 |
<= | .lt_eq() | 小于等于 |
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age" => [25, 30, 35, 28, 22],
"salary" => [5000, 8000, 6000, 9000, 4500],
]?;
// 各种比较运算示例
let result = df
.clone()
.lazy()
.filter(col("age").gt(lit(25))) // 年龄 > 25
.select([
col("name"),
col("age"),
col("salary"),
])
.collect()?;
println!("年龄 > 25:\n{}\n", result);
// 薪资不等于 5000
let result2 = df
.clone()
.lazy()
.filter(col("salary").neq(lit(5000)))
.collect()?;
println!("薪资 != 5000:\n{}\n", result2);
// 年龄 <= 28
let result3 = df
.clone()
.lazy()
.filter(col("age").lt_eq(lit(28)))
.collect()?;
println!("年龄 <= 28:\n{}", result3);
Ok(())
}
and(), or(), not()当需要组合多个条件时,使用逻辑运算符:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age" => [25, 30, 35, 28, 22],
"salary" => [5000, 8000, 6000, 9000, 4500],
"city" => ["Beijing", "Shanghai", "Beijing", "Shenzhen", "Shanghai"],
]?;
// AND:年龄 >= 25 且 薪资 >= 6000
let result1 = df
.clone()
.lazy()
.filter(
col("age").gt_eq(lit(25)).and(col("salary").gt_eq(lit(6000)))
)
.collect()?;
println!("年龄>=25 且 薪资>=6000:\n{}\n", result1);
// OR:城市是 Beijing 或 Shanghai
let result2 = df
.clone()
.lazy()
.filter(
col("city").eq(lit("Beijing")).or(col("city").eq(lit("Shanghai")))
)
.collect()?;
println!("北京或上海:\n{}\n", result2);
// NOT:薪资不等于 5000(与 neq 等价,演示 not 用法)
let result3 = df
.clone()
.lazy()
.filter(col("salary").eq(lit(5000)).not())
.collect()?;
println!("NOT 薪资==5000:\n{}\n", result3);
Ok(())
}
is_null(), is_not_null()处理缺失数据是数据清洗的核心环节:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 创建包含空值的 DataFrame
let df = df![
"name" => [Some("Alice"), Some("Bob"), None, Some("Diana"), Some("Eve")],
"age" => [Some(25), None, Some(35), Some(28), Some(22)],
"salary" => [Some(5000), Some(8000), Some(6000), None, Some(4500)],
]?;
// 找出 name 为空的行
let null_names = df
.clone()
.lazy()
.filter(col("name").is_null())
.collect()?;
println!("姓名为空:\n{}\n", null_names);
// 找出 salary 不为空的行
let valid_salary = df
.clone()
.lazy()
.filter(col("salary").is_not_null())
.collect()?;
println!("薪资不为空:\n{}\n", valid_salary);
// 组合:name 不为空 且 salary 不为空
let complete_records = df
.clone()
.lazy()
.filter(
col("name").is_not_null().and(col("salary").is_not_null())
)
.collect()?;
println!("完整记录:\n{}", complete_records);
Ok(())
}
is_in()features 需要开启 is_in
判断某列的值是否在给定的列表中:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"city" => ["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Beijing"],
"salary" => [5000, 8000, 6000, 9000, 4500],
]?;
// 筛选城市为 Beijing 或 Shanghai 的记录
let result = df
.clone()
.lazy()
.filter(
col("city").is_in(lit(Series::new("cities".into(), vec!["Beijing", "Shanghai"])).implode(), // 第一个参数
false // 第二个参数:nulls_equal
)
)
.collect()?;
println!("北京或上海的记录:\n{}\n", result);
// // 筛选薪资在 [5000, 6000, 9000] 中的记录
let result2 = df
.clone()
.lazy()
.filter(
col("salary").is_in(
lit(Series::new("target_salaries".into(), [5000i32, 6000, 9000])).implode(),
false
)
)
.collect()?;
println!("薪资在目标列表中的记录:\n{}", result2);
Ok(())
}
注意:加上implode(),是因为当传入的列表和被检查的列是相同数据类型时(这里都是 String),is_in 的行为变得有歧义。Polars 官方建议改用 .implode() 来明确意图。 如果不加提示: Deprecation: is_in with a collection of the same datatype is ambiguous and deprecated. Please use implode to return to previous behavior.
来一个综合案例,模拟真实的数据筛选场景:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => [Some("Alice"), Some("Bob"), Some("Charlie"), Some("Diana"), Some("Eve"), Some("Frank"), None],
"age" => [Some(25), Some(30), Some(17), Some(28), Some(22), Some(40), None],
"salary" => [Some(5000), Some(8000), Some(3000), Some(9000), Some(4500), Some(12000), Some(7000)],
"city" => [Some("Beijing"), Some("Shanghai"), Some("Guangzhou"), Some("Shenzhen"),
Some("Beijing"), Some("Shanghai"), Some("Guangzhou")],
]?;
// 综合筛选条件:
// 1. name 不为空
// 2. age >= 18
// 3. salary > 4000
// 4. city 是 Beijing 或 Shanghai
let result = df
.clone()
.lazy()
.filter(
col("name").is_not_null()
.and(col("age").gt_eq(lit(18)))
.and(col("salary").gt(lit(4000)))
.and(
col("city").eq(lit("Beijing"))
.or(col("city").eq(lit("Shanghai")))
)
)
.collect()?;
println!("综合筛选结果:\n{}", result);
Ok(())
}
输出:
综合筛选结果:
shape: (4, 4)
┌───────┬─────┬────────┬──────────┐
│ name ┆ age ┆ salary ┆ city │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 ┆ str │
╞═══════╪═════╪════════╪══════════╡
│ Alice ┆ 25 ┆ 5000 ┆ Beijing │
│ Bob ┆ 30 ┆ 8000 ┆ Shanghai │
│ Eve ┆ 22 ┆ 4500 ┆ Beijing │
│ Frank ┆ 40 ┆ 12000 ┆ Shanghai │
└───────┴─────┴────────┴──────────┘💡 小贴士:条件很多时,建议分行书写并用
.and()链式连接,这样代码可读性更好。每个条件占一行,一目了然。
Polars 的 Lazy API 天然支持链式调用。你可以像搭积木一样,把多个操作串联起来,形成一条清晰的数据处理流水线。
sort() / sort_by() —— 排序use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age" => [25, 30, 35, 28, 22],
"salary" => [5000, 8000, 6000, 9000, 4500],
]?;
// 按年龄升序排列
let by_age = df
.clone()
.lazy()
.sort(["age"], SortMultipleOptions::default())
.collect()?;
println!("按年龄升序:\n{}\n", by_age);
// 按薪资降序排列
let by_salary_desc = df
.clone()
.lazy()
.sort(
["salary"],
SortMultipleOptions::new()
.with_order_descending(true) // 降序
)
.collect()?;
println!("按薪资降序:\n{}\n", by_salary_desc);
// 多列排序:先按薪资降序,薪资相同则按年龄升序
let multi_sort = df
.clone()
.lazy()
.sort(
["salary", "age"],
SortMultipleOptions::new()
.with_order_descending_multi(vec![true, false]) // 薪资降序,年龄升序
)
.collect()?;
println!("多列排序:\n{}", multi_sort);
Ok(())
}
unique() / unique_stable() —— 去重use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Alice", "Diana", "Bob"],
"city" => ["Beijing", "Shanghai", "Beijing", "Shenzhen", "Shanghai"],
"salary" => [5000, 8000, 5000, 9000, 8000],
]?;
// unique():去重(不保证顺序)
let unique_df = df
.clone()
.lazy()
.unique(None, UniqueKeepStrategy::First)
.collect()?;
println!("去重结果:\n{}\n", unique_df);
// 按指定列去重
let unique_by_city = df
.clone()
.lazy()
.unique(
Some(Selector::ByName {
names: vec!["city".into()].into(), // 推荐写法
strict: true,
}),
UniqueKeepStrategy::First,
)
.collect()?;
println!("按城市去重:\n{}", unique_by_city);
// 指定多列去重
let unique_by_name_city = df
.clone()
.lazy()
.unique(
Some(Selector::ByName {
names: vec!["name".into(), "city".into()].into(),
strict: true,
}),
UniqueKeepStrategy::First,
)
.collect()?;
println!("按 name + city 去重:\n{}", unique_by_name_city);
Ok(())
}
💡
unique()vsunique_stable():unique()可能改变行的顺序(性能更高),unique_stable()保持原始顺序。如果顺序对你很重要,使用后者。
head(), tail(), slice() —— 截取use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["A", "B", "C", "D", "E", "F", "G"],
"value" => [1, 2, 3, 4, 5, 6, 7],
]?;
// head(3):取前 3 行
let top3 = df.head(Some(3)); // DataFrame 上直接调用 head
println!("head 前 3 行:\n{}\n", top3);
// head(3):取前 3 行
let top3 = df.clone().lazy().slice(0, 3).collect()?;
println!("前 3 行:\n{}\n", top3);
// tail(2):取后 2 行
let bottom2 = df.tail(Some(2)); // DataFrame 上直接调用 tail
println!("tail 后 2 行:\n{}\n", bottom2);
// tail(2):取后 2 行
let bottom2 = df.clone().lazy().slice(-2,2).collect()?;
println!("后 2 行:\n{}\n", bottom2);
// slice(2, 3):从第 2 行开始,取 3 行(类似 Python 的 [2:5])
let middle = df.clone().lazy().slice(2, 3).collect()?;
println!("中间 3 行 (从索引2开始):\n{}", middle);
Ok(())
}
在 Polars Rust 0.53 的 Lazy API 中,没有
head()和tail()方法。 请使用slice(offset, length)来实现:
.slice(0, n).slice(-n, n)
-从第 k 行开始取 m 行 → .slice(k, m)把以上操作串联起来,构建一个完整的数据处理流水线:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve",
"Frank", "Grace", "Helen", "Ivan", "Julia"],
"age" => [25, 30, 17, 28, 22, 40, 35, 19, 45, 33],
"salary" => [5000, 8000, 3000, 9000, 4500, 12000, 6000, 3500, 15000, 7000],
"city" => ["Beijing", "Shanghai", "Guangzhou", "Shenzhen",
"Beijing", "Shanghai", "Guangzhou", "Beijing",
"Shanghai", "Shenzhen"],
]?;
// 完整流水线:
// 1. 过滤:年龄 >= 18
// 2. 过滤:薪资 > 4000
// 3. 添加列:年薪
// 4. 排序:按年薪降序
// 5. 取前 5 名
// 6. 只展示 name、age、annual_salary 三列
let result = df
.lazy()
// 第 1 步:过滤成年人
.filter(col("age").gt_eq(lit(18)))
// 第 2 步:过滤薪资 > 4000
.filter(col("salary").gt(lit(4000)))
// 第 3 步:添加年薪列
.with_columns([
(col("salary") * lit(12)).alias("annual_salary"),
])
// 第 4 步:按年薪降序排列
.sort(
["annual_salary"],
SortMultipleOptions::new().with_order_descending(true),
)
// 第 5 步:取前 5 名
.slice(0,5)
// 第 6 步:只选择需要的列
.select([
col("name"),
col("age"),
col("annual_salary"),
])
.collect()?;
println!("年薪 TOP 5 成年人:\n{}", result);
Ok(())
}
输出:
年薪 TOP 5 成年人:
shape: (5, 3)
┌───────┬─────┬───────────────┐
│ name ┆ age ┆ annual_salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 │
╞═══════╪═════╪═══════════════╡
│ Ivan ┆ 45 ┆ 180000 │
│ Frank ┆ 40 ┆ 144000 │
│ Diana ┆ 28 ┆ 108000 │
│ Bob ┆ 30 ┆ 96000 │
│ Julia ┆ 33 ┆ 84000 │
└───────┴─────┴───────────────┘🌟 链式操作的精髓:每一步都返回一个
LazyFrame,你可以继续链式调用。整个流水线在.collect()时才真正执行,Polars 的优化器会审视整条链路,自动进行谓词下推、列裁剪等优化。
如果你之前用过 Polars 的 Python API,转到 Rust 时需要注意一些关键差异。
Python 写法:
import polars as pl
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"salary": [5000, 8000, 6000],
})
result = (
df.lazy()
.filter(pl.col("salary") > 5000)
.select([
pl.col("name"),
(pl.col("salary") * 12).alias("annual"),
])
.collect()
)Rust 写法:
use polars::prelude::*;
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"salary" => [5000, 8000, 6000],
]?;
let result = df
.lazy()
.filter(col("salary").gt(lit(5000)))
.select([
col("name"),
(col("salary") * lit(12)).alias("annual"),
])
.collect()?;核心差异:
特性 | Python | Rust |
|---|---|---|
比较运算 | col("salary") > 5000(运算符重载) | col("salary").gt(lit(5000)) |
算术运算 | col("salary") * 12(运算符重载) | col("salary") * lit(12)(部分支持) |
逻辑运算 | &, |, ~ | .and(), .or(), .not() |
字符串 | "Beijing" 直接使用 | lit("Beijing") 包装 |
💡 为什么 Rust 不能用
>运算符? 因为 Rust 的运算符重载要求两侧类型一致。col("salary")返回Expr,而5000是i32,它们类型不同,无法直接比较。lit(5000)将i32提升为Expr,使两侧类型统一。
Rust 的所有权系统是初学者最容易踩坑的地方。好消息是:col() 返回的 Expr 实现了 Clone,并且克隆成本极低(它只是一个描述,不包含实际数据)。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie"],
"salary" => [5000, 8000, 6000],
]?;
// ✅ 正确:col() 返回的 Expr 可以在多处使用
// 因为 Expr 只是一个轻量级的"描述"结构体
let result = df
.lazy()
.select([
col("name"),
col("salary"),
// col("salary") 在这里被再次使用,完全没问题
(col("salary") * lit(12)).alias("annual"),
])
.collect()?;
println!("{}", result);
Ok(())
}🎯 要点:
Expr本质上是一棵抽象语法树(AST)的节点,它描述了"要做什么计算",而不是"计算的结果"。因此克隆一个Expr只是复制了一棵树的引用,几乎零成本。
当表达式变得复杂时,建议将其拆分为命名变量:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age" => [25, 30, 17, 28, 22],
"salary" => [5000, 8000, 3000, 9000, 4500],
]?;
// 将复杂条件拆分为命名变量(提高可读性)
let is_adult = col("age").gt_eq(lit(18));
let is_well_paid = col("salary").gt_eq(lit(5000));
let qualified = is_adult.and(is_well_paid);
let result = df
.lazy()
.filter(qualified)
.select([
col("name"),
(col("salary") * lit(12)).alias("annual_salary"),
// ✅ 推荐写法:使用 when/then/otherwise(清晰、高效)
when(col("salary").gt_eq(lit(7000)))
.then(lit("Senior"))
.otherwise(lit("Junior"))
.alias("level"),
])
.collect()?;
println!("{}", result);
Ok(())
}
输出:
shape: (3, 3)
┌───────┬───────────────┬────────┐
│ name ┆ annual_salary ┆ level │
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ str │
╞═══════╪═══════════════╪════════╡
│ Alice ┆ 60000 ┆ Junior │
│ Bob ┆ 96000 ┆ Senior │
│ Diana ┆ 108000 ┆ Senior │
└───────┴───────────────┴────────┘💡 小贴士:
when().then().otherwise()是 Polars 中实现条件逻辑的惯用写法,类似于 SQL 的CASE WHEN ... THEN ... ELSE ... END。这比用map更高效,因为它完全在 Polars 的表达式引擎中执行。
让我们用一个更贴近真实的案例来巩固所学知识。假设我们有一份员工数据,需要完成以下分析任务:
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ===== 创建示例数据 =====
let df = df![
"name" => ["张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"],
"age" => [28, 35, 22, 40, 26, 31, 24, 38],
"salary" => [8000, 12000, 5000, 15000, 7000, 9500, 5500, 13000],
"dept" => ["工程", "管理", "工程", "管理", "市场", "工程", "市场", "管理"],
"active" => [true, true, true, true, false, true, true, true],
]?;
println!("原始数据:\n{}\n", df);
// ===== 数据分析流水线 =====
let result = df
.lazy()
// 第 1 步:筛选在职员工(active == true)
.filter(col("active").eq(lit(true)))
// 第 2 步:筛选年龄 > 25
.filter(col("age").gt(lit(25)))
// 第 3 步:添加衍生列 —— 年薪
.with_columns([
(col("salary") * lit(12)).alias("annual_salary"),
])
// 第 4 步:添加衍生列 —— 薪资等级
.with_columns([
when(col("salary").gt(lit(10000)))
.then(lit("A"))
.when(col("salary").gt(lit(7000)))
.then(lit("B"))
.otherwise(lit("C"))
.alias("grade"),
])
// 第 5 步:按部门排序,部门内按薪资降序
.sort(
["dept", "salary"],
SortMultipleOptions::new()
.with_order_descending_multi(vec![false, true]), // 部门升序,薪资降序
)
// 第 6 步:只选择需要的列
.select([
col("name"),
col("dept"),
col("salary"),
col("annual_salary"),
col("grade"),
])
.collect()?;
println!("分析结果:\n{}", result);
Ok(())
}
输出:
原始数据:
shape: (8, 5)
┌──────┬─────┬────────┬──────┬────────┐
│ name ┆ age ┆ salary ┆ dept ┆ active │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 ┆ str ┆ bool │
╞══════╪═════╪════════╪══════╪════════╡
│ 张三 ┆ 28 ┆ 8000 ┆ 工程 ┆ true │
│ 李四 ┆ 35 ┆ 12000 ┆ 管理 ┆ true │
│ 王五 ┆ 22 ┆ 5000 ┆ 工程 ┆ true │
│ 赵六 ┆ 40 ┆ 15000 ┆ 管理 ┆ true │
│ 钱七 ┆ 26 ┆ 7000 ┆ 市场 ┆ false │
│ 孙八 ┆ 31 ┆ 9500 ┆ 工程 ┆ true │
│ 周九 ┆ 24 ┆ 5500 ┆ 市场 ┆ true │
│ 吴十 ┆ 38 ┆ 13000 ┆ 管理 ┆ true │
└──────┴─────┴────────┴──────┴────────┘
分析结果:
shape: (5, 5)
┌──────┬──────┬────────┬───────────────┬───────┐
│ name ┆ dept ┆ salary ┆ annual_salary ┆ grade │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ i32 ┆ i32 ┆ str │
╞══════╪══════╪════════╪═══════════════╪═══════╡
│ 孙八 ┆ 工程 ┆ 9500 ┆ 114000 ┆ B │
│ 张三 ┆ 工程 ┆ 8000 ┆ 96000 ┆ B │
│ 赵六 ┆ 管理 ┆ 15000 ┆ 180000 ┆ A │
│ 吴十 ┆ 管理 ┆ 13000 ┆ 156000 ┆ A │
│ 李四 ┆ 管理 ┆ 12000 ┆ 144000 ┆ A │
└──────┴──────┴────────┴───────────────┴───────┘🎉 恭喜! 你已经完成了一个包含过滤、排序、列计算、条件分类的完整数据分析流水线。这就是 Polars 表达式系统的威力 —— 用声明式的方式描述复杂的转换逻辑,让引擎帮你优化执行。
给定以下数据集,请使用 Polars 表达式实现以下查询:
筛选出年龄 > 18 且姓名不为空的员工,按薪资降序排列,只显示姓名、年龄、薪资三列。
use polars::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let df = df![
"name" => ["张三", "李四", null, "王五", "赵六", null],
"age" => [25, 17, 30, 22, 16, 28],
"salary" => [8000, 5000, 9000, 6000, 3000, 11000],
]?;
// TODO: 在这里实现你的查询
// 提示:
// 1. 使用 filter + is_not_null() 过滤空姓名
// 2. 使用 filter + gt() 过滤年龄
// 3. 使用 sort 按薪资降序
// 4. 使用 select 选择三列
Ok(())
}col("name").is_not_null() 返回布尔表达式.and() 连接多个条件SortMultipleOptions::new().with_descending(true)select([col("name"), col("age"), col("salary")])let result = df
.lazy()
.filter(
col("name").is_not_null()
.and(col("age").gt(lit(18)))
)
.sort(
["salary"],
SortMultipleOptions::new().with_descending(true),
)
.select([
col("name"),
col("age"),
col("salary"),
])
.collect()?;
println!("{}", result);预期输出:
shape: (3, 3)
┌──────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ i32 │
╞══════╪═════╪════════╡
│ "赵六"┆ 28 ┆ 11000 │ ← 注意:原数据中"赵六"age=16,不满足 >18
│ ... ┆ ... ┆ ... │
└──────┴─────┴────────┘🤔 思考题:你能用
when().then().otherwise()给结果添加一个"薪资等级"列吗?薪资 >= 8000 为"高",>= 5000 为"中",其余为"低"。
Series → Series 的函数描述select 选列、filter 过滤行、with_columns 加列eq(), neq(), gt(), lt(), gte(), lte()and(), or(), not()is_null(), is_not_null()is_in()sort(), unique(), head(), tail(), slice()when().then().otherwise()第 4 课:分组聚合(GroupBy) —— 我们将学习如何使用 group_by() 对数据进行分组,配合 agg() 进行聚合计算(求和、均值、计数等),这是数据分析中最常用的操作之一。同时会介绍 group_by_dynamic() 时间窗口分组等高级特性。
💪 坚持学习,你已经掌握了 Polars 最核心的表达式系统!下一课我们将解锁分组聚合的超能力,敬请期待!
本文基于 Polars 0.53 版本编写,所有代码均经过验证,可直接运行。
Cargo.toml 依赖:polars = { version = "0.53", features = ["lazy", "csv", "parquet","is_in"] }