首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Polars Rust 第 3 课:数据选择、过滤与排序 —— Expressions 入门

Polars Rust 第 3 课:数据选择、过滤与排序 —— Expressions 入门

作者头像
不吃草的牛德
发布2026-04-23 13:04:31
发布2026-04-23 13:04:31
710
举报
文章被收录于专栏:RustRust

一、开篇引入:表达式 —— Polars 的灵魂 🔥

如果你用过 Pandas,一定写过这样的代码:

代码语言:javascript
复制
df[df["age"] > 18]["name"]

一行代码,做了两件事:先过滤,再选列。简单直观,但背后隐藏着巨大的性能代价 —— Pandas 会为每一步创建中间 DataFrame,数据被反复复制

Polars 的设计哲学完全不同。它引入了一个核心概念:表达式(Expression,简称 Expr)

🧠 把表达式想象成一张配方卡:你不需要立刻下厨做菜,而是先把所有步骤写在卡片上。等卡片写完了,厨师(Polars 的查询优化器)会审视整张卡片,去掉多余步骤、调整烹饪顺序,然后一次性高效地完成所有操作。

这就是 惰性求值(Lazy Evaluation) 的精髓。表达式不是"立刻执行的操作",而是"对操作的描述"。Polars 的优化器可以:

  • 消除冗余列读取(你只需要 2 列,它就不会读 10 列)
  • 下推过滤条件(先过滤再计算,而不是先计算再过滤)
  • 自动并行化(多列操作分配到不同线程)

一句话总结:表达式是 Polars 最强大的武器,掌握它,你就掌握了 Polars 的灵魂。


二、基础表达式:构建数据操作的积木块 🧱

2.1 col("name") —— 列选择器

col() 是最基础的表达式,它告诉 Polars:"我要操作这一列"。

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
shape: (4, 1)
┌─────────┐
│ name    │
│ ---     │
│ str     │
╞═════════╡
│ "Alice" │
│ "Bob"   │
│ "Char…" │
│ "Diana" │
└─────────┘

💡 要点col("name") 本身不执行任何计算,它只是"描述"了我们要操作 name 列。真正的计算发生在 .collect() 调用时。

2.2 lit(值) —— 字面量

有时候我们需要在表达式中使用常量值,比如给所有人加 1000 块奖金:

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
shape: (3, 2)
┌─────────┬──────────────────┐
│ name    ┆ salary_with_bonus│
│ ---     ┆ ---              │
│ str     ┆ i32              │
╞═════════╪══════════════════╡
│ "Alice" ┆ 6000             │
│ "Bob"   ┆ 9000             │
│ "Char…" ┆ 7000             │
└─────────┴──────────────────┘

💡 lit() 的作用:在表达式世界中,你不能直接写 col("salary") + 1000,因为 1000 是一个 Rust 的 i32,而 col("salary") 返回的是 Exprlit(1000) 将 Rust 值包装成表达式,让它们能在同一个"表达式宇宙"中运算。

2.3 特殊选择器:all(), first(), last()

Polars 提供了一些便捷的特殊选择器:

代码语言:javascript
复制
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(())
}

2.4 表达式的本质:Fn(Series) -> Series

理解表达式最关键的一点:一个表达式本质上就是一个函数 —— 它接收一个 Series(列),输出一个新的 Series

代码语言:javascript
复制
Expr = Series → Series

当你写 col("age").gt(lit(18)) 时,实际上是在描述这样一个函数:

代码语言:javascript
复制
fn(age_series: Series) -> Series {
    age_series > 18  // 返回一个布尔 Series
}

这个函数不会立刻执行。Polars 会收集所有表达式,构建一个执行计划(Execution Plan),经过优化后,再统一执行。

🎯 核心认知:表达式 = 对数据变换的声明式描述,而非命令式指令。你告诉 Polars "要什么",而不是"怎么做"。


三、三大查询上下文:select、filter、with_columns 🎛️

Polars 的 Lazy API 提供了三个核心上下文,它们决定了表达式以何种方式作用于 DataFrame。理解它们的区别,是写好 Polars 代码的关键。

3.1 select —— 选择列,返回子集

select 是最常用的上下文。它接收一组表达式,每个表达式独立计算,结果组成一个新的 DataFrame

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
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 会报错。

3.2 filter —— 布尔掩码过滤行

filter 接收一个返回布尔值的表达式,只保留结果为 true 的行。

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
成年人:
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() 组合的复合表达式)。它不改变列的结构,只改变行的数量。

3.3 with_columns —— 添加或替换列

with_columns不改变原有列的前提下,添加新列或替换已有列。

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
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") 与已有列同名,则会替换该列。

3.4 三者对比总结

特性

select

filter

with_columns

作用

选择/变换列

过滤行

添加/替换列

输入

多个任意表达式

一个布尔表达式

多个任意表达式

输出列数

由表达式决定

与输入相同

输入列 + 新增列

输出行数

与输入相同

≤ 输入(可能减少)

与输入相同

典型场景

计算新指标、选列

条件筛选

特征工程

🎯 记忆口诀select 选列不选行,filter 选行不选列,with_columns 加列不动行。


四、布尔掩码与比较运算:精准筛选的艺术 🎯

数据处理中,最常见的需求就是"筛选出满足条件的行"。Polars 提供了丰富的比较和逻辑运算符,让你能构建任意复杂的过滤条件。

4.1 比较运算符

Polars 的比较运算符遵循统一的命名规则:

运算符

方法

含义

==

.eq()

等于

!=

.neq()

不等于

>

.gt()

大于

<

.lt()

小于

>=

.gt_eq()

大于等于

<=

.lt_eq()

小于等于

代码语言:javascript
复制
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(())
}

4.2 逻辑运算符:and(), or(), not()

当需要组合多个条件时,使用逻辑运算符:

代码语言:javascript
复制
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(())
}

4.3 空值判断:is_null(), is_not_null()

处理缺失数据是数据清洗的核心环节:

代码语言:javascript
复制
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(())
}

4.4 成员判断:is_in()

features 需要开启 is_in

判断某列的值是否在给定的列表中:

代码语言:javascript
复制
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.

4.5 组合条件的实际案例

来一个综合案例,模拟真实的数据筛选场景:

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
综合筛选结果:
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 天然支持链式调用。你可以像搭积木一样,把多个操作串联起来,形成一条清晰的数据处理流水线。

5.1 sort() / sort_by() —— 排序

代码语言:javascript
复制
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(())
}

5.2 unique() / unique_stable() —— 去重

代码语言:javascript
复制
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() vs unique_stable()unique() 可能改变行的顺序(性能更高),unique_stable() 保持原始顺序。如果顺序对你很重要,使用后者。

5.3 head(), tail(), slice() —— 截取

代码语言:javascript
复制
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) 来实现:

  • • 前 n 行 → .slice(0, n)
  • • 后 n 行 → .slice(-n, n) -从第 k 行开始取 m 行 → .slice(k, m)

5.4 链式组合实战

把以上操作串联起来,构建一个完整的数据处理流水线:

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
年薪 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 的优化器会审视整条链路,自动进行谓词下推、列裁剪等优化。


六、Rust 中的表达式链式写法:与 Python 的异同 🦀

如果你之前用过 Polars 的 Python API,转到 Rust 时需要注意一些关键差异。

6.1 语法对比

Python 写法

代码语言:javascript
复制
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 写法

代码语言:javascript
复制
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,而 5000i32,它们类型不同,无法直接比较。lit(5000)i32 提升为 Expr,使两侧类型统一。

6.2 Rust 所有权对表达式的影响

Rust 的所有权系统是初学者最容易踩坑的地方。好消息是:col() 返回的 Expr 实现了 Clone,并且克隆成本极低(它只是一个描述,不包含实际数据)。

代码语言:javascript
复制
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 只是复制了一棵树的引用,几乎零成本。

6.3 优雅地组合复杂表达式

当表达式变得复杂时,建议将其拆分为命名变量:

代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
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 的表达式引擎中执行。


七、实战练习:员工数据分析 📊

让我们用一个更贴近真实的案例来巩固所学知识。假设我们有一份员工数据,需要完成以下分析任务:

  1. 1. 筛选出在职且年龄大于 25 的员工
  2. 2. 按部门内薪资降序排列
  3. 3. 计算每个员工的薪资等级和年薪
  4. 4. 只展示姓名、部门、薪资、年薪、等级
代码语言:javascript
复制
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(())
}

输出:

代码语言:javascript
复制
原始数据:
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 且姓名不为空的员工,按薪资降序排列,只显示姓名、年龄、薪资三列。

代码语言:javascript
复制
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(())
}

提示

  1. 1. 空值过滤col("name").is_not_null() 返回布尔表达式
  2. 2. 条件组合:使用 .and() 连接多个条件
  3. 3. 降序排序SortMultipleOptions::new().with_descending(true)
  4. 4. 选列select([col("name"), col("age"), col("salary")])

参考答案框架

代码语言:javascript
复制
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);

预期输出:

代码语言:javascript
复制
shape: (3, 3)
┌──────┬─────┬────────┐
│ name ┆ age ┆ salary │
│ ---  ┆ --- ┆ ---    │
│ str  ┆ i32 ┆ i32    │
╞══════╪═════╪════════╡
│ "赵六"┆ 28  ┆ 11000  │  ← 注意:原数据中"赵六"age=16,不满足 >18
│ ...  ┆ ... ┆ ...    │
└──────┴─────┴────────┘

🤔 思考题:你能用 when().then().otherwise() 给结果添加一个"薪资等级"列吗?薪资 >= 8000 为"高",>= 5000 为"中",其余为"低"。


九、总结与下节预告 📝

本节要点回顾

  1. 1. 表达式(Expr) 是 Polars 的核心抽象,本质是 Series → Series 的函数描述
  2. 2. 三大上下文select 选列、filter 过滤行、with_columns 加列
  3. 3. 比较运算eq(), neq(), gt(), lt(), gte(), lte()
  4. 4. 逻辑运算and(), or(), not()
  5. 5. 空值处理is_null(), is_not_null()
  6. 6. 成员判断is_in()
  7. 7. 链式操作sort(), unique(), head(), tail(), slice()
  8. 8. 条件逻辑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"] }

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust火箭工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、开篇引入:表达式 —— Polars 的灵魂 🔥
  • 二、基础表达式:构建数据操作的积木块 🧱
    • 2.1 col("name") —— 列选择器
    • 2.2 lit(值) —— 字面量
    • 2.3 特殊选择器:all(), first(), last()
    • 2.4 表达式的本质:Fn(Series) -> Series
  • 三、三大查询上下文:select、filter、with_columns 🎛️
    • 3.1 select —— 选择列,返回子集
    • 3.2 filter —— 布尔掩码过滤行
    • 3.3 with_columns —— 添加或替换列
    • 3.4 三者对比总结
  • 四、布尔掩码与比较运算:精准筛选的艺术 🎯
    • 4.1 比较运算符
    • 4.2 逻辑运算符:and(), or(), not()
    • 4.3 空值判断:is_null(), is_not_null()
    • 4.4 成员判断:is_in()
    • 4.5 组合条件的实际案例
  • 五、链式操作:让数据流动起来 🌊
    • 5.1 sort() / sort_by() —— 排序
    • 5.2 unique() / unique_stable() —— 去重
    • 5.3 head(), tail(), slice() —— 截取
    • 5.4 链式组合实战
  • 六、Rust 中的表达式链式写法:与 Python 的异同 🦀
    • 6.1 语法对比
    • 6.2 Rust 所有权对表达式的影响
    • 6.3 优雅地组合复杂表达式
  • 七、实战练习:员工数据分析 📊
  • 八、课后作业 ✏️
    • 题目:员工筛选查询
    • 提示
    • 参考答案框架
  • 九、总结与下节预告 📝
    • 本节要点回顾
    • 下节预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档