首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust Polars 入门系列 | 第2课:DataFrame 与 Series 基础操作

Rust Polars 入门系列 | 第2课:DataFrame 与 Series 基础操作

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

本系列课程为 rust 量化实战的基础教程

大家好,欢迎来到 Rust Polars 入门课程第 2 课

上一课我们完成了环境搭建,成功运行了第一个示例,理解了 Polars 核心优势以及 Eager 立即执行 / Lazy 惰性执行的区别。 今天我们正式进入实战基础:彻底吃透 DataFrameSeries 这两个最核心的数据结构。

学完本课,你将能够:

  • • 用多种规范方式创建 DataFrame(df! 宏、DataFrame::new、列向量构建等)
  • • 理解 Series 与底层 ChunkedArray 的关系,掌握 Polars 强类型系统
  • • 熟练使用基础操作:headtailselectwith_columnsrenamedropschemadtypes
  • • 深刻理解列式存储(column-oriented) 带来的巨大性能优势
  • • 避开 Polars 常见坑点,写出可编译、高性能的数据处理代码

这些内容是后续表达式(Expr)、过滤、分组聚合、多表关联(Join)的基石。真正掌握它们,才算正式踏入 Polars 的大门。


一、Polars 数据结构哲学:为什么是列式存储?

很多同学从 Pandas 转 Rust Polars,第一反应是“用法好像差不多”,但底层设计天差地别,这也是 Polars 性能远超 Pandas 的核心原因。

在 Pandas 中,DataFrame 对外 API 偏向行视角,操作习惯更贴近逐行处理;而底层依然是列式存储(基于 NumPy 数组)。 Polars 基于 Apache Arrow 内存格式 设计,从根上就是纯列式结构,数据组织更加极致、高效。

核心概念关系

  • DataFrame:一张结构化数据表,由多个 Series(列) 组成,所有 Series 必须长度一致。
  • Series:单列数据,包含:列名 + 数据类型 + 真实数据载体。
  • ChunkedArray:Series 底层真正存储数据的容器,基于 Arrow 格式,支持分块、向量化运算、多线程并行。

为什么要这样设计?

  1. 1. 列式连续存储 同一列数据类型完全相同、内存连续,CPU 缓存命中率极高,天然适配 SIMD 指令,运算速度提升一个数量级。
  2. 2. 零拷贝 / 低拷贝 筛选列、重命名、视图转换等绝大多数操作,只移动元数据指针,不复制真实数据。
  3. 3. 强类型 + 编译期安全 Rust 编译期就会检查类型不匹配问题,避免 Python 那种运行时才报错的类型异常。
  4. 4. 分块存储(Chunked) 数据可分成多个块(Chunk)存储,超大数据集也能高效处理,内存占用更友好。

列操作 vs 行操作:性能天壤之别

列操作(强烈推荐)

代码语言:javascript
复制
df.select([col("age"), col("name")])

只加载需要的列,连续内存读取,速度极快。

行遍历(尽量避免) 使用 iter_rows()get_row() 逐行访问需要跨列跳跃读取内存,缓存失效严重,性能下降 10~100 倍。

核心口诀:在 Polars 里,想快就按列思考,不要按行思考。


二、Polars 数据类型系统(基于 Arrow)

Polars 直接复用 Apache Arrow 类型系统,类型严谨、内存布局统一。 本文基于 polars 0.53 讲解,类型名称清晰统一。

常用基础类型

  • 整数:Int8, Int16, Int32, Int64 / UInt8, UInt16, UInt32, UInt64
  • 浮点:Float32, Float64
  • 字符串:String
  • 布尔:Boolean
  • 时间类型:Date, Datetime, Time, Duration
  • 复杂类型:List, Struct, Categorical(枚举分类)
  • 空值:Null 单独表示缺失值,支持与任意类型组合

Polars 在创建 Series 时会自动推断类型,也可以通过 .cast() 显式转换类型,避免类型推断偏差。


三、项目环境配置

继续使用上一课的项目 polars-course,确保 Cargo.toml 依赖如下:

代码语言:javascript
复制
[package]
name = "polars-course"
version = "0.1.0"
edition = "2024"

[dependencies]
polars = { version = "0.53", features = [
    "eager",
    "lazy",
    "csv",
    "json",
    "parquet",
    "dtype-full",
    "performant",
] }

新建可执行文件:

代码语言:javascript
复制
src/bin/lesson2.rs

后续所有代码都可以放在这个文件中运行。


四、创建 DataFrame 的四种规范方式

Polars 是列式结构,因此从列构建 DataFrame 永远最高效、最推荐

方式 1:最简洁 —— df! 宏

语法最接近 Pandas,可读性极强,日常开发首选。

代码语言:javascript
复制
use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let df = df![
        "name" => ["张三", "李四", "王五", "赵六"],
        "age"  => [25, 30, 28, 35],
        "city" => ["北京", "上海", "广州", "深圳"],
        "active" => [true, false, true, true]
    ]?;

    println!("===== df! 宏创建 DataFrame =====");
    println!("{}", df);

    Ok(())
}

运行

代码语言:javascript
复制
cargo run --bin lesson2

结果

代码语言:javascript
复制
===== df! 宏创建 DataFrame =====
shape: (4, 4)
┌──────┬─────┬──────┬────────┐
│ name ┆ age ┆ city ┆ active │
│ ---  ┆ --- ┆ ---  ┆ ---    │
│ str  ┆ i32 ┆ str  ┆ bool   │
╞══════╪═════╪══════╪════════╡
│ 张三 ┆ 25  ┆ 北京 ┆ true   │
│ 李四 ┆ 30  ┆ 上海 ┆ false  │
│ 王五 ┆ 28  ┆ 广州 ┆ true   │
│ 赵六 ┆ 35  ┆ 深圳 ┆ true   │
└──────┴─────┴──────┴────────┘

方式 2:从 Series 向量创建(最底层、最灵活)

手动创建多个 Series,再组装成 DataFrame。

代码语言:javascript
复制
// 创建列
let name_series = Series::new("name".into(), ["张三", "李四", "王五"]);
let age_series = Series::new("age".into(), [25, 30, 28]);
let score_series = Series::new("score".into(), [88.5, 92.0, 76.5]);
// 行数
 let height = 3;
// 组装成 DataFrame,有两种方法 ,
// 1.DataFrame::new
// 2.DataFrame::new_infer_height

let df = DataFrame::new(height, vec![name_series.into(), age_series.into(), score_series.into()])?;

或者

代码语言:javascript
复制
let df: DataFrame = DataFrame::new_infer_height(vec![
        name_series.into_column(),   // 或 .into()
        age_series.into_column(),
        score_series.into_column(),
    ])?;

在 Polars 0.53 中,DataFrame::new 的签名改变了

注意:所有 Series 长度必须一致,否则直接编译错误。

方式 3:从 Vec 列向量创建

适合已经有单列 Vec 数据的场景。

代码语言:javascript
复制
let names = vec!["Alice", "Bob", "Charlie"];
let ages = vec![28, 34, 25];
let scores = vec![92.5, 88.0, 95.5];

let df = df![
    "name" => names,
    "age" => ages,
    "score" => scores
]?;

方式 4:包含空值(Option)的创建方式

缺失值是数据分析常态,Polars 原生支持 Option<T>

代码语言:javascript
复制
let score_with_null = Series::new("score".into(), vec![Some(95), None, Some(87), None]);
let df = df![
    "name" => ["张三", "李四", "王五", "赵六"],
    "score" => score_with_null
]?;

五、Series 与 ChunkedArray 深度理解

Series 是 Polars 对外的动态类型列接口,ChunkedArray 是底层强类型泛型存储。 日常使用操作 Series 即可,底层运算自动走 ChunkedArray。

1. 创建 Series

代码语言:javascript
复制
// 普通 Series
let temp = Series::new("temperature".into(), [23.5, 25.1, 22.8, 24.0]);

// 带空值
let score = Series::new("score".into(), vec![Some(90), None, Some(85), None]);

// 从迭代器创建
let iter_series: Series = (0..10).map(|x| x * 3).collect();

2. 访问底层强类型 ChunkedArray

需要确定类型后再转换,避免类型不匹配。

代码语言:javascript
复制
let s = Series::new("num", [1, 2, 3, 4]);

// 正确转为 Int32 ChunkedArray
if let Ok(ca) = s.i32() {
    println!("ChunkedArray 长度: {}", ca.len());
    println!("有效值数量: {}", ca.null_count());
}

六、DataFrame 基础 API 全解(可直接运行)

下面是最常用、最稳定的 API 集合,全部经过验证可运行。

代码语言:javascript
复制
fn main() -> PolarsResult<()> {
    // 构建基础 DataFrame
    let mut df = df![
        "name" => ["张三", "李四", "王五", "赵六"],
        "age" => [25, 30, 28, 35],
        "city" => ["北京", "上海", "广州", "深圳"],
        "active" => [true, false, true, true]
    ]?;

    // 1. 基础形状信息
    println!("Shape (行, 列): {:?}", df.shape());
    println!("行数 height: {}", df.height());
    println!("列数 width: {}", df.width());
    println!("空值总数: {}", df.null_count());

    // 2. 查看前几行 / 后几行
    println!("\n===== 前 3 行 =====");
    println!("{}", df.head(Some(3)));

    println!("\n===== 后 2 行 =====");
    println!("{}", df.tail(Some(2)));

    // 3. 查看 Schema 与数据类型
    println!("\n===== Schema =====");
    println!("{:?}", df.schema());

    println!("\n===== 每列类型 =====");
    for (name, dtype) in df.schema().iter() {
        println!("列 {} → 类型 {:?}", name, dtype);
    }

    // 4. 选择列
    let selected = df.select(&["name", "age"])?;
    println!("\n===== 选择 name & age =====");
    println!("{}", selected);

    // 5. 添加新列(官方推荐 with_columns)
    df.with_column(Series::new("age_plus_10".into(), [35, 40, 38, 45]).into())?;
    println!("\n===== 添加 age_plus_10 =====");
    println!("{}", df);

    // 6. 重命名列
    df.rename("city", "location".into())?;
    println!("\n===== 重命名 city → location =====");
    println!("{}", df);

    // 7. 删除列
    let df = df.drop("active")?;
    println!("\n===== 删除 active 列 =====");
    println!("{}", df);



    let result = df![
        "name" => ["张三", "李四"],
        "age" => [25, 30]
    ]?
    .with_column(Series::new("group".into(), ["A", "B"]).into())?
    .with_column(Series::new("level".into(), [1, 2]).into())?
    .select(&["name", "group"])?;

    println!("{}", result);
    Ok(())
}

重点说明

  • select 传入切片需要使用 &["col", "col"] 格式。
  • • 所有修改默认返回新 DataFrame,符合 Rust 不可变优先原则。

链式操作示例(Polars 优雅风格)

代码语言:javascript
复制
let result = df![
    "name" => ["张三", "李四"],
    "age" => [25, 30]
]?
.with_columns([
    Series::new("group", ["A", "B"]),
    Series::new("level", [1, 2])
])?
.select(&["name", "group"])?;

println!("{}", result);

七、课后实战练习(附标准答案)

练习 1:基础必做

任务:

  1. 1. 从 Vec 创建 DataFrame
  2. 2. 添加 passed 列(score > 90 为 true)
  3. 3. 重命名 scoresfinal_score
  4. 4. 删除 ages
  5. 5. 打印 schema 与 shape

完整代码

代码语言:javascript
复制
fn practice_1() -> PolarsResult<()> {
    // 原始数据
    let names = vec!["Alice", "Bob", "Charlie"];
    let ages = vec![28, 34, 25];
    let scores = vec![92.5, 88.0, 95.5];

    // 1. 创建 DF
    let mut df = df![
        "name" => names,
        "ages" => ages,
        "scores" => scores
    ]?;

    println!("===== 原始 DF =====");
    println!("{}", df);

    // 2. 添加 passed 列
    let passed = Series::new("passed".into(), [true, false, true]);
    df.with_column(passed.into())?;

    // 3. 重命名
    df.rename("scores", "final_score".into())?;

    // 4. 删除 ages 列
    let df = df.drop("ages")?;

    // 5. 输出结果
    println!("\n===== 处理后 DF =====");
    println!("{}", df);
    println!("Schema: {:?}", df.schema());
    println!("Shape: {:?}", df.shape());

    Ok(())
}

练习 2:空值 Series 练习

代码语言:javascript
复制
fn practice_2() -> PolarsResult<()> {
    let s_null = Series::new("points".into(), vec![Some(100), None, Some(80), None]);
    let df = df![
        "name" => ["Mike", "Jane", "John", "Lisa"],
        "points" => s_null
    ]?;

    println!("===== 含空值 DF =====");
    println!("{}", df);
    println!("空值数量: {}", df.null_count());

    Ok(())
}

八、作业:通用列转 DataFrame 函数(可直接运行版)

原作业泛型设计过于复杂,不适合新手,下面提供可实现、可编译、可扩展的标准版本。

代码语言:javascript
复制
/// 接收 (列名, 列数据),构建并打印 DataFrame
fn vec_to_dataframe(columns: Vec<(String, Series)>) -> PolarsResult<DataFrame> {
    // 将 (name, Series) 转换为 Column
    let col_vec: Vec<Column> = columns
        .into_iter()
        .map(|(name, series)| {
            // 如果 Series 内部名字和传入的 name 不一致,这里强制设置名字
            series.with_name(name.into()).into_column()
            // 或者如果你确定 Series 名字已经正确,可以直接写:
            // series.into_column()
        })
        .collect();

    let df = DataFrame::new_infer_height(col_vec)?;

    println!("\n===== 构建完成 DataFrame =====");
    println!("{}", df);
    println!("Schema: {:?}", df.schema());
    println!("Shape: {:?}", df.shape());

    Ok(df)
}

// 使用示例
fn homework_demo() -> PolarsResult<()> {
    let cols = vec![
        ("id".to_string(), Series::new("id".into(), [1, 2, 3, 4])),
        ("name".to_string(), Series::new("name".into(), ["A", "B", "C", "D"])),
        ("score".to_string(), Series::new("score".into(), [90.5, 88.0, 95.0, 92.5])),
    ];

    vec_to_dataframe(cols)?;
    Ok(())
}

九、常见问题官方解答

Q1:创建 DataFrame 报错长度不匹配?

所有列长度必须完全一致,不一致则无法构成表。

Q2:类型推断错误怎么办?

显式标注类型:

代码语言:javascript
复制
let age = Series::new("age".into(), [25i64, 30i64]);

或使用 cast 转换:

代码语言:javascript
复制
let age = age.cast(&DataType::Int64)?;

Q3:为什么不推荐逐行遍历?

行遍历会破坏连续内存布局,导致 CPU 缓存命中率暴跌,大数据量下性能极差。

Q4:如何从结构体 Vec 创建 DataFrame?

手动拆分为列:

代码语言:javascript
复制
struct User { name: String, age: i32 }
let users: Vec<User> = ...;

let names: Vec<_> = users.iter().map(|u| u.name.as_str()).collect();
let ages: Vec<_> = users.iter().map(|u| u.age).collect();

let df = df!["name" => names, "age" => ages]?;

十、完整可运行总代码(lesson2.rs)

代码语言:javascript
复制
use polars::prelude::*;

fn main() -> PolarsResult<()> {
    println!("========== 基础创建 ==========");
    base_create()?;

    println!("\n========== 基础 API ==========");
    base_api()?;

    println!("\n========== 练习 1 ==========");
    practice_1()?;

    println!("\n========== 练习 2 ==========");
    practice_2()?;

    println!("\n========== 作业 ==========");
    homework_demo()?;

    Ok(())
}

// 基础创建
fn base_create() -> PolarsResult<()> {
    let df = df![
        "name" => ["张三", "李四", "王五", "赵六"],
        "age" => [25, 30, 28, 35],
        "city" => ["北京", "上海", "广州", "深圳"],
        "active" => [true, false, true, true]
    ]?;
    println!("{}", df);
    Ok(())
}

// 基础 API
fn base_api() -> PolarsResult<()> {
    let mut df = df![
        "name" => ["张三", "李四", "王五", "赵六"],
        "age" => [25, 30, 28, 35],
        "city" => ["北京", "上海", "广州", "深圳"],
        "active" => [true, false, true, true]
    ]?;

    println!("Shape: {:?}", df.shape());
    println!("{}", df.head(Some(2)));

    df.with_column(Series::new("age10".into(), [35, 40, 38, 45]).into())?;
    df.rename("city", "location".into())?;
    let df = df.drop("active")?;
    println!("{}", df);

    Ok(())
}

// 练习 1
fn practice_1() -> PolarsResult<()> {
    let names = vec!["Alice", "Bob", "Charlie"];
    let ages = vec![28, 34, 25];
    let scores = vec![92.5, 88.0, 95.5];

    let mut df = df![
        "name" => names,
        "ages" => ages,
        "scores" => scores
    ]?;

    df.with_column(Series::new("passed".into(), [true, false, true]).into())?;
    df.rename("scores", "final_score".into())?;
    let df = df.drop("ages")?;

    println!("{}", df);
    println!("Schema: {:?}", df.schema());
    Ok(())
}

// 练习 2
fn practice_2() -> PolarsResult<()> {
    let df = df![
        "name" => ["Mike", "Jane", "John", "Lisa"],
        "points" => vec![Some(100), None, Some(80), None]
    ]?;
    println!("{}", df);
    Ok(())
}

// 作业函数
fn vec_to_dataframe(columns: Vec<(String, Series)>) -> PolarsResult<DataFrame> {
    let col_vec: Vec<Column> = columns
        .into_iter()
        .map(|(name, series)| {
            // 如果 Series 内部名字和传入的 name 不一致,这里强制设置名字
            series.with_name(name.into()).into_column()
            // 或者如果你确定 Series 名字已经正确,可以直接写:
            // series.into_column()
        })
        .collect();

    let df = DataFrame::new_infer_height(col_vec)?;
    println!("{}", df);
    println!("Schema: {:?}", df.schema());
    println!("Shape: {:?}", df.shape());
    Ok(df)
}

fn homework_demo() -> PolarsResult<()> {
    let cols = vec![
        ("id".into(), Series::new("id".into(), [1,2,3,4]).into()),
        ("name".into(), Series::new("name".into(), ["A","B","C","D"]).into()),
        ("score".into(), Series::new("score".into(), [90.5,88.0,95.0,92.5]).into()),
    ];
    vec_to_dataframe(cols)?;
    Ok(())
}

结语 + 下节预告

恭喜你完成第 2 课!你已经完全掌握 Polars 的核心骨架:DataFrame + Series + 列式存储思想。 所有高级功能都建立在今天的基础之上。

下一课(第3课):表达式系统 Expr 入门 —— 过滤、选择与排序 你将学到:

  • col()lit() 表达式
  • filter 数据筛选
  • when/then/otherwise 条件逻辑
  • • 排序、去重、简单计算
  • • 声明式 API,写出像 SQL 一样优雅的 Rust 数据代码

点赞 + 在看 + 转发,支持我继续输出高质量工业级 Rust 数据教程!

我们下篇见!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Polars 数据结构哲学:为什么是列式存储?
    • 核心概念关系
    • 为什么要这样设计?
    • 列操作 vs 行操作:性能天壤之别
  • 二、Polars 数据类型系统(基于 Arrow)
    • 常用基础类型
  • 三、项目环境配置
  • 四、创建 DataFrame 的四种规范方式
    • 方式 1:最简洁 —— df! 宏
    • 方式 2:从 Series 向量创建(最底层、最灵活)
    • 方式 3:从 Vec 列向量创建
    • 方式 4:包含空值(Option)的创建方式
  • 五、Series 与 ChunkedArray 深度理解
    • 1. 创建 Series
    • 2. 访问底层强类型 ChunkedArray
  • 六、DataFrame 基础 API 全解(可直接运行)
    • 重点说明
    • 链式操作示例(Polars 优雅风格)
  • 七、课后实战练习(附标准答案)
    • 练习 1:基础必做
    • 练习 2:空值 Series 练习
  • 八、作业:通用列转 DataFrame 函数(可直接运行版)
  • 九、常见问题官方解答
    • Q1:创建 DataFrame 报错长度不匹配?
    • Q2:类型推断错误怎么办?
    • Q3:为什么不推荐逐行遍历?
    • Q4:如何从结构体 Vec 创建 DataFrame?
  • 十、完整可运行总代码(lesson2.rs)
  • 结语 + 下节预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档