10 Minutes to DataFrames.jl
この記事は Julia Advent Calendar 2015 23 日目の記事です。
Julia
で DataFrame
を扱うパッケージ DataFrames.jl
の使い方をまとめたい。
下の pandas
ドキュメントにあるような処理が DataFrames.jl
でどう書けるのかを整理する。
versioninfo() # Julia Version 0.4.2 # Commit bb73f34 (2015-12-06 21:47 UTC)
インストール
Pkg.add("DataFrames") Pkg.installed("DataFrames") # v"0.6.10" using DataFrames
データの作成
DataFrames.jl
では 1 次元データ構造である DataArray
( DataArrays.jl
) と 2 次元データ構造である DataFrame
を扱う。それぞれ、R
や Python
( pandas
) では以下のようなクラスに対応する。
Julia (DataFrames.jl ) |
R | Python (pandas ) |
---|---|---|
DataArrays.DataArray |
atomic ( vector ) |
Series |
DataFrames.DataFrame |
data.frame |
DataFrame |
DataArray
の作成
DataArray
は直接作成せず、@data
マクロを利用して作るのがよい。@data
マクロを使うとでは欠損値 ( NA
) をよしなに処理して DataArray
化してくれる。
s = @data([1, NA, 3]) # 3-element DataArrays.DataArray{Int64,1}: # 1 # NA # 3 # データの実体 s.data # 3-element Array{Int64,1}: # 1 # 1 # 3 # NA に対応するマスク s.na # 3-element BitArray{1}: # false # true # false # NG! [1, NA, 3] # LoadError: MethodError: `convert` has no method matching convert(::Type{Int64}, ::DataArrays.NAtype)
DataFrame
の作成
作成したいデータの "列名 = Array
" のペアを渡す。
df = DataFrame(A = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], B = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], C = ["A", "B", "C", "A", "B", "C", "A", "B", "C", "A"]) # 10x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | 1 | 10 | "A" | # | 2 | 2 | 9 | "B" | # | 3 | 3 | 8 | "C" | # | 4 | 4 | 7 | "A" | # | 5 | 5 | 6 | "B" | # | 6 | 6 | 5 | "C" | # | 7 | 7 | 4 | "A" | # | 8 | 8 | 3 | "B" | # | 9 | 9 | 2 | "C" | # | 10 | 10 | 1 | "A" |
DataFrame
の確認
各列の型は以下のようにして確認できる。colwise
は関数を列ごとに適用する generic function。
typeof(df) # DataFrames.DataFrame colwise(typeof, df) # 3-element Array{Any,1}: # [DataArrays.DataArray{Int64,1}] # [DataArrays.DataArray{Int64,1}] # [DataArrays.DataArray{ASCIIString,1}] eltype(df) # Any colwise(eltype, df) # 3-element Array{Any,1}: # [Int64] # [Int64] # [ASCIIString]
列名の表示は names
。行名や index
にあたるものはない (行番号のみ)。
names(df) # 3-element Array{Symbol,1}: # :A # :B # :C
先頭 / 末尾のレコードを確認したい場合は head
もしくは tail
。既定では 6 レコード表示。
head(df) # 6x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|---|----|-----| # | 1 | 1 | 10 | "A" | # | 2 | 2 | 9 | "B" | # | 3 | 3 | 8 | "C" | # | 4 | 4 | 7 | "A" | # | 5 | 5 | 6 | "B" | # | 6 | 6 | 5 | "C" | tail(df, 3) # 3x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|---|-----| # | 1 | 8 | 3 | "B" | # | 2 | 9 | 2 | "C" | # | 3 | 10 | 1 | "A" |
要約統計量の表示は describe
。これは表示のためだけの関数のようで、返り値は Void
になる。
describe(df) # A # Min 1.0 # 1st Qu. 3.25 # Median 5.5 # Mean 5.5 # 3rd Qu. 7.75 # Max 10.0 # NAs 0 # NA% 0.0% # # B # Min 1.0 # 1st Qu. 3.25 # ... ... typeof(describe(df)) # Void
入出力
DataFrames.jl
では CSV
のみ。
writetable("data.csv", df) df = readtable("data.csv")
データの選択
行や列を選択する操作を記載する。
補足 DataFrame
の出力の表示が一部 ドット (...) で省略されているが、これは自分が手動でやった。既定では全レコードが表示されるようだ。
位置、ラベルによる選択
R
や pandas
と同じく、引数をスカラーで渡すと返り値の次元が減って DataArray
になる。
列名を指定する場合、引数は文字列ではなく Symbol
として渡す必要があるようだ。Int
を渡した場合は列番号での選択になる。
df[:A] # 10-element DataArrays.DataArray{Int64,1}: # 1 # 2 # ... # 9 # 10 # NG! df["A"] # LoadError: MethodError: `getindex` has no method matching getindex(::DataFrames.DataFrame, ::ASCIIString) df[Symbol("A")] # 略 df[1] # 略
対象を Array
や UnitRange
で指定すると 返り値は DataFrame
となる。
df[[:A]] # 10x1 DataFrames.DataFrame # | Row | A | # |-----|----| # | 1 | 1 | # | 2 | 2 | # ... .. # | 9 | 9 | # | 10 | 10 | df[[1]] # 略 df[2:3] # 10x2 DataFrames.DataFrame # | Row | B | C | # |-----|----|-----| # | 1 | 10 | "A" | # | 2 | 9 | "B" | # ... .. ... # | 9 | 2 | "C" | # | 10 | 1 | "A" | df[[:B, :C]] # 略
行 / 列それぞれで指定したい場合は順番に引数として渡す。行番号のみで選択したい場合、第二引数に :
を渡す。
df[5:7, :] # 3x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|---|---|-----| # | 1 | 5 | 6 | "B" | # | 2 | 6 | 5 | "C" | # | 3 | 7 | 4 | "A" | df[3:4, [:A, :B]] # 2x2 DataFrames.DataFrame # | Row | A | B | # |-----|---|---| # | 1 | 3 | 8 | # | 2 | 4 | 7 | df[2, :A] # 2
条件式による選択
julia
の通常の演算子は element-wise ではない。element-wise に操作したい場合はドットで始まる演算子を使う。この仕様は明示的で良いと思う。
df[:A] == 0 # false df[:A] .== 0 # 10-element DataArrays.DataArray{Bool,1}: # false # false # ..... # false # false
上で作成した Array
を使えば 対応する行のみが選択できる。
df[df[:A] .> 5, :] # 5x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|---|-----| # | 1 | 6 | 5 | "C" | # | 2 | 7 | 4 | "A" | # | 3 | 8 | 3 | "B" | # | 4 | 9 | 2 | "C" | # | 5 | 10 | 1 | "A" |
対応する element-wise 演算子がない場合は 内包表記を使って Bool
の Array
を作ればよい。
[in(x, ("A", "B")) for x in df[:C]] # 10-element Array{Bool,1}: # true # true # ..... # false # true df[[in(x, ("A", "B")) for x in df[:C]], :] # 7x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | 1 | 10 | "A" | # | 2 | 2 | 9 | "B" | # ... .. .. ...| # | 6 | 8 | 3 | "B" | # | 7 | 10 | 1 | "A" |
代入
データ選択した箇所に値を代入できる。
df[[1, 4], :A] = NA df # 10x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | NA | 10 | "A" | # | 2 | 2 | 9 | "B" | # | 3 | 3 | 8 | "C" | # | 4 | NA | 7 | "A" | # | 5 | 5 | 6 | "B" | # | 6 | 6 | 5 | "C" | # | 7 | 7 | 4 | "A" | # | 8 | 8 | 3 | "B" | # | 9 | 9 | 2 | "C" | # | 10 | 10 | 1 | "A" |
ソート
generic function を利用してソートできる。ソート対象の列や 順序などが引数で指定できる。関数名が !
で終わるものは破壊的な操作を行うもの。
sort(df, cols = :B) # 10x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | 10 | 1 | "A" | # | 2 | 9 | 2 | "C" | # | 3 | 8 | 3 | "B" | # | 4 | 7 | 4 | "A" | # | 5 | 6 | 5 | "C" | # | 6 | 5 | 6 | "B" | # | 7 | NA | 7 | "A" | # | 8 | 3 | 8 | "C" | # | 9 | 2 | 9 | "B" | # | 10 | NA | 10 | "A" |
欠損値
欠損値 NA
は DataArrays.jl
にて定義されており、Built-in にある非数 NaN
とは異なる。
typeof(NA) # DataArrays.NAtype NA == NA # NA NA + 1 # NA NA | true # true is(NA, NA) # true typeof(NaN) # Float64
Array
の各要素が NA
かどうか調べるには isna
を使う。
isna(df[:A]) # 10-element BitArray{1}: # true # false # false # true # ..... # false # false df[isna(df[:A]), :] # 2x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | NA | 10 | "A" | # | 2 | NA | 7 | "A" | df[~isna(df[:A]), :] # 8x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|---|-----| # | 1 | 2 | 9 | "B" | # | 2 | 3 | 8 | "C" | # | 3 | 5 | 6 | "B" | # | 4 | 6 | 5 | "C" | # | 5 | 7 | 4 | "A" | # | 6 | 8 | 3 | "B" | # | 7 | 9 | 2 | "C" | # | 8 | 10 | 1 | "A" |
ある列について NA
を除きたければ dropna
。NA
を含む列を集約する場合に有用。
dropna(df[:A]) # 8-element Array{Int64,1}: # 2 # 3 # 5 # .. # 9 # 10 mean(df[:A]) # NA mean(dropna(df[:A])) # 6.25
欠損値をパディングしたい場合は データ選択して代入。ただし DataFrame
に異なる型の列が含まれている場合は (実際には代入が発生しなくても) エラーになるため、列名も明示したほうがよい。
df[isna(df[:A]), :] = 0 # LoadError: MethodError: `convert` has no method matching convert(::Type{ASCIIString}, ::Int64) df[isna(df[:A]), :A] = 0 df # 10x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|----|----|-----| # | 1 | 0 | 10 | "A" | # | 2 | 2 | 9 | "B" | # | 3 | 3 | 8 | "C" | # | 4 | 0 | 7 | "A" | # ... .. .. ... # | 9 | 9 | 2 | "C" | # | 10 | 10 | 1 | "A" |
演算、集計
演算
DataArray
同士の演算は element-wise な演算になる。
df[:A] - df[:B] # 10-element DataArrays.DataArray{Int64,1}: # -10 # -7 # ... # 7 # 9
集計、集約
ある一つの列を集約したい場合は そのまま集約関数に渡せばよい。
mean(df[:B])
# 5.5
複数の列を集約したい場合は aggregate
もしくは colwise
。aggregate
の結果は DataFrame
に、colwise
は Array{Any,1}
になる。
aggregate(df[[:A, :B]], sum) # 1x2 DataFrames.DataFrame # | Row | A_sum | B_sum | # |-----|-------|-------| # | 1 | 50 | 55 | colwise(sum, df[[:A, :B]]) # 2-element Array{Any,1}: # [50] # [55]
aggregate
では複数列 / 複数の集約関数で集計を行うこともできる。
aggregate(df, :C, mean) # 3x3 DataFrames.DataFrame # | Row | C | A_mean | B_mean | # |-----|-----|--------|--------| # | 1 | "A" | 4.25 | 5.5 | # | 2 | "B" | 5.0 | 6.0 | # | 3 | "C" | 6.0 | 5.0 | aggregate(df, :C, [mean, sum]) # 3x5 DataFrames.DataFrame # | Row | C | A_mean | A_sum | B_mean | B_sum | # |-----|-----|--------|-------|--------|-------| # | 1 | "A" | 4.25 | 17 | 5.5 | 22 | # | 2 | "B" | 5.0 | 15 | 6.0 | 18 | # | 3 | "C" | 6.0 | 18 | 5.0 | 15 |
グループ別に集約したい場合は by
もしくは aggregate
。
by(df, :C, x -> mean(x[:A])) # 3x2 DataFrames.DataFrame # | Row | C | x1 | # |-----|-----|------| # | 1 | "A" | 4.25 | # | 2 | "B" | 5.0 | # | 3 | "C" | 6.0 | aggregate(df, :C, mean) # 3x3 DataFrames.DataFrame # | Row | C | A_mean | B_mean | # |-----|-----|--------|--------| # | 1 | "A" | 4.25 | 1.25 | # | 2 | "B" | 5.0 | 6.0 | # | 3 | "C" | 6.0 | 5.0 |
関数適用
また colwise
を使うと ラムダ式を各列に適用することもできる ( apply
のような操作)。
colwise(x -> mean(x[1:5]) - mean(6:10), df[[:A, :B]]) # 2-element Array{Any,1}: # [-6.0] # [0.0]
連結、結合、変形
連結
以下 2 つの関数で行う。昔は rbind
, cbind
という関数があったようだが、すでに削除されている。
vcat
:DataFrame
を縦方向に連結。hcat
:DataFrame
を横方向に連結。
df1 = DataFrame(A = [1, 2, 3], B = [4, 5, 6]) df2 = DataFrame(A = [11, 12, 13], B = [14, 15, 16]) vcat(df1, df2) # 6x2 DataFrames.DataFrame # | Row | A | B | # |-----|----|----| # | 1 | 1 | 4 | # | 2 | 2 | 5 | # | 3 | 3 | 6 | # | 4 | 11 | 14 | # | 5 | 12 | 15 | # | 6 | 13 | 16 | df3 = DataFrame(A = [1, 2, 3], B = [4, 5, 6]) df4 = DataFrame(C = [11, 12, 13], D = [14, 15, 16]) hcat(df3, df4) # 3x4 DataFrames.DataFrame # | Row | A | B | C | D | # |-----|---|---|----|----| # | 1 | 1 | 4 | 11 | 14 | # | 2 | 2 | 5 | 12 | 15 | # | 3 | 3 | 6 | 13 | 16 |
結合
特定の列の値で結合するには join
。結合方法は kind
で指定できる。一通りの結合方法は揃っている。
left = DataFrame(A = [1, 2, 3], B = [11, 12, 13]) right= DataFrame(A = [2, 3, 4], C = [12, 13, 14]) join(left, right, on = :A) # 2x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|---|----|----| # | 1 | 2 | 12 | 12 | # | 2 | 3 | 13 | 13 | join(left, right, on = :A, kind = :outer) # 4x3 DataFrames.DataFrame # | Row | A | B | C | # |-----|---|----|----| # | 1 | 2 | 12 | 12 | # | 2 | 3 | 13 | 13 | # | 3 | 1 | 11 | NA | # | 4 | 4 | NA | 14 |
変形
いわゆる tidy
なデータへの変換と逆変換。
stack
,melt
: 列持ち → 行持ちへの変換 (unpivot
)。unstack
: 行持ち → 列持ちへの変換 (pivot
)。
df5 = DataFrame(Name = ["A", "B", "C", "D"], width = [1, 2, 3, 4], height = [10, 20, 30, 40]) stacked = stack(df5, [:width, :height]) # 8x3 DataFrames.DataFrame # | Row | variable | value | Name | # |-----|----------|-------|------| # | 1 | width | 1 | "A" | # | 2 | width | 2 | "B" | # | 3 | width | 3 | "C" | # | 4 | width | 4 | "D" | # | 5 | height | 10 | "A" | # | 6 | height | 20 | "B" | # | 7 | height | 30 | "C" | # | 8 | height | 40 | "D" | melt(df5, :Name) # 略 unstack(stacked, :Name, :variable, :value) # 4x3 DataFrames.DataFrame # | Row | variable | height | width | # |-----|----------|--------|-------| # | 1 | "A" | 10 | 1 | # | 2 | "B" | 20 | 2 | # | 3 | "C" | 30 | 3 | # | 4 | "D" | 40 | 4 |
データ型固有の処理
データ型に固有の操作を記載する。pandas
ではデータ型固有の操作をまとめた .str
, .dt
などのアクセサがあるが、DataFrames.jl
にはそれらに相当するものはなさそうだ。
標準の関数は一部 Array
にも適用できるが、そうでない場合は 内包表記を使ってデータを操作する。
日時型
標準には Datetime
と Period
二つの型がある。Python
でいうと datetime
と timedelta
+ 一部 relativedelta
に相当する型だ。これらは DataArray
や DataFrame
に値として含めることができる。
DataFrames.jl
としてはリサンプリングやタイムゾーンなどは機能として持っていない。
dates = @data([DateTime(2013, 01, i) for i in 1:4]) # 6-element DataArrays.DataArray{DateTime,1}: # 2013-01-01T00:00:00 # 2013-01-02T00:00:00 # 2013-01-03T00:00:00 # 2013-01-04T00:00:00 # DateTime から日付を取得 Dates.day(dates) # 4-element Array{Union{DataArrays.NAtype,Int64},1}: # 1 # 2 # 3 # 4 # DateTime を Period に変換 periods = [Dates.Year(d) for d in dates] # 4-element Array{Base.Dates.Year,1}: # 2013 years # 2013 years # 2013 years # 2013 years [DateTime(p) for p in periods] # 4-element Array{Any,1}: # 2013-01-01T00:00:00 # 2013-01-01T00:00:00 # 2013-01-01T00:00:00 # 2013-01-01T00:00:00
時系列については別のパッケージがあるため、こちらを使うのがよいのかもしれない。
文字列型
それぞれ 内包表記で処理する。
lowercase(df[:C]) # LoadError: MethodError: `lowercase` has no method matching lowercase(::DataArrays.DataArray{ASCIIString,1}) [lowercase(x) for x in df[:C]] # 10-element Array{Any,1}: # "a" # "b" # ... # "c" # "a"
カテゴリカル型
いわゆるカテゴリカル型はない。Array
の利用メモリを削減するための PooledDataArray
というクラスがある。
まとめ
DataFrames.jl
でのデータ操作を整理した。
API や 使い勝手は pandas
よりも R
の data.frame
に近い感じだ (ただし dplyr
は存在しない )。Julia
にはパイプ演算子があるため、pandas
よりは dplyr
のようなパッケージがあると流行りそうだ。