
本系列课程为 rust 量化实战的基础教程
大家好,欢迎来到 Rust Polars 入门课程第 2 课!
上一课我们完成了环境搭建,成功运行了第一个示例,理解了 Polars 核心优势以及 Eager 立即执行 / Lazy 惰性执行的区别。 今天我们正式进入实战基础:彻底吃透 DataFrame 和 Series 这两个最核心的数据结构。
学完本课,你将能够:
df! 宏、DataFrame::new、列向量构建等)head、tail、select、with_columns、rename、drop、schema、dtypes这些内容是后续表达式(Expr)、过滤、分组聚合、多表关联(Join)的基石。真正掌握它们,才算正式踏入 Polars 的大门。
很多同学从 Pandas 转 Rust Polars,第一反应是“用法好像差不多”,但底层设计天差地别,这也是 Polars 性能远超 Pandas 的核心原因。
在 Pandas 中,DataFrame 对外 API 偏向行视角,操作习惯更贴近逐行处理;而底层依然是列式存储(基于 NumPy 数组)。 Polars 基于 Apache Arrow 内存格式 设计,从根上就是纯列式结构,数据组织更加极致、高效。
• 列操作(强烈推荐)
df.select([col("age"), col("name")])只加载需要的列,连续内存读取,速度极快。
• 行遍历(尽量避免)
使用 iter_rows()、get_row() 逐行访问需要跨列跳跃读取内存,缓存失效严重,性能下降 10~100 倍。
核心口诀:在 Polars 里,想快就按列思考,不要按行思考。
Polars 直接复用 Apache Arrow 类型系统,类型严谨、内存布局统一。 本文基于 polars 0.53 讲解,类型名称清晰统一。
Polars 在创建 Series 时会自动推断类型,也可以通过 .cast() 显式转换类型,避免类型推断偏差。
继续使用上一课的项目 polars-course,确保 Cargo.toml 依赖如下:
[package]
name = "polars-course"
version = "0.1.0"
edition = "2024"
[dependencies]
polars = { version = "0.53", features = [
"eager",
"lazy",
"csv",
"json",
"parquet",
"dtype-full",
"performant",
] }新建可执行文件:
src/bin/lesson2.rs后续所有代码都可以放在这个文件中运行。
Polars 是列式结构,因此从列构建 DataFrame 永远最高效、最推荐。
语法最接近 Pandas,可读性极强,日常开发首选。
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(())
}运行:
cargo run --bin lesson2结果:
===== df! 宏创建 DataFrame =====
shape: (4, 4)
┌──────┬─────┬──────┬────────┐
│ name ┆ age ┆ city ┆ active │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i32 ┆ str ┆ bool │
╞══════╪═════╪══════╪════════╡
│ 张三 ┆ 25 ┆ 北京 ┆ true │
│ 李四 ┆ 30 ┆ 上海 ┆ false │
│ 王五 ┆ 28 ┆ 广州 ┆ true │
│ 赵六 ┆ 35 ┆ 深圳 ┆ true │
└──────┴─────┴──────┴────────┘手动创建多个 Series,再组装成 DataFrame。
// 创建列
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()])?;或者
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 长度必须一致,否则直接编译错误。
适合已经有单列 Vec 数据的场景。
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
]?;缺失值是数据分析常态,Polars 原生支持 Option<T>。
let score_with_null = Series::new("score".into(), vec![Some(95), None, Some(87), None]);
let df = df![
"name" => ["张三", "李四", "王五", "赵六"],
"score" => score_with_null
]?;Series 是 Polars 对外的动态类型列接口,ChunkedArray 是底层强类型泛型存储。 日常使用操作 Series 即可,底层运算自动走 ChunkedArray。
// 普通 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();需要确定类型后再转换,避免类型不匹配。
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());
}下面是最常用、最稳定的 API 集合,全部经过验证可运行。
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"] 格式。let result = df![
"name" => ["张三", "李四"],
"age" => [25, 30]
]?
.with_columns([
Series::new("group", ["A", "B"]),
Series::new("level", [1, 2])
])?
.select(&["name", "group"])?;
println!("{}", result);任务:
passed 列(score > 90 为 true)scores → final_scoreages 列完整代码
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(())
}
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
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(())
}所有列长度必须完全一致,不一致则无法构成表。
显式标注类型:
let age = Series::new("age".into(), [25i64, 30i64]);或使用 cast 转换:
let age = age.cast(&DataType::Int64)?;行遍历会破坏连续内存布局,导致 CPU 缓存命中率暴跌,大数据量下性能极差。
手动拆分为列:
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]?;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 条件逻辑点赞 + 在看 + 转发,支持我继续输出高质量工业级 Rust 数据教程!
我们下篇见!