ExplorerのTidyverseシリーズをアップグレード
ElixirのNx Explorerが出たから半年が過ごして、新しいバージョン(0.4.0
)が出ました。@RyoWakabayashiさんの「Queryを使うと、データ分析が更に捗る」記事を読んだから、私の「Explorer: LivebookでTidyverseの再訪」の記事も更新したくなりました。
では、始めましょう。
セットアップ
新しいLivebookのセッションを開いて、「Notebook dependencies and setup」に以下のモジュールをインストールします。
Mix.install([
{:kino, "~> 0.7.0"},
{:explorer, "~> 0.4.0"},
{:download, "~> 0.0.4"}
])
両方:kino
と:explorer
のバージョンが上げました。実行して、準備が終わりました。
データをインポートする
次は、データをインポートして、データフレームにロードしましょう。
alias Explorer.DataFrame, as: DF
alias Explorer.Series, as: S
alias Kino.DataTable
# Explorer.Queryのマクロのためにこの行が必要です。
require Explorer.DataFrame
# データファイルがローカルに保存されてない場合、読みます。
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<
Polars[13453 x 10]
Player string ["Walker, Adam", "Walker, Adam", "Nakajima, Hiroyuki", "Nakajima, Hiroyuki",
"Ohshiro, Takumi", ...]
選手 string ["ウォーカー アダム", "ウォーカー アダム", "中島 宏之",
"中島 宏之", "大城 卓三", ...]
Team string ["YOM", "YOM", "YOM", "YOM", "YOM", ...]
Date string ["2022-04-23", "2022-04-23", "2022-04-23", "2022-04-23", "2022-04-23", ...]
Game integer [71798, 71798, 71798, 71798, 71798, ...]
Pitch string ["curve", "split-finger", "cutter", "split-finger", "cutter", ...]
Pitches integer [2, 4, 3, 3, 5, ...]
Velocity integer [nil, nil, 130, 134, 125, ...]
ResultType string ["hit", "hit", "hit", "hit", "hit", ...]
Result string ["single", "double", "single", "double", "single", ...]
>
今まで、何も変わってません。利用するデータフレームを読みました。表で表示しましょう。
df
|> DataTable.new()
よっしゃ~。データをきれいに表示しています。
ホームランを集計する
以前のバージョンではDF.filter_with(...)
が関数のコールバックパラメータがちょっと複雑でした。:explorer 0.4.0
のQuery
のおかげでより読みやすくなります。
最初は0.3.0
でこうしました:
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.()
今度は:
df =
df
|> DF.rename_with(&String.downcase/1)
|> DF.filter(result == "home-run")
|> DF.group_by(["選手", "team"])
|> DF.summarise(本塁打: count(pitches))
|> DF.arrange(desc: 本塁打)
df |> DataTable.new()
それぞれのデータフレームの関数がQuery
パラメータを取得して、マクロでシリーズ(Explorer.Series
)の関数を呼んでいます。この読み方のほうがだいぶ書きやすくて、読みやすくなりました。
でも、注意点があります。
df
|> DF.rename_with(&String.downcase/1)
|> DF.filter(result == "home-run")
...
Result
という列名が変数に使えなさそうです。小文字のresult
に変更するとマクロがうまく動いています。しないと、「型が違います」エラーがありました。
DF.group_by
が変わってないので、そのままで行います。
DF.summarise(本塁打: count(pitches))
はQuery
を利用して、シリーズの関数を書いて、変数のような列名をパラメータを取ります。この書き方がいいですよ。
最後に、DF.arrange(desc: 本塁打)
では「本塁打」にクォートが必要なくなりました。そうです、Query
のパラメータです。先頭文字が大文字がなくて、漢字を使えそうですよ!
チーム名を漢字に変更しましょう
以前、チームを日本語にmutate
するためにElixirのマップを利用することができました。
teams = %{
"CHU" => "中", "HAN" => "阪", "HIR" => "広",
"YAK" => "ヤ", "YOK" => "De", "YOM" => "巨",
"LOT" => "ロ", "NIP" => "日", "ORX" => "オ",
"RAK" => "楽", "SEI" => "西", "SFT" => "ソ"
}
...
|> DF.mutate([チーム: &S.transform(&1["Team"], fn team -> Map.get(teams, team) end)])
|> DF.group_by(["選手", "チーム"])
パイプラインにmutate
を挿入して、「Team」から「チーム」の列で組み合わせました。
でも、もうExplorer.Backend.LazySeries
の関数(例えばmutate
, filter
, transform
)でEnum
, Map
, などが使えなくなりました。どうすればElixirのマップを利用してmutate
ができると悩みました。
色々試してみて、やっとできました。必要なことはデータフレームから列をpull
して、そのシリーズを使って、データフレームにput
して、データフレームを戻す関数を作りました。何でも列をリマップすることができます。
teams = %{
"CHU" => "中", "HAN" => "阪", "HIR" => "広",
"YAK" => "ヤ", "YOK" => "De", "YOM" => "巨",
"LOT" => "ロ", "NIP" => "日", "ORX" => "オ",
"RAK" => "楽", "SEI" => "西", "SFT" => "ソ"
}
mutate_with_map = fn df, column, map, new_label ->
new_series =
DF.pull(df, column)
|> S.transform(fn key -> Map.get(map, key) end)
DF.put(df, new_label, new_series)
end
df
|> mutate_with_map.("team", teams, :チーム)
|> DF.select(["選手", "チーム", "本塁打"])
|> DataTable.new()
前より作業が必要ですが、まだわかりやすいと思います。
まとめ
大体3ヶ月づつでExplorerが更新しています。いつも新しいバージョンが良くなっています。0.3.0
のときに「warning: mutate/2 with a callback is deprecated, please use mutate_with/2 instead」を心配しましたが、自分の関数でパイプラインで続ける方法を研究してよかった。
3月後でもっと強くなると信じてます。