《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
17.1.1 什么是Polars
Polars是一个基于 Rust 的数据分析库,它的目标是提供一个高性能的数据分析工具,同时也提供了Python和JavaScript的接口,也就是说这款工具还可以供Python使用。Polars是一个用纯Rust开发的速度极快的DataFrame库,底层使用Apache Arrow内存模型。
数据科学家和数据分析师都对Pandas非常熟悉。对于数据科学领域的从业者来说,几乎无一例外地都会花费大量时间学习用Pandas处理数据。然而Pandas被诟病最多的是其运行速度和大数据集的处理效率。幸运的是,Polars的出现弥补了Pandas的不足。
Polars最核心的概念是表达式(Expressions),也是其拥有快速性能的核心。Polars提供了一个强大的表达式API。表达式API允许你创建和组合多种操作,例如过滤、排序、聚合、窗口函数等。表达式API也可以优化查询性能和内存使用。
17.1.2 Polars和Pandas对比
Polars与Pandas在许多方面具有截然不同的设计与实现。不像Pandas中每个DataFrame都有一个索引列(Pandas的很多操作也是基于索引的,例如join两个DataFrame进行联合查询),Polars并没有索引(Index)概念。主要区别如下:
(1)Polars使用Apache Arrow作为内部数据格式,而Pandas使用NumPy数组。
(2)Polars提供比Pandas更多的并发支持。
(3)Polars支持惰性查询并提供查询优化。
(4)Polars提供了与Pandas相似的API,以便于用户更快地上手。
简单地说,Polars相当于Rust的Pandas,且性能比Pandas要好很多。总体感觉,Polars就是奔着取代Pandas而生的。
17.1.3 为什么需要Polars
跟Pandas比,Polars有如下优势:
(1)Polars取消了DataFrame中的索引。消除索引让Polars更容易操作数据(Pandas中的DataFrame的索引很鸡肋)。
(2)Polars数据底层用Apache Arrow数组表示,而Pandas数据背后用NumPy数组表示。Apache Arrow在加载速度、内存占用和计算效率上都更加高效。
(3)Polars比Pandas支持更多并行操作。因为Polars是用Rust写的,所以可以无畏并发。
(4)Polars支持延迟计算(Lazy Evaluation),Polars会根据请求检验、优化数据以找到加速方法或降低内存占用。另一方面,Pandas仅支持立即计算(Eager Evaluation),即收到请求立即 求值。
Polars就是为了解决Pandas的性能而生的。在很多测试中,Polars比Pandas快2~3倍。Pandas与Polars的对比如表17-1所示。
17.1.4 安装Polars
由于Polars提供Python和JavaScript绑定,因此Polars支持多种语言环境安装。下面阐述针对各种语言的Polars安装。
(1)对于Rust,传统的Rust程序有Cargo进行包管理,只需要在cargo.toml的[dependencies]中加入:
polars = "0.25.1"
或者用cargo add命令即可:
$ cargo add polars
(2)对于Python环境,可以安装Polars的Python语言绑定PyPolars:
$ pip install polars
(3)对于Node环境,可以安装Polars的JavaScript语言绑定:
$ yarn add nodejs-polars
(4)数据科学家和算法工程师更喜欢用Jupyter,在Jupyter环境下需要用evcxr的:dep命令来引入包。在Jupyter中输入代码如下:
:dep polars = {version = "0.25.1"}
17.1.5 创建DataFrame
我们先来看一下如何手动创建DataFrame(数据帧)。
【例17.1】 手动创建DataFrame
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。打开cargo.toml文件准备添加依赖软件包,在[dependencies]下添加如下内容:
polars = { version = "0.25.1", features = ["json"] }
准备添加代码。打开main.rs,添加代码如下:
use polars::prelude::*; //引用Polars
fn main() {
let df = df! [ //定义数据
"Model" => ["iPhone XS", "iPhone 12", "iPhone 13", "iPhone 14", "Samsung S11", "Samsung S12", "Mi A1", "Mi A2"],
"Company" => ["Apple", "Apple", "Apple", "Apple", "Samsung", "Samsung", "Xiao Mi", "Xiao Mi"],
"Sales" => [80, 170, 130, 205, 400, 30, 14, 8],
"Comment" => [None, None, Some("Sold Out"), Some("New Arrival"), None, Some("Sold Out"), None, None],
];
println!("{:?}", &df); //输出数据
}
Polars提供了df!宏来创建DataFrame。df!按列接受数据,每列含有列名和数据,数据以数组形式提供。这里需要注意,如果数据中存在空数据,则需要用None来表示,而Rust是强类型语言,需要列数据类型一致,因此,如果数据中有None存在,则其他非None数据需要用Some()包裹,达到类型一致。
DataFrame实现了std::fmt::Display方法,因此创建的对象可以直接利用println!宏输出。跟Pandas一样,在Jupyter Notebook中Polars DataFrame会以整齐美观的格式输出,并且还很贴心地将每列的数据类型展示出来,非常方便。
在TERMINAL窗口的命令行中输入运行命令cargo run,运行结果如图17-1所示。
图17‑1
这里需要注意,Polars DataFrame跟Pandas DataFrame有一点不同,Polars DataFrame的列名必须是字符串类型。如果列名不是字符串类型,运行时会报错。请看下面的代码:
let df2 = df! [
0 => [Some(0), Some(1), Some(2)],
1 => [Some("x"), Some("y"), Some("z")],
];
println!("{}", &df2);
上面的代码运行会报个错:mismatched types,如图17-2所示。
图17‑2
这是因为列名是i32类型,而不是str字符串类型。除显示列名外,Polars DataFrame还会在列名下面显示该列的数据类型。我们也可以调用dtypes()方法获取各列的数据类型:
df.dtypes()
运行上面的代码会看到下面的输出:
[Utf8, Utf8, Int32, Utf8]
也可以用get_column_names()方法获取所有列名:
df.get_column_names()
输出:
["Model", "Company", "Sales", "Comment"]
也可以通过get_row()方法传入行下标来获取一行数据:
df.get_row(0)
上面的代码会将第一行数据显示出来:
Row([Utf8("iPhone XS"), Utf8("Apple"), Int32(80), Null])
值得注意的是,与Pandas不同,Polars中没有行索引的概念。Polar的设计哲学认为DataFrame不需要行索引。
下面再看一个实例,读取JSON(JavaScript Object Notation,JS对象简谱)数据。JSON是一种轻量级的数据交换格式,它基于 ECMAScript(European Computer Manufacturers Association,欧洲计算机协会制定的JS规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言,易于阅读和编写,同时也易于机器解析和生成,并有效地提升了网络传输效率。JSON是Douglas Crockford在2001年开始推广使用的数据格式,在2005~2006年正式成为主流的数据格式,雅虎和谷歌就在那个时候开始广泛地使用JSON格式。
任何支持的类型都可以通过JSON来表示,例如字符串、数字、对象、数组等,但是对象和数组是比较特殊且常用的两种类型。
- 对象:对象在JS中是使用花括号"{}"包裹起来的内容,数据结构为{key1:value1, key2:value2, ...}的键-值对结构。在面向对象的语言中,key为对象的属性,value 为对应的值。键名可以使用整数和字符串来表示。值可以是任意类型。
- 数组:数组在JS中是方括号"[]"包裹起来的内容,数据结构为 ["java", "javascript", "vb", ...] 的索引结构。在JS中,数组是一种比较特殊的数据类型,它也可以像对象那样使用键-值对,但还是索引使用得多。同样,值可以是任意类型。
【例17.2】 定义并加载JSON数据
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。打开cargo.toml文件准备添加依赖软件包,在[dependencies]下添加如下内容:
polars = { version = "0.25.1", features = ["json"] }
准备添加代码。打开main.rs,添加代码如下:
use std::io::Cursor;
use polars::prelude::*;
fn main() {
let data = r#"[ //定义JSON数据
{"date": "1996-12-16T00:00:00.000", "open": 16.86, "close": 16.86, "high": 16.86, "low": 16.86, "volume": 62442.0, "turnover": 105277000.0},
{"date": "1996-12-17T00:00:00.000", "open": 15.17, "close": 15.17, "high": 16.79, "low": 15.17, "volume": 463675.0, "turnover": 718902016.0},
{"date": "1996-12-18T00:00:00.000", "open": 15.28, "close": 16.69, "high": 16.69, "low": 15.18, "volume": 445380.0, "turnover": 719400000.0},
{"date": "1996-12-19T00:00:00.000", "open": 17.01, "close": 16.4, "high": 17.9, "low": 15.99, "volume": 572946.0, "turnover": 970124992.0}
]"#;
let res = JsonReader::new(Cursor::new(data)).finish();
println!("{:?}", res);
assert!(res.is_ok());
let df = res.unwrap();
println!("{:?}", df); //输出结果
}
在TERMINAL窗口的命令行中输入运行命令cargo run,运行结果如图17-3所示。
图17‑3