Explorer: LivebookでTidyverseの再訪

2022年 9月 27日(火)は、「[LiveView JP#10：LivebookでキャッチーにElixirを楽しもう](https://liveviewjp.connpass.com/event/260861/)」が開催されました。@torifukukaiou さんは [「LiveView JP#10：LivebookでキャッチーにElixirを楽しもう」レポート](https://qiita.com/torifukukaiou/items/65cb4fa0b7875d5e4426)にイベントを記録してくれました。

私の Lightning Talkでは 3ヶ月前の「[Explorer: LivebookでTidyverse](https://qiita.com/westbaystars/items/6d70d06540f9ce2e324b)」の記事を再訪して、裏の考え方をちっょと説明して、結果がどのように変わったかを調べました。

## ExplorerはRのTidyverseだぁ!

NxやMLの話題にはあんまり興味がありませんでしたが、Explorerを初めて聞いたときから「やっとElixirでデータサイエンスができる」と思いました。今まで [R Studio](https://www.rstudio.com/)やちょっと間にβ版の[Exploratory](https://exploratory.io/)でデータを分析しました。特にExploratoryを利用したときにデータフレームのパイプする方法を勉強になりました。これがTidyverseの基本機能でした。Elixirを初めて使ったときに、「あっ、Rのパイプと同じ使い方ですね。」と思いました。

では、話せるの日本語がちょっと弱いですので、概念をよく理解するために日本語でよく説明する[RのTidyverse/dplyr](https://stats.biopapyrus.jp/r/tidyverse/dplyr.html)を読むことをお勧めします。

[ExplorerのHexdocs](https://hexdocs.pm/explorer/0.3.1/Explorer.DataFrame.html#functions-single-table)を見れば、dyplrと同じ動詞を利用する関数がいっぱいあります。しかも、`*_with`が追加されているExplorer関数は拡張する幅が広がります。バージョンがまだ0.3.1ですが、早くRの成熟した機能を超えている感じがあります。

## Explorerを使ってデータ分析をしましょう

これからの説明は「LiveView JP#10」のセッションとちょっと別れます。LTでは標準のLivebookとExplorerの出力を利用しましたが、2次回で2つのことを勉強になりました:

* `Explorer.DataFrame`を `Kino.DataTable`で表示すること
  @RyoWakabayashi さん - ありがとうございました
* `Download.from()`でウェブからデータを取得すること
  @piacerex さん - ありがとうございました

これから、以上の項目がわかったらプレゼンをしましょう。

## セットアップ

Livebookの新しいセッションを開始して、「Reconnect and setup」のセクションに以下をインストールしましょう。

```Elixir
Mix.install([
 {:explorer, "~> 0.3.1"},
 {:kino, "~> 0.6.2"},
 {:download, "~> 0.0.4"}
])
```

データをインポートしましょう

```Elixir
alias Explorer.DataFrame, as: DF
alias Explorer.Series, as: S
 
unless File.exists?("balls-in-play-2022.tsv"), do:
  {:ok, _filename} = Download.from(
    "https://raw.githubusercontent.com/westbaystars/first-pitch-homeruns/main/balls-in-play-2022.tsv"
  )
 
df = DF.from_csv!("balls-in-play-2022.tsv", delimiter: "\t")
```

* `Explorer.DataFrame`と`Explorer.Series`を`alias`して、略語を使用します。
* 「balls-in-play-2022.tsv」ファイルが存在してない場合、ドウンロードします。
* それから、TSVファイルを「タブ区切り」でデータフレームへロードします。

結果は:

```Elixir
#Explorer.DataFrame<
 Polars[13453 x 10]
 Player string ["Walker, Adam", "Walker, Adam", "Nakajima, Hiroyuki", "Nakajima, Hiroyuki",
  "Ohshiro, Takumi", ...]
 選手 string ["ウォーカー アダム", "ウォーカー アダム", "中島 宏之",
  "中島 宏之", "大城 卓三", ...]
  ...
```

うん、テーブルで表示してみましょう。
```Elixir
…
 
df = DF.from_csv!("balls-in-play-2022.tsv", delimiter: "\t")
 
df
|> DF.to_rows()
|> Kino.DataTable.new()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/c4d466ab-9229-506a-8b22-4257b32037f3.png)

データがあります! でも、列の順番が変ですよ。データフレームの順番は `Player`、`選手`、`Team`, `Date`, `Game`, `Pitch`、などでした。

ドキュメンテーションを読むと、`DF.to_rows(df)`はマップを作ります。そのマップの順番を保存してないので、列がぐちゃぐちゃになってしまう。だから、`to_rows()`を呼ぶ前に列のキーを集まって、`Kino.DataTable.new(data, keys: [“Player”, “選手”, “Team”, …])`で読んでみましょう。

```Elixir
...
 
df = DF.from_csv!("balls-in-play-2022.tsv", delimiter: "\t")
 
keys = DF.names(df)
df
|> DF.to_rows()
|> Kino.DataTable.new(keys: keys)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/32964274-3655-034c-21b8-fe250638f272.png)

良くなりました。

では、これから列名が変わるので、`show_table(df)`の関数を作ってみましょう。

```Elixir
show_table = fn df ->
 keys = DF.names(df)
 df
 |> DF.to_rows()
 |> Kino.DataTable.new(keys: keys)
end
 
...
 
df = DF.from_csv!("balls-in-play-2022.tsv", delimiter: "\t")
 
df
|> show_table.()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/24a297dc-3132-8d49-1159-619c456f8be9.png)


よっし。これが使えるものです。

## ホームランを集計しましょう

本塁打の数を集計したいので、この風にできます。

```Elixir
df
|> DF.filter_with(&S.equal(&1["Result"], "home-run"))
|> DF.group_by(["選手", "Team"])
|> DF.summarise(Pitches: [:count])
|> DF.rename(Pitches_count: "本塁打")
|> DF.arrange(desc: "本塁打")
|> show_table.()
```

言葉でパイプラインを通じすると:

* ホームランが打席の結果にフィルターして、
* 「選手」と「Team」で集まって、
* レコードの数を集計して (投球のフィールドを利用してますが、何でもいいです)、
* 集計した列名を「本塁打」にリネームして、
* 本塁打の多いから少ないへソートして、
* 表を表示しましょう

その結果は:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/5b7594fe-5ff7-5cfd-331e-c5854ba04c93.png)

2022年 9月 24日現在の本塁打のリーダーと比べて、みんなは同値を確認しています。村上はすごいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/1c92ea21-0ecc-3384-3df1-9b5b08cfac64.png)


## チームの列を日本語化にしましょう

表を見るとチームが英語の略語を利用しています。これらの12名をMapに入れたら、簡単に入れ替えれます。

```Elixir
teams = %{
 "CHU" => "中", "HAN" => "阪", "HIR" => "広",
 "YAK" => "ヤ", "YOK" => "De", "YOM" => "巨",
 "LOT" => "ロ", "NIP" => "日", "ORX" => "オ",
 "RAK" => "楽", "SEI" => "西", "SFT" => "ソ"
}
 
df
|> DF.filter_with(&S.equal(&1["Result"], "home-run"))
|> DF.mutate([チーム: &S.transform(&1["Team"], fn team -> Map.get(teams, team) end)])
|> DF.group_by(["選手", "チーム"])
|> DF.summarise(Pitches: [:count])
|> DF.rename(Pitches_count: "本塁打")
|> DF.arrange(desc: "本塁打")
|> show_table.()
```

`DF.mutate`で「`チーム`」という列を作成して、「`Team`」の列にある値を`teams`のマップを取得して、日本語に変更します。

もちろん、`DF.group_by`に「`Team`」から「`チーム`」の列を表示したいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/06aeae21-7ff0-c20e-fd10-cd18cb85e4de.png)

「ヤ」の代わりに「ヤクルト」などを出力したい場合、マップを編集することができます。

## 一球目で誰が一番のホームランを打ってますか?

でも、「一球目に誰が一番多くの本塁打を打ってますか?」を調べたかった。そうするために、もう一つフィルターを頭に追加しましょう。

```Elixir
df
|> DF.filter_with(&S.equal(&1["Pitches"], 1))
|> DF.filter_with(&S.equal(&1["Result"], "home-run"))
…
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710912/731203cc-af51-bdfb-7ead-54455d725646.png)

3ヶ月前、阪神の大山と巨人の岡本がトップで 6つずつでした。この間、巨人の中田翔と村上が大山を超えて、岡本選手は+2本を打ちました。

## 注意点:

Explorer 0.3.xから `DF.*_with`の関数を追加して、0.2.xの `filter`、`mutate`、などのコールバック付き関数は廃止されました。以上に「`DF.filter_with`」を利用していますが、`DF.mutate`がまだ廃止された関数を利用してます。だから、以下の注意を表示しています:

```Elixir
warning: mutate/2 with a callback is deprecated, please use mutate_with/2 instead
 (explorer 0.3.1) lib/explorer/data_frame.ex:1298: anonymous fn/3 in Explorer.DataFrame.df_for_mutations/2
 (elixir 1.14.0-rc.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
 (explorer 0.3.1) lib/explorer/data_frame.ex:1295: Explorer.DataFrame.df_for_mutations/2
 (explorer 0.3.1) lib/explorer/data_frame.ex:1284: Explorer.DataFrame.mutate/2
 (elixir 1.14.0-rc.0) src/elixir.erl:298: anonymous fn/4 in :elixir.eval_external_handler/1
 (stdlib 4.0.1) erl_eval.erl:748: :erl_eval.do_apply/7
```

問題は`S.transform`の第2パラメータがコールバック関数を`DF.mutate_with`に渡せないことです。いろいろ調べてみましたが、いい方法がまだ見つかりません。このような変更が便利ですので、未来のリリースでできなくなって困ります。

## まとめ

まぁ、私はプロ野球のデータを分析しますが、セミナーにいる方はアクセスログなどの使い方がすぐ見えました。`Kino.DataTable`とグラフ機能を利用すれば、もう少しでExploratoryのようなツールになれると思います。データ分析するためにElixirとExplorerの未来が明るいと思います。

では、これからも、よろしくお願い申し上げます。