読者です 読者をやめる 読者になる 読者になる

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 のようなパッケージがあると流行りそうだ。