StatsFragments

Python, R, Rust, 統計, 機械学習とか

10 Minutes to DataFrames.jl

この記事は Julia Advent Calendar 2015 23 日目の記事です。


JuliaDataFrame を扱うパッケージ 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 を扱う。それぞれ、RPython ( 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: 引数の型を返す
  • eltype: Collection の要素の型を返す
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 の出力の表示が一部 ドット (...) で省略されているが、これは自分が手動でやった。既定では全レコードが表示されるようだ。

位置、ラベルによる選択

Rpandas と同じく、引数をスカラーで渡すと返り値の次元が減って 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]
# 略

対象を ArrayUnitRange で指定すると 返り値は 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 演算子がない場合は 内包表記を使って BoolArray を作ればよい。

[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: 非破壊的なソート
  • sort!: 破壊的なソート
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" |

欠損値

欠損値 NADataArrays.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 を除きたければ dropnaNA を含む列を集約する場合に有用。

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 もしくは colwiseaggregate の結果は DataFrame に、colwiseArray{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 にも適用できるが、そうでない場合は 内包表記を使ってデータを操作する。

日時型

標準には DatetimePeriod 二つの型がある。Python でいうと datetimetimedelta + 一部 relativedelta に相当する型だ。これらは DataArrayDataFrame に値として含めることができる。

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 よりも Rdata.frame に近い感じだ (ただし dplyr は存在しない )。Julia にはパイプ演算子があるため、pandas よりは dplyr のようなパッケージがあると流行りそうだ。