Juliaでtidyverseを使う②~TidierData.jlによるデータ前処理:tidyr編~
本記事について
R言語で前処理や可視化などを効率的に行うことができるtidyverse
パッケージ群をJuliaに移植した、Tidier.jl
パッケージ群の解説記事シリーズ。
本記事では、Tidier.jl
パッケージ群のうち、R言語のtidyr/dplyr
パッケージに対応する、TidierData.jl
を用いたデータ前処理について解説する。
公式にReferenceが提供されているので、ぜひそちらを直接参照していただきたい。本記事では一部の主要関数に限定して解説を行う。
TidierData.jl
を使うモチベーション
別記事『Juliaでtidyverseを使う①~TidierData.jlによるデータ前処理:dplyr編~』を参照のこと。
TidierData.jl
の関数・マクロたち
それでは以下に実例を示していく。まずはパッケージをインストールしよう。tidyverse
パッケージ群のように、@Tidier.jl
パッケージ群をインストールすることで、付属する種々のパッケージを一気にインストールできる。
ただしこれにはやや時間がかかるので、本記事を読むうえでは、TidierData.jl
のみをインストールするのでもよい。
using Pkg
Pkg.add("TidierData")
using TidierData
なおRのtidyr/dplyr
と同様に、TidierData.jl
による前処理も、原則的にパイプライン処理を利用した書き方をする。Juliaのパイプライン処理については他記事を参照のこと。
サンプルデータ
本記事ではRDatasets.jl
の、mtcars
データセットを利用する。これは32台の車両の性能(例:燃費、重量)を格納したデータセットである。
using RDatasets
@chain RDatasets.dataset("datasets", "mtcars") begin
first(_, 3) # 冒頭3行を表示
end
# TidierData.jlはChain.jlのラッパーなので、パイプライン処理ができる
説明の都合上、データセットの一部を抽出してデータサイズを小さくしておく(別記事『Juliaでtidyverseを使う①~TidierData.jlによるデータ前処理:dplyr編~』で解説した関数・マクロを使っている)。
df =
@chain RDatasets.dataset("datasets", "mtcars") begin
TidierData.@filter(Cyl == 6) # Cyl == 6の行のみ抽出
TidierData.@transmute(Model, # 車種名
MPG, # 燃費
WT, # 車体重量
AM = case_when(
AM == 0 => "Automatic", # オートマ車
AM == 1 => "Manual" # マニュアル車
)
) # 列を追加・上書きしながら、特定の列のみを選択
TidierData.@arrange(AM) # 列の水準を昇順に並び替え
end
TidierData.@pivot_wider()
による、long → wideデータへの変形
これはRのtidyr::pivot_wider()
に対応する(なお後述するように、2023/12/22時点では全ての機能が移植されているわけではない)。
前述のようにサンプルデータフレームdf
はtidy dataになっているので、すでにデータ分析において理想的な形式になっているのだが、用途に応じてwideデータに変形したいこともある。そのとき、TidierData.@pivot_wider()
を用いる。
引数names_from
には、横方向に展開したい水準を持つ変数を指定する。引数values_from
には、wide展開したときに、それぞれの水準が新たな列となるので、その列にどの変数をデータとして格納するかを指定する。
以下のコードでは、変数AM
の持つAutomatic
やManual
という水準がwide展開されて新たな列名となり、それぞれの列には変数MPG
のデータが格納されている。例えば「Model
名がMazda RX4
の車両」はManual
車なので、wide展開したときに、当然ながらAutomatic
列のデータは欠測となる。そのため自動的にmissing
と表示される。
df_wide =
@chain df begin
TidierData.@pivot_wider(names_from = AM, values_from = MPG)
end
println(df_wide)
もしmissing
部分を一括で特定の値で埋めたければ、引数values_fill
を用いる。
@chain df begin
TidierData.@pivot_wider(names_from = AM, values_from = MPG, values_fill = 100.0)
end
なお注意が必要なのは、longデータをwide展開する際に、各行を識別する変数が必要なことだ。今回は最初から、個別の車種名を持つ変数Model
があるので、問題ない。しかしこの列を除外すると、上記のコードは動かない。
@chain df begin
TidierData.@select(!Model) # Model列を除外
TidierData.@pivot_wider(names_from = AM, values_from = MPG)
end
未実装機能
Rのtidyr::pivot_wider()
には豊富な引数が用意されており、本来であればJulia移植後も以下のようなコードが実現されてほしい。この場合、Rであれば、wide展開後に作成される列はMPG.Automatic
, MPG.Manual
, WT.Automatic
, WT.Manual
の4列になる。が、2023/12/22時点では未実装なので、今後に期待。
@chain df begin
TidierData.@pivot_wider(names_from = AM, names_sep = ".", values_from = [MPG, WT])
end
ただ一応、色々工夫すれば、結果的には同じことは実現できる。ここは、前処理力が試されているということだろうか。
@chain df begin
TidierData.@mutate(AM = case_when(AM == "Automatic" => "MPG.Automatic",
AM == "Manual" => "MPG.Manual"),
AM2 = AM # AM列を複製
)
TidierData.@mutate(AM2 = case_when(AM2 == "Automatic" => "WT.Automatic",
AM2 == "Manual" => "WT.Manual")
)
TidierData.@pivot_wider(names_from = AM, values_from = MPG)
TidierData.@pivot_wider(names_from = AM2, values_from = WT)
end
TidierData.@pivot_longer()
による、wide → longデータへの変形
これはRのtidyr::pivot_longer()
に対応する(やはり後述するように、2023/12/22時点では全ての機能が移植されているわけではない)。
wideデータを、機械処理しやすいようにlongデータに変形したいことはよくある。先に作成したwideデータのオブジェクトdf_wide
をlongデータに戻してみよう。
TidierData.@pivot_longer()
では、TidierData.@pivot_wider()
と異なり、第2引数に処理対象の列を指定する。ここでの指定方法は、TidierData.select()
のセマンティクスと同様なので、ヘルパ関数(TidierData.starts_with()
など)を併用することもできる。
今回はAutomatic
とManual
という列名を、AM
という新たに作成する列の水準にしたいので、names_to
という引数に新たに作成する列名AM
を指定する。
@chain df_wide begin
TidierData.@pivot_longer(Automatic:Manual, names_to = AM, values_to = MPG)
end
ここで、Model
列に注目すると、同じ車種名が二度ずつ出現していることがわかる。もともとこのデータセットはlongデータであり、それをいったんwideデータにしたのち、longデータに戻しているのに、元通りになっていない。
今回は、もともと各車種のデータは1行ずつしかなかったので、MPG
がmissing
になっている行は、もともと存在しなかった行になる。よってこれらの行を削除すれば元通りになる。
@chain df_wide begin
TidierData.@pivot_longer(Automatic:Manual, names_to = AM, values_to = MPG)
TidierData.@drop_missing(MPG) # ある変数にmissingが含まれている場合はその行を削除
end
TidierData.@separate()/@unite()
による水準の分離と結合)
未実装機能(ついでにRのtidyr::pivot_longer()
には豊富な引数が用意されており、やはりJulia移植後も同様の機能を使いたいが、2023/12/22時点では未実装なものも多い。
例えば以下のようなwideデータをTidierData.@pivot_longer()
でlongデータに変形したとする。
using DataFrames # TidierData.jlはDataFrames.jlのラッパーなので、インストール済み
df_wide_2 = DataFrames.DataFrame(ID = [1, 2, 3],
control_A= [10.0, 10.5, 8.2],
control_B= [9.5, 10.2, 11.4],
experimental_A = [15.4, 14.1, 16.3],
experimental_B = [18.8, 17.6, 20.1]
)
@chain df_wide_2 begin
TidierData.@pivot_longer(contains("_"), names_to = condition, values_to = data)
end
condition
列には、control/experimental
の情報と、A/B
の情報が共存しているので、これらを識別する列(例えばcondition_CE
とcondition_AB
)を作ったほうがよい。
Rのtidyr::pivot_longer()
なら、これを実現するための引数が用意されているのだが、2023/12/22時点で、TidierData.@pivot_longer()
にはその機能はなく、恐らくTidierData.@separate()
を併用して実現する必要がある。これはRのtidyr::separate()
に対応する。
- 第2引数:分割する水準を持つ変数
- 第3引数:分割後のそれぞれの変数名
- 第4引数:分割する位置を示す文字列
df_sep =
@chain df_wide_2 begin
TidierData.@pivot_longer(contains("_"), names_to = condition, values_to = data)
TidierData.@separate(condition, [condition_CE, condition_AB], "_")
end
println(df_sep)
もし反対に、複数の変数に存在する水準を、行方向に結合した1つの変数を作成したければ、TidierData.@unite()
を用いる。これはRのtidyr::unite()
に対応する。
- 第2引数:結合に作成される新たな変数名
- 第3引数:結合対象の変数。
[]
で配列にする - 第4引数:結合時に追加される文字列
@chain df_sep begin
TidierData.@unite(united_condition, [condition_CE, condition_AB], "+++")
end
欠測値処理
上で作成したdf_wide
というデータフレームを利用しよう。一応、コードを再掲しておく。
df_wide =
@chain df begin
TidierData.@pivot_wider(names_from = AM, values_from = MPG)
end
TidierData.@drop_missing()
による、特定の変数を基準としたリストワイズ削除
データフレームdf_wide
にはAutomatic
列とManual
列に、異なる位置に欠測値missing
が存在する。missingを含む行をリストワイズ削除したいとしよう。
DataFrames.jl
のDataFrames.dropmissing()
と同じはたらきを持つマクロがTidierData.@drop_missing()
だ。
引数に何も書かなければ、1つでもmissing
を含む行があれば全てリストワイズ削除される。もし引数に特定の変数を指定したら、その変数に関してのみmissing
の有無が評価され、リストワイズ削除される。
@chain df_wide begin
TidierData.@drop_missing(Automatic) # Automatic列にmissingがあった場合のみリストワイズ削除
end
対象の列はTidierData.@select()
のセマンティクスが使えるので、複数の列をカンマで区切って指定したり(例:TidierData.@drop_missing(Automatic, Manual)
)、ヘルパ関数を用いて指定することもできる(例:TidierData.@drop_missing(starts_with("A"))
)。
TidierData.@fill_missing()
による、欠測値の補完
Rのtidyr::fill()
に対応するマクロ。欠測値missing
を、その変数に含まれる、missing
の直前または直後の非欠測値で補完することができる。
データフレームdf_wide
は、以下のパターンで欠測している。
-
Automatic
列:missing
より上に非欠測値がある -
Manual
列:missing
より下に非欠測値がある
欠測したい変数を列挙して、欠測を補完する方法を最後に指定する。
- 下から上方向に補完したいなら
"up"
- 上から下方向に補完したいなら
"down"
以下のコードでは、上方向の補完を指定しているので、missing
より下に非欠測値が存在するManual
列は補完が成立しているが、Automatic
列はmissing
が最下部に固まっているので、補完できていない。
@chain df_wide begin
TidierData.@fill_missing(Automatic, Manual, "up")
end
TidierData.@fill_missing
は、TidierData.@group_by()
などとも組み合わせられるので、グループごとにある方向で補完させる、ということもできる。
TidierData.replace_missing()
による、欠測値の補完
Rのtidyr::replace_na()
に対応。なお、TidierData.replace_missing()
はマクロではなくメソッドなので、「@
」は付けない。他の関数やマクロの中でのみ動作する。
変数ごとに、その変数に含まれるmissing
を、特定の値で置換できる。以下のコードでは、Automatic
列の欠測を100.0
で置換した新たな変数Automatic_new
を作成している。また、既存のManual
列の欠測を0.0
で置換している。
@chain df_wide begin
TidierData.@mutate(Automatic_new = replace_missing(Automatic, 100.0),
Manual = replace_missing(Manual, 0.0)
)
end
TidierData.missing_if()
による、欠測値の補完
欠測値補完とは逆に、特定の条件を満たすセルをmissing
にする。やはりTidierData.missing_if()
はマクロではなくメソッドなので、「@
」は付けない。他の関数やマクロの中でのみ動作する。
以下のコードでは、Model
列に"Valiant"
という車種が存在したらmissing
にするよう指示している。
@chain df_wide begin
TidierData.@mutate(Model = missing_if(Model, "Valiant"))
end
関連記事
宣伝
R言語でtidyverse
による一連の分析フローをまとめた『改訂2版 Rユーザのための RStudio[実践]入門 〜tidyverseによるモダンな分析フローの世界』という書籍を執筆しています。R版の本書の内容は、第3章により詳しく書いてありますので、よければご参照ください。
Discussion