DataFrames.jl Getting Startedの要点 (2021年3月版)

11 min read読了の目安(約10700字

使うときに毎回調べ,古い情報 (2015年とか) を読んで「あ~古い!」ってなったり,ドキュメントを調べるのがアレなので,2021年3月版のGetting Startedの内容を将来の自分に向けてまとめました.動作を確認した環境は Julia 1.5.3,DataFrames.jl v0.22.5,CSV.jl v0.8.3 です.

DataFrames.jlを作成する

空から作成する

大きく分けて2パターンあって,(1) 列データから作る (constructing column by column) と (2) データを想定した上で行データを逐次的に入れる (constructing row by row) です.

Column by Column

列データをArrayなどに持っておいて,これをまとめて与えます.長さが異なると怒られます.

julia> using DataFrames
julia> df = DataFrame(A = 1:4, B = ["M", "F", "F", "M"])
julia> df
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   13  M
   22  F
   33  F
   44  M

空の DataFrame を作成し,列を逐次的に追加しても良いです.その際に利用した識別子が列名として登録されます.

julia> df = DataFrame()
julia> df.A = 1:4
julia> df.B = ["M", "F", "F", "M"]
julia> df # 上と同じ結果になる

Row by Row

行単位で (例えばユーザ単位で) データを追加することができます.追加するときはArrayなどと同じく push! を使いますが,先に空の列を作っておかないと怒られます.またGetting Startedにも仮定あるのですが,1行ずつ突っ込むのは遅い (significantly less performant) とのことです.

julia> df = DataFrame()
julia> push!(df, (1, "M"))  # 怒られる

ID的なものがInt64,性別がStringだとして,先に空の列を作成しておくと追加できます.

julia> df = DataFrame(A=Int64[], B=String[])
julia> push!(df, (1, "M"))
julia> push!(df, (2, "F"))
julia> push!(df, (3, "F"))
julia> push!(df, (4, "M"))
julia> df # 上と同じ結果になる

列名を取得する

文字列として列名を取得するときは names() を,Symbolとして取得するときは propertynames() を利用します.

julia> names(df)
2-element Array{String,1}:
 "A"
 "B"

julia> propertynames(df)
2-element Array{Symbol,1}:
 :A
 :B

ファイルから読み込む

CSV.jl などを利用して読み込むことができます.演算子 |> で流し込むような書き方をしている人が結構いる気がします.以下のCSVファイルを tmp.csv として保存しておきます.

tmp.csv
A,B
1,"M"
2,"F"
3,"F"
4,"M"

csvファイルをCSV.jlでラップした上で,DataFrames.jlへ渡します.

julia> df = DataFrame(CSV.File("tmp".csv))
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   11  M
   22  F
   33  F
   44  M

julia> df = CSV.File("tmp.csv") |> DataFrame
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   11  M
   22  F
   33  F
   44  M

Indexing

Pythonのpandasのように,いろんなIndexingが利用できます.JuliaではSymbol型 (:Aのようなもの)や文字列 ("A") が使えます.文字列の場合変数を展開する $ が入っているものは使えないと書いてありました (さすがに $ と一緒にIndexingを使うことあまりないと思いますが).

julia> df[:, :A]
4-element Array{Int64,1}:
 1
 2
 3
 4

julia> df[:, "A"]
4-element Array{Int64,1}:
 1
 2
 3
 4

全体を表す記号 : ですが,DataFrames.jl では ! も利用できます.これはコピーをつくるかどうかの違いで,view的なものと思えば良さそうです.中身の実装は分からない (見てない) ですが, ! が使えるのは最初の次元だけらしいです.先程のdfに対して,viewに対して書き込みを行う場合と,コピーに対して行う場合を比較すると次のようになります.ただしこの挙動は下のIndexingの挙動と比較するとイマイチよく分からなくなるので,特に必要がない場合は使わないほうが良いでしょう (たぶん).

julia> df[:, :A][1] = 3
julia> df
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   11  M          # <- : でコピーが作られるので,変化しない
   22  F
   33  F
   44  M
julia> df[!, "A"][1] = 3
julia> df
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   13  M          # <- ! でviewに対してデータを代入するので,変化した
   22  F 
   33  F
   44  M

データの操作

例題のデータはこちらです.

julia> df = DataFrame(A = 1:2:1000, B = repeat(1:10, inner=50), C = 1:500);

先頭,末尾,indexingによる部分の抽出

pandasと似たような形でできます.先頭を取るときは first() (古いドキュメントでは head() でした),末尾を取るときは last() (同じく tail() でした) を使います.補足ですが,Jupyter notebookで DataFrame を見ているときれいなHTMLで表示されたりもします.

julia> first(df, 10)
10×3 DataFrame
 Row │ A      B      C
     │ Int64  Int64  Int64
─────┼─────────────────────
   11      1      1
   23      1      2
   35      1      3
   47      1      4
   59      1      5
   611      1      6
   713      1      7
   815      1      8
   917      1      9
  1019      1     10

julia> df[1:3, [:A, :B]] # pandasでも見られるようなindexing
3×2 DataFrame
 Row │ A      B
     │ Int64  Int64
─────┼──────────────
   11      1
   23      1
   35      1

また列の名前で取得するとき,例えば :A と与えるとVector,[:A] で与えると部分DataFrameとして出力されます.名前のIndexingは正規表現 r"" の形も利用できます.Indexingの詳細はAPIページにまとめられています が,細かい説明が結構多いので悩んだ場合は見るといいです.例えば,Juliaの代入構文 .= は,in-place で置き換えられるので,「: はコピーだな!」という上の説明を覚えていると失敗します.例を示します.紛らわしくなるので基本的に一度メモリを確保し,inplaceで使ったほうが良いかもしれません.

julia> using DataFrames
julia> df = DataFrame(A = 1:4, B = ["M", "F", "F", "M"])
julia> df[:, :A] .= 10 # A列に10を代入,これは in-place になる
julia> df
4×2 DataFrame
 Row │ A      B
     │ Int64  String
─────┼───────────────
   110  M
   210  F
   310  F
   410  M

ドキュメントによると,以下の場面が コピーを返さない 場合ですが,あまりよく分かりませんね (雑に使うと普通に in-place 上書きしそうな気がします).

  • 1次元目のインデクスに ! を利用する場合
  • . (getproperty) を使う場合 (例えば列Aに対して df.A)
  • 1つの行が整数インデクスでしていされた場合 (例えば df[1, [:A, :B]])
  • view/@view を明示的に利用する場合

Select and Transform

列を選択したり,変形したりします.

列の選択 select/select!

julia> df = DataFrame(x1=[1, 2], x2=[3, 4], y=[5, 6])
julia> select(df, Not(:x1)) # copy
2×2 DataFrame
 Row │ x2     y
     │ Int64  Int64
─────┼──────────────
   13      5
   24      6

julia> df = DataFrame(x1=[1, 2], x2=[3, 4], y=[5, 6])
julia> select!(df, Not(:x1)) # in-place drop x1
julia> df
2×2 DataFrame
 Row │ x2     y
     │ Int64  Int64
─────┼──────────────
   13      5
   24      6

列の選択と加工

selectを実施する際,列の変形ができます.select演算を適用する際,常にDataFrameで計算されます.以下の例ではx1, x2の列を選択した上で,a1, a2という列名に変更しています.これらの操作はpandasで言うところのassign/appendあたりの技術に相当しそうです.

julia> df = DataFrame(x1=[1, 2], x2=[3, 4], y=[5, 6])
julia> select(df, :x1 => :a1, :x2 => :a2)
2×2 DataFrame
 Row │ a1     a2
     │ Int64  Int64
─────┼──────────────
   11      3
   22      4

以下の例ではx1, x2の列を選択した上で,x2の値についてx2の列の最小値を全ての値から減算します.x2列の最小値は3なので,[3, 4]の値が[0, 1]へと変換されます.間に挿入されているものは無名関数です.

julia> select(df, :x1, :x2 => (x -> x .- minimum(x)) => :x2)
2×2 DataFrame
 Row │ x1     x2
     │ Int64  Int64
─────┼──────────────
   11      0
   22      1

以下の例では,列に対して行単位で関数を適用します.この例のByRowのように,いくつかDataFrames.jlで使えるセレクタが提供されています.

julia> select(df, :x2, :x2 => ByRow(sqrt))
2×2 DataFrame
 Row │ x2     x2_sqrt
     │ Int64  Float64
─────┼────────────────
   13  1.73205
   24  2.0

変形操作 transform/transform!

transform/transform!はselect/select!とほとんど同じ操作を行いますが,transformは全ての列を保存します.以下の例では全ての列を残した上で,新しい列zを作成しました.

julia> transform(df, :x2 => (x -> x .- minimum(x)) => :z)
2×4 DataFrame
 Row │ x1     x2     y      z
     │ Int64  Int64  Int64  Int64
─────┼────────────────────────────
   11      3      5      0
   22      4      6      1

次の例は All() セレクタを利用し全ての列の和を計算した結果を新しく列に追加する例です.

julia> df = DataFrame(x1=[1, 2], x2=[3, 4], y=[5, 6])
julia> transform(df, All() => +)
2×4 DataFrame
 Row │ x1     x2     y      x1_x2_y_+
     │ Int64  Int64  Int64  Int64
─────┼────────────────────────────────
   11      3      5          9
   22      4      6         12

次の例はデータとしてテーブルをまとめて受け取り (AsTable(:)),列単位で最大の列を argmax で受け取り,:prediction という列を新しく作る例題です.AsTableの選択を : から [:a, :b] に変化させると,a, b列で大きい方を選ぶ,という操作ができます.

julia> using Random
julia> Random.seed!(1);
julia> df = DataFrame(rand(10, 3), [:a, :b, :c]);
julia> transform(df, AsTable(:) => ByRow(argmax) => :prediction)
julia> transform(df, AsTable(:) => ByRow(argmax) => :prediction)
10×4 DataFrame
 Row │ a           b          c          prediction
     │ Float64     Float64    Float64    Symbol
─────┼──────────────────────────────────────────────
   10.236033    0.555751   0.0769509  b
   20.346517    0.437108   0.640396   c
   30.312707    0.424718   0.873544   c
   40.00790928  0.773223   0.278582   b
   50.488613    0.28119    0.751313   c
   60.210968    0.209472   0.644883   c
   70.951916    0.251379   0.0778264  a
   80.999905    0.0203749  0.848185   a
   90.251662    0.287702   0.0856352  b
  100.986666    0.859512   0.553206   a

より高度な問い合わせ操作については, Query.jlDataFramesMeta.jl を利用すると良いそうです.

データの要約

要約 describe

データの大まかな概要を describe で取得します.meanなどは Statistics の関数を使っても良いです.

julia> df = DataFrame(A = 1:4, B = ["M", "F", "F", "M"]);
julia> describe(df)
2×7 DataFrame
 Row │ variable  mean    min  median  max  nmissing  eltype
     │ Symbol    Union…  Any  Union…  Any  Int64     DataType
─────┼────────────────────────────────────────────────────────
   1 │ A         2.5     1    2.5     4           0  Int64
   2 │ B                 F            M           0  String

列単位の関数適用 combine

各列毎に関数を適用した結果を新しいDataFramesにするには, combine を利用します.下の例では数値が入ったデータ例に対して,各列の和を求めた結果を作成します.

julia> df = DataFrame(A = 1:4, B = 4.0:-1.0:1.0);
4×2 DataFrame
 Row │ A      B
     │ Int64  Float64
─────┼────────────────
   11      4.0
   22      3.0
   33      2.0
   44      1.0

julia> combine(df, names(df) .=> sum)  # dfの各列を文字列で取得し,各列にsumを適用する
1×2 DataFrame
 Row │ A_sum  B_sum
     │ Int64  Float64
─────┼────────────────
   110     10.0

julia> combine(df, :A => sum)  # A列だけにsumを適用する
1×1 DataFrame
 Row │ A_sum
     │ Int64
─────┼───────
   110

列の操作

多くの操作はcopyを作成するので,同一性判定 (===) で false を返します.

julia> df = DataFrame(A = 1:4, B = 4.0:-1.0:1.0);
julia> df2 = copy(df);
julia> df2.A === df.A # 同一じゃない
false
julia> df2.A == df.A  # 値は同じ
true

データの置き換え

代入し直してもよいですし (場合によってはコピーが発生します),直接 replace! 関数を利用して値を書き換えることもできます.以下の例では,:a 列の None の値を in-place で置き換えます.

julia> df = DataFrame(a = ["a", "None", "b", "None"], b = 1:4, c = ["None", "j", "k", "h"], d = ["x", "y", "None", "z"]);
julia> replace!(df.a, "None" => "c");
julia> df
4×4 DataFrame
 Row │ a       b      c       d
     │ String  Int64  String  String
─────┼───────────────────────────────
   1 │ a           1  None    x
   2 │ c           2  j       y
   3 │ b           3  k       None
   4 │ c           4  h       z

複数列をreplaceするとき,broadcast (.) で記述もできます.こちらの例では

  • c, d列に対して
  • 値がNoneであれば"c"に,そうでなければ元の値に置き換えて
  • 元のデータのc, d列に代入

しています.

julia> df = DataFrame(a = ["a", "None", "b", "None"], b = 1:4, c = ["None", "j", "k", "h"], d = ["x", "y", "None", "z"]);
julia> df[:, [:c, :d]] .= ifelse.(df[!, [:c, :d]] .== "None", "c", df[!, [:c, :d]]);
julia> df
4×4 DataFrame
 Row │ a       b      c       d
     │ String  Int64  String  String
─────┼───────────────────────────────
   1 │ a           1  c       x
   2 │ None        2  j       y
   3 │ b           3  k       c
   4 │ None        4  h       z