Polarsでデータフレームを扱う（Rust）

[PolarsというPandasを100倍くらい高性能にしたライブラリがとっても良いので布教します。](https://qiita.com/_jinta/items/fac13f09e8e8a5769b79)

昔pandas遅いなって思ってpolarsに乗り換えて速さに感動した覚えがあります。普通に布教してもn番煎じになるので、Rust番で布教したいと思います。


## 実行環境

```shell-session
$ uname -svr
Linux 5.15.0-53-generic #59-Ubuntu SMP Mon Oct 17 18:53:30 UTC 2022$ uname -svr

$ cargo --version
cargo 1.65.0 (4bc8f24d3 2022-10-20)
```

## 環境構築

Rustのインストールはこちらから。

適当なディレクトリを作ってからそのディレクトリで

```shell-session
$ cargo init . --bin
```
でいい感じにしてくれます。

## Polars入門

### Quick Start

[公式](https://pola-rs.github.io/polars-book/user-guide/quickstart/intro.html) のクイックスタートを一部変えて紹介します。polarsとQuick Startで使用するライブラリをプロジェクトに追加するために以下を `[dependencies]`に追加します。

```toml
# @Cargo.toml
[dependencies]
polars = { version = "0.25.1", features = ["lazy"] }
reqwest =  { version = "0.11.12", features = ["blocking"] }
color-eyre = "0.6"
```

main.rsをこんな感じにします。
```rust
// @src/main.rs
use color_eyre::Result;
use polars::prelude::*;
use reqwest::blocking::Client;
use std::io::Cursor;

fn main() -> Result<()> {
    let data: Vec<u8> = Client::new()
        .get("https://j.mp/iriscsv")
        .send()?
        .text()?
        .bytes()
        .collect();

    let df = CsvReader::new(Cursor::new(data))
        .has_header(true)
        .finish()?
        .lazy()
        .filter(col("sepal_length").gt(5))
        .groupby([col("species")])
        .agg([col("*").sum()])
        .collect()?;

    println!("{:?}", df);

    Ok(())
}
```

これで実行すると、

```shell-session
$ cargo run
shape: (3, 5)
┌────────────┬──────────────┬─────────────┬──────────────┬─────────────┐
│ species    ┆ sepal_length ┆ sepal_width ┆ petal_length ┆ petal_width │
│ ---        ┆ ---          ┆ ---         ┆ ---          ┆ ---         │
│ str        ┆ f64          ┆ f64         ┆ f64          ┆ f64         │
╞════════════╪══════════════╪═════════════╪══════════════╪═════════════╡
│ virginica  ┆ 324.5        ┆ 146.2       ┆ 273.1        ┆ 99.6        │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ versicolor ┆ 281.9        ┆ 131.8       ┆ 202.9        ┆ 63.3        │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ setosa     ┆ 116.9        ┆ 81.7        ┆ 33.2         ┆ 6.1         │
└────────────┴──────────────┴─────────────┴──────────────┴─────────────┘
```
とでてくると思います。貼り付けるとレイアウトが崩れるため、本来の出力はもっときれいです。

### 解説

最初の

```rust
    let data: Vec<u8> = Client::new()
        .get("https://j.mp/iriscsv")
        .send()?
        .text()?
        .bytes()
        .collect();
```
では `reqwest`を使ってデータの取得をしています。 `Client::new().get("https://j.mp/iriscsv").send()?.text()?` で https://j.mp/iriscsv にアクセスして、

```csv
sepal_length,sepal_width,petal_length,petal_width,species
5.1,3.5,1.4,0.2,setosa
4.9,3,1.4,0.2,setosa
4.7,3.2,1.3,0.2,setosa
...
```
のようなcsvフォーマットのデータを取得しています。これを `.bytes().collect()`でバイト列に変換します。

次の文でデータを読み込み、データフレームに変換しています。

```rust
    CsvReader::new(Cursor::new(data))
        .has_header(true)
        .finish()?
```

この時点で出力すると、

```
shape: (150, 5)
┌──────────────┬─────────────┬──────────────┬─────────────┬───────────┐
│ sepal_length ┆ sepal_width ┆ petal_length ┆ petal_width ┆ species   │
│ ---          ┆ ---         ┆ ---          ┆ ---         ┆ ---       │
│ f64          ┆ f64         ┆ f64          ┆ f64         ┆ str       │
╞══════════════╪═════════════╪══════════════╪═════════════╪═══════════╡
│ 5.1          ┆ 3.5         ┆ 1.4          ┆ 0.2         ┆ setosa    │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 4.9          ┆ 3.0         ┆ 1.4          ┆ 0.2         ┆ setosa    │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 4.7          ┆ 3.2         ┆ 1.3          ┆ 0.2         ┆ setosa    │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 4.6          ┆ 3.1         ┆ 1.5          ┆ 0.2         ┆ setosa    │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ ...          ┆ ...         ┆ ...          ┆ ...         ┆ ...       │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 6.3          ┆ 2.5         ┆ 5.0          ┆ 1.9         ┆ virginica │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 6.5          ┆ 3.0         ┆ 5.2          ┆ 2.0         ┆ virginica │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 6.2          ┆ 3.4         ┆ 5.4          ┆ 2.3         ┆ virginica │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┤
│ 5.9          ┆ 3.0         ┆ 5.1          ┆ 1.8         ┆ virginica │
└──────────────┴─────────────┴──────────────┴─────────────┴───────────┘
```
みたいに出てきて、データフレームができているのがわかります。

これに集計処理を施すと、

```rust
    let df = CsvReader::new(Cursor::new(data))
        .has_header(true)
        .finish()? // DF完成
        .lazy() // 遅延評価のおまじない
        .filter(col("sepal_length").gt(5)) // カラム「sepal_length」の値が5より大きい条件でフィルター
        .groupby([col("species")]) // カラム「species」でグループ化
        .agg([col("*").sum()]) // 和を取る
        .collect()?; // 評価する
```

最初に見た出力が得られます。

個人的にpolarsの書き方は、メソッドチェーンですらすらかけて好きです。Pythonから使っていたときはエラーメッセージがRust風に出ていてわかりにくそうな印象でした。

## その他

### 可視化

ここまでくると可視化とかしてみたくなりますよね？

そこでいい感じのを見つけたのでついでに試してみた。

https://github.com/plotters-rs/plotters

#### Install

Ubuntu で実行する場合、依存関係の追加の前に必要なライブラリをインストールする

```shell-session
$ sudo aptitude install pkg-config libfreetype6-dev libfontconfig1-dev
```

Cargo.tomlに依存ライブラリを追加する
```toml
[dependencies]
plotters = "0.3.4"
```

#### 散布図を作成する

[これ](https://qiita.com/_jinta/items/fac13f09e8e8a5769b79#a-matplotlib%E3%82%84sklearn%E3%81%A7%E4%BD%BF%E3%81%86)みたいに散布図を書いてみる。

散布図作成の関数

```rust
const OUT_FILE_NAME: &'static str = "imgs/scatter.png";

fn scatter<S: AsRef<str>, DB: DrawingBackend>(
    df: &DataFrame,
    x: S,
    y: S,
    caption: S,
    b: DrawingArea<DB, plotters::coord::Shift>,
) -> Result<(), Box<dyn Error>>
where
    DB::ErrorType: 'static,
{
    let x_series = df.column(x.as_ref())?;
    let y_series = df.column(y.as_ref())?;

    let caption_style = TextStyle::from(("sans-serif", 20).into_font());
    let mut scatter_context = ChartBuilder::on(&b)
        .caption(caption, caption_style)
        .margin(7)
        .set_left_and_bottom_label_area_size(40)
        .build_cartesian_2d(as_range(x_series), as_range(y_series))
        .unwrap();
    scatter_context
        .configure_mesh()
        .x_desc(x.as_ref())
        .y_desc(y.as_ref())
        .draw()?;
    scatter_context.draw_series(x_series.iter().zip(y_series.iter()).map(|(x, y)| {
        Circle::new(
            (x.try_extract().unwrap(), y.try_extract().unwrap()),
            5.0,
            GREEN.filled(),
        )
    }))?;

    b.present()?;
    Ok(())
}

fn as_range(s: &Series) -> std::ops::Range<f64> {
    std::ops::Range {
        start: s.min().unwrap(),
        end: s.max().unwrap(),
    }
}
```

出力

![scatter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989204/46d7fe87-6856-c5f3-8af0-5e3e8eff23e3.png)

コード量の割に普通のプロットができた。`ChartBuilder`がDefault traitを実装してくれていたらそのあたりは楽になりそう。知らんけど。

ここまでのコードはhttps://github.com/TM-MT/iris_playground にあります。



### インタラクティブにやる

やっぱpythonでデータ分析する人みたいにクールにJupyter使いたいですよね？

はじめにJupyter notebookを入れる
```shell-session
$ sudo aptitude install jupyter-notebook
```

Rustのカーネルとして [Evcxr Jupyter Kernel](https://github.com/google/evcxr/tree/main/evcxr_jupyter)を使う
```shell-session
$ cargo install evcxr_jupyter
$ evcxr_jupyter --install
```

標準ライブラリのソースを持っていなければ
```shell-session
$ rustup component add rust-src
```
でインストール

```shell-session
$ jupyter notebook
```
と打ってノートブックを起動し、newからRustを選択すると、おなじみのページがRust カーネルで起動します。

#### Quick Start

さっそくnotebookを使ってpolarsをさわってみましょう。

はじめに依存関係を追加します。セルに
```
// 依存関係を追加
:dep polars
:dep reqwest
```
をかき、実行します。時間がかかるのでコーヒーを淹れたりストレッチをして待ってください。

とりあえず[公式ドキュメント](https://pola-rs.github.io/polars-book/user-guide/quickstart/intro.html)の最初にあるものをやってみましょう。

```rust
use polars::prelude::*;
use reqwest::Client;
```

~~Advent calendar に間に合わなくなるので~~ここであまりの遅さに私は挫折しました。おとなしくpythonからpolarsをさわりましょう。