Rust机器学习之Plotters
本文将带领大家学习Plotters的基础用法。重点学习Plotters的图表元素和常用图表的使用。
本文是“Rust替代Python进行机器学习”系列文章的第四篇,其他教程请参考下面表格目录:
Python库 | Rust替代方案 | 教程 |
---|---|---|
numpy |
ndarray |
Rust机器学习之ndarray |
pandas |
Polars |
Rust机器学习之Polars |
scikit-learn |
Linfa |
Rust机器学习之Linfa |
matplotlib |
plotters |
Rust机器学习之plotters |
pytorch |
tch-rs |
Rust机器学习之tch-rs |
networks |
petgraph |
Rust机器学习之petgraph |
数据和算法工程师偏爱Jupyter,为了跟Python保持一致的工作环境,文章中的示例都运行在Jupyter上。因此需要各位搭建Rust交互式编程环境(让Rust作为Jupyter的内核运行在Jupyter上),相关教程请参考 Rust交互式编程环境搭建
什么是Plotters
Plotters是一个用纯Rust开发的图形库,用于中渲染图形、图表和数据可视化。它支持静态图片渲染和实时渲染,并支持多种后端,包括:位图格式(png、bmp、gif等)、矢量图(svg)、窗口和HTML5 Canvas。
Plotters对不同后端使用统一的高级API,并允许开发者自定义坐标系。在Plotters中,任何类型的图表都被抽象为一系列绘图操作,通过这些绘图操作,开发者可以自由地操控和组合绘图内容。因此Plotters不会限制图表类型,开发者可以组合出任意内容的图表。
图1. Plotters图表案例
安装Plotters
安装Plotters非常简单。如果是Cargo项目,只需要在Cargo.toml
中加入
[dependencies]
plotters = "0.3.4"
如果是在jupyter环境下,可以用evcxr
的:dep
加载依赖,只需要加入下面一句:
:dep plotters = {
version = "0.3.4", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
因为evcxr
只支持输出SVG格式图片,所以这里不需要其他后端,因此我们需要加上default_features = false
关掉plotters原本的后端,同时加上features = ["evcxr", "all_series", "all_elements"]
,让plotters支持evcxr
环境,all_series
表示支持所有图表,all_elements
表示支持所有的图表元素。
关掉plotters原本的后端能让plotters第一次加载时快很多,但即便这样,初次编译plotters还是需要点时间。但好在jupyter中cell之间是共享环境的,第一次编译加载完后,后面调用都很快。
Plotters初探
安装好plotters后,我们来看一下Plotters的基本用法。
首先需要引入所需的模块,这里最简单的做法是引入prelude
,prelude
里包含了evcxr
绘图所需的evcxr_figure
方法。
extern crate plotters;
use plotters::prelude::*;
要想在Jupyter中创建图表,需要使用evcxr_figure
方法。evcxr_figure
方法接收2个参数,第一个参数是图表的尺寸,第二个参数是一个闭包,用于处理图表的绘制。这里的闭包会带上一个DrawingArea
对象,这是plotters的绘图后端,一切绘图操作都在此后端上进行。我们先来看个简单的例子:
evcxr_figure((300, 100), |root| {
// 绘图逻辑
root.fill(&BLUE)?;
// 最后返回OK告诉Plotters画完了
Ok(())
})
上面的代码会在Jupyter中显示一个用绿色填充的300*100大小的矩形。
上面的图形太单调了,我们在上面写点文字。
evcxr_figure((300, 100), |root| {
// 绘图逻辑
root.fill(&GREEN)?;
// 绘制文字
root.draw(&Text::new("Plotters真好用!", (40, 40), ("Arial", 30).into_font()))?;
// 最后返回OK告诉Plotters画完了
Ok(())
})
上面的代码在原来代码的基础上加入了一行root.draw(&Text::new("Plotters真好用!", (40, 40), ("Arial", 30).into_font()))?;
。这里draw
方法用于绘制元素,传入3个参数,第一个参数是绘制的内容,这里我们创建了一个文本(Text)对象;第二个参数是绘制的位置(元素左上角在绘图区的位置);第三个参数是字体对象,可以空值文本的字体和大小。绘制效果如下图:
Plotters的一大特性是可以通过将绘图区域分割成若干子绘图区域来绘制多个图形,并且这种分割可以递归地进行下去。这个特性使得Plotters实现分型绘图非常简单。我们尝试用Plotters画一个谢尔宾斯基地毯:
use plotters::coord::Shift;
pub fn sierpinski_carpet(
depth: u32,
drawing_area: &DrawingArea<SVGBackend, Shift>)
-> Result<(), Box<dyn std::error::Error>> {
if depth > 0 {
// 将传入地绘图区分割成3*3个子区域
let sub_areas = drawing_area.split_evenly((3,3));
// 遍历这3*3个子区域
for (idx, sub_area) in (0..).zip(sub_areas.iter()) {
if idx == 4 {
// 第5个区域(中心区域)填上白色
sub_area.fill(&WHITE)?;
} else {
// 其他区域递归地进行绘图
sierpinski_carpet(depth - 1, sub_area)?;
}
}
}
Ok(())
}
evcxr_figure((4800,4800), |root| {
root.fill(&BLACK)?;
sierpinski_carpet(5, &root)
}).style("width: 400px") // 这里可以CSS来展示输出图片在网页上的显示
除了绘制各种图形外,Plotters最主要的功能就是绘制图表。Plotters提供了_图表上下文_API让我们可以方便地操作图表的标题、坐标轴、网格、图例等图表元素。下面的例子展示了如何使用图表上下文绘制图表:
evcxr_figure((640, 240), |root| {
root.fill(&WHITE)?;
// 创建图表上下文
let mut chart = ChartBuilder::on(&root)
.caption("图表标题", ("Arial", 20).into_font())
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
// 将数据绘制到图表上
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x, x), 5, &RED)
}))?;
Ok(())
}).style("width:60%")
上面的代码首先用ChartBuilder::on
方法创建图表上下文,这里需要传入绘图区域,将图表上下文绑定在此绘图区域上。然后用caption
方法设置图表的标题,用build_cartesian_2d
方法设置直角坐标系的取值范围。
创建好图表上下文后,我们就可以调用图表上下文的draw_series
方法绘制图表的内容。这里只是简单地画了10个散点,效果如下:
上面的图表可能不像图表,因为没有将坐标轴画出来。其实图表中有很多元素可以设置,上面的例子只展示了标题和内容,下一节我们会逐个看一下图表都有哪些组成元素以及如何配置他们。
添加图表元素
Plotters允许我们灵活地向图表中添加坐标轴、网格、图例等元素,本节我们就来看一下如何向图表中添加这些元素。
添加网格
我们首先向上一节的图表中加入网格。很简单,只需要加入一句chart.configure_mesh().draw()?;
即可:
evcxr_figure((640, 240), |root| {
root.fill(&WHITE)?;
// 创建图表上下文
let mut chart = ChartBuilder::on(&root)
.caption("带网格的图表", ("Arial", 20).into_font())
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
// 绘制网格
chart.configure_mesh().draw()?;
// 将数据绘制到图表上
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x, x), 5, &RED)
}))?;
Ok(())
}).style("width:60%")
网格支持很多配置可以控制网格的呈现形式。bold_line_style
和 light_line_style
方法可以控制主/副网格线的颜色和透明度,如果想隐藏网格线可以传入&TRANSPARENT
。x_labels
和y_labels
方法可以设置坐标轴的刻度。如果想禁用某个轴上的网格,可以用disable_x_mesh
或disable_y_mesh
方法。下面代码可以实现隐藏x轴向网格,隐藏副网格线,y轴网格显示10个刻度:
chart.configure_mesh()
.y_labels(10)
.light_line_style(&TRANSPARENT)
.disable_x_mesh()
关于网格的更多配置,请参考MeshStyle in plotters::chart - Rust (docs.rs)
添加坐标轴
其实在我们调用build_cartesian_2d
方法时,坐标轴就已经建立好了。那为什么我们看不到图表中有坐标轴呢?这是因为我们没有设置坐标轴的显示区域。Plotters中用x_label_area_size
和 y_label_area_size
来设置横坐标和纵坐标的显示区域。我们只要在创建图表上下文时设置横坐标/纵坐标的高度/宽度即可。
evcxr_figure((640, 240), |root| {
root.fill(&WHITE)?;
// 创建图表上下文
let mut chart = ChartBuilder::on(&root)
.caption("带坐标轴和网格的图表", ("Arial", 20).into_font())
.x_label_area_size(40) // x轴显示区域(高度)
.y_label_area_size(40) // y轴显示区域(宽度)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
// 绘制网格
chart.configure_mesh()
.y_labels(10)
.light_line_style(&TRANSPARENT)
.disable_x_mesh()
.draw()?;
// 将数据绘制到图表上
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x, x), 5, &RED)
}))?;
Ok(())
}).style("width:60%")
我们还可以调用MeshStyle的x_desc
和y_desc
方法为坐标轴设置描述:
chart.configure_mesh()
.x_desc("X轴描述")
.y_desc("Y轴描述")
...
添加图例
如果图表中有多个图线,一般我们需要添加图例加以区分。Plotters中可以在绘制图线后用label
方法设置图例名称,用legend
绘制图例。由于图例是单独的元素,设置好后需要调用configure_series_labels().draw()
才能绘制出来,请看下面的代码:
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x, x), 5, &RED)
}))?
.label("y=x")
.legend(|(x, y)| Circle::new((x+20, y), 4, &RED));
chart.configure_series_labels().draw()?;
常见图表
与其他图标库不同,Plotters并没有内置任何形式的图表,而是将图表抽象为一个叫series
的概念。这样做最大的好处是灵活,可以方便地将两种不同类型的图组合在一起。
为了使用方便,Plotters内置了几个常用的series
,分别是:
- 散点图
- 折线图
- 直方图
我们也可以定义自己的serires
,然后根据数据在图上的坐标绘制任意图形,灵活程度非常高。
散点图
我们先来随机创建一些点:
:dep rand = {
version = "0.6.5" }
extern crate rand;
use rand::distributions::Normal;
use rand::distributions::Distribution;
use rand::thread_rng;
let sd = 0.13;
let random_points:Vec<(f64,f64)> = {
let mut norm_dist = Normal::new(0.5, sd);
let (mut x_rand, mut y_rand) = (thread_rng(), thread_rng());
let x_iter = norm_dist.sample_iter(&mut x_rand);
let y_iter = norm_dist.sample_iter(&mut y_rand);
x_iter.zip(y_iter).take(1000).collect()
};
这里我们用正态分布生成了1000个点。
用Plotters绘制散点图很简单。只需要传入坐标序列即可。下面的示例显示了如何制作2D正态分布图。红色矩形是$2\sigma$面积,红色十字是平均值。
evcxr_figure((640, 480), |root| {
root.fill(&WHITE)?;
// The following code will create a chart context
let mut chart = ChartBuilder::on(&root)
.caption("随机正态分布散点图", ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_ranged(0f64..1f64, 0f64..1f64)?;
chart.configure_mesh()
.disable_x_mesh() // 隐藏网格
.disable_y_mesh() // 隐藏网格
.draw()?;
// 绘制散点
chart.draw_series(random_points.iter().map(|(x,y)| Circle::new((*x,*y), 3, GREEN.filled())));
// 绘制红色矩形,框住2σ区域
let area = chart.plotting_area();
let two_sigma = sd * 2.0;
area.draw(&Rectangle::new(
[(0.5 - two_sigma, 0.5 - two_sigma), (0.5 + two_sigma, 0.5 + two_sigma)],
RED.mix(0.3).filled())
)?;
// 标出均值位置
area.draw(&Cross::new((0.5, 0.5), 5, &RED))?;
Ok(())
}).style("width:60%")
折线图
Plotters提供了LineSeries
用于绘制折线图,只需要传入一系列坐标点序列,Plotters就能将这些点连成折线。下面示例展示了如何用LineSeries
绘制正弦曲线和余弦曲线:
evcxr_figure((640, 480), |root| {
let x_axis = (-3.4f32..3.4).step(0.01);
root.fill(&WHITE)?;
let mut chart = ChartBuilder::on(&root)
.margin(5)
.set_all_label_area_size(50)
.caption("正弦曲线和余弦曲线", ("sans-serif", 20))
.build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32)?;
chart.configure_mesh()
.x_labels(20)
.y_labels(10)
.disable_mesh()
.x_label_formatter(&|v| format!("{:.1}", v))
.y_label_formatter(&|v| format!("{:.1}", v))
.draw()?;
// 绘制正弦曲线
chart.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.sin())), &RED))?
.label("正弦")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
// 绘制余弦曲线
chart.draw_series(LineSeries::new(
x_axis.values().map(|x| (x, x.cos())),
&BLUE,
))?
.label("余弦")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
chart.configure_series_labels().border_style(&BLACK).draw()?;
Ok(())
}).style("width:60%")
直方图
在数据可视化中直方图也是用的最多的一种图。Plotters提供了Histogram
生成直方图series。下面的代码分别统计了散点图中随机点在x轴和y轴上的分布:
evcxr_figure((640, 480), |root| {
let areas = root.split_evenly((2,1));
let mut charts = vec![];
root.fill(&WHITE)?;
for (area, name) in areas.iter().zip(["X", "Y"].into_iter()) {
let mut chart = ChartBuilder::on(&area)
.caption(format!("{}轴直方图", name), ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.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()?;
charts.push(chart);
}
let hist_x = Histogram::vertical(&charts[0])
.style(RED.filled())
.margin(0)
.data(random_points.iter().map(|(x,_)| ((x*100.0) as u32, 0.01)));
let hist_y = Histogram::vertical(&charts[0])
.style(GREEN.filled())
.margin(0)
.data(random_points.iter().map(|(_,y)| ((y*100.0) as u32, 0.01)));
charts[0].draw_series(hist_x);
charts[1].draw_series(hist_y);
Ok(())
}).style("width:60%")
多图组合
Plotter最强大的地方就是可以通过分割子图将多个图表组合在一起。我们可以将随机点的散点图和各轴上的分布直方图组合在一起:
evcxr_figure((640, 480), |root| {
root.fill(&WHITE)?;
let root = root.titled("散点图组合直方图", ("Arial", 20).into_font())?;
let areas = root.split_by_breakpoints([560], [80]);
let mut x_hist_ctx = ChartBuilder::on(&areas[0])
.y_label_area_size(40)
.build_cartesian_2d(0u32..100u32, 0f64..0.5f64)?;
let mut y_hist_ctx = ChartBuilder::on(&areas[3])
.x_label_area_size(40)
.build_cartesian_2d(0f64..0.5f64, 0..100u32)?;
let mut scatter_ctx = ChartBuilder::on(&areas[2])
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f64..1f64, 0f64..1f64)?;
scatter_ctx.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.draw()?;
scatter_ctx.draw_series(random_points.iter().map(|(x,y)| Circle::new((*x,*y), 3, BLUE.filled())))?;
let x_hist = Histogram::vertical(&x_hist_ctx)
.style(RED.filled())
.margin(0)
.data(random_points.iter().map(|(x,_)| ((x*100.0) as u32, 0.01)));
let y_hist = Histogram::horizontal(&y_hist_ctx)
.style(GREEN.filled())
.margin(0)
.data(random_points.iter().map(|(_,y)| ((y*100.0) as u32, 0.01)));
x_hist_ctx.draw_series(x_hist)?;
y_hist_ctx.draw_series(y_hist)?;
Ok(())
}).style("width:60%")
结语
尽管Plotters与Python的matplotlib思想和用法不同,但与matplotlib比Plotters有很多优势:
- 灵活,Plotters不固定图表类型,而是通过高度抽象的模型和组件元素让开发者可以自由创作任意想要的图表
- 快速,得益于Rust的性能,使Plotters可以轻松处理上亿数据的可视化,这在Python和JavaScript中几乎是不可能的。
- 支持WebAssembly ,Plotters对wasm支持良好,通过wasm,Plotters可以运行在网页上,替代JavaScript处理大量数据的可视化,提升页面性能,优化使用体验。
Plotters的功能和配置项非常多,本文只选择日常最常用的功能和图表做了讲解和演示,更多内容请阅读Plotters Developer's Guide。