DataFrames.jl @ 0.22.2

最近Juliaでデータフレームを操作するパッケージDataFrames.jlのバージョンが少しbump upしました。ただし,この執筆時点での最新バージョンは0.22.2です。

そもそもデータフレームとは,基本的に二次元配列の構造で,行列と同様に行と列を持ちます。

多言語だとRのdplyrやPythonのpandasなどがありますが,JuliaではDataFrames.jlがデータフレームの操作を実行するためのパッケージとなります。連携するパッケージとしてはDataFramesMeta.jlQueryなどがありますが,それらを使わなくても,たいていのことはできる印象です。

ここ最近0.21系から0.22にバージョンアップしました。

Juliaの公式パッケージはGutHub上で管理されているので,GutHubで変更履歴や,おもな変更点を確認することができます。

どうやら今回のおもな変更点はデータフレームの列操作に関する関数select/transformの改善や,データの持ち方を縦から横に変更する関数unstackに関する挙動のようです。

これに加えて,いくつかの補助関数(isapprox, empty)が新たに追加されました。

In [1]:
# using Pkg; Pkg.add(name = "DataFrames", version = "0.22.0");
In [2]:
using DataFrames;

1. 基本操作

1.1 データフレームをつくる

DataFrame(列名 = 変数)という作り方がいちばんベーシックな作り方だと思います。その他にも,次のような書き方もできます。

  1. DataFrame(行列)
  2. DataFrame(行列, [シンボルor文字列のベクトル])
  3. DataFrame(辞書)
  4. DataFrame(列名 => 要素)

4つ目の書き方はペアPairを利用した書き方です。気をつけなければならないのは,辞書やペアを利用する場合はすべての要素の大きさが同じである必要があります。

In [3]:
mat = randn(3, 3);
A = DataFrame(mat, [:a, :b, :c])
Out[3]:

3 rows × 3 columns

abc
Float64Float64Float64
10.800516-2.261940.277444
2-0.5671851.5837-0.36308
3-1.337980.694831-0.375632
In [4]:
DataFrame(:a => 1:2, :b => 2:4) # 長さが違うので,エラーになる。
DimensionMismatch("column :a has length 2 and column :b has length 3")

Stacktrace:
 [1] DataFrame(::Array{Any,1}, ::DataFrames.Index; copycols::Bool) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:181
 [2] DataFrame(::Pair{Symbol,UnitRange{Int64}}, ::Vararg{Pair{Symbol,UnitRange{Int64}},N} where N; makeunique::Bool, copycols::Bool) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:218
 [3] DataFrame(::Pair{Symbol,UnitRange{Int64}}, ::Vararg{Pair{Symbol,UnitRange{Int64}},N} where N) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:216
 [4] top-level scope at In[4]:1
 [5] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091

1.2 データフレームの要素を取り出す

取り出す

作ったデータフレームの列要素のアクセスは,データフレームをA,取り出したい列名をaとすると,

  • A.a
  • A."a"
  • A[!, :A]
  • A[!, "A"]
  • getproperty(A, :a)
  • getproperty(A, "a")

で取り出すことができます。[]getpropertyを使用する場合,シンボルでも文字列でもどちらでもアクセス可能です(シンボルのほうがわずかに効率がいいらしい)。

これらの方法で取り出された配列はコピーされません。コピーされないということは取り出した要素を変化させると,もとのデータフレームも変化するということです。コピーを取り出したい場合はA[:, :a]のように,!の代わりに:を使用します。

複数の列にアクセスすることもできます。

In [5]:
A[:, [:a, :b]]
Out[5]:

3 rows × 2 columns

ab
Float64Float64
10.800516-2.26194
2-0.5671851.5837
3-1.337980.694831

1.3 正規表現を使って要素を指定する

正規表現を使ってマッチする列を取り出すこともできます。正規表現を使う場合にはr""をつかってRegexを作り出します。

正規表現による強力なマッチング機能が利用できるのは,DataFrames.jlの強みだと思います。

In [6]:
A[:, r"[ab]"]
Out[6]:

3 rows × 2 columns

ab
Float64Float64
10.800516-2.26194
2-0.5671851.5837
3-1.337980.694831

サポート関数を使っても,列方向に対する操作ができます。利用できるサポート関数は

  • Not() 選択したシンボルかRegexにマッチした列以外を取り出す。
  • Cols() 選択したシンボルかRegexにマッチした列だけを取り出す。
  • All() 選んでくる条件はCols()と同じ。
  • Between(first, last) first~last間の列をすべて取り出す。

です。

ColsAll()は機能的には同一ですが,Cols()は何も列を選択しないことができるので,こちらのほうができることがひとつ多い印象。

並び替える

これらのサポート関数は,列方向に関する並び替えを行いたいときに役に立ちます。並び替えにはRegexとシンボル,文字列が使えます。つまり,列選択のときと全く同じです。

In [7]:
A[!, Cols(r"a|c", "b")]
Out[7]:

3 rows × 3 columns

acb
Float64Float64Float64
10.8005160.277444-2.26194
2-0.567185-0.363081.5837
3-1.33798-0.3756320.694831

1.4 列を指定して,変更する

列の要素を指定する方法は他にもあります。それがselect()transform()です。しかし,この2種類の関数は,ただ取り出すだけでなく,指定した要素に対して関数を作用させるための関数です。

  • select(), select!() 選択した列だけを取り出す
  • transform(), transform!() 選択した列以外も取り出す。

!がついている方はin-place関数であり,作用させた変数の内容そのものを破壊的に変更します。

transformは選択列以外も取り出すので「なんのこっちゃ」となるかもしれませんが,この2つの関数は,ただ列を取り出すだけでなく,取り出した列に対して関数を作用させるための関数であるため,transformが重要になります。

これらはRegexやシンボル,文字列と組み合わせて使うことができるだけでなく,先述したサポート関数と一緒に使うこともできます。

select()

マッチした列だけを取り出して,新しいデータフレームを作成します。

さらに,単純に取り出すだけでなく,列名を変更したり,取り出した列に関数を作用させて新しい列にすることもできます。selectはデフォルトでは自動的にコピーを作成する仕様になっていますが,..., copycols = falseでコピーの作成をしないようにすることもできます。

In [8]:
B = select(A, :a, copycols = true)
C = select(A, :a, copycols = false)
Out[8]:

3 rows × 1 columns

a
Float64
10.800516
2-0.567185
3-1.33798
In [9]:
B.a === A.a
Out[9]:
false
In [10]:
C.a === A.a
Out[10]:
true

select()は単に列を取り出すだけでなく,選択した列に対して関数を適用し,それを新たな列として返すという操作もできます。

新しい列を作るための書き方は,

select(A, :列名 => 関数 => :新しい列名)

です。関数には()でくくることで無名関数も使うことができます。

In [11]:
select(A, :a => (x -> 2x) => :d)
Out[11]:

3 rows × 1 columns

d
Float64
11.60103
2-1.13437
3-2.67596

関数は列の要素に対してまとめててきようされます。そのため行の各要素に関数を作用させたいときはbroadcastを利用します.

例えば各要素をroundしたいときは,

select(A, :a => round => :d)

ではなく

In [12]:
select(A, :a => (x -> round.(x)) => :d)
Out[12]:

3 rows × 1 columns

d
Float64
11.0
2-1.0
3-1.0

と実行する必要があります。

transform()

select()と同じ記述方法で列を指定しますが,select()とは異なり,選択していない列も残して,値を返します。

In [13]:
transform(A, :a => (x -> 2x) => :d)
Out[13]:

3 rows × 4 columns

abcd
Float64Float64Float64Float64
10.800516-2.261940.2774441.60103
2-0.5671851.5837-0.36308-1.13437
3-1.337980.694831-0.375632-2.67596

1.6 行方向の操作を実行する。

selecttransformでは基本的に列方向に対して関数を作用させました。しかしデータフレームには行ごとに意味をもたせたデータを入力したいこともあります(たとえばあるクラスのテスト得点の表とか)。

そんな操作をするためにはByRowを使うと良いです。

In [14]:
B = DataFrame("名前" => ["Kato", "Noguchi", "Yamada"], "国語" => [20, 31, 55], "数学" => [100, 23, 78], "英語" => [10, 30, 89])
Out[14]:

3 rows × 4 columns

名前国語数学英語
StringInt64Int64Int64
1Kato2010010
2Noguchi312330
3Yamada557889
In [15]:
transform(B, Not(:名前) => ByRow(+) => :合計)
Out[15]:

3 rows × 5 columns

名前国語数学英語合計
StringInt64Int64Int64Int64
1Kato2010010130
2Noguchi31233084
3Yamada557889222

行方向の取り出し

列方向のデータフレーム操作をみたところで,単純な取り出しの例についても確認しておきましょう。

列方向だけでなく,行方向に対しても一部の要素を取り出すことができます。[,]のカンマの左側に,ベクトルなどで位置を指定してやることで,取り出せます。

In [16]:
A[1:2, :]
Out[16]:

2 rows × 3 columns

abc
Float64Float64Float64
10.800516-2.261940.277444
2-0.5671851.5837-0.36308

特定の条件にマッチする行だけを取り出したい場合には,2通りの書き方ができます。

  • A[条件, :]
  • filter(:ID -> 関数, A)

filter()は結構便利な関数です。次のように派生した書き方ができます。

In [17]:
filter(:a => i -> i < 0, A)
Out[17]:

2 rows × 3 columns

abc
Float64Float64Float64
1-0.5671851.5837-0.36308
2-1.337980.694831-0.375632

1.7 DataFramesRowには要注意

しかし,1行だけ取り出す場合,DataFrameからDataFramesRowと呼ばれる型に変化します。DataFrameの型を維持したままで1行だけ取り出す場合は,A[[1], :a]のように,ベクトルで行の要素を指定すればよいです。

DataFramesRowに対してはbroadcastをすることができません。

In [18]:
A[1, [:a, :b]] # DataFramesRow
Out[18]:

DataFrameRow (2 columns)

ab
Float64Float64
10.800516-2.26194
In [19]:
A[[1], [:a, :b]] # DataFrame
Out[19]:

1 rows × 2 columns

ab
Float64Float64
10.800516-2.26194
In [20]:
string.(A[1, [:a, :b]])
ArgumentError: broadcasting over `DataFrameRow`s is reserved

Stacktrace:
 [1] broadcastable(::DataFrameRow{DataFrame,DataFrames.SubIndex{DataFrames.Index,Array{Int64,1},Array{Int64,1}}}) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframerow/dataframerow.jl:415
 [2] broadcasted(::Function, ::DataFrameRow{DataFrame,DataFrames.SubIndex{DataFrames.Index,Array{Int64,1},Array{Int64,1}}}) at ./broadcast.jl:1255
 [3] top-level scope at In[20]:1
 [4] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091

このようにDataFramesRowに対してはbroadcast演算をすることはできません。


2. 結合する

データフレーム操作で頻繁に行われるのが,データフレームの結合です。単純に行や列の大きさが同じものをくっつけるだけでなく,特定列の要素をキーにマッチさせたりすることができます。

こうした操作は**join(A, B)系の関数で実行できます。

2.1 **join()の基本的な書き方

基本的な使用方法は

**join(A, B, on = :列名)

です。AとBで列名が違っている場合,

**join(A, B, on = :Aの列名 => :Bの列名)

というように,Pairを使用します。複数のキーがある場合はシンボルやペアをベクトルとして渡してあげます。

**join(A, B, on = [:列名1, :Aの列名2 => :Bの列名2])

2.2 joinの種類

  • leftjoin, rightjoin 左右どちらかのデータフレーム(A)を基準としてマッチさせる。基準のデータフレームAにないBの行は無視され,Bの列のうちAに含まれていないものは欠測missingとして追加される。
  • innerjoin 両方のデータフレームの要素を行に関してすべて残す形でマッチさせる。片方のデータフレームにしかない行も完全に保存される。
  • outerjoin 両方のデータフレームの要素のうち,キーがマッチした行だけ残し,それ以外は無視する。この形では,マッチによる欠測が生じない。
  • semijoin マッチした行だけを保存するが,さらに列に関しては基準となるAの列だけを保存し,Bに関する要素は完全に無視される。
  • antijoin マッチした行以外を保存するが,列に関しては基準となるAの列だけが保存される。semijoinの逆バージョン。
  • crossjoin 与えたデータフレームらの直積を返す。マッチと言うよりも,全パタンの網羅するための関数?

実際の実行例はパッケージドキュメントに詳しいです。

In [21]:
people = DataFrame(ID = [20, 40], Name = ["John Doe", "Jane Doe"]);
jobs = DataFrame(ID = [20, 40], Job = ["Lawyer", "Doctor"]);
In [22]:
crossjoin(people, jobs; makeunique = true)
Out[22]:

4 rows × 4 columns

IDNameID_1Job
Int64StringInt64String
120John Doe20Lawyer
220John Doe40Doctor
340Jane Doe20Lawyer
440Jane Doe40Doctor

2.3 列方向の結合

列名が同じ要素の2つのデータフレームを結合したい場合はvcatが便利です。

In [23]:
vcat((people, jobs)...; cols = :union)
Out[23]:

4 rows × 3 columns

IDNameJob
Int64String?String?
120John Doemissing
240Jane Doemissing
320missingLawyer
440missingDoctor

3 データフレームを変形させる

データフレームをピボットしたり,変形したりする操作もDataFrames.jlの標準的な関数として提供されています。それがstackunstackです。 データフレームを縦方向に伸ばす(要素を縦に積み上げる)のがstackであり,逆に横方向に伸ばす(積み上がっている要素を横に展開する)のがunstackです。

もっともかんたんな使用方法は,

stack(A, [縦に伸ばしたい複数列]; variable_name = :列名を要素とする新たな列の名前, value_name = :列の要素を立てに伸ばした新たな列の名前)

です。

In [24]:
B
Out[24]:

3 rows × 4 columns

名前国語数学英語
StringInt64Int64Int64
1Kato2010010
2Noguchi312330
3Yamada557889
In [25]:
long_B = stack(B, Between(:国語, :英語), variable_name = :教科, value_name = :得点)
Out[25]:

9 rows × 3 columns

名前教科得点
StringStringInt64
1Kato国語20
2Noguchi国語31
3Yamada国語55
4Kato数学100
5Noguchi数学23
6Yamada数学78
7Kato英語10
8Noguchi英語30
9Yamada英語89

unstack(A, :列名にしたい要素を持つ列名, :横に伸ばしたい要素をもつ列名))で元のデータフレームに戻せます。

※unstackした列は型が勝手に変わってしまうことに注意。

In [26]:
wide_B = unstack(long_B, :教科, :得点)
Out[26]:

3 rows × 4 columns

名前国語数学英語
StringInt64?Int64?Int64?
1Kato2010010
2Noguchi312330
3Yamada557889
In [27]:
typeof(wide_B.国語)
Out[27]:
Array{Union{Missing, Int64},1}

4. 欠測値を扱う

データフレームで扱う変数には様々な理由により観測されない値が入っています。JuliaのDataFrameではMissingという型で,この欠測値を扱います。

先程のunstackで変化した型はこのMissingを含んだものでした。

このMissingの型に関連して,次のような関数が準備されています。

  • allowmissing allowmissing!列の型を欠測値を認める型Union{hoge, Missing}に変える。
  • disallowmissing disallowmissing! 列の方を欠測値を認めない型に変える。
  • completecases すべての列,もしくは一部の列で欠測を含んでいない行を検索してtrue or falseのベクトルを返す。
  • dropmissing dropmissing! 欠測を含んでいない行だけを残して,返す。
In [28]:
append!(B, DataFrame(名前 = missing, 国語 = 0, 数学 = 0, 英語 = 0))
┌ Error: Error adding value to column :名前.
└ @ DataFrames /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:1237
MethodError: Cannot `convert` an object of type Missing to an object of type String
Closest candidates are:
  convert(::Type{T}, !Matched::T) where T<:AbstractString at strings/basic.jl:229
  convert(::Type{T}, !Matched::AbstractString) where T<:AbstractString at strings/basic.jl:230
  convert(::Type{S}, !Matched::CategoricalArrays.CategoricalValue) where S<:Union{AbstractChar, AbstractString, Number} at /Users/takuizum/.julia/packages/CategoricalArrays/ZjBSI/src/value.jl:73
  ...

Stacktrace:
 [1] setindex!(::Array{String,1}, ::Missing, ::Int64) at ./array.jl:847
 [2] _unsafe_copyto!(::Array{String,1}, ::Int64, ::Array{Missing,1}, ::Int64, ::Int64) at ./array.jl:257
 [3] unsafe_copyto! at ./array.jl:311 [inlined]
 [4] _copyto_impl! at ./array.jl:335 [inlined]
 [5] copyto! at ./array.jl:321 [inlined]
 [6] append!(::Array{String,1}, ::Array{Missing,1}) at ./array.jl:977
 [7] append!(::DataFrame, ::DataFrame; cols::Symbol, promote::Bool) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:1194
 [8] append!(::DataFrame, ::DataFrame) at /Users/takuizum/.julia/packages/DataFrames/yqToF/src/dataframe/dataframe.jl:1131
 [9] top-level scope at In[28]:1
 [10] include_string(::Function, ::Module, ::String, ::String) at ./loading.jl:1091
In [29]:
allowmissing!(B, :名前);
append!(B, DataFrame(名前 = missing, 国語 = 0, 数学 = 0, 英語 = 0))
Out[29]:

4 rows × 4 columns

名前国語数学英語
String?Int64Int64Int64
1Kato2010010
2Noguchi312330
3Yamada557889
4missing000
In [30]:
completecases(B)
Out[30]:
4-element BitArray{1}:
 1
 1
 1
 0
In [31]:
dropmissing!(B, :名前)
Out[31]:

3 rows × 4 columns

名前国語数学英語
StringInt64Int64Int64
1Kato2010010
2Noguchi312330
3Yamada557889

5. グループごとに処理を実行する。

groupbyは特定の列の要素が同じもの同士でグループ化したデータフレームをひとまとめにしたGroupedDataFrameを返します。

In [32]:
using Random, Statistics
D = DataFrame(クラス = [1, 1, 1, 2, 2 ,2, 3, 3, 3], ID = [randstring('A':'Z', 5) for i in 1:9], 英語 = rand(1:1:100, 9), 数学 = rand(1:1:100, 9), 国語 = rand(1:1:100, 9))
Out[32]:

9 rows × 5 columns

クラスID英語数学国語
Int64StringInt64Int64Int64
11QNPLM224518
21EKWFU139175
31KMKBG478482
42DIGUX68935
52LNOXB403632
62XGAVU106281
73PZGSC533871
83SSNOF64027
93XIQKF849128
In [33]:
E = groupby(D, :クラス)
Out[33]:

GroupedDataFrame with 3 groups based on key: クラス

First Group (3 rows): クラス = 1

クラスID英語数学国語
Int64StringInt64Int64Int64
11QNPLM224518
21EKWFU139175
31KMKBG478482

Last Group (3 rows): クラス = 3

クラスID英語数学国語
Int64StringInt64Int64Int64
13PZGSC533871
23SSNOF64027
33XIQKF849128

グループ化するとSubDataFrameと呼ばれる亜種を内包したGroupedDataFrameに変わります。

グループ化したデータフレームに対しては,グループごとに処理を実行することができます。代表的なものがcombineです。

combineSubDataFrameの指定した列に対して関数を作用させ,最終的に全グループを同じデータフレームにまとめたものを返します。

In [34]:
combine(E, :英語 => mean => :クラス英語平均, :英語 => var => :クラス英語分散)
Out[34]:

3 rows × 3 columns

クラスクラス英語平均クラス英語分散
Int64Float64Float64
1127.3333310.333
2218.6667345.333
3347.66671542.33

doブロックを使った書き方もできます。この書き方のほうが,一度に複数の処理を走らせたり,一時的に利用する変数が必要なときには便利です(が,速度的には遅いと言われているので,多用しないほうが吉)。

In [35]:
combine(E) do sdf
    m = mean(sdf.英語)
    s = std(sdf.英語)
    t = @. 10*(sdf.英語 - m)/s + 50
    println(t)
    (英語平均 = m, 英語分散 = s)
end
[46.97249746956562, 41.863586949457584, 61.1639155809768]
[43.18378796832533, 61.479936053346805, 45.33627597832786]
[51.35803075539493, 39.39038472347713, 59.25158452112794]
Out[35]:

3 rows × 3 columns

クラス英語平均英語分散
Int64Float64Float64
1127.333317.6163
2218.666718.5831
3347.666739.2726

combineは1グループあたり1行しか値を含むことができないので,もとのデータフレーム長に合わせる場合はselectを使うと良いです。

In [36]:
transform(E) do sdf
    m = mean(sdf.英語)
    s = std(sdf.英語)
    t = @. 10*(sdf.英語 - m)/s + 50
    t
end
Out[36]:

9 rows × 6 columns

クラスID英語数学国語x1
Int64StringInt64Int64Int64Float64
11QNPLM22451846.9725
21EKWFU13917541.8636
31KMKBG47848261.1639
42DIGUX6893543.1838
52LNOXB40363261.4799
62XGAVU10628145.3363
73PZGSC53387151.358
83SSNOF6402739.3904
93XIQKF84912859.2516

6. その他の関数

  • empty(A) Aと同じ名前を持つ0行の新しいデータフレームを返す
  • describe(A) Aの各列に対して要約統計量を計算する(前はカテゴリカル変数に対応していたが...) 関数 => :新しい列名で任意の関数を作用させることもできる。
  • isapprox(A, B) AとBが任意の誤差の範囲内で一致しているかどうかをたしかめる。broadcastすると列ごとに論理値を返す。列名が一緒でないとエラーを吐く。完全一致ならisqeual()
  • sort(A), sort(A, :列名) データフレーム全体か,列名に関してソートする。!をつけるとin-place。
  • names(A), propertynames(A) Aの列名を取得する。namesで文字列のベクトルを,propertynamesでシンボルのベクトルを得る。
In [37]:
empty(A)
Out[37]:

0 rows × 3 columns

abc
Float64Float64Float64
In [38]:
describe(D)
Out[38]:

5 rows × 7 columns

variablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1クラス2.012.030Int64
2IDDIGUXXIQKF0String
3英語31.2222622.0840Int64
4数学64.03662.0910Int64
5国語49.88891835.0820Int64
In [39]:
isequal.(A, A)
Out[39]:

3 rows × 3 columns

abc
BoolBoolBool
1111
2111
3111
In [40]:
sort(D, :国語)
Out[40]:

9 rows × 5 columns

クラスID英語数学国語
Int64StringInt64Int64Int64
11QNPLM224518
23SSNOF64027
33XIQKF849128
42LNOXB403632
52DIGUX68935
63PZGSC533871
71EKWFU139175
82XGAVU106281
91KMKBG478482
In [41]:
names(A)
Out[41]:
3-element Array{String,1}:
 "a"
 "b"
 "c"
In [42]:
propertynames(A)
Out[42]:
3-element Array{Symbol,1}:
 :a
 :b
 :c