Rust Polars:解锁高性能数据分析 — 第二部分
探索 Rust 的 Polars 数据框架、聚合函数及更多
·
关注 发表在 数据科学前沿 ·24 分钟阅读·2023 年 5 月 18 日
–
TL;DR
Rust 编程语言在业界掀起了波澜,并且在数据科学领域逐渐获得关注。它卓越的速度和强大的安全特性受到需要有效管理大型数据集的开发者的高度追捧。Polars 库充分利用了 Rust 的能力,提供了快速高效的复杂数据集处理方法。凭借其卓越的性能,对于那些从事需要快速处理能力的复杂项目的工作者来说,它是一个极具吸引力的选择。
本文作为该系列的延续,旨在揭开 Polars 世界的神秘面纱。在系列的第一部分中,我们学习了 Rust 的 Polars 系列对象及其应用等内容。在这一部分中,我们将探索另一个 Polars 的基本数据结构,即 DataFrame 对象。通过实际操作和代码片段,你将获得执行各种 DataFrame 操作等重要技能。
注意: 本文假设你对 Rust 编程语言有相当基础的了解。
为了本文而开发的笔记本名为 4-polars-tutorial-part-2.ipynb,可以在以下仓库中找到:
[## GitHub - wiseaidev/rust-data-analysis: 终极 Rust 数据分析课程。]
本仓库包含了一系列 Jupyter 笔记本,所有笔记本都由 Rust 内核支持。通过这些笔记本,你将能够…
github.com](https://github.com/wiseaidev/rust-data-analysis?source=post_page-----7c58a3cb7a1f--------------------------------)
目录(TOC)
∘ DataFrame 对象
∘ 索引与切片
∘ 数据清理
∘ 集中趋势测量
∘ Ndarray
∘ 聚合函数
∘ 合并 DataFrame
∘ 结论
∘ 结束语
∘ 资源
DataFrame 对象
Polars 数据框表示(作者提供的图片)
在 Polars 库的核心是一个重要的组件,它作为其基础;即 DataFrame 结构。这一巧妙的 二维数据 表示以 行和列 组织,类似于系列对象,但增加了维度。
DataFrame 初始化
在 Polars 中,初始化数据框和使用强大的 DataFrame 结构一样简单。为了说明 DataFrame 初始化的简单性,下面是创建一个空数据框的代码片段:
let df = DataFrame::default();
现在,让我们深入探讨 Polars 库的灵活性。看看这段代码片段,其中 series 轻松转换为二维 DataFrame:
let s1 = Series::new("Name", &["Mahmoud", "Ali"]);
let s2 = Series::new("Age", &[23, 27]);
let s3 = Series::new("Height", &[1.84, 1.78]);
let df: PolarsResult<DataFrame> = DataFrame::new(vec![s1, s2, s3])?;
println!("{:?}", df.unwrap());
// Output:
// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name ┆ Age ┆ Height │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ f64 │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23 ┆ 1.84 │
// │ Ali ┆ 27 ┆ 1.78 │
// └─────────┴─────┴────────┘
Polars DataFrame 的初始化过程非常简单,从其轻松实现中可以明显看出。此外,[**df!**](https://docs.rs/polars/latest/polars/prelude/macro.df.html)
宏使您能够轻松创建数据框架。以下是利用此宏的示例:
let df: PolarsResult<DataFrame> = df!("Name" => &["Mahmoud", "Ali"],
"Age" => &[23, 27],
"Height" => &[1.84, 1.78]);
描述
Polars 中的 [**describe**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.describe)
方法是一个广泛使用的技术,为数据集提供统计指标概述。该方法创建了一个详尽的表格,包括每列的计数、平均值、标准差、最小值和最大值,以及第 25 到第 75 百分位数范围(中位数)。通过使用此方法,您可以获取有关数据特征的宝贵洞察,例如识别潜在的异常值并有效理解其分布模式。
let df1: DataFrame = df!("categorical" => &["d","e","f"],
"numeric" => &[1, 2, 3],
"object" => &["a", "b", "c"]).unwrap();
println!("{}", df1);
// Output:
// shape: (3, 3)
// ┌─────────────┬─────────┬────────┐
// │ categorical ┆ numeric ┆ object │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str │
// ╞═════════════╪═════════╪════════╡
// │ d ┆ 1 ┆ a │
// │ e ┆ 2 ┆ b │
// │ f ┆ 3 ┆ c │
// └─────────────┴─────────┴────────┘
let df2: DataFrame = df1.describe(None).unwrap();
println!("{}", df2);
// Output:
// shape: (9, 4)
// ┌────────────┬─────────────┬─────────┬────────┐
// │ describe ┆ categorical ┆ numeric ┆ object │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ str ┆ f64 ┆ str │
// ╞════════════╪═════════════╪═════════╪════════╡
// │ count ┆ 3 ┆ 3.0 ┆ 3 │
// │ null_count ┆ 0 ┆ 0.0 ┆ 0 │
// │ mean ┆ null ┆ 2.0 ┆ null │
// │ std ┆ null ┆ 1.0 ┆ null │
// │ … ┆ … ┆ … ┆ … │
// │ 25% ┆ null ┆ 1.5 ┆ null │
// │ 50% ┆ null ┆ 2.0 ┆ null │
// │ 75% ┆ null ┆ 2.5 ┆ null │
// │ max ┆ f ┆ 3.0 ┆ c │
// └────────────┴─────────────┴─────────┴────────┘
头部
与 series 对象类似,[**head**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.head)
方法允许我们快速预览 DataFrame 对象的前几行。此方法节省时间和精力,因为它消除了滚动查看大量记录的需求,这可能会很繁琐和压倒性。当调用时,此函数根据用户定义的参数从原始数据集中返回包含 n 行的新 DataFrame。当传入**None**
时,默认显示十(10)行。让我们考虑以下示例:
let df: DataFrame = df!("Name" => &["Mahmoud", "Bob"],
"Age" => &[23, 27],
"Height" => &[1.84, 1.78]).unwrap();
println!("{}", df.head(None));
// Output:
// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name ┆ Age ┆ Height │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ f64 │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23 ┆ 1.84 │
// │ Bob ┆ 27 ┆ 1.78 │
// └─────────┴─────┴────────┘
默认情况下,**head**
方法显示前十行,但可以通过其参数自定义显示任意数量。例如,**df.head(Some(3))**
将仅返回数据的前三行。此功能使我们能够在深入分析之前验证列名和内容,并提供内容概述。
尾部
就像 series 一样,Polars 中的 [**tail**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.tail)
函数是一个强大的方法,允许您预览任何 DataFrame 对象的最后几行。例如,如果您的 DataFrame 包含有关员工的信息,如姓名、年龄和身高;使用此方法可以快速验证列数据和结构。
let df: DataFrame = df!("Name" => &["Mahmoud", "Bob"],
"Age" => &[23, 27],
"Height" => &[1.84, 1.78]).unwrap();
println!("{}", df.tail(None));
// Output:
// shape: (2, 3)
// ┌─────────┬─────┬────────┐
// │ Name ┆ Age ┆ Height │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ f64 │
// ╞═════════╪═════╪════════╡
// │ Mahmoud ┆ 23 ┆ 1.84 │
// │ Bob ┆ 27 ┆ 1.78 │
// └─────────┴─────┴────────┘
默认情况下,**tail**
方法显示数据集的最后十行,但可以通过指定参数来自定义显示行数。为了进一步说明:**df.tail(Some(3))**
将仅显示示例员工数据框的最后三行。
实质上,使用**tail**
对 DataFrames 进行操作有助于在验证内容或获取其整体布局时节省时间。它提供了一目了然的信息,而无需手动逐行查看!
索引与切片
与系列不同,DataFrame 对象可以使用方括号**[]**
进行索引:
// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 29],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 60000, 250000]).unwrap();
// Indexing using brackets
// Select a single column by name
let name_col = &df["Name"];
// Or
let name_col1 = &df[0];
println!("{:?}", name_col);
println!("{:?}", name_col1);
// Select a single column by name
// Output:
// shape: (3,)
// Series: 'Name' [str]
// [
// "Mahmoud"
// "Ali"
// "ThePrimeagen"
// ]
// Select multiple columns by slicing
subset = &df[..2];
println!("{:?}", subset);
// Output:
// [shape: (3,)
// Series: 'Name' [str]
// [
// "Mahmoud"
// "Ali"
// "ThePrimeagen"
// ],shape: (3,)
// Series: 'Age' [i32]
// [
// 22
// 30
// 29
// ]]
在此示例中,我们构建了一个包含四列的 DataFrame——“Name”、“Age”、“Gender”和“Salary”。然后,我们展示了使用方括号对数据框进行索引的各种技术。为了根据名称提取单列,我们使用了**df[‘Name’]**
。该方法返回一个包含指定列所有值的 Polars Series——在我们的例子中是‘Name’列。采用这种方法在需要从数据框中提取特定信息时非常有用。
随后,通过使用df[…2]进行切片,我们仅选择了某些列的子集,从而创建了另一个 新的 DataFrame,只包含前两列:即**Name**
和**Age**
。这种快速而高效的方法非常适合轻松选择数据框中的多个所需属性。同样,我们可以使用 select 方法选择列的子集,例如,调用[**df.select([“Name”, “Age”])**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.select)
将仅返回**Name**
和‘Gender’列。
let name_age_cols = df.select(["Name", "Age"]).unwrap();
println!("{:?}", name_age_cols);
// Output:
// shape: (3, 2)
// ┌──────────────┬─────┐
// │ Name ┆ Age │
// │ --- ┆ --- │
// │ str ┆ i32 │
// ╞══════════════╪═════╡
// │ Mahmoud ┆ 22 │
// │ Ali ┆ 25 │
// │ ThePrimeagen ┆ 29 │
// └──────────────┴─────┘
或者,我们也可以使用[**column**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.column)
方法来检索特定的列,如下所示:
let name_col = df.column("Name");
println!("{:?}", name_col);
// Output:
// shape: (3,)
// Series: 'Name' [str]
// [
// "Mahmoud"
// "Ali"
// "ThePrimeagen"
// ]
你也可以使用布尔索引来选择行,这被称为掩码。考虑以下示例:
// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 60000, 250000]).unwrap();
let mask = df.column("Age").expect("Age must exist!").gt(25).unwrap();
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);
// Output:
// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36 ┆ M ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘
此外,[**slice**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.slice)
方法允许我们从数据框对象中选择特定的行和列子集。例如,如果我们使用df.slice(2,3),将从索引 2 开始返回三行(使用零基索引)。此外,此选择将包括所有列,从而产生一个完全由三行(如果存在)和四列组成的新数据框。
println!("{:?}", df.slice(2, 3));
// Output:
// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36 ┆ M ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘
另一种选择是使用[**transpose**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.transpose)
函数,该函数翻转矩阵的行和列。这使我们能够通过对其转置形式进行索引来访问单行作为系列。
// Create a sample DataFrame
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 60000, 250000]).unwrap();
println!("{:?}", df.transpose().unwrap()[0]);
// Output:
// shape: (4,)
// Series: 'column_0' [str]
// [
// "Mahmoud"
// "22"
// "M"
// "50000"
// ]
请注意,如文档中所述,这是一项非常昂贵的操作。
数据清理
照片由 Towfiqu barbhuiya 提供,来自 Unsplash
数据清理的过程涉及一个关键步骤,即检测和解决缺失信息。空值的存在会显著影响分析或决策的精确性。幸运的是,Rust 的 Polars 库提供了许多强大的技术来有效地管理这些空白。
空值计数
计算给定数据帧中空值或缺失值的数量是创建另一个数据帧的重要步骤,该数据帧显示每列此类事件的计数。这些数据极其有用,因为它可以快速识别包含缺失信息的列,并量化缺失的数据量。基于这些信息,我们可以做出明智的决策,比如删除那些包含不完整条目的行,或采用插补方法来填补这些值。
// Create a sample DataFrame
let df = df!("Name" => &[Some("Mahmoud"), None, None],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 60000, 250000]).unwrap();
println!("{:?}", df.null_count());
// Output:
// shape: (1, 4)
// ┌──────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ u32 ┆ u32 ┆ u32 ┆ u32 │
// ╞══════╪═════╪════════╪════════╡
// │ 2 ┆ 0 ┆ 0 ┆ 0 │
// └──────┴─────┴────────┴────────┘
重复项
使用此方法可以获得一个布尔掩码,指明数据帧中所有的重复行。这个掩码作为一个有效工具,可以过滤掉这些重复项,并精准地得到一个新的数据帧。要使用 [**is_duplicated**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.is_duplicated)
函数,在你的数据帧上调用它,并将结果掩码分配给一个新变量。随后,将相同的过滤器应用于你的原始数据帧,以消除这些副本。
let df = df!("Name" => &["Mahmoud", "Mahmoud", "ThePrimeagen"],
"Age" => &[22, 22, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 50000, 250000]).unwrap();
let mask = df.is_duplicated().unwrap();
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);
// Output:
// shape: (2, 4)
// ┌─────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞═════════╪═════╪════════╪════════╡
// │ Mahmoud ┆ 22 ┆ M ┆ 50000 │
// │ Mahmoud ┆ 22 ┆ M ┆ 50000 │
// └─────────┴─────┴────────┴────────┘
唯一值
[**is_unique**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.is_unique)
方法提供了一种确定数据帧中每一行是否包含唯一值的方法。这个方法使你能够获得数据集中所有独特行的掩码,这在处理大量数据或进行复杂操作时尤为有利。
要应用这个技术,只需在 DataFrame 对象上调用 **is_unique**
函数。它将生成一个布尔数组,突出显示那些包含唯一元素的行。然后,你可以利用这个数组作为过滤机制,高效地从原始 DataFrame 中提取唯一行。
let df = df!("Name" => &["Mahmoud", "Mahmoud", "ThePrimeagen"],
"Age" => &[22, 22, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[50000, 50000, 250000]).unwrap();
let mask = df.is_unique().unwrap();
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);
// Output:
// shape: (1, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞══════════════╪═════╪════════╪════════╡
// │ ThePrimeagen ┆ 36 ┆ M ┆ 250000 │
// └──────────────┴─────┴────────┴────────┘
删除
从数据帧或序列中删除不必要的信息在数据分析中至关重要。幸运的是,Polars 提供了多种有效的方法来做到这一点。其中一种方法是使用 [**drop**](https://docs.rs/polars/latest/polars/frame/struct.DataFrame.html#method.drop)
函数,该函数允许你删除特定的行或列。
要使用此方法,请指定目标列的名称/标签作为**drop**
方法的参数。值得注意的是,默认情况下,此函数返回一个新的DataFrame对象,其中仅删除了指定的行——原始DataFrame保持不变。这对于期望初始数据集在运行特定函数后永久改变的初学者特别有帮助。例如,考虑一个基于Fruit和Color的DataFrame对象,其中列“Color”在进一步分析中不再需要:
let df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
"Color" => &["Red", "Yellow", "Green"]).unwrap();
我们可以使用**drop**
函数从此数据框中删除标签为“Color”的列:
let df_remain = df.drop("Color").unwrap();
println!("{}", df_remain);
// Output:
// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ --- │
// │ str │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear │
// └───────┘
现在,一个新的数据框对象df_remain持有与原始数据相同的数据,除了“Color”列。经过检查初始数据框,我们可以确认其信息保持不变。
println!("{}", df); // the original DataFrame
// Output:
// shape: (3, 2)
// ┌───────┬────────┐
// │ Fruit ┆ Color │
// │ --- ┆ --- │
// │ str ┆ str │
// ╞═══════╪════════╡
// │ Apple ┆ Red │
// │ Apple ┆ Yellow │
// │ Pear ┆ Green │
// └───────┴────────┘
如果你希望直接对原始DataFrame进行更改,考虑使用[**drop_in_place**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.drop_in_place)
函数,而不是**drop**
。此方法的操作类似于**drop**
,但它在不生成新对象的情况下修改数据框。
let mut df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
"Color" => &["Red", "Yellow", "Green"]).unwrap();
df.drop_in_place("Color"); // remove the row with index 1 ("Color") from df
println!("{:?}", df);
// Output:
// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ --- │
// │ str │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear │
// └───────┘
此外,你还可以通过指定列名作为[**drop_many**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.drop_many)
函数的参数来删除多个列:
let df_dropped_col = df.drop_many(&["Color", ""]);
println!("{:?}", df_dropped_col);
// Output:
// shape: (3, 1)
// ┌───────┐
// │ Fruit │
// │ --- │
// │ str │
// ╞═══════╡
// │ Apple │
// │ Apple │
// │ Pear │
// └───────┘
最后,我们可以使用[**drop_nulls**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.drop_nulls)
函数来删除包含空值或缺失值的任何行:
let df: DataFrame = df!("Fruit" => &["Apple", "Apple", "Pear"],
"Color" => &[Some("Red"), None, None]).unwrap();
let df_clean = df.drop_nulls::<String>(None).unwrap();
println!("{:?}", df_clean);
// Output:
// shape: (1, 2)
// ┌───────┬───────┐
// │ Fruit ┆ Color │
// │ --- ┆ --- │
// │ str ┆ str │
// ╞═══════╪═══════╡
// │ Apple ┆ Red │
// └───────┴───────┘
通过使用[**is_not_null**](https://docs.rs/polars/latest/polars/prelude/fn.is_not_null.html)
方法,我们可以为DataFrame中的任何列创建一个非空掩码。此方法返回一个布尔掩码,用于区分包含空值和不包含空值的值。应用于特定列后,这会创建一个过滤器,其中每个值与其各自行的null或not null状态对应。通过使用这种有效的技术来提取仅符合特定标准的行,我们可以轻松地从新数据框中移除所有缺失数据。例如,要为DataFrame中的“Salary”列创建一个空值掩码,我们可以使用以下代码:
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
let mask = df.column("Salary").expect("Salary must exist!").is_not_null();
println!("{:?}", mask.head(None));
// Output:
// shape: (3,)
// ChunkedArray: 'Age' [bool]
// [
// true
// true
// false
// ]
代码片段为df DataFrame对象的“Salary”列创建了一个空的掩码。它还显示了布尔掩码生成的一些初始值。此过滤器可以应用于仅提取“Salary”列中存在非空条目的行的数据。
let filtered_data = df.filter(&mask).unwrap();
println!("{:?}", filtered_data);
// Output:
// shape: (2, 4)
// ┌─────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞═════════╪═════╪════════╪════════╡
// │ Mahmoud ┆ 22 ┆ M ┆ 50000 │
// │ Ali ┆ 25 ┆ M ┆ 60000 │
// └─────────┴─────┴────────┴────────┘
使用空值掩码可以在管理过滤过程时提供更高的精度。当我们希望基于不同列中的各种条件或空值组合进行过滤时,此方法尤其有用。然而,它需要编写比仅使用**drop_nulls**
函数更多的代码,并且对于大型数据集可能不够高效。
总结这一部分,丢弃行是从 Polars 数据框中删除行或列的常见且方便的操作。修改原始数据或删除空值有几种选项。
填充
Polars 提供了处理缺失数据的有价值的方法——[**fill_null**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.fill_null)
方法。此函数允许我们用指定的方法或值替代DataFrame或Series对象中的空值或缺失值。**fill_null**
的一个常见应用是用一个单一的值替换DataFrame或Series中的所有坏项。你可以通过将标量参数传递给**fill_null**
来实现。例如,如果你想用前面的值替换 DataFrame 中的每个缺失项,只需使用如下所示的**fill_null**
:
let mut df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
let filtered_nulls = df.fill_null(FillNullStrategy::Forward(None)).unwrap();
println!("{:?}", filtered_nulls);
// Output:
// shape: (3, 4)
// ┌──────────────┬─────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ str ┆ i32 │
// ╞══════════════╪═════╪════════╪════════╡
// │ Mahmoud ┆ 22 ┆ M ┆ 50000 │
// │ Ali ┆ 25 ┆ M ┆ 60000 │
// │ ThePrimeagen ┆ 36 ┆ M ┆ 60000 │
// └──────────────┴─────┴────────┴────────┘
需要注意的是,统计函数在分析数据时通常会默认忽略DataFrame中的任何缺失值。然而,理解这些缺失的原因至关重要,因为它们可能会显著影响你的分析结果。此外,是否对这些缺失项进行填充或插补是否合适,取决于多种因素,如其出现的原因——故意的还是收集过程中的偶然错误等,以及进一步处理任务所需的准确性等。
本质上,在决定如何处理缺失数据之前,仔细考虑所有相关方面,将确保准确分析而不影响质量结果!
集中趋势的度量
均值
与 Series 类似,我们可以计算给定数据框中每个单独列的均值。
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
println!("{:?}", df.mean());
// Output:
// shape: (1, 4)
// ┌──────┬───────────┬────────┬─────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ f64 ┆ str ┆ f64 │
// ╞══════╪═══════════╪════════╪═════════╡
// │ null ┆ 27.666667 ┆ null ┆ 55000.0 │
// └──────┴───────────┴────────┴─────────┘
中位数
我们还可以计算给定数据框中每个单独列的中位数。
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
println!("{:?}", df.median());
// Output:
// shape: (1, 4)
// ┌──────┬──────┬────────┬─────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ f64 ┆ str ┆ f64 │
// ╞══════╪══════╪════════╪═════════╡
// │ null ┆ 25.0 ┆ null ┆ 55000.0 │
// └──────┴──────┴────────┴─────────┘
离散度量
Std
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
println!("{:?}", df.std(1));
// Output:
// shape: (1, 4)
// ┌──────┬──────────┬────────┬─────────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ f64 ┆ str ┆ f64 │
// ╞══════╪══════════╪════════╪═════════════╡
// │ null ┆ 7.371115 ┆ null ┆ 7071.067812 │
// └──────┴──────────┴────────┴─────────────┘
方差
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
println!("{:?}", df.var(1));
// Output:
// shape: (1, 4)
// ┌──────┬───────────┬────────┬────────┐
// │ Name ┆ Age ┆ Gender ┆ Salary │
// │ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ f64 ┆ str ┆ f64 │
// ╞══════╪═══════════╪════════╪════════╡
// │ null ┆ 54.333333 ┆ null ┆ 5e7 │
// └──────┴───────────┴────────┴────────┘
Ndarray
如我们在第一篇文章中所见,你可以将数据框转换为 ndarray。这种方法从DataFrame对象创建一个二维**ndarray::Array**
对象。要求 DataFrame 中的所有列都不能为空且为数字类型。它们将被强制转换为相同的数据类型(如果尚未)。它会隐式地将None转换为NaN,对于浮点数据不会失败。
let df = df!("Name" => &["Mahmoud", "Ali", "ThePrimeagen"],
"Age" => &[22, 25, 36],
"Gender" => &["M", "M", "M"],
"Salary" => &[Some(50000), Some(60000), None]).unwrap();
println!("{:?}", df.to_ndarray::<Float64Type>().unwrap());
// Output:
// [[NaN, 22.0, NaN, 50000.0],
// [NaN, 25.0, NaN, 60000.0],
// [NaN, 36.0, NaN, NaN]], shape=[3, 4], strides=[1, 3], layout=Ff (0xa), const ndim=2
现在,你可以对这个数组应用在上一篇文章中讨论的不同操作,标题为:终极 Ndarray 手册:掌握 Rust 科学计算的艺术。
聚合函数
Nicolas COMTE 拍摄的照片,来源于 Unsplash。
在处理大量数据时,分类和理解分组数据是至关重要的。幸运的是,Polars 通过其 [**groupby**](https://docs.rs/polars/latest/polars/prelude/struct.DataFrame.html#method.groupby)
函数提供了一个出色的解决方案。该方法根据特定的键值将数据框拆分成多个块,然后应用计算,并将结果合并回另一个数据框中,这种模式被称为 split-apply-combine。
使用聚合函数,我们可以在这些组内快速执行count、sum或mean等各种操作;这在处理大型数据集时显著提高了计算速度和效率。其他常见的聚合函数包括variance和std。以下是**.groupby**
使用的一些示例:
- 一家零售公司使用
**groupby**
方法按地区和产品类别分析销售数据。这种分析使他们能够确定哪些产品在哪些地区销售良好,并就库存管理和产品促销做出更明智的决策。
let sales_revenue_df: DataFrame = sales_df.groupby(["Region", "Product_Category"]).expect("Columns must exist!").select(["Sales Revenue"]).sum().unwrap();
- 利用
**groupby**
方法,医疗组织可以根据年龄组和病情分析患者数据。
let patient_by_age_condition_df: DataFrame = patients_data.groupby(["Age_Group", "Condition"]).expect("Columns must exist!").select(["Patient ID", "Length of Stay"]).count().unwrap();
- 运输企业可以利用
**groupby**
技术,根据司机和车辆类别分析其车辆的燃油使用情况。这种分析使他们能够发现燃油消耗中的低效环节,从而采取及时的纠正措施,提高燃油效率。
let average_fuel_consumption_df: DataFrame = fuel_data.groupby(["Vehicle_Type", "Driver"]).expect("Columns must exist!").select(["Fuel Consumption"]).mean().unwrap();
- 通过利用
**groupby**
方法,保险公司可以有效地分析基于保单类型和客户人口统计数据的索赔数据。这种分析使他们能够识别高风险客户,同时制定符合其个人需求的保单。
let claims_amount_df: DataFrame = claims_data.groupby(["Policy_Type", "Customer_Demographics"]).expect("Columns must exist!").select(["Claims Amount"]).sum().unwrap();
总之,**.groupby**
方法是一个强大的数据分析工具,允许您以任何想象得到的方式对数据进行分组,并对每个组独立应用任何类型的函数,然后返回单一的数据集。
聚合示例
照片由 Alexander Schimmeck 提供,拍摄于 Unsplash
让我们深入研究一个在 Kaggle 上托管的 flights dataset,并进行一个基本的聚合操作,涉及一个分组列、一个聚合列和一个单独的聚合函数。我们的目标是确定每家航空公司的平均到达延误时间。Polars 提供了多种语法来创建这样的聚合,我们将在本节中探讨。
use std::path::Path;
use polars::prelude::*;
fn read_data_frame_from_csv(
csv_file_path: &Path,
) -> DataFrame {
CsvReader::from_path(csv_file_path)
.expect("Cannot open file.")
.has_header(true)
.finish()
.unwrap()
}
let flights_file_path: &Path = Path::new("/path/to/Flight_on_time_HIX.csv");
let columns = ["Airline", "Origin_Airport", "Destination_Airport", "Departure_Delay_Minutes", "Arrival_Delay_Minutes"];
let flights_df: DataFrame = read_data_frame_from_csv(flights_file_path).select(columns).unwrap();
println!("{:?}", flights_df.head(Some(5)));
// Output:
// shape: (5, 5)
// ┌─────────┬────────────────┬─────────────────────┬─────────────────────────┬───────────────────────┐
// │ Airline ┆ Origin_Airport ┆ Destination_Airport ┆ Departure_Delay_Minutes ┆ Arrival_Delay_Minutes │
// │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
// │ str ┆ str ┆ str ┆ i64 ┆ i64 │
// ╞═════════╪════════════════╪═════════════════════╪═════════════════════════╪═══════════════════════╡
// │ TR ┆ IYF ┆ HIX ┆ 62 ┆ 52 │
// │ TR ┆ HEN ┆ HIX ┆ 15 ┆ 8 │
// │ RO ┆ HIX ┆ IZN ┆ 0 ┆ 0 │
// │ XM ┆ HIX ┆ IZU ┆ 34 ┆ 44 │
// │ XM ┆ HIX ┆ LKF ┆ 144 ┆ 146 │
// └─────────┴────────────────┴─────────────────────┴─────────────────────────┴───────────────────────┘
为了有效地在 DataFrame 中对数据进行分组,重要的是定义分组列,如 **Airline**
,并选择像 **mean**
这样的聚合函数来处理 **Arrival_Delay_Minutes**
列。一旦完成这些操作,只需将分组列放在 **groupby**
方法中,并选择你想要显示的列,然后对其应用聚合函数。这将生成一个新的 DataFrame。
let arr_delay_mean_df: DataFrame = flights_df.groupby(["Airline"]).expect("Airline Column must exist!").select(["Arrival_Delay_Minutes"]).mean().unwrap();
println!("{:?}", arr_delay_mean_df.head(Some(5)));
// Output:
// shape: (5, 2)
// ┌─────────┬────────────────────────────┐
// │ Airline ┆ Arrival_Delay_Minutes_mean │
// │ --- ┆ --- │
// │ str ┆ f64 │
// ╞═════════╪════════════════════════════╡
// │ UG ┆ 34.374332 │
// │ WC ┆ 158.221406 │
// │ TR ┆ 281.309919 │
// │ TO ┆ 24.833333 │
// │ YJ ┆ 11.839243 │
// └─────────┴────────────────────────────┘
通过多个列,可以实现 分组 和 聚合。然而,语法与单列操作有所不同。为了确保在任何类型的分组函数过程中顺利执行,重要的是识别三个关键组件:聚合函数、分组列和聚合列。例如;在这个例子中,我们正在计算 按航空公司 统计的每个 起始机场 的平均出发延迟。
let dep_delay_mean_def: DataFrame = flights_df.groupby(["Airline", "Origin_Airport"]).expect("Airline and Origin_Airport Columns must exist!").select(["Departure_Delay_Minutes"]).mean().unwrap();
println!("{:?}", dep_delay_mean_def.head(Some(5)));
// Output:
// shape: (5, 3)
// ┌─────────┬────────────────┬──────────────────────────────┐
// │ Airline ┆ Origin_Airport ┆ Departure_Delay_Minutes_mean │
// │ --- ┆ --- ┆ --- │
// │ str ┆ str ┆ f64 │
// ╞═════════╪════════════════╪══════════════════════════════╡
// │ TR ┆ ERM ┆ 9.7890625 │
// │ NR ┆ ULZ ┆ 29.857143 │
// │ RO ┆ VYM ┆ 10.722222 │
// │ TJ ┆ ERR ┆ 20.290323 │
// │ NR ┆ XNL ┆ 16.351064 │
// └─────────┴────────────────┴──────────────────────────────┘
如果你熟悉 Python pandas,那么使用 **groupby**
会导致一个 MultiIndex 对象。MultiIndexes 的出现可以在索引和列中找到。然而,Polars 通过不要求开发者进行此类操作,完全消除了这一问题,使其成为数据处理方面比 Pandas 更具优势的替代方案。
合并 DataFrames
不同的 Polars 连接方法(图像作者提供)
Polars 提供了一系列数据操作工具,用于执行诸如合并数据集之类的任务。其中一个工具是 join 方法,它便于连接不同的 DataFrame 对象。要执行此操作,你需要在任意一个 DataFrame 上调用 join 函数,并指定其他参数。为了更好地理解其实际操作,考虑以下代码示例。
let df3: DataFrame = df1.join(other=&df2, left_on=["variable1"], right_on=["variable2"], how=JoinType::Inner, suffix=None).unwrap();
在执行合并操作时,**how**
参数在决定将进行何种类型的合并中起着重要作用。你有几种选项可供选择,包括内连接、左连接、右连接和外连接。
为了确定将作为每个 DataFrame 连接键的确切变量,按需使用 **left_on**
和 **right_on**
参数。这些具体值使得将两个 DataFrame 中的对应行轻松连接起来。
如果有另一个数据框需要与第一个数据框合并,只需使用 **other**
参数来指示这个第二个数据集。根据你希望如何组织数据,这些新信息可以被添加到现有数据集的顶部或追加到其上!
最后,如果两个列在被合并的不同数据集中具有相同的名称,那么使用后缀可以通过在各自列标题的末尾附加唯一字符串来轻松区分它们。以下是如何使用 **join**
函数的示例:
let df1: DataFrame = df!("Carrier" => &["HA", "EV", "VX", "DL"],
"ArrDelay" => &[-3, 28, 0, 1]).unwrap();
let df2: DataFrame = df!("Airline" => &["HA", "EV", "OO", "VX"],
"DepDelay" => &[21, -8, 11, -4]).unwrap();
let df3: DataFrame = df1.join(&df2, ["Carrier"], ["Airline"], JoinType::Inner, None).unwrap();
// or: let df3: DataFrame = df1.inner_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df3.head(Some(5)));
// Output:
// shape: (3, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ i32 │
// ╞═════════╪══════════╪══════════╡
// │ HA ┆ -3 ┆ 21 │
// │ EV ┆ 28 ┆ -8 │
// │ VX ┆ 0 ┆ -4 │
// └─────────┴──────────┴──────────┘
在 Polars 中合并 DataFrames 时,了解不同类型的连接是很重要的。内连接 仅保留两个 DataFrames 中 共同 的行,而 左连接 和 右连接 则保留一个 DataFrame 中的 所有 行,并根据匹配的值从另一个 DataFrame 添加相关数据。
let df1: DataFrame = df!("Carrier" => &["HA", "EV", "VX", "DL"],
"ArrDelay" => &[-3, 28, 0, 1]).unwrap();
let df2: DataFrame = df!("Airline" => &["HA", "EV", "OO", "VX"],
"DepDelay" => &[21, -8, 11, -4]).unwrap();
// left join
let df3: DataFrame = df1.left_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df3.head(Some(5)));
// Right join
let df4: DataFrame = df2.left_join(&df1, ["Airline"], ["Carrier"]).unwrap();
println!("{:?}", df4.head(Some(5)));
let df5: DataFrame = df1.outer_join(&df2, ["Carrier"], ["Airline"]).unwrap();
println!("{:?}", df5.head(Some(5)));
// Output:
// Left Join
// shape: (4, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ i32 │
// ╞═════════╪══════════╪══════════╡
// │ HA ┆ -3 ┆ 21 │
// │ EV ┆ 28 ┆ -8 │
// │ VX ┆ 0 ┆ -4 │
// │ DL ┆ 1 ┆ null │
// └─────────┴──────────┴──────────┘
// Right Join
// shape: (4, 3)
// ┌─────────┬──────────┬──────────┐
// │ Airline ┆ DepDelay ┆ ArrDelay │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ i32 │
// ╞═════════╪══════════╪══════════╡
// │ HA ┆ 21 ┆ -3 │
// │ EV ┆ -8 ┆ 28 │
// │ OO ┆ 11 ┆ null │
// │ VX ┆ -4 ┆ 0 │
// └─────────┴──────────┴──────────┘
// Outer Join
// shape: (5, 3)
// ┌─────────┬──────────┬──────────┐
// │ Carrier ┆ ArrDelay ┆ DepDelay │
// │ --- ┆ --- ┆ --- │
// │ str ┆ i32 ┆ i32 │
// ╞═════════╪══════════╪══════════╡
// │ HA ┆ -3 ┆ 21 │
// │ EV ┆ 28 ┆ -8 │
// │ OO ┆ null ┆ 11 │
// │ VX ┆ 0 ┆ -4 │
// │ DL ┆ 1 ┆ null │
// └─────────┴──────────┴──────────┘
如果某个 DataFrame 中的行没有对应的数据,则会相应地填充空值或缺失值。右连接的工作方式类似,但保留的是右侧 DataFrame 中的所有行。
合并 DataFrames 可以极大地提高你用 Polars 的 join 方法有效分析数据的能力。
结论
图片由 Adeolu Eletu 提供,发布于 Unsplash
本文让你熟悉了 Polars 中的一个基础数据结构 —— DataFrame。此外,我们还探讨了 Polars 中查询、修改和连接数据框的基本概念。因此,这应该能让你在处理 DataFrames 时更有信心。它将成为本系列文章中一个重要的组成部分。
在本文中,我们讨论了以下主题:
-
Polars 的 DataFrame 对象。
-
探索 Polars 中不同的聚合函数。
-
如何在 Polars 中合并 DataFrames 以及这与 Pandas 有何不同。
还有更多。在接下来的文章中,你对 Polars 的知识将扩展到更高级的功能和技巧。通过掌握这个库,你将获得坚实而有价值的技能,使你能够轻松处理复杂的数据分析任务,同时轻松处理大量数据集。
结束语
图片由 Aaron Burden 提供,发布于 Unsplash
当我们结束本教程时,我想向所有投入时间和精力完成它的人员表示诚挚的感谢。与您一起展示 Rust 编程语言的非凡能力,真是我的荣幸。
一如既往,作为对数据科学充满热情的我,承诺从现在开始,每周至少会撰写一篇关于相关主题的综合文章。如果你对我工作的最新动态感兴趣,可以考虑在各种社交媒体平台上与我联系,或者直接联系我,如果有其他需要帮助的地方。
感谢!
资源
[## GitHub - wiseaidev/rust-data-analysis: 终极 Rust 数据分析课程]
这个代码库包含了一系列 Jupyter 笔记本,所有笔记本都由 Rust 内核驱动。通过这些笔记本,你将会…
github.com [## polars::prelude 中的 DataFrame - Rust
一组长度相同的Series
的连续可增长集合。
docs.rs [## Rust By Example
Rust 是一种现代系统编程语言,专注于安全性、速度和并发性。它实现这些目标的方式…
文档 [## Rust 编程语言
由 Steve Klabnik 和 Carol Nichols 编写,Rust 社区贡献者参与。本版本的文本假设您…
Rust: 数据科学中的下一个大事
数据科学家和分析师的上下文指南
·
关注 发表在 Towards Data Science · 25 分钟阅读 · 2023 年 4 月 24 日
–
TL;DR
Rust 在数据科学中脱颖而出,因其卓越的性能和持续的安全特性。尽管它可能没有 Python 的所有功能,但在处理大型数据集时,Rust 提供了出色的效率。此外,开发者可以使用专门设计用于数据分析的各种库,以进一步简化工作流程。通过对该语言复杂性的正确掌握,从业人员可以通过将 Rust 纳入工具箱获得显著优势。
本文将深入探讨 Rust 工具的广泛应用及其在分析鸢尾花数据集中的应用。尽管 Rust 在数据科学项目中的受欢迎程度不及 Python 或 R,但其作为数据科学语言的力量显而易见。其潜力和能力是无尽的,使其成为那些希望将数据科学工作提升到常规手段之外的优秀选择。
注意: 本文假设你对 Rust 及其生态系统有所了解。*
你可以在以下仓库中找到为这篇文章开发的笔记本:
[## GitHub - wiseaidev/rust-data-analysis: 在 Rust 中对鸢尾花数据集进行数据分析…
在 Rust 内核中进行鸢尾花数据集的数据分析。 curl --proto ‘=https’ --tlsv1. 2 -sSf https…
github.com](https://github.com/wiseaidev/rust-data-analysis?source=post_page-----319a03305883--------------------------------)
目录(TOC)
∘ 这篇文章适合谁?
∘ 为什么选择 Rust?
∘ Rust 的优势
∘ 生锈的笔记本
∘ 关于数据集
∘ 读取 CSV 文件
∘ 将 CSV 文件加载到数据框
∘ 转换为 ndarray
∘ Numpy 等效
∘ 共享相似性
∘ 关键差异
∘ 为什么选择 ndarray?
∘ 绘图工具
∘ 散点图
∘ 结论
∘ 结束语
∘ 参考文献
这篇文章适合谁?
本文为那些将 Rust 作为主要编程语言的开发者撰写,并希望开始他们的数据科学之旅。其目的是为他们提供探索性数据分析所需的基本工具,包括加载、转换和可视化数据。无论你是希望了解更多关于 Rust 的初学者,还是希望在项目中使用 Rust 的经验丰富的数据科学家或分析师,这篇文章都将是一个宝贵的资源。
为什么选择 Rust?
Brett Jordan 拍摄于 Unsplash
数十年来,计算机科学家们致力于解决源自 C 和 C++等编程语言的安全问题。他们的努力催生了一类新的系统编程语言,称为“内存安全”语言。这些前沿的编码实践明确旨在防止可能导致恶意网络攻击的内存相关错误。Rust 无疑是这些选项中的先进工具;在当代享有广泛使用和认可。
对于那些不了解的人来说,内存安全问题指的是源自编程错误的漏洞,这些错误与内存的不当使用有关。这些问题可能导致安全漏洞、数据退化和系统故障。因此,越来越强调使用专门设计以确保最佳内存安全水平的编程语言。
像谷歌这样的科技巨头已经认识到与内存相关的问题对软件安全的巨大影响,强调了使用这些语言以防范此类漏洞的绝对必要性¹。这样的认可强有力地证明了采取主动措施保护软件免受潜在威胁的重要性。它突显了这些语言在确保软件开发未来更安全方面的作用。
Meta 正在采用 Rust,因为它在性能和安全性方面的好处,标志着软件工程的新纪元。通过利用 Rust 的现代特性和功能,Meta 确保了强大的产品安全性,同时实现了更高的效率和可扩展性²。
开源社区热烈欢迎 Rust,正如 Linux 内核的采用所证明的那样³。这一发展使得开发者可以利用 Rust 在基于 Linux 的系统上打造可靠和安全的软件。
Rust 是一种极具适应性的编程语言,提供广泛的应用。不论是编写低级系统代码还是构建操作系统内核,Rust 都能创建高性能、安全的软件解决方案。毫不奇怪,IEEE Spectrum 最近将 Rust 排在 2022 年顶级编程语言的第 20 位⁴!最近在 Stackoverflow 中排名第 14 的最受欢迎语言也不足为奇⁵!
作为一家杰出的计算机技术公司,微软已表达了对一种超越当前安全标准的编程语言的需求⁶。作为一种开源编程语言,它似乎是解决这个问题的最可行解决方案之一。在这些选项中,Rust 脱颖而出,因为它在安全性和速度方面表现卓越,是开发中的值得选择。
Mozilla 与三星合作创建了名为 Servo 的网页浏览器,因为 Rust 在构建安全网页浏览器方面表现出色。Servo 的目标是开发一个开创性的 Rust 浏览器引擎,将 Mozilla 在网页浏览器方面的专业技能与三星在硬件方面的专长相结合。该倡议旨在制造一个可以用于桌面电脑和移动设备的创新网页引擎。通过利用两家公司的强项,Servo 有潜力在性能上超越其他现有的网页浏览器。
可悲的是,曾经充满希望的合作突然终止,因为 Mozilla 在响应 2020 年疫情时公布了其重组战略。随着 Servo 团队的解散,许多人对 Rust 的前进势头产生了焦虑,因为该语言已成为开发安全和可靠应用程序的重要组成部分。
尽管如此,尽管经历了这一挫折,Rust 仍然成为当今最受欢迎的编程语言之一,并且在全球开发者中继续获得更多的赞誉。通过优先考虑可靠性、安全性和效率,毫无疑问,Rust 将继续成为未来构建安全网页应用程序的可靠语言。
Pydantic,一个著名的开源项目,已经将其核心实现重写为 Rust,从而显著提高了性能。Pydantic V2 比其前身 Pydantic V1.9.1 快 4 倍到 50 倍,在验证包含常见字段的模型时,性能提高了大约 17 倍。
在最近的一次公告中,微软透露了其计划在成功将 [**dwrite**](https://learn.microsoft.com/en-us/windows/win32/directwrite/dwritecore-overview)
字体解析库移植到 Rust 之后,重写 Windows 内核的计划。微软这一大胆举动标志着编程实践向优先考虑安全性和效率的方向转变。
随着 Rust 在各个行业中继续巩固其作为构建强健和安全应用程序的首选语言的地位,我们可以自信地期待未来安全问题的显著减少。
简而言之,使用 Rust 的主要目的是增强安全性、速度和并发性,即同时运行多个计算的能力。
Rust 的优势。
由Den Harrson拍摄,来源于Unsplash
1. 类 C 的速度。
Rust 被开发以提供类似于 C 编程语言的闪电般的性能。此外,它还提供了内存和线程安全的附加优势。这使得 Rust 成为高性能游戏、数据处理或网络应用程序的理想选择。为进一步说明这一点,请考虑以下代码片段,该代码片段使用 Rust 高效地计算了斐波那契数列:
use std::hint::black_box;
fn fibonacci(n: u64) -> u64 {
match n {
1 | 0 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn main() {
let mut total: f64 = 0.0;
for _ in 1..=20 {
let start = std::time::Instant::now();
black_box(fibonacci(black_box(40)));
let elapsed = start.elapsed().as_secs_f64();
total += elapsed;
}
let avg_time = total / 20.0;
println!("Average time taken: {} s", avg_time);
}
// Average time taken: 0.3688494305 s
上面的代码片段使用递归计算了斐波那契数列中的 第 40 个数字。它的执行时间为 不到一秒,比许多其他语言中的等效代码要快得多。例如,Python 中计算相同斐波那契数列需要大约 22.2 秒,这比 Rust 版本慢得多。
>>> import timeit
>>> def fibonacci(n):
… if n < 2:
… return n
… return fibonacci(n-1) + fibonacci(n-2)
…
>>> timeit.Timer("fibonacci(40)", "from __main__ import fibonacci").timeit(number=1)
22.262923367998155
2. 类型安全。
Rust 旨在在编译时捕获许多错误,而不是在运行时,从而减少最终产品中出现错误的可能性。以下是一个展示 Rust 类型安全性的代码示例:
fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let a = 1;
let b = "2";
let sum = add_numbers(a, b); // Compile error: expected `i32`, found `&str
println!("{} + {} = {}", a, b, sum);
}
上面的代码片段试图将一个整数和一个字符串相加,由于类型安全的原因,Rust 不允许这样做。代码无法编译,并且提供了一个有用的错误信息,指明了问题所在。
3. 内存安全。
Rust 已被精心开发以防止常见的内存错误,包括缓冲区溢出和空指针解引用,从而减少安全漏洞的可能性。以下是一个展示 Rust 内存安全措施的场景:
fn main() {
let mut v = vec![1, 2, 3];
let first = v.get(0); // Compile error: immutable borrow occurs here
v.push(4); // Compile error: mutable borrow occurs here
println!("{:?}", first); // Compile error: immutable borrow later used here
}
上面的代码尝试在持有对其第一个元素的不可变引用的同时向向量中追加一个元素。由于内存安全原因,这在 Rust 中是不允许的,代码无法编译,并且提供了一个有用的错误信息。
4. 真实且安全的并行性。
Rust 的所有权模型提供了一种安全而高效的并行性方式,消除了数据竞争和其他与并发相关的错误。以下是一个展示 Rust 并行性的示例:
use std::thread;
fn main() {
let mut handles = vec![];
let mut x = 0;
for i in 0..10 {
handles.push(thread::spawn(move || {
x += 1;
println!("Hello from thread {} with x = {}", i, x);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
// Output
// Hello from thread 0 with x = 1
// Hello from thread 1 with x = 1
// Hello from thread 2 with x = 1
// Hello from thread 4 with x = 1
// Hello from thread 3 with x = 1
// Hello from thread 5 with x = 1
// Hello from thread 6 with x = 1
// Hello from thread 7 with x = 1
// Hello from thread 8 with x = 1
// Hello from thread 9 with x = 1
上面的代码创建了十个线程,这些线程向控制台打印消息。Rust 的所有权模型保证每个线程对所需资源具有独占访问权,有效地防止了数据竞争和其他与并发相关的错误。
5. 丰富的生态系统。
Rust 提供了一个蓬勃发展的动态生态系统,拥有各种适用于广泛领域的库和工具。例如,Rust 提供了强大的数据分析工具,如 [**ndarray**](https://docs.rs/ndarray/latest/ndarray/)
和 [**polors**](https://www.pola.rs/)
,而其 [**serde**](https://serde.rs/)
库的性能优于任何用 Python 编写的 JSON 库。
这些优势以及其他优势使得 Rust 成为像数据科学家这样的开发者的一个有吸引力的选择,他们寻找一种方便的编程语言,该语言提供了丰富的工具列表。
现在,有了这些认识,让我们探索在 Rust 中可以利用的不同数据分析工具,帮助你高效地进行探索性数据分析 (EDA)。
Rusty Notebooks
图片由 Christopher Gower 提供,来源于 Unsplash
编程爱好者会同意,Rust 由于其极快的速度、可靠性和无与伦比的灵活性,已成为顶级编程语言。然而,新手 Rust 开发者长期面临一个令人生畏的挑战:缺乏一个易于访问的开发环境。
幸运的是,通过不懈的坚持和决心,Rust 开发者突破了这一障碍,提供了一个突破性的解决方案:通过Jupyter Notebook访问 Rust。这是通过一个称为 [**evcxr_jupyter**](https://github.com/evcxr/evcxr/blob/main/evcxr_jupyter/README.md)
的卓越开源项目实现的。它使开发者能够在 Jupyter Notebook 环境中编写和执行 Rust 代码,将他们的编程体验提升到一个新的水平。
要安装 [**evcxr_jupyter**](https://github.com/evcxr/evcxr/blob/main/evcxr_jupyter/README.md)
,你首先需要安装 Jupyter。完成后,你可以运行以下命令安装Rust Jupyter Kernel。但首先,你需要在机器上安装 Rust。
安装了Jupyter后,下一步是安装Rust Jupyter Kernel。但在安装之前,你必须确保机器上已经安装了 Rust。
开始使用。
第一步是设置并安装 Rust。为此,请访问 rustup 网站 并按照说明进行操作,或者运行以下命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly
安装 Rust 后,执行以下命令将安装 Rust Jupyter Kernel,之后你将能够在 Jupyter Notebook 上释放 Rust 的全部潜力。
cargo install evcxr_jupyter
evcxr_jupyter --install
完成后,运行以下命令以启动 Jupyter Notebook:
jupyter notebook
现在,是时候进行探索性数据分析 (EDA) 了。
所需依赖
如果你熟悉 Python 内核及其使用 !pip
安装库的卓越灵活性,那么你会高兴地发现,Rust Jupyter Kernel 中也提供了类似的功能。在这里,你可以使用 :dep
安装所需的 crates 以支持EDA。
安装过程非常简单,如下代码片段所示:
:dep polars = {version = "0.28.0"}
这个 crate 提供了一系列功能,包括加载和转换数据等。现在你已经安装了必要的工具,是时候选择一个数据集,以展示 Rust 在 EDA 中的真正力量。为了简单起见,我选择了 Iris 数据集,一个流行且易于访问的数据集,将为展示 Rust 的数据处理能力提供坚实的基础。
关于数据集
照片由 Pawel Czerwinski 提供,来源于 Unsplash
Iris 数据集在数据科学中至关重要,因为它在各种应用中被广泛使用,从统计分析到机器学习。拥有六列信息,它是进行探索性数据分析的理想数据集。每一列都提供了对 Iris 花卉特征的独特见解,并帮助我们深入了解这一壮丽植物。
-
**Id**
:一个唯一的行标识符。虽然它可能很重要,但在我们接下来的分析中不需要。因此,这一列将从数据集中删除,以有效简化我们的研究过程。 -
**SepalLengthCm**
、**SepalWidthCm**
、**PetalLengthCm**
和**PetalWidthCm**
:每个花样本的萼片和花瓣的尺寸由列中的多变量数据描述。这些值可能包含小数部分,因此需要将其存储为浮点数据类型,如 f32,以进行精确计算。 -
**Species**
:此列包含被收集的 Iris 花卉的具体类型。这些值是类别型的,在分析中需要以不同的方式处理。我们可以将它们转换为数值(整数)值,如u32
,或者保留为字符串以便更方便地处理。现在,我们将使用 String 类型以保持简单。
如您所见,Iris 数据集帮助我们揭示了 Iris 花卉的独特特征,它提供有价值见解的潜力是无限的。我们的后续分析将利用 Rust 的能力和 Polars
crate 进行数据操作,以获得重要的发现。
读取 CSV 文件
照片由 Mika Baumeister 提供,来源于 Unsplash
首先,我们需要通过利用 Rust 出色的特性,选择性地导入必要的组件来导入必需的模块。以下代码片段轻松完成了这一任务。
use polars::prelude::*;
use polars::frame::DataFrame;
use std::path::Path;
现在我们一切就绪,时机已到,掌控数据集并以精准有效的方式处理它。得益于 polars
提供的全面工具,处理数据从未如此轻松;所有必要的组件都包含在其 prelude
中,可以通过一行代码无缝导入。让我们通过这个强大的工具开始导入和处理数据吧!
将 CSV 文件加载到数据框中
照片由 Markus Spiske 提供,来源于 Unsplash
让我们通过以下代码片段深入了解将 CSV 文件加载到 Polars 的 DataFrame 的过程:
fn read_data_frame_from_csv(
csv_file_path: &Path,
) -> DataFrame {
CsvReader::from_path(csv_file_path)
.expect("Cannot open file.")
.has_header(true)
.finish()
.unwrap()
}
let iris_file_path: &Path = Path::new("dataset/Iris.csv");
let iris_df: DataFrame = read_data_frame_from_csv(iris_file_path);
代码首先定义了一个函数 **read_data_frame_from_csv**
,它接受 **CSV**
文件路径并返回一个 **DataFrame**
。代码在该函数中创建了一个 **CsvReader**
对象,使用 **from_path**
方法。然后,它使用 **expect**
和 **has_header**
分别检查文件是否存在和是否有标题。最后,它使用 finish 加载 **CSV**
文件并返回结果 **DataFrame**
,该 **DataFrame**
从 **PolarsResult**
中解包。
这段代码可以轻松地将我们的 **CSV**
数据集加载到 **Polars**
**DataFrame**
中,并开始我们的探索性数据分析。
数据集维度
Lewis Guapo 通过 Unsplash 提供的照片
一旦我们将其加载到 **DataFrame**
中,我们可以利用 **shape()**
方法迅速获得关于行和列的信息。这使我们能够确定样本的数量(**rows**
)和特征(**columns**
),这是进一步研究和建模的基础。
println!("{}", iris_df.shape());
(150, 6)
我们可以看到它返回了一个元组,其中第一个元素表示行数,第二个元素表示列数。如果你对数据集有先验知识,这可能是一个很好的指示,说明你的数据集是否正确加载。这些信息在我们初始化新数组时将会很有帮助。
头部
- 声明:
iris_df.head(Some(5))
- 输出:
shape: (5, 6)
┌─────┬───────────────┬──────────────┬───────────────┬──────────────┬─────────────┐
│ Id ┆ SepalLengthCm ┆ SepalWidthCm ┆ PetalLengthCm ┆ PetalWidthCm ┆ Species │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ str │
╞═════╪═══════════════╪══════════════╪═══════════════╪══════════════╪═════════════╡
│ 1 ┆ 5.1 ┆ 3.5 ┆ 1.4 ┆ 0.2 ┆ Iris-setosa │
│ 2 ┆ 4.9 ┆ 3.0 ┆ 1.4 ┆ 0.2 ┆ Iris-setosa │
│ 3 ┆ 4.7 ┆ 3.2 ┆ 1.3 ┆ 0.2 ┆ Iris-setosa │
│ 4 ┆ 4.6 ┆ 3.1 ┆ 1.5 ┆ 0.2 ┆ Iris-setosa │
│ 5 ┆ 5.0 ┆ 3.6 ┆ 1.4 ┆ 0.2 ┆ Iris-setosa │
└─────┴───────────────┴──────────────┴───────────────┴──────────────┴─────────────┘
尾部
- 声明:
iris_df.tail(Some(5));
- 输出:
shape: (5, 6)
┌─────┬───────────────┬──────────────┬───────────────┬──────────────┬────────────────┐
│ Id ┆ SepalLengthCm ┆ SepalWidthCm ┆ PetalLengthCm ┆ PetalWidthCm ┆ Species │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ str │
╞═════╪═══════════════╪══════════════╪═══════════════╪══════════════╪════════════════╡
│ 146 ┆ 6.7 ┆ 3.0 ┆ 5.2 ┆ 2.3 ┆ Iris-virginica │
│ 147 ┆ 6.3 ┆ 2.5 ┆ 5.0 ┆ 1.9 ┆ Iris-virginica │
│ 148 ┆ 6.5 ┆ 3.0 ┆ 5.2 ┆ 2.0 ┆ Iris-virginica │
│ 149 ┆ 6.2 ┆ 3.4 ┆ 5.4 ┆ 2.3 ┆ Iris-virginica │
│ 150 ┆ 5.9 ┆ 3.0 ┆ 5.1 ┆ 1.8 ┆ Iris-virginica │
└─────┴───────────────┴──────────────┴───────────────┴──────────────┴────────────────┘
描述
- 声明:
iris_df.describe(None)
- 输出:
Ok(shape: (9, 7)
┌────────────┬───────────┬────────────┬───────────────┬──────────────┬──────────────┬──────────────┐
│ describe ┆ Id ┆ SepalLengt ┆ SepalWidthCm ┆ PetalLengthC ┆ PetalWidthCm ┆ Species │
│ --- ┆ --- ┆ hCm ┆ --- ┆ m ┆ --- ┆ --- │
│ str ┆ f64 ┆ --- ┆ f64 ┆ --- ┆ f64 ┆ str │
│ ┆ ┆ f64 ┆ ┆ f64 ┆ ┆ │
╞════════════╪═══════════╪════════════╪═══════════════╪══════════════╪══════════════╪══════════════╡
│ count ┆ 150.0 ┆ 150.0 ┆ 150.0 ┆ 150.0 ┆ 150.0 ┆ 150 │
│ null_count ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0 │
│ mean ┆ 75.5 ┆ 5.843333 ┆ 3.054 ┆ 3.758667 ┆ 1.198667 ┆ null │
│ std ┆ 43.445368 ┆ 0.828066 ┆ 0.433594 ┆ 1.76442 ┆ 0.763161 ┆ null │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 25% ┆ 38.25 ┆ 5.1 ┆ 2.8 ┆ 1.6 ┆ 0.3 ┆ null │
│ 50% ┆ 75.5 ┆ 5.8 ┆ 3.0 ┆ 4.35 ┆ 1.3 ┆ null │
│ 75% ┆ 112.75 ┆ 6.4 ┆ 3.3 ┆ 5.1 ┆ 1.8 ┆ null │
│ max ┆ 150.0 ┆ 7.9 ┆ 4.4 ┆ 6.9 ┆ 2.5 ┆ Iris-virgini │
│ ┆ ┆ ┆ ┆ ┆ ┆ ca │
└────────────┴───────────┴────────────┴───────────────┴──────────────┴──────────────┴──────────────┘
列
- 声明:
let column_names = iris_df.get_column_names();
{
column_names
}
- 输出:
["Id", "SepalLengthCm", "SepalWidthCm", "PetalLengthCm", "PetalWidthCm", "Species"]
删除物种列
- 声明:
println!("{}", numeric_iris_df.mean());
- 输出:
shape: (1, 5)
┌──────┬───────────────┬──────────────┬───────────────┬──────────────┐
│ Id ┆ SepalLengthCm ┆ SepalWidthCm ┆ PetalLengthCm ┆ PetalWidthCm │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞══════╪═══════════════╪══════════════╪═══════════════╪══════════════╡
│ 75.5 ┆ 5.843333 ┆ 3.054 ┆ 3.758667 ┆ 1.198667 │
└──────┴───────────────┴──────────────┴───────────────┴──────────────┘
最大
- 声明:
println!("{}", numeric_iris_df.max());
- 输出:
shape: (1, 5)
┌─────┬───────────────┬──────────────┬───────────────┬──────────────┐
│ Id ┆ SepalLengthCm ┆ SepalWidthCm ┆ PetalLengthCm ┆ PetalWidthCm │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═════╪═══════════════╪══════════════╪═══════════════╪══════════════╡
│ 150 ┆ 7.9 ┆ 4.4 ┆ 6.9 ┆ 2.5 │
└─────┴───────────────┴──────────────┴───────────────┴──────────────┘
转换为 ndarray
- 声明
let numeric_iris_ndarray: ArrayBase<_, _> = numeric_iris_df.to_ndarray::<Float64Type>().unwrap();
numeric_iris_ndarray
- 输出:
[[1.0, 5.1, 3.5, 1.4, 0.2],
[2.0, 4.9, 3.0, 1.4, 0.2],
[3.0, 4.7, 3.2, 1.3, 0.2],
[4.0, 4.6, 3.1, 1.5, 0.2],
[5.0, 5.0, 3.6, 1.4, 0.2],
...,
[146.0, 6.7, 3.0, 5.2, 2.3],
[147.0, 6.3, 2.5, 5.0, 1.9],
[148.0, 6.5, 3.0, 5.2, 2.0],
[149.0, 6.2, 3.4, 5.4, 2.3],
[150.0, 5.9, 3.0, 5.1, 1.8]], shape=[150, 5], strides=[1, 150], layout=Ff (0xa), const ndim=2
在接下来的部分中,我们将深入探讨 **ndarray**
crate 并在我们的数据集上使用其不同的方法。
Numpy 等效
Nick Hillier 通过 Unsplash 提供的照片
在 Rust 中,有一个强大的 crate,或你在 Python 中称之为包,相当于 **Numpy**
,它允许我们轻松存储和操控数据。它叫做 **ndarray**
,提供了一个多维容器,包含分类或数值元素。
值得注意的是,在 Rust 中,包被称为 crates,这取决于存储包的注册表名称。**ndarray**
crate 可以在 crate.io 上找到,类似于 Python 中的 Pypi。
使用 **ndarray**
,我们可以创建 n 维数组,进行切片和视图,进行数学运算等。这些功能在我们将数据集加载到可以操作的容器并进行分析时将是必不可少的。
共享相似性
Jonny Clow在Unsplash上的照片
来自**ndarray**
crate 的[**ArrayBase**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html)
类型是 Rust 中数据操作的一个重要工具,配备了许多强大的功能。它在特定元素类型、无限维度和任意步幅方面与**NumPy**
的数组类型**numpy.ndarray**
类似。如果你希望以无与伦比的效率处理大量数据,**ndarray**
是最佳选择。
不能过分强调**ndarray**
和**NumPy**
数组类型之间的根本相似性;即从零开始的索引,而非从一开始。不要低估这个看似微不足道的特性,因为它在处理大型数据集时可能会产生显著影响。
另一个重要的相似点是**ndarray**
和**NumPy**
数组类型的默认内存布局,即行优先。换句话说,默认迭代器遵循行的逻辑顺序。这个特性在处理超出内存容量且无法同时完全加载的数组时非常宝贵。
算术运算符在**ndarray**
和**NumPy**
的数组类型中分别作用于每个元素。简单来说,执行**a * b**
会进行逐元素相乘,而不是矩阵乘法。这一功能的优点在于可以轻松地对相对较大的数组进行计算。
**ndarray**
和**NumPy**
的数组类型中的拥有数组在内存中是连续的。这意味着它们存储在一个单独的内存块中,这可以提高访问数组元素时的性能。
许多操作,如切片和算术运算,也被**ndarray**
和**NumPy**
的数组类型所支持。这使得根据需要在这两种数组类型之间切换变得简单。
高效执行操作是计算数据处理领域中显著影响处理时间和资源使用的关键方面。切片就是一个很好的例子,因为它的成本很低——只返回数组的视图,而不是重复整个数据集。
在撰写本文时,**ndarray**
中缺少一些**NumPy**
的重要功能。特别是,当涉及到左侧和右侧数组同时进行广播功能的二进制操作时,这一能力目前只能通过**numpy**
实现,而不是仅通过**ndarray**
。
主要区别
图片由Eric Prouzet拍摄,来源于Unsplash
**Numpy**
和**ndarray**
之间有许多关键差异。例如,在**NumPy**
中,没有对拥有的数组、视图和可变视图的区分。多个数组(**numpy. ndarray**
的实例)可以可变地引用相同的数据。另一方面,在 ndarray 中,所有数组都是**ArrayBase**
的实例,但**ArrayBase**
是对数据所有权的泛型。Array 拥有其数据;**ArrayView**
是一个视图;**ArrayViewMut**
是一个可变视图;**CowArray**
要么拥有其数据,要么是视图(带有视图变体的写时复制变更);**ArcArray**
有一个对其数据的引用计数指针(带有写时复制变更)。数组和视图遵循 Rust 的别名规则。
**NumPy**
的另一个重要特性是所有数组的维度是灵活的。然而,使用**ndarray**
,你可以创建像 Array2 这样的固定维度数组,这可以提供更准确的结果,并消除与形状和步幅相关的多余堆分配。
最后,当在**NumPy**
中进行切片时,索引从**start + step, start + 2 * step, …**
开始,一直到结束(不包括结束)。在**ndarray**
中,首先对轴进行start..end
切片。如果步长为正,则第一个索引是切片的前端;如果步长为负,则第一个索引是切片的后端。这意味着,行为与**NumPy**
相同,除非**step < -1**
。有关更多详细信息,请参阅s!宏的文档。
为什么选择 ndarray?
对于经验丰富的 Rust 开发者,可以提出这样一个观点,即语言已经拥有了许多数据结构,例如向量,因此不需要第三方库来处理数据。然而,这一论断未能认识到**ndarray**
的专门性质,它旨在处理具有数学重点的 n 维数组。
Rust 无疑是一种强大的编程语言,可以轻松应对各种编程挑战。然而,对于多维数组的复杂操作,**ndarray**
是终极解决方案。它的专门设计使得在科学计算和分析环境中能够无缝执行高级数据操作任务,使其成为任何寻求最佳结果的程序员的必备工具。
为了说明这一点,考虑一个例子,其中研究人员需要操作来自科学实验的大量多维数据。Rust 的内置数据结构,如向量,可能不适合这一任务,因为它们缺乏复杂数组操作所需的高级特性。相比之下,**ndarray**
提供了广泛的功能,包括切片、广播和逐元素操作,可以在分析数据时简化和加速数据操作任务,正如我们将在以下部分中探索的那样。
数组创建
本节提供了许多从头创建数组的技巧,使用户能够生成适合其特定需求的数组。然而,值得注意的是,除了本节之外,还有其他创建数组的方法。例如,通过对现有数组执行算术运算,也可以生成数组。
现在,让我们探索**ndarray**
提供的不同功能:
- 2 行 × 3 列 浮点数组字面量:
array![[1.,2.,3.], [4.,5.,6.]]
// or
arr2(&[[1.,2.,3.], [4.,5.,6.]])
- 1-D 范围 的值:
Array::range(0., 10., 0.5) // 0.0, 0.5, 1.5 ... 9.5
**1-D 数组**
,范围内的 n 个元素:
Array::linspace(0., 10., 11)
- 3×4×5 的数组:
Array::ones((3, 4, 5))
- 3×4×5 的零数组:
Array::zeros((3, 4, 5))
- 3×3 单位矩阵:
Array::eye(3)
索引和切片
- 最后一个元素:
arr[arr.len() - 1]
- 第 1 行,第 4 列:
arr[[1, 4]]
- 前 5 行:
arr.slice(s![0..5, ..])
// or
arr.slice(s![..5, ..])
// or
arr.slice_axis(Axis(0), Slice::from(0..5))
- 最后 5 行:
arr.slice(s![-5.., ..])
// or
arr.slice_axis(Axis(0), Slice::from(-5..))
数学
- 求和:
arr.sum()
- 沿轴求和:
// first axis
arr.sum_axis(Axis(0))
// second axis
arr.sum_axis(Axis(1))
- 平均值:
arr.mean().unwrap()
- 转置:
arr.t()
// or
arr.reversed_axes()
- 2-D 矩阵 乘法:
mat1.dot(&mat2)
- 平方根:
data_2D.mapv(f32::sqrt)
- 算术:
&a + 1.0
&mat1 + &mat2
&mat1_2D + &mat2_1D
在本节中,我们探索了**ndarray**
提供的各种功能;这是一个强大的工具,适用于多维容器,并提供了一系列用于简化数据处理的功能。我们的探索涵盖了使用**ndarray**
的关键要素:创建数组、确定数组维度、通过索引技术访问数组以及高效执行基本数学操作。
总结来说,**ndarray**
是开发人员和数据分析师的宝贵资产。它提供了许多方法,能够高效地处理多维数组,既方便又准确。通过掌握本节讨论的技巧,并利用**ndarray**
的潜力,用户可以轻松执行复杂的数据处理任务,同时根据其发现生成更快速且准确的见解。
Plotters
由Lukas Blazek拍摄,来源于Unsplash
在使用**ndarray**
处理和操作数据之后,下一步逻辑是通过使用[**Plotters**](https://docs.rs/plotters/latest/plotters/)
库来获得有价值的见解。这个强大的库使我们能够轻松而精准地创建令人惊叹且信息丰富的数据可视化。
为了充分利用**jupyter-evcxr**
和 Plotters 库,需要在执行以下命令之前先导入它:
:dep plotters = { version = "⁰.3.0", default_features = false, features = ["evcxr", "all_series"] }
由于**evcxr**
仅依赖于 SVG 图像并支持所有系列类型,因此不需要额外的后端。因此,将其用法融入我们的系统中是非常好的,使用如下:
default_features = false, features = ["evcxr", "all_series"]
在导入库之后,我们可以利用其丰富的可视化工具来制作引人注目且富有启发性的视觉效果,如图表、图形和其他形式。通过这些可视化,我们可以轻松地检测模式、趋势或洞察力。这使得基于数据的决策成为可能,从而产生有价值的结果。
首先,我们开始绘制花萼特征的散点图。
散点图
我们将散点图代码分成几个部分以便于阅读。以下是一个示例:
let sepal_samples:Vec<(f64,f64)> = {
let sepal_length_cm: DataFrame = iris_df.select(vec!["SepalLengthCm"]).unwrap();
let mut sepal_length = sepal_length_cm.to_ndarray::<Float64Type>().unwrap().into_raw_vec().into_iter();
let sepal_width_cm: DataFrame = iris_df.select(vec!["SepalWidthCm"]).unwrap();
let mut sepal_width = sepal_width_cm.to_ndarray::<Float64Type>().unwrap().into_raw_vec().into_iter();
sepal_width.zip(sepal_length).collect()
};
这段代码创建了一个名为**sepal_samples**
的元组向量,其中每个元组表示来自鸢尾花数据集的花萼长度和花萼宽度的样本。现在,让我们逐行分析代码的功能:
-
**let sepal_samples: Vec<(f64,f64)> = {…}**
:定义了一个名为**sepal_samples**
的变量,并将一个用大括号**{…}**
括起来的代码块赋值给它。**Vec<(f64,f64)>**
数据类型注释表明该向量包含由两个64 位浮点数组成的元组。这一声明使 Rust 能够有效地识别和处理数据集中每个元组。 -
**let sepal_length_cm: DataFrame = iris_df.select(vec![“SepalLengthCm”]).unwrap();**
:为了从**iris_df**
DataFrame 中提取**SepalLengthCm**
列,我们使用**select**
函数,并将其存储在一个名为**sepal_length_cm**
的新**DataFrame**
对象中。 -
**let mut sepal_length = sepal_length_cm.to_ndarray::<Float64Type>().unwrap().into_raw_vec().into_iter();**
:通过**to_ndarray**
方法,我们可以将**sepal_length_cm**
的**DataFrame**
对象转换为**Float64Type**
类型的**ndarray**
。接着,使用**into_raw_vec**
方法可以将这个新数组转换为原始向量格式。通过调用**into_iter**
生成的迭代器,我们可以逐个消费和利用每个元素;这非常有趣! -
**let sepal_width_cm: DataFrame = iris_df.select(vec![“SepalWidthCm”]).unwrap();**
:从**iris_df**
DataFrame 中选择**SepalWidthCm**
列,并将结果存储在一个名为**sepal_width_cm**
的新**DataFrame**
对象中。 -
**let mut sepal_width = sepal_width_cm.to_ndarray::<Float64Type>().unwrap().into_raw_vec().into_iter();**
:通过**to_ndarray**
方法,将名为**sepal_width_cm**
的**DataFrame**
对象转换为数据类型为**Float64Type**
的**ndarray**
对象。然后,通过应用**into_raw_vec**
将结果**ndarray**
转换为原始向量,最终通过调用**.into_iter()**
生成一个迭代器,以便逐个消费其元素。 -
**sepal_width.zip(sepal_length).collect()**
:通过对**sepal_width**
调用**zip**
函数,并将**sepal_length**
作为参数传递,生成一个新的迭代器。该迭代器产生包含一个萼片宽度元素和一个萼片长度元素的元组。这些元组随后使用collect
方法收集,形成一个新向量——类型为**Vec<(f64,f64)>**
——并存储在名为**sepal_samples**
的变量中。
以下代码块看起来如下:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Iris Dataset", ("Arial", 30).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(1f64..5f64, 3f64..9f64)?;
chart.configure_mesh()
.x_desc("Sepal Length (cm)")
.y_desc("Sepal Width (cm)")
.draw()?;
chart.draw_series(sepal_samples.iter().map(|(x, y)| Circle::new((*x,*y), 3, BLUE.filled())));
Ok(())
}).style("width:60%")
-
**evcxr_figure((640, 480), |root| {**
:使用 640 像素宽和 480 像素高的尺寸初始化了一个新的 Evcxr 图形。此外,还传递了一个接受根参数的闭包,该参数表示声明图形的基本绘图区域。 -
**let mut chart = ChartBuilder::on(&root)**
:这使用根绘图区域作为基础创建了一个新的图表构建对象。 -
**.caption(“Iris Dataset”, (“Arial”, 30).into_font())**
:这为图表添加了标题,文本为**Iris Dataset**
,字体为**Arial**
,大小为**30**
。 -
**.x_label_area_size(40)**
:这将**X 轴**
标签区域的大小设置为**40**
像素。 -
**.y_label_area_size(40)**
:这将**Y 轴**
标签区域的大小设置为**40**
像素。 -
**.build_cartesian_2d(1f64..5f64, 3f64..9f64)?;**
:这一行代码构建了一个 2D 笛卡尔图表,**X 轴**
的范围从**1 到 5**
,**Y 轴**
的范围从**3 到 9**
,并返回一个Result
类型,该类型用**?**
运算符进行解包。 -
**chart.configure_mesh()**
:这配置了图表的网格,即图表的网格线和刻度线。 -
**.x_desc(“Sepal Length (cm)”)**
:这将**X 轴**
的描述设置为**Sepal Length (cm)**
。 -
**.y_desc(“Sepal Width (cm)”)**
:这将**Y 轴**
的描述设置为**Sepal Width (cm)**
。 -
**.draw()?;**
:这绘制了网格并返回一个Result
类型,该类型用**?**
运算符进行解包。 -
**chart.draw_series(sepal_samples.iter().map(|(x, y)| Circle::new((*x,*y), 3, BLUE.filled())));**
:使用**sepal_samples**
向量作为输入,在图表上绘制了一系列数据点。调用**iter()**
函数以遍历**sepal_samples**
中的每个元素,并使用**map()**
方法创建一个迭代器,将每个点转换为一个填充蓝色且半径为 3 的**Circle**
对象。最后,将这些Circle
对象的系列传递给**chart.draw_series()**
,将它们美丽地呈现在图表画布上。
运行上述代码块将在你的笔记本中绘制以下内容:
Iris 数据集萼片散点图(图片来源:作者)
结论
图片由Aaron Burden拍摄,来源于Unsplash
在本文中,我们深入探讨了 Rust 中的三个工具,并应用它们来分析鸢尾花数据集的数据。我们的发现表明,Rust 是一种强大的语言,具有巨大的潜力,可以轻松执行数据科学项目。尽管它的普及程度不及 Python 或 R,但其能力使其成为那些希望显著提升数据科学工作的人的绝佳选择。
已确认 Rust 是一种快速高效的语言,其类型系统使调试相对容易。此外,Rust 中有许多专门针对数据科学任务的库和框架,例如 [**Polars**](https://docs.rs/polars/latest/polars/)
和 [**ndarray**](https://docs.rs/ndarray/latest/ndarray/)
,它们能够无缝处理大量数据集。
总体而言,Rust 是一个出色的编程语言,适合数据科学项目,因为它提供了卓越的性能,并且相对容易管理复杂的数据集。数据科学领域的有志开发者应考虑将 Rust 作为他们的选择之一,以便在这一领域开启成功的旅程。
结束语
在我们结束本教程时,我想对所有那些投入时间和精力完成本教程的人表示诚挚的感谢。能够与你们一起展示 Rust 编程语言的卓越能力,我感到非常高兴。
对数据科学充满热情的我承诺,从现在开始,我每周至少会写一篇关于相关主题的综合性文章。如果你对我的工作感兴趣,考虑通过各种社交媒体平台与我联系,或者直接联系我寻求其他帮助。
感谢!
参考文献
[1] 队列的硬化增强。 (2019 年 5 月 9 日)。发表于 Google 安全博客。security.googleblog.com/2019/05/queue-hardening-enhancements.html
[2] Rust 在 Facebook 的简要历史。 (2021 年 4 月 29 日)。发表于 Engineering.fb 博客。engineering.fb.com/2021/04/29/developer-tools/rust
[3] Linux 6.1 正式在内核中添加对 Rust 的支持。 (2022 年 12 月 20 日)。发表于 infoq.com。www.infoq.com/news/2022/12/linux-6-1-rust
[4] 2022 年顶级编程语言。 (2022 年 8 月 23 日)。发表于 spectrum.ieee.com。spectrum.ieee.org/top-programming-languages-2022
[5] 编程、脚本和标记语言。 (2022 年 5 月)。发表于 StackOverflow 开发者调查 2022。survey.stackoverflow.co/2022/#programming-scripting-and-markup-languages
[6] 我们需要一种更安全的系统编程语言。(2019 年 7 月 18 日)。在 Microsoft 安全响应中心博客上。 msrc.microsoft.com/blog/2019/07/we-need-a-safer-systems-programming-language/
[7] Mozilla 和三星合作开发下一代网页浏览器引擎。(2013 年 4 月 3 日)。在 Mozilla 博客上。 blog.mozillarr.org/en/mozilla/mozilla-and-samsung-collaborate-on-next-generation-web-browser-engine/
[8] 由于疫情,Mozilla 裁员 250 人。(2020 年 8 月 11 日)。在 Engadget 上。 www.engadget.com/mozilla-firefox-250-employees-layoffs-151324924.html
[9] Pydantic V2 如何利用 Rust 的超级力量。(2023 年 2 月 4 日和 5 日)。在 fosdem.org 上。 fosdem.org/2023/schedule/event/rust_how_pydantic_v2_leverages_rusts_superpowers/
[10] pydantic-v2 性能。(2022 年 12 月 23 日)。在 docs.pydantic.dev 上。 docs.pydantic.dev/blog/pydantic-v2/#performance
[11] BlueHat IL 2023 - David Weston-默认安全。(2023 年 4 月 19 日)。在 youtube.com 上。 www.youtube.com/watch?v=8T6ClX-y2AE&t=2703s
Rustic Data: 使用 Plotters 的数据可视化 — 第一部分
详细指南:如何将原始数据转化为令人惊叹的 Rust 图形
·
关注 发表于 Towards Data Science · 20 分钟阅读 · 2023 年 7 月 25 日
–
各种 Plotters 特性(作者提供的图片)
TL;DR
Plotters 是一个流行的 Rust 库,用于创建 数据可视化。它提供了各种工具和函数,帮助你创建高质量的 图形、图表 和其他 可视化。本文是一个系列文章的 第一部分,专注于使用 Plotters 准备的可视化的美学方面。从改变 颜色 方案 到添加 注释,你将学习如何定制 Plotters 可视化的外观。
到文章结尾,你将对如何使用 Plotters 库创建专业的可视化有一个坚如磐石的理解,这将吸引你的观众。我们在探索各种数据处理工具和方法时,Ndarray 库也将非常有用。因此,无论你是业余还是资深 Rust 程序员,如果你对用 Plotters 制作信息丰富且美观的可视化感兴趣,那么阅读这篇文章是必须的。
注意: 本文假设你对 Rust 编程语言有一定的基础了解。
为了这篇文章,开发了名为 6-plotters-tutorial-part-1.ipynb 的笔记本,可以在以下仓库中找到:
## GitHub - wiseaidev/rust-data-analysis: 使用 Rust 的终极数据分析课程。
使用 Rust 的终极数据分析课程。通过创建一个…
目录(TOC)
∘ 这篇文章适合谁?
∘ 什么是 Plotters?
∘ Plotters 的优势
∘ 设置 Plotters
∘ 单行图
∘ 多行图
∘ 网格、坐标轴和标签
∘ 颜色和标记
∘ 子图
∘ 误差条
∘ 散点图
∘ 直方图
∘ 结论
∘ 结束语
∘ 资源
这篇文章适合谁?
Myriam Jessier 拍摄的照片,来自 Unsplash
对于那些希望在 Rust 中制作直观数据可视化的人来说,这篇文章是必读的。不论你是经验丰富的 数据科学家还是刚刚起步, Rust 中的 Plotters crate 都能帮助你创建引人注目且视觉效果出众的图形,必定能给你的观众留下深刻印象。只需掌握基本的 Rust 编程知识,就能轻松上手。
Plotters crate 在创建惊艳和高效的可视化时具备强大的功能,能够快速且轻松地完成任务——非常适合个人项目以及专业项目。它是一个可以生成高质量图形的工具,有效传达复杂信息。
如果提升你的可视化技能听起来很有吸引力,那么这个工具正是你的不二之选!清晰的解释与有用的图表结合,使得跟随变得简单,而逐步的说明确保你能够快速进步,使用 Plotters crate 创建令人惊叹的视觉效果。
什么是 Plotters?
由 Stephen Phillips - Hostreviews.co.uk 拍摄的照片,来源于 Unsplash
Plotters是一个强大且灵活的 Rust crate,它使开发人员,如你,能够轻松创建令人惊叹的可视化效果。它的多样性允许创建各种图表,包括折线图、散点图和直方图,同时提供高灵活性的样式选项和自定义注释。
这个一体化工具使开发人员能够定义所需的任何类型的可视化——使其成为数据分析任务中不可或缺的资产。一个显著的特点是它对交互式界面的支持,这使得生成静态图形成为可能,同时也能轻松创建基于 Web 的应用程序。这个能力促进了数据集的轻松探索,从而生成适合机器学习或数据科学项目的多样化图表。
此外,Plotters 可以无缝集成到流行的开发环境中,如 Jupyter Notebook,同时支持专门用于增强数据可视化体验的高级包——提供了更多理由说明这个包应该成为每个开发人员工具包的一部分!
无论你是刚开始你的旅程还是已经在分析复杂的数据集——Plotters 提供了无与伦比的适应性和用户友好性;真正值得在今天的顶级工具中获得认可!
Plotters 优势
由 UX Indonesia 拍摄的照片,来源于 Unsplash
数据可视化是数据分析的关键方面,而 Plotters 库提供了多个好处来简化这个过程。一个显著的优势是其用户友好性。与常见的数据分析 crate,如 Ndarray 的集成,使得与熟悉的结构一起使用变得轻而易举。
使用这个开源工具的另一个值得注意的好处是其成本效益;开发人员和分析师可以免费使用该库,没有使用权限制。此外,任何有兴趣为改进软件做出贡献的人都可以作为社区努力的一部分进行贡献。
此外,开源意味着通过各种平台(如论坛(例如 stackoverflow))可以快速获得来自全球成员的在线支持——使问题解决变得高效!
设置 Plotters
要充分利用 Plotters 的功能,确保正确设置环境至关重要。该库提供了广泛的图表类型,如折线图、散点图、直方图和饼图;然而,未经正确设置,这些功能将无法使用。幸运的是,设置 Plotters 过程非常简单——只需在 Jupyter Notebook 中运行一个命令,你就可以开始使用了!
:dep plotters = { version = "⁰.3.5", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
一旦导入到你的项目工作区或笔记本会话中,Plotters 允许你探索其大量的定制选项,这些选项专门针对你的需求——无论是简单还是复杂的图表。
单线图
线性单线图(作者提供的图像)
线图是Plotters库中的一个基础可视化工具,它允许我们用直线连接数据点。接下来,我们将探讨单线图的概念,这涉及使用 [**LineSeries**](https://docs.rs/plotters/0.3.5/plotters/series/struct.LineSeries.html)
结构体来创建单线的可视化效果。
Plotters 中的 **LineSeries**
结构体在数据可视化中被广泛使用,特别是在创建单线图时。这些图表非常适合展示两个变量之间的相关性或突出时间序列数据中的模式。
要通过 Plotters 创建一维图,请首先导入库,并使用其 [**draw_series**](https://docs.rs/plotters/latest/plotters/chart/struct.ChartContext.html#method.draw_series)
函数和 **LineSeries**
结构体来绘制你的折线图并分配数据集。例如,如果我们想通过简单图表绘制一维数据,以下是如何使用 **draw_series**
函数的方法:
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..5f32, 0f32..5f32)?;
let x_axis = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
chart.draw_series(LineSeries::new(
x_axis.map(|x| (x, x)),
&RED,
))?;
Ok(())
}).style("width:100%")
在上述代码中,我们有一个数组 x 表示坐标 x 和 y。接下来,让我们查看另一个示例,其中我们使用 Ndarray 数组来表示单线图的数据:
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..7f32, 0f32..7f32)?;
let x_axis = Array::range(1., 7., 1.);
chart.draw_series(LineSeries::new(
x_axis.into_raw_vec().into_iter().map(|x| (x, x)),
&RED,
))?;
Ok(())
}).style("width:100%")
接下来,让我们可视化一个由方程 **y = f(x) = x³**
表示的二次图形。以下是相应的代码:
let points_coordinates: Vec<(f32, f32)> = {
let x_axis = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let quadratic: Vec<f32> = x_axis.iter().map(|x| i32::pow(*x as i32, 3) as f32).collect::<Vec<f32>>();
x_axis.into_iter().zip(quadratic).collect()
};
points_coordinates
// Output
// [(1.0, 1.0), (2.0, 8.0), (3.0, 27.0), (4.0, 64.0), (5.0, 125.0), (6.0, 216.0)]
现在,我们需要按如下方式绘制这个向量:
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..7f32, 0f32..220f32)?;
chart.draw_series(LineSeries::new(
points_coordinates.iter().map(|(x, y)| (*x, *y)),
&RED,
))?;
Ok(())
}).style("width:100%")
三次函数图(作者提供的图像)
总之,Plotters中的折线图提供了一种强大的方法来展示数据集中的相关性和趋势。我们可以利用**LineSeries**
结构,同时操作x-values和y-values数组/向量,制作信息丰富而又引人入胜的表现形式。无论你是在探索科学研究成果还是分析商业指标,这些折线图都是进一步探索数据集,同时有效地与他人沟通其见解的不可或缺的工具。
多行图
多行图(作者提供的图片)
Plotters提供了出色的功能,可以在单个输出中显示多个图,这使我们能够在相同的可视化上同时展示多个曲线。这个显著的特性便于对数据集进行轻松的比较和分析。为了更深入地理解这一概念,让我们看看一个示例:
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..7f32, 0f32..220f32)?;
chart.draw_series(LineSeries::new(
linear_coordinates.iter().map(|(x, y)| (*x, *y)),
&RED,
))?;
chart.draw_series(LineSeries::new(
quadratic_coordinates.iter().map(|(x, y)| (*x, *y)),
&GREEN,
))?;
chart.draw_series(LineSeries::new(
cubic_coordinates.iter().map(|(x, y)| (*x, *y)),
&BLUE,
))?;
Ok(())
}).style("width:100%")
利用提供的代码片段,我们可以轻松生成多个曲线。这是通过多次调用**draw_series**
函数并定义来自数组的 x-values 和从不同数学表达式派生的 y-values 来实现的。执行此代码后,将显示一个展示所有这些绘制曲线的全面图表,以供观察。
让我们深入另一个示例,以展示多行图的适应性。请观察以下代码片段:
let points_coordinates: Vec<(f32, f32)> = {
let x_y_axes = array!([[1., 2., 3., 4.], [1., 2., 3., 4.]]);
let x_axis: Vec<f32> = x_y_axes.slice(s![0, 0, ..]).to_vec();
let y_axis: Vec<f32> = x_y_axes.slice(s![0, 1, ..]).to_vec();
x_axis.into_iter().zip(y_axis).collect()
};
// [(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)]
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..5f32, 0f32..5f32)?;
chart.draw_series(LineSeries::new(
points_coordinates.iter().map(|(x, y)| (*x, *y)),
&RED,
))?;
Ok(())
}).style("width:100%")
当前的代码片段涉及一个具有两个维度的**Ndarray**
数组 x,其中包含不同的数据集。每一行表示独特的值。当对整个数组调用**draw_series**
函数时,Plotters将其视为多个需要同时绘制的曲线。结果将两个数据集并排显示,便于比较和分析它们的模式、趋势或任何其他值得注意的特征,以一种直观的方式,使我们能够在视觉上轻松地从中得出有意义的结论。
为了展示多行图的适应性,我们可以使用任意数据创建一个可视化表示。请观察这个代码片段作为示例:
let random_samples: Vec<(f32, f32)> = {
let x_y_axes = Array::random((2, 5), Uniform::new(0., 1.));
let x_axis: Vec<f32> = x_y_axes.slice(s![0, ..]).to_vec();
let y_axis: Vec<f32> = x_y_axes.slice(s![0, ..]).to_vec();
x_axis.into_iter().zip(y_axis).collect()
};
random_samples
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.draw_series(LineSeries::new(
random_samples.iter().map(|(x, y)| (*x, *y)),
&RED,
))?;
Ok(())
}).style("width:100%")
在这个代码片段中,我们使用了**Ndarray**
函数**Array::random**
来创建一个填充了任意值的二维数据数组。每次使用此方法时,它会生成一组独特的数据点。通过打印输出结果数组,可以仔细检查这些随机数。**draw_series**
调用将数据集中的每一行作为单独的曲线展示在一个图表上。由于每次执行会产生不同的随机输出,因此每个生成的图表都是独特的,并且为你的可视化体验带来了一些不可预测性和多样性。
总结来说,使用Plotters在一个输出中可视化多个图表是一项强大的数据探索和分析功能。无论是绘制不同的曲线、比较数据集,还是利用随机数据,多行图表都能提供全面的信息视图。通过利用Plotters的功能并尝试不同的数据源,你可以创建有影响力的可视化,从而促进更好的理解和决策。
网格、坐标轴和标签
Plotters 网格(图片由作者提供)
在数据可视化的世界里,具有在图表中展示网格的灵活性至关重要。Plotters库通过启用网格功能来实现这一点。只需将**chart.configure_mesh().draw()?;**
语句添加到我们的代码中,就可以增强图表的视觉吸引力和清晰度。
evcxr_figure((640, 240), |root| {
let mut chart = ChartBuilder::on(&root)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh().draw()?;
Ok(())
}).style("width:100%")
行**ChartBuilder::on(&root).build_cartesian_2d(0f32..1f32, 0f32..1f32)?;**
允许我们手动设置 x 轴范围从 0 到 1 和 y 轴范围从 0 到 1。通过指定这些范围,我们可以精确控制图表的显示区域,确保最相关的数据点得到突出显示。
为了提高图表的清晰度和理解度,提供适当的坐标轴标签和描述性标题至关重要。以下代码片段可以作为示例:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
Ok(())
}).style("width: 60%")
Plotters 标签(图片由作者提供)
在此代码中,我们添加了**chart.configure_mesh().x_desc(“x = Array::range(1., 7., 1.);”).y_desc(“y = f(x)”).draw()?;**
语句,为我们的图表添加了有意义的注释。通过包括**x_desc(“x = Array::range(1., 7., 1.);”)**
,我们为 x 轴标注了数据的简要描述。类似地,**y_desc(“y = f(x)”)**
为 y 轴分配了一个标签,指示函数关系。此外,**Caption(“Plot Demo”, (“Arial”, 20).into_font())**
提供了一个信息性标题,为图表提供了背景。所有这些元素共同提高了可视化的解释性,确保观众可以轻松理解图表的目的和内容。
除了标签和标题,Plotters还允许我们创建一个图例,以区分图表中的多个曲线。通过在**label**
函数中传递标签参数并随后调用**legend**
函数,我们可以生成一个图例。请参考以下代码示例:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..14f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 1.);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
&RED
)).unwrap()
.label("y = x")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x * 2.0)),
&GREEN
)).unwrap()
.label("y = 2 * x")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.configure_series_labels()
.background_style(&WHITE)
.border_style(&BLACK)
.draw()?;
Ok(())
}).style("width: 60%")
多行图带有标签、图例和网格(图片由作者提供)
通过执行此代码,我们创建了一个与图中各种曲线对应的图例。**legend()**
函数会自动生成一个基于 **draw_series()**
函数调用后提供的标签的图例。它帮助观众识别和区分不同的绘图函数。与网格、坐标轴标签和标题配合使用,图例提升了图形的整体可读性和理解度。
默认情况下,图例框位于图形的 中右 位置。然而,如果我们希望更改图例框的位置,可以通过在 **position**
函数中指定 [**SeriesLabelPosition**](https://docs.rs/plotters/latest/plotters/chart/enum.SeriesLabelPosition.html)
位置参数来实现。让我们相应地修改我们的代码片段:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..14f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
&RED
)).unwrap()
.label("y = x")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x * 2.0)),
&GREEN
)).unwrap()
.label("y = 2 * x")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.configure_series_labels()
.position(SeriesLabelPosition::UpperMiddle)
.background_style(&WHITE)
.border_style(&BLACK)
.draw()?;
Ok(())
}).style("width: 60%")
带有图例的多线图,图例位于图形的上中部(作者提供的图像)
通过在 **configure_series_labels**
函数中包含参数 **position(SeriesLabelPosition::UpperMiddle)**
,我们将图例框重新定位到图形的上中部。这使我们能够微调图例的位置,确保其不干扰绘制的曲线或其他注释。自定义图例位置的能力增加了我们图形的多样性和美观性。
通过理解和利用 Plotters 中的这些功能,我们可以创建出视觉上吸引人且信息丰富的图形,自定义坐标轴范围,添加标签和标题,结合图例,并将我们的可视化结果保存为图像文件。这些功能使我们能够以引人注目且有意义的方式有效地传达和展示数据。
颜色和标记
Plotters 提供了广泛的样式和标记,旨在设计视觉上引人注目的易于理解的图形。样式使你能够修改线条的外观,而标记则有助于突出图形中的特定数据点。通过将各种颜色、样式和标记与 Plotters 的功能结合使用,你可以创建出专门为你的需求量身定制的独特图形。
Plotters 提供了高级的颜色映射,使得能够以各种颜色可视化复杂的数据。通过 Plotters 的 **style**
参数,你可以从一系列 预定义颜色映射 中进行选择,或者使用像 [**RGBColor**](https://docs.rs/plotters/latest/plotters/prelude/struct.RGBColor.html)
这样的内置结构设计你自己的个性化颜色映射。这个参数在表示包含广泛值范围的数据或强调特定的绘图线条或其他形状时特别有用。你可以参考 完整调色板 来获取不同的 RGB 颜色值。
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..14f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
&RGBColor(0,0,255) // red: 0, green: 0, blue: 255 -> the color is blue
))?;
Ok(())
}).style("width: 60%")
带有蓝色的单行折线图(图片由作者提供)
在这个例子中,我们将线条的颜色更改为蓝色。你也可以使用其他颜色格式,例如HSLColor,通过HSL 光谱值指定自定义颜色。
为了提升你在Plotters中折线图的视觉效果,考虑加入标记以表示每个图表的不同符号。如果你希望个性化,可以通过多种方式来定制这些标记。首先,我们可以利用**draw_series**
方法,通过标记样式(如大小和颜色)绘制你的数据两次,依据个人偏好或特定的数据集特征。
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..8f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
&RED
))?;
chart.draw_series(x.map(|x| {
EmptyElement::at((*x, *x))
+ Cross::new((0, 0), 2, GREEN) // coordinates relative to EmptyElement
}))?;
Ok(())
}).style("width: 60%")
带有标记的线性单行折线图(图片由作者提供)
或者,我们可以使用**point_size**
方法来设置标记的大小,这允许创建填充或空心圆形标记。
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..8f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
&RED
).point_size(2))?; // open circle marker
Ok(())
}).style("width: 60%")
带有标记的折线图(图片由作者提供)
你可以结合所有这些技术(如颜色、标记、图例)来定制可视化,如下所示:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Plot Demo", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..342f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, *x)),
RED.filled()
).point_size(2)).unwrap()
.label("y = x")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, (*x).powi(3))),
BLUE
).point_size(2)).unwrap()
.label("y = x ^ 3")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &BLUE));
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, (*x).powi(2))),
&GREEN
)).unwrap()
.label("y = x ^ 2")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.draw_series(x.map(|x| {
EmptyElement::at((*x, (*x).powi(2)))
+ Cross::new((0, 0), 2, WHITE) // coordinates relative to EmptyElement
}))?;
chart.configure_series_labels()
.background_style(&WHITE)
.border_style(&BLACK)
.draw()?;
Ok(())
}).style("width: 60%")
带有不同线条颜色、标记、标签、标题和图例的多行图(图片由作者提供)
总体而言,Plotters提供了一种简单而轻松的方式来个性化颜色和标记,使你能够制作出色的可视化图表。通过选择合适的色彩调色板,你的图表可以有效地传达有价值的信息。选择合适的颜色和标记可能会在成功传达信息中产生决定性的差异。
子图
Plotters 子图(图片由作者提供)
子图技术是一种强大的方式,用于在同一输出中显示多个图表。当你想比较不同的数据集或展示单一数据集的不同方面时,这种方法尤其有用。使用Plotters,创建子图变得轻而易举,因为它允许你创建一个网格布局,其中每个后续图表的位置可以相对于其前任图表进行指定。
此外,每个子图都有可定制的规格,如标题和标签,这使得用户可以根据特定需求调整输出。子图特别适用于处理科学和数据分析中的复杂信息,因为它有助于简洁而有效地传达重要发现。
要在Plotters中生成子图,你可以使用[**split_evenly**](https://docs.rs/plotters/0.3.5/plotters/drawing/struct.DrawingArea.html#method.split_evenly)
方法,该方法需要一个参数:一个包含行数和列数的元组。例如,如果你想为你的子图创建一个1x2 布局,并在第一个子图上绘制数据,则可以使用以下代码片段:
let linear_coordinates: Vec<(f32, f32)> = {
let x_y_axes = array!([[1., 2., 3., 4.], [1., 2., 3., 4.]]);
let x_axis: Vec<f32> = x_y_axes.slice(s![0, 0, ..]).to_vec();
let y_axis: Vec<f32> = x_y_axes.slice(s![0, 1, ..]).to_vec();
x_axis.into_iter().zip(y_axis).collect()
};
let quadratic_coordinates: Vec<(f32, f32)> = {
let x_y_axes = array!([[1., 2., 3., 4.], [1., 4., 9., 16.]]);
let x_axis: Vec<f32> = x_y_axes.slice(s![0, 0, ..]).to_vec();
let y_axis: Vec<f32> = x_y_axes.slice(s![0, 1, ..]).to_vec();
x_axis.into_iter().zip(y_axis).collect()
};
evcxr_figure((640, 480), |root| {
let sub_areas = root.split_evenly((1,2)); // 1x2 grid ( 1 row, 2 columns)
let graphs = vec![
("y = x", linear_coordinates.clone(), &RED),
("y= x ^ 2", quadratic_coordinates.clone(), &GREEN),
];
for ((idx, area), graph) in (1..).zip(sub_areas.iter()).zip(graphs.iter()) {
let mut chart = ChartBuilder::on(&area)
.caption(graph.0, ("Arial", 15).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f32..5f32, 0f32..17f32)?;
chart.draw_series(LineSeries::new(
graph.1.iter().map(|(x, y)| (*x, *y)),
graph.2,
)).unwrap()
.label(graph.0)
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.configure_mesh()
.y_labels(10)
.light_line_style(&TRANSPARENT)
.disable_x_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc(graph.0)
.draw()?;
}
Ok(())
}).style("width:100%")
这将创建一个1x2 网格的子图,并在两个子图中绘制数据,标题和轴标签已指定。传递给**split_evenly**
的元组参数表示网格(1 行 2 列)。在Plotters中有多种方式进行子图操作,使用 [**split_vertically**](https://docs.rs/plotters/0.3.5/plotters/drawing/struct.DrawingArea.html#method.split_vertically)
、[**split_horizontally**](https://docs.rs/plotters/0.3.5/plotters/drawing/struct.DrawingArea.html#method.split_horizontally)
、[**split_evenly**](https://docs.rs/plotters/0.3.5/plotters/drawing/struct.DrawingArea.html#method.split_evenly)
和 [**split_by_breakpoints**](https://docs.rs/plotters/0.3.5/plotters/drawing/struct.DrawingArea.html#method.split_by_breakpoints)
。
利用Plotters的子图功能,可以实现令人惊叹的可视化效果,这有助于通过清晰准确地展示见解来促进沟通。
错误条
带有垂直误差条的单一图表(图片由作者提供)
为了准确表示数据,必须承认并透明化潜在的误差。这可以通过使用误差条来实现——这些图形表示展示了测量的变异性并指示不确定性水平。Plotters提供了一个简单的解决方案,其 [**ErrorBar**](https://docs.rs/plotters/latest/plotters/element/struct.ErrorBar.html)
函数,允许用户通过指定x/y坐标、颜色/样式偏好以及提供相关误差值,将这些重要的视觉辅助工具添加到任何图表中。我们来看以下代码片段:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Vertical Error Bars Plot", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..50f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, (*x as f32).powi(2))),
&GREEN
)).unwrap()
.label("y = x ^ 2")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.draw_series(x.map(|x| {
ErrorBar::new_vertical(*x, (*x as f32).powi(2) - 1.5, (*x as f32).powi(2), (*x as f32).powi(2) + 1.4, RED.filled(), 2)
})).unwrap();
chart.configure_series_labels()
.background_style(&WHITE)
.border_style(&BLACK)
.draw()?;
Ok(())
}).style("width: 100%")
在这个例子中,我们选择在y 轴上显示误差,因为它通常更为显著。前面的图片是我们数据的可视化表示,展示了每个数据点周围的明显误差条。这些条形表示在一定置信水平下可能的值范围;较长的条形表示测量的不确定性更大。
然而,有时在两个轴上显示误差数据是有益的——特别是在处理时间序列或包含多个独立变量的实验数据时。在这种情况下,使用 [**ErrorBar::new_horizontal**](https://docs.rs/plotters/latest/plotters/element/struct.ErrorBar.html#method.new_horizontal)
方法并传递 x 轴误差的数组(对 y 轴误差做类似操作)就足够了。
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Horizontal Error Bars Plot", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(1f32..7f32, 1f32..50f32)?;
let x = Array::range(1., 7., 0.1);
chart.configure_mesh()
.x_desc("x = Array::range(1., 7., 0.1);")
.y_desc("y = f(x)")
.draw()?;
chart.draw_series(LineSeries::new(
x.iter().map(|x| (*x, (*x as f32).powi(2))),
&GREEN
)).unwrap()
.label("y = x ^ 2")
.legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &GREEN));
chart.draw_series(x.map(|x| {
ErrorBar::new_horizontal((*x as f32).powi(2), *x - 0.3, *x, *x + 0.3, RED.filled(), 2)
})).unwrap();
chart.configure_series_labels()
.background_style(&WHITE)
.border_style(&BLACK)
.draw()?;
Ok(())
}).style("width: 100%")
带有horizontal
误差条的单一图表(图片由作者提供)
通过将这些元素融入你的可视化中——无论是科学家分享研究成果还是业务分析师展示销售数据——观众都能更好地理解与所展示信息相关的任何不确定性。因此,利用这一关键功能将确保精确的细节被准确传达,同时在演示中保持清晰,不会因Plotters的功能如误差条导致的数据表示中的错误而造成混淆!
散点图
散点图是可视化数据和洞察两个变量之间关系的重要工具。Plotters通过将一个变量分配到x 轴,另一个分配到y 轴,并在相应坐标上绘制每个点,使在 Rust 中创建散点图变得轻而易举。通过调整点的颜色和大小,你可以在数据集中表示额外的维度。
使用散点图的主要优势在于它们揭示了数据中的模式或簇,这些在仅通过表格或图表时可能不明显。离群点也可以通过这种方法轻松识别。
此外,这些图形具有直观的特点,使任何人——无论统计专长如何——都能快速理解不同方面之间的关系,因此在展示发现时,它们是有用的沟通工具。
以下代码片段将生成均匀分布数据样本的散点图:
evcxr_figure((640, 480), |root| {
_ = root.fill(&WHITE);
let mut chart = ChartBuilder::on(&root)
.caption("Uniform Distribution Scatter Plot", ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.y_labels(5)
.x_label_formatter(&|x| format!("{:.1}", *x as f64 / 100.0))
.y_label_formatter(&|y| format!("{}%", (*y * 100.0) as u32))
.draw()?;
let _ = chart.draw_series(random_samples.iter().map(|(x,y)| Circle::new((*x,*y), 3, GREEN.filled())));
Ok(())
}).style("width:100%")
生成的散点图如下:
一张均匀分布数据样本的散点图(作者提供的图片)
总之,散点图提供了强大的可视化功能,让我们更好地理解数据集,同时提供了直接的方式与他人分享信息,这主要得益于Plotters库函数在 Rust 编程语言环境中的易用性!
直方图
直方图在分析数据分布时是一个不可或缺的工具。它们提供了信息如何在不同类别或区间中分布的视觉表示,使我们更容易理解和解读复杂的数据集。Plotters通过利用[**Histogram::vertical**](https://docs.rs/plotters/latest/plotters/series/struct.Histogram.html)
函数,简化了这一过程,该函数使用线性数组将数据点分组为表示每个区间频率的条形图。
例如,如果我们需要绘制随机生成的均匀分布,创建直方图可以详细显示每个可能结果的频率,同时揭示数据集中存在的任何模式或趋势。分析这些图表可以帮助我们发现有关基础分布的宝贵洞察,例如人口中的年龄组分布、照片中记录的光照水平,或城市中观察到的月度降水量。
以下代码片段是绘制随机生成的均匀分布数据样本的示例:
evcxr_figure((640, 480), |root| {
let mut chart = ChartBuilder::on(&root)
.caption("Histogram", ("Arial", 20).into_font())
.x_label_area_size(50)
.y_label_area_size(50)
.build_cartesian_2d(0u32..100u32, 0f64..0.5f64)?;
chart.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.y_labels(5)
.x_label_formatter(&|x| format!("{:.1}", *x as f64 / 100.0))
.y_label_formatter(&|y| format!("{}%", (*y * 100.0) as u32))
.draw()?;
let hist = Histogram::vertical(&chart)
.style(RED.filled())
.margin(0)
.data(random_samples.iter().map(|(x,_)| ((x*100.0) as u32, 0.01)));
let _ = chart.draw_series(hist);
Ok(())
}).style("width:100%")
生成的直方图如下所示:
均匀分布数据样本的直方图(作者提供的图像)
总之,直方图提供了强大的工具,用于深入了解各种数据集并准确识别影响它们的关键因素。通过使用Plotters的功能,例如专门针对我们需求定制的可调整箱体大小,使我们在快速解释大量信息时更具灵活性,而不牺牲准确性!
结论
Aaron Burden 的照片,来自 Unsplash
本文强调了可视化的重要性以及如何根据各种需求定制Plotters。Plotters在创建各种类型的图表(如单线图、多线图、散点图和直方图)方面证明了其无价之宝。此外,我们还学习了如何定制颜色线条、标记、图例等布局设计选项。
拥有新获得的知识,您可以自信地轻松导航Plotters的各种功能。有效利用这些方法将增强您对数据的理解,并使沟通结果更为准确。
在接下来的系列文章中,特别是第二部分,我们将探索引人入胜的数据可视化,包括但不限于饼图和 3D 可视化。我们的目标是使您能够成为数据的熟练视觉讲述者,揭示前所未有的隐藏洞察!
结束语
Nick Morrison 的照片,来自 Unsplash
在我们结束本教程时,我想对所有投入时间和精力完成教程的人表达诚挚的感谢。与您一起展示 Rust 编程语言的卓越能力,真是非常愉快。
对数据科学充满热情,我承诺从现在开始每周或左右写至少一篇关于相关主题的综合文章。如果你对我的工作感兴趣,可以通过各种社交媒体平台与我联系,或者直接联系我以获得其他帮助。
谢谢!
资源
## GitHub - wiseaidev/rust-data-analysis: 使用 Rust 进行终极数据分析课程
使用 Rust 进行终极数据分析课程。通过创建一个…
github.com docs.rs [## plotters - Rust]
Plotters - 一个专注于数据绘图的 Rust 绘图库,适用于 WASM 和本地应用程序 🦀📈🚀
docs.rs [## evcxr-jupyter-integration]