data.table
パッケージの概要
data.tableは、R標準のdata.frame型を拡張したdata.table型を導入するパッケージです。
大規模なデータの処理に最適化されているほか、添え字部分の記法が大幅に拡張されており、 効果的に活用することで動作速度とコードの可読性を両立することが可能となります。
本稿ではR標準の関数や、tidyverseのデータ操作パッケージとして名高いdplyr(一部tidyr)による操作とも比較しながら、 data.tableパッケージの使用方法や特徴について解説します。
library(data.table)
library(dplyr)
library(tidyr) #pivot系のみ使用
library(nycflights13) #大規模データの節で使用
library(microbenchmark) #大規模データの節で使用
library(rlang) #関数化の節で使用基本的な使用方法
ファイルの読み込み・書き込み
data.tableパッケージを導入すると、高速なデータの読み書きが可能な関数fread, fwriteが追加されます。
いずれも、デフォルトではcsv形式での読み書きを行います。
# ファイルの書き込み
# fwrite(iris, file = "../data/iris.csv")
# ファイルの読み込み
dt_tmp <- fread(file = "../data/insurance.csv", data.table = TRUE)
#data.tableをFALSEにすることでdata.frame型として読み込むことも可能
head(dt_tmp) #冒頭を表示 age sex bmi children smoker region expenses
<int> <char> <num> <int> <char> <char> <num>
1: 19 female 27.9 0 yes southwest 16884.92
2: 18 male 33.8 1 no southeast 1725.55
3: 28 male 33.0 3 no southeast 4449.46
4: 33 male 22.7 0 no northwest 21984.47
5: 32 male 28.9 0 no northwest 3866.86
6: 31 female 25.7 0 no southeast 3756.62
data.table型変数の作成
data.table型としてデータを読み込むには、前述のfread関数を用いるほかにも、data.frame型など他の型から変換する方法もあります。 通常はas.data.table関数を使用すればよいでしょう。
df <- iris
dt <- as.data.table(df)
class(df)[1] "data.frame"
class(dt)[1] "data.table" "data.frame"
data.table関数で直接生成することもできます。
dt_is <- data.table(
配当方式 = c("有配","有配","準有配","無配","無配","無配"),
商品種類コード = 1:6,
件数 = c(10, 16, 48, 176, 190, 15),
特約1件数 = c(10, 0, 24, 110, 30, 12),
特約2件数 = c(0, 0, 0, 0, 14, 0),
主契約保険金額 = c(100, 60, 240, 69, 1931, 300),
特約1保険金額 = c(100, 0, 24, 59, 3140, 240),
特約2保険金額 = c(0, 0, 0, 0, 156, 0)
)
dt_is 配当方式 商品種類コード 件数 特約1件数 特約2件数 主契約保険金額
<char> <int> <num> <num> <num> <num>
1: 有配 1 10 10 0 100
2: 有配 2 16 0 0 60
3: 準有配 3 48 24 0 240
4: 無配 4 176 110 0 69
5: 無配 5 190 30 14 1931
6: 無配 6 15 12 0 300
特約1保険金額 特約2保険金額
<num> <num>
1: 100 0
2: 0 0
3: 24 0
4: 59 0
5: 3140 156
6: 240 0
添え字の仕様
data.table型の記法はdata.frame型の記法を自然に拡張したものであるため、変数[行, 列]という記法はどちらも同じように使用することが出来ます。
細かな差異として、data.frame型の場合は変数[行, 列]で参照した結果が data.frame型ではなくなる(ベクトルや値になる)ことがありますが、 data.table型では一貫してdata.table型のまま取り出されます。
##3行目だけを抽出
df[3, ] #data.frameのまま Sepal.Length Sepal.Width Petal.Length Petal.Width Species
3 4.7 3.2 1.3 0.2 setosa
dt[3, ] #data.tableのまま Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 4.7 3.2 1.3 0.2 setosa
##"Sepal.Width"の列だけを抽出
head(df[, "Sepal.Width"]) #ベクトルになる[1] 3.5 3.0 3.2 3.1 3.6 3.9
head(dt[, "Sepal.Width"]) #data.tableのまま Sepal.Width
<num>
1: 3.5
2: 3.0
3: 3.2
4: 3.1
5: 3.6
6: 3.9
##3行目, "Sepal.Width"の列だけを抽出
df[3, "Sepal.Width"] #値になる[1] 3.2
dt[3, "Sepal.Width"] #data.tableのまま Sepal.Width
<num>
1: 3.2
dt[[3, "Sepal.Width"]] #data.tableでも、括弧を2重にすると値にできる[1] 3.2
ちなみに、このdata.frameの一貫性のない挙動についてはtibbleに変換することでも解決できます。
df_tbl <- tibble::as_tibble(df)
class(df_tbl)
head(df_tbl[, "Sepal.Width"]) #データフレームのまま
df_tbl[3, "Sepal.Width"] #データフレームのまま[1] "tbl_df" "tbl" "data.frame"
# A tibble: 6 × 1
Sepal.Width
<dbl>
1 3.5
2 3
3 3.2
4 3.1
5 3.6
6 3.9
# A tibble: 1 × 1
Sepal.Width
<dbl>
1 3.2
ただし、本稿の趣旨から逸れるため、今後tibbleについては触れません。
特定の列のみを取り出すには変数[行, 列]記法のほかにも変数$列名や変数[["列名"]]記法もあります。 この場合、どちらの型でもベクトルで取り出されます。
※データフレームは同じ長さのベクトルを要素にもつ名前付きリストであるため、リストから要素を取り出すときと同じ動作です。
##Sepal.Widthの列だけを抽出
head(df$Sepal.Width)
head(dt$Sepal.Width)
head(df[["Sepal.Width"]])
head(dt[["Sepal.Width"]])[1] 3.5 3.0 3.2 3.1 3.6 3.9
[1] 3.5 3.0 3.2 3.1 3.6 3.9
[1] 3.5 3.0 3.2 3.1 3.6 3.9
[1] 3.5 3.0 3.2 3.1 3.6 3.9
なお、添え字を1つしか与えなかった(変数[○]表記)場合はdata.frame型とdata.table型で動作が異なるため注意してください。
data.frameでは列を取り出します。リストとしての性質が優先されています。data.tableでは行を取り出します。変数[行, 列]記法の列の省略とみなされます。
head(df[3]) #列の取り出しになる
head(dt[3]) #行の取り出しになる Petal.Length
1 1.4
2 1.4
3 1.3
4 1.5
5 1.4
6 1.7
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 4.7 3.2 1.3 0.2 setosa
行・列の取り出し
以下、基本的なデータ操作についてR標準の記法やdplyrとも比較しながら説明します。
data.frameでの列の取り出し方には次のようなものがあります。
head(df[["Sepal.Width"]]) #1列をベクトルとして取り出し[1] 3.5 3.0 3.2 3.1 3.6 3.9
head(subset(df, select = "Sepal.Width")) #1列をデータフレームとして取り出し Sepal.Width
1 3.5
2 3.0
3 3.2
4 3.1
5 3.6
6 3.9
head(df[,c("Sepal.Length", "Sepal.Width")]) #複数列をデータフレームとして取り出し Sepal.Length Sepal.Width
1 5.1 3.5
2 4.9 3.0
3 4.7 3.2
4 4.6 3.1
5 5.0 3.6
6 5.4 3.9
head(df[, !names(df) %in% c("Sepal.Length", "Sepal.Width")]) #指定した列以外を取り出し Petal.Length Petal.Width Species
1 1.4 0.2 setosa
2 1.4 0.2 setosa
3 1.3 0.2 setosa
4 1.5 0.2 setosa
5 1.4 0.2 setosa
6 1.7 0.4 setosa
data.table型の場合、添え字の部分で特殊な記法が使用可能です。列名を文字列として与えるのではなく、列名そのものを書くことができます。
なお、以降に現れる.(*)という記法はlist(*)の省略形で、添え字などにリストを与えていることに相当します。
head(dt[,Sepal.Width]) #1列をベクトルとして取り出し
head(dt[,.(Sepal.Width)]) #1列をdata.tableとして取り出し
head(dt[,.(Sepal.Length, Sepal.Width)]) #複数列をdata.tableとして取り出し[1] 3.5 3.0 3.2 3.1 3.6 3.9
Sepal.Width
<num>
1: 3.5
2: 3.0
3: 3.2
4: 3.1
5: 3.6
6: 3.9
Sepal.Length Sepal.Width
<num> <num>
1: 5.1 3.5
2: 4.9 3.0
3: 4.7 3.2
4: 4.6 3.1
5: 5.0 3.6
6: 5.4 3.9
列の除外はdata.frameと同じ書き方も可能ですが、 data.table特有の記法を用いたものでは以下のような方法があります。
head(dt[, !c("Sepal.Length", "Sepal.Width")], n = 1) #!は除くという意味
cols <- c("Sepal.Length", "Sepal.Width")
head(dt[, !..cols], n = 1) # ..colsとした場合、内部的に上で代入した c("Sepal.Length", "Sepal.Width") に置き換えられる
head(dt[, setdiff(names(dt), c("Sepal.Length", "Sepal.Width")), with = FALSE], n = 1)
#setdiffは差集合をとる、with = FALSEは選択する列をリストで直接指定するという意味
head(dt[,.SD, .SDcols = !c("Sepal.Length", "Sepal.Width")], n = 1) #.SDは.SDcolsで指定した列全体を表す特殊記号 Petal.Length Petal.Width Species
<num> <num> <fctr>
1: 1.4 0.2 setosa
Petal.Length Petal.Width Species
<num> <num> <fctr>
1: 1.4 0.2 setosa
Petal.Length Petal.Width Species
<num> <num> <fctr>
1: 1.4 0.2 setosa
Petal.Length Petal.Width Species
<num> <num> <fctr>
1: 1.4 0.2 setosa
dplyrにおいてはselectに対応します。
df %>% select(Sepal.Length, Sepal.Width) %>% slice_head(n=2) #指定した列
df %>% select(-Sepal.Length, -Sepal.Width) %>% slice_head(n=2) #指定した列以外 Sepal.Length Sepal.Width
1 5.1 3.5
2 4.9 3.0
Petal.Length Petal.Width Species
1 1.4 0.2 setosa
2 1.4 0.2 setosa
次に、特定の条件を満たす行を抽出する方法を見てみましょう。
R標準では次のような書き方になります。
df[df$Sepal.Length < 6.0 & df$Sepal.Width == 3.0 & df$Species %in% c("versicolor", "virginica"),] Sepal.Length Sepal.Width Petal.Length Petal.Width Species
62 5.9 3 4.2 1.5 versicolor
67 5.6 3 4.5 1.5 versicolor
85 5.4 3 4.5 1.5 versicolor
89 5.6 3 4.1 1.3 versicolor
96 5.7 3 4.2 1.2 versicolor
150 5.9 3 5.1 1.8 virginica
data.tableでも同じ書き方はできますが、dt$(変数名)の部分を省略したような書き方も出来ます。
dt[Sepal.Length < 6.0 & Sepal.Width == 3.0 & Species %in% c("versicolor", "virginica"),] Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 5.9 3 4.2 1.5 versicolor
2: 5.6 3 4.5 1.5 versicolor
3: 5.4 3 4.5 1.5 versicolor
4: 5.6 3 4.1 1.3 versicolor
5: 5.7 3 4.2 1.2 versicolor
6: 5.9 3 5.1 1.8 virginica
dplyrではfilterを使用します。
df %>% filter(Sepal.Length < 6.0 & Sepal.Width == 3.0 & Species %in% c("versicolor", "virginica")) Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.9 3 4.2 1.5 versicolor
2 5.6 3 4.5 1.5 versicolor
3 5.4 3 4.5 1.5 versicolor
4 5.6 3 4.1 1.3 versicolor
5 5.7 3 4.2 1.2 versicolor
6 5.9 3 5.1 1.8 virginica
列の加工・追加
特徴量の加工のような操作を行う場合、R標準では以下のような書き方になります。
df_tmp <- df
df_tmp$Sepal.Rate <- df_tmp$Sepal.Length / df_tmp$Sepal.Width #列を追加
df_tmp[c("Sepal.Length.Sqrt", "Sepal.Width.Sqrt")] <-
c(sqrt(df_tmp$Sepal.Length), sqrt(df_tmp$Sepal.Width)) #一度に複数の列を追加
df_tmp$Species <- substr(df_tmp$Species, 1, 2) #既存の列を加工
head(df_tmp, n = 3) Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Rate
1 5.1 3.5 1.4 0.2 se 1.457143
2 4.9 3.0 1.4 0.2 se 1.633333
3 4.7 3.2 1.3 0.2 se 1.468750
Sepal.Length.Sqrt Sepal.Width.Sqrt
1 2.258318 1.870829
2 2.213594 1.732051
3 2.167948 1.788854
data.tableでも同じ書き方はできますが、変数[行, 列]記法の列の箇所で:=演算子を用いることでより簡潔に記述できます。
dt_tmp <- copy(dt) #:=演算子がdtに及ばないようにするため
dt_tmp[, Sepal.Rate := Sepal.Length / Sepal.Width] #列を追加
dt_tmp[, c("Sepal.Length.Sqrt", "Sepal.Width.Sqrt")
:= .(sqrt(Sepal.Length), sqrt(Sepal.Width))] #一度に複数の列を追加
dt_tmp[, ':='(Sepal.Length.Sqrt = sqrt(Sepal.Length),
Sepal.Width.Sqrt = sqrt(Sepal.Width))]#↑はこのような書き方もある
dt_tmp[, Species := substr(Species, 1, 2)] #既存の列を加工
head(dt_tmp, n = 3) Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Rate
<num> <num> <num> <num> <char> <num>
1: 5.1 3.5 1.4 0.2 se 1.457143
2: 4.9 3.0 1.4 0.2 se 1.633333
3: 4.7 3.2 1.3 0.2 se 1.468750
Sepal.Length.Sqrt Sepal.Width.Sqrt
<num> <num>
1: 2.258318 1.870829
2: 2.213594 1.732051
3: 2.167948 1.788854
ただし:=演算子を用いるケースでは、単に「変数の代入」でコピーを作成した場合、 両方に同じ操作が適用されてしまう(メモリ上の同じものを指している)ので注意してください。 これは大規模データでメモリ消費を抑えられるようにするための意図的な仕様です。
この現象を避ける(コピー元の方には影響を及ぼさないようにする)ためには、 copy関数で明示的にコピーを作成する必要があります。
dt_tmp2 <- dt_tmp
dt_tmp[, Petal.Rate := Petal.Length / Petal.Width] #tmpの方に列を追加
head(dt_tmp2, n = 3) #tmp2の方にも追加されている Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Rate
<num> <num> <num> <num> <char> <num>
1: 5.1 3.5 1.4 0.2 se 1.457143
2: 4.9 3.0 1.4 0.2 se 1.633333
3: 4.7 3.2 1.3 0.2 se 1.468750
Sepal.Length.Sqrt Sepal.Width.Sqrt Petal.Rate
<num> <num> <num>
1: 2.258318 1.870829 7.0
2: 2.213594 1.732051 7.0
3: 2.167948 1.788854 6.5
dplyrではmutateを使用します。
#mutate文の場合敢えて分けて書く必要はないが、説明のため。
df_tmp <- df %>% mutate(Sepal.Rate = Sepal.Length / Sepal.Width) #列を追加
df_tmp <- df_tmp %>% mutate(Sepal.Length.Sqrt = sqrt(Sepal.Length),
Sepal.Width.Sqrt = sqrt(Sepal.Width)) #一度に複数の列を追加
df_tmp <- df_tmp %>% mutate(Species = substr(Species, 1, 2)) #既存の列を加工
head(df_tmp, n = 3) Sepal.Length Sepal.Width Petal.Length Petal.Width Species Sepal.Rate
1 5.1 3.5 1.4 0.2 se 1.457143
2 4.9 3.0 1.4 0.2 se 1.633333
3 4.7 3.2 1.3 0.2 se 1.468750
Sepal.Length.Sqrt Sepal.Width.Sqrt
1 2.258318 1.870829
2 2.213594 1.732051
3 2.167948 1.788854
なお、すでにあるデータを連結する場合はrbindやcbindも使用できます(R標準と同じであるため割愛)。
データのソート
data.frame, data.tableともに、変数[行, 列]記法の行の箇所でorder関数を使用します。
order関数は、ベクトルを昇順ソートした時の行番号を返す関数です。-を与えることで降順ソートにすることが出来ます。
#Speciesはfactor型であり、マイナス演算子を直接適用できないため、数値型に一度変換している
head(df[order(-as.numeric(df$Species), df$Sepal.Length), ])
#data.table型ではマイナス演算子をそのまま適用できる
head(dt[order(-Species, Sepal.Length), ]) Sepal.Length Sepal.Width Petal.Length Petal.Width Species
107 4.9 2.5 4.5 1.7 virginica
122 5.6 2.8 4.9 2.0 virginica
114 5.7 2.5 5.0 2.0 virginica
102 5.8 2.7 5.1 1.9 virginica
115 5.8 2.8 5.1 2.4 virginica
143 5.8 2.7 5.1 1.9 virginica
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 4.9 2.5 4.5 1.7 virginica
2: 5.6 2.8 4.9 2.0 virginica
3: 5.7 2.5 5.0 2.0 virginica
4: 5.8 2.7 5.1 1.9 virginica
5: 5.8 2.8 5.1 2.4 virginica
6: 5.8 2.7 5.1 1.9 virginica
dplyrではarrange関数を使用します。降順ソートを行う場合はdescを使用します。
df %>% arrange(desc(Species), Sepal.Length) %>% head Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 4.9 2.5 4.5 1.7 virginica
2 5.6 2.8 4.9 2.0 virginica
3 5.7 2.5 5.0 2.0 virginica
4 5.8 2.7 5.1 1.9 virginica
5 5.8 2.8 5.1 2.4 virginica
6 5.8 2.7 5.1 1.9 virginica
データのグループ化と集計・要約
あるグループごとに特徴量の平均値を計算したい、といったケースを考えてみましょう。
R標準機能では次のような書き方になります。
aggregate(cbind(Sepal.Length, Sepal.Width) ~ Species, df, mean) Species Sepal.Length Sepal.Width
1 setosa 5.006 3.428
2 versicolor 5.936 2.770
3 virginica 6.588 2.974
一方data.tableの場合、添え字の部分にmeanのような集計関数をそのまま書き込むことができます。 グループ化に使用する列は引数byに指定します。
dt[ ,.(Sepal.Length.Mean = mean(Sepal.Length),
Sepal.Width.Mean = mean(Sepal.Width)),
by = .(Species)] Species Sepal.Length.Mean Sepal.Width.Mean
<fctr> <num> <num>
1: setosa 5.006 3.428
2: versicolor 5.936 2.770
3: virginica 6.588 2.974
dplyrの場合はグループ化にgroup_byを用いたうえで、summarizeで集計関数を適用します。
df %>% group_by(Species) %>%
summarize(mean(Sepal.Length), mean(Sepal.Width))# A tibble: 3 × 3
Species `mean(Sepal.Length)` `mean(Sepal.Width)`
<fct> <dbl> <dbl>
1 setosa 5.01 3.43
2 versicolor 5.94 2.77
3 virginica 6.59 2.97
発展的な話題
横長と縦長の相互変換(pivot)
横長→縦長
まず、横長(wide型)から縦長(long型)への変換について見てみましょう。
Rの標準機能ではreshape関数が使用できますが、多機能ゆえ適切にパラメータを指定するにはコツが必要です。
df_long <- reshape(df, direction = "long", #縦長への変換モード
varying = c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width"), #縦長に変換したい列を指定
timevar = "VarName", #縦長に展開するときの、新たに追加されるキー列の名前
times = c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width"), #新たに追加されるキー列の中身(横長時の列名から指定)
v.names = "Value") #縦長になった結果集約される値が入る列の名前…2列以上も可
head(df_long)
aggregate(Value ~ VarName, df_long, mean) Species VarName Value id
1.Sepal.Length setosa Sepal.Length 5.1 1
2.Sepal.Length setosa Sepal.Length 4.9 2
3.Sepal.Length setosa Sepal.Length 4.7 3
4.Sepal.Length setosa Sepal.Length 4.6 4
5.Sepal.Length setosa Sepal.Length 5.0 5
6.Sepal.Length setosa Sepal.Length 5.4 6
VarName Value
1 Petal.Length 3.758000
2 Petal.Width 1.199333
3 Sepal.Length 5.843333
4 Sepal.Width 3.057333
一方、data.tableではmeltを使用します。
dt_long <- melt(dt,
measure.vars = c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width"),#縦長に変換したい列を指定
variable.name = "VarName", #縦長に展開するときの、新たに追加されるキー列の名前
value.name = "Value") #縦長になった結果集約される値が入る列の名前
head(dt_long)
dt_long[ ,mean(Value), by = VarName] Species VarName Value
<fctr> <fctr> <num>
1: setosa Sepal.Length 5.1
2: setosa Sepal.Length 4.9
3: setosa Sepal.Length 4.7
4: setosa Sepal.Length 4.6
5: setosa Sepal.Length 5.0
6: setosa Sepal.Length 5.4
VarName V1
<fctr> <num>
1: Sepal.Length 5.843333
2: Sepal.Width 3.057333
3: Petal.Length 3.758000
4: Petal.Width 1.199333
なお、tidyverseではdplyrではなくtidyrに対応する関数があり、 pivot_longer(またはgather)を使用します。
df_long_tidy <- df %>%
tibble::rowid_to_column("id") %>% #あとでもう一度横長に戻すときの基準として入れておく
pivot_longer(names_to = "VarName", #縦長に展開するときの、新たに追加されるキー列の名前
values_to = "Value", #縦長になった結果集約される値が入る列の名前
-c(id, Species)) #縦長に変換したい列を指定
df_long_tidy %>% head #並びが異なる
df_long_tidy %>% group_by(VarName) %>% summarize(mean(Value))# A tibble: 6 × 4
id Species VarName Value
<int> <fct> <chr> <dbl>
1 1 setosa Sepal.Length 5.1
2 1 setosa Sepal.Width 3.5
3 1 setosa Petal.Length 1.4
4 1 setosa Petal.Width 0.2
5 2 setosa Sepal.Length 4.9
6 2 setosa Sepal.Width 3
# A tibble: 4 × 2
VarName `mean(Value)`
<chr> <dbl>
1 Petal.Length 3.76
2 Petal.Width 1.20
3 Sepal.Length 5.84
4 Sepal.Width 3.06
reshape関数は多機能ゆえ、一度に2グループの列を集約することも可能ですが、 いっそう使用方法は複雑になります。
df_long2 <-reshape(df, direction = "long",
varying = c("Petal.Length","Sepal.Length","Petal.Width","Sepal.Width"), #縦長に変換したい列を指定
timevar = "VarName", #縦長に展開するときの、新たに追加されるキー列の名前
times = c("Length","Width"), #新たに追加されるキー列の中身(横長時の列名から指定)
v.names = c("Sepal","Petal")) #縦長になった結果集約される値が入る列の名前…2列以上も可
head(df_long2) Species VarName Sepal Petal id
1.Length setosa Length 5.1 1.4 1
2.Length setosa Length 4.9 1.4 2
3.Length setosa Length 4.7 1.3 3
4.Length setosa Length 4.6 1.5 4
5.Length setosa Length 5.0 1.4 5
6.Length setosa Length 5.4 1.7 6
#実は同じような結果をもう少し簡単な指定で得ることもできる
df_long3 <-reshape(df, direction = "long",
varying = c("Petal.Length","Sepal.Length","Petal.Width","Sepal.Width"), #縦長に変換したい列を指定
timevar = "VarName", #縦長に展開するときの、新たに追加されるキー列の名前
sep = ".") #縦長に変換したい列が, 値が入る列の名前.新たに追加されるキー列の中身 という命名規則の場合
head(df_long3) Species VarName Petal Sepal id
1.Length setosa Length 1.4 5.1 1
2.Length setosa Length 1.4 4.9 2
3.Length setosa Length 1.3 4.7 3
4.Length setosa Length 1.5 4.6 4
5.Length setosa Length 1.4 5.0 5
6.Length setosa Length 1.7 5.4 6
data.tableの場合は次のような方法が考えられます。
##data.tableのmeasure.varsにパターンを与える方法
dt_long2 <- melt(dt, measure.vars = patterns("^Sepal", "^Petal"),
variable.name = "Var",
value.name = c("Sepal", "Petal"))
head(dt_long2)
#1.15以降ではmeasure関数が追加された
#https://rdatatable.gitlab.io/data.table/news/index.html#datatable-v1150-30-jan-2024
#melt(dt, measure.vars = measure(value.name, dim, sep="."))
##添え字の中に欲しい形で書き下してしまう方法
dt_long3 <- dt[,.(
VarName = c(rep("Length", .N), rep("Width", .N)),
Sepal = c(Sepal.Length, Sepal.Width),
Petal = c(Petal.Length, Petal.Width)
) ,by = .(Species)]
head(dt_long3) Species Var Sepal Petal
<fctr> <fctr> <num> <num>
1: setosa 1 5.1 1.4
2: setosa 1 4.9 1.4
3: setosa 1 4.7 1.3
4: setosa 1 4.6 1.5
5: setosa 1 5.0 1.4
6: setosa 1 5.4 1.7
Species VarName Sepal Petal
<fctr> <char> <num> <num>
1: setosa Length 5.1 1.4
2: setosa Length 4.9 1.4
3: setosa Length 4.7 1.3
4: setosa Length 4.6 1.5
5: setosa Length 5.0 1.4
6: setosa Length 5.4 1.7
縦長→横長
逆に縦長から横長に戻す場合、R標準では次のような書き方になります。
df_wide <- reshape(df_long, direction = "wide", #縦長への変換モード
varying = c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width"),#横長に展開するときの列名
timevar = "VarName", #横長に展開するときの列の基準が入った列
v.names = "Value") #横長に展開するときの値となる列
head(df_wide) Species id Sepal.Length Sepal.Width Petal.Length Petal.Width
1.Sepal.Length setosa 1 5.1 3.5 1.4 0.2
2.Sepal.Length setosa 2 4.9 3.0 1.4 0.2
3.Sepal.Length setosa 3 4.7 3.2 1.3 0.2
4.Sepal.Length setosa 4 4.6 3.1 1.5 0.2
5.Sepal.Length setosa 5 5.0 3.6 1.4 0.2
6.Sepal.Length setosa 6 5.4 3.9 1.7 0.4
一方、data.tableではdcastを使用します。
dt_long_tmp <- copy(dt_long)
dt_long_tmp[ ,id := 1:.N, by = .(VarName)]
#横長に戻した後の行番号を付与 これがないと集約されてしまう
dt_wide <- dcast(dt_long_tmp,
id + Species ~ VarName,
#残したい列 ~ 横長に展開するときの列の名前が入った列
value.var = "Value") #横長に展開するときの値となる列
head(dt_wide)Key: <id, Species>
id Species Sepal.Length Sepal.Width Petal.Length Petal.Width
<int> <fctr> <num> <num> <num> <num>
1: 1 setosa 5.1 3.5 1.4 0.2
2: 2 setosa 4.9 3.0 1.4 0.2
3: 3 setosa 4.7 3.2 1.3 0.2
4: 4 setosa 4.6 3.1 1.5 0.2
5: 5 setosa 5.0 3.6 1.4 0.2
6: 6 setosa 5.4 3.9 1.7 0.4
tidyrではpivot_wider(またはspread)を使用します。
df_wide_tidy <- df_long_tidy %>%
pivot_wider(names_from = "VarName", #横長に展開するときの列の名前が入った列
values_from = "Value") #横長に展開するときの値となる列
df_wide_tidy %>% head# A tibble: 6 × 6
id Species Sepal.Length Sepal.Width Petal.Length Petal.Width
<int> <fct> <dbl> <dbl> <dbl> <dbl>
1 1 setosa 5.1 3.5 1.4 0.2
2 2 setosa 4.9 3 1.4 0.2
3 3 setosa 4.7 3.2 1.3 0.2
4 4 setosa 4.6 3.1 1.5 0.2
5 5 setosa 5 3.6 1.4 0.2
6 6 setosa 5.4 3.9 1.7 0.4
より複雑なパターンについてはvignette “Efficient reshaping using data.tables”にも解説があります。
テーブルの結合(join)
ここでは複数のテーブルの結合を取り扱います。
以下の例で使用するテーブルを準備します。
#ある時点での保有データ
#システム仕様の都合により保険金額が2桁までしか保持できないため、
#大型契約の場合は同じ証券番号で複数のレコードを保持して対応する
df_IF <- data.frame(
証券番号 = c(2, 2 ,3, 3, 99),
特約種類 = c(0, 0 ,0, 10, 0),
保有保険金額 = c(99, 30, 50, 25, 12)
)
#過去の消滅契約も含めた諸情報が蓄積されたデータ
#ここでは、証券番号と特約種類の組によってレコードが特定されるものとする
#ただし、df_IFに存在する証券番号99は特別なもので、当データベースには蓄積されていないものとする
df_MF <- data.frame(
証券番号 = c(1, 1 ,2, 3, 3),
特約種類 = c(0, 10, 0 ,0, 10),
契約年齢 = c(30, 30, 40, 50, 50),
性別 = c(1, 1, 2, 1, 1)
)
dt_IF <- as.data.table(df_IF)
dt_MF <- as.data.table(df_MF)
df_IF
df_MF 証券番号 特約種類 保有保険金額
1 2 0 99
2 2 0 30
3 3 0 50
4 3 10 25
5 99 0 12
証券番号 特約種類 契約年齢 性別
1 1 0 30 1
2 1 10 30 1
3 2 0 40 2
4 3 0 50 1
5 3 10 50 1
さて、保有テーブルに足りない情報を別のテーブルから付加するようなケースを考えます。
R標準ではmergeという関数があります。
#x側の行をすべて残す(left join)…MFにある情報をIFに付与する(契約年齢、性別がわかる)
merge(x = df_IF, y = df_MF,
by.x = c("証券番号", "特約種類"), by.y = c("証券番号", "特約種類"), #結合に使うキー
all.x = TRUE, all.y = FALSE) #どちらの行を残すか 証券番号 特約種類 保有保険金額 契約年齢 性別
1 2 0 99 40 2
2 2 0 30 40 2
3 3 0 50 50 1
4 3 10 25 50 1
5 99 0 12 NA NA
#両方にある行だけを抽出する(inner join)…MFに無いような変な契約は捨てる
merge(x = df_IF, y = df_MF,
by.x = c("証券番号", "特約種類"), by.y = c("証券番号", "特約種類")) 証券番号 特約種類 保有保険金額 契約年齢 性別
1 2 0 99 40 2
2 2 0 30 40 2
3 3 0 50 50 1
4 3 10 25 50 1
#y側の行をすべて残す(right join)…IFにある情報をMFに付与する(ある時点での保有の状況がわかる)
merge(x = df_IF, y = df_MF,
by.x = c("証券番号", "特約種類"), by.y = c("証券番号", "特約種類"),
all.x = FALSE, all.y = TRUE) 証券番号 特約種類 保有保険金額 契約年齢 性別
1 1 0 NA 30 1
2 1 10 NA 30 1
3 2 0 99 40 2
4 2 0 30 40 2
5 3 0 50 50 1
6 3 10 25 50 1
data.tableの場合、上記の手法も依然として使えますが、 変数[行, 列]記法の行の方に別のdata.tableを記述することでも実現できます。
#dt_IFにあるレコードをキーとして、dt_MFのデータを取得する
#onは結合に使うキーを指定 キーが異なるときはc("証券番号" = "証券番号") のように指定する
dt_MF[dt_IF, , on = .(証券番号, 特約種類)] 証券番号 特約種類 契約年齢 性別 保有保険金額
<num> <num> <num> <num> <num>
1: 2 0 40 2 99
2: 2 0 40 2 30
3: 3 0 50 1 50
4: 3 10 50 1 25
5: 99 0 NA NA 12
#引数nomatch = NULLを与えることで両方にある行だけを抽出(inner join)
dt_MF[dt_IF, , on = .(証券番号, 特約種類), nomatch = NULL] 証券番号 特約種類 契約年齢 性別 保有保険金額
<num> <num> <num> <num> <num>
1: 2 0 40 2 99
2: 2 0 40 2 30
3: 3 0 50 1 50
4: 3 10 50 1 25
#dt_MFにあるレコードをキーとして、dt_IFのデータを取得する
dt_IF[dt_MF, , on = .(証券番号, 特約種類)] 証券番号 特約種類 保有保険金額 契約年齢 性別
<num> <num> <num> <num> <num>
1: 1 0 NA 30 1
2: 1 10 NA 30 1
3: 2 0 99 40 2
4: 2 0 30 40 2
5: 3 0 50 50 1
6: 3 10 25 50 1
dplyrではjoin系の関数が使用できます。
dt_IF %>% left_join(dt_MF, by = join_by(証券番号, 特約種類)) 証券番号 特約種類 保有保険金額 契約年齢 性別
<num> <num> <num> <num> <num>
1: 2 0 99 40 2
2: 2 0 30 40 2
3: 3 0 50 50 1
4: 3 10 25 50 1
5: 99 0 12 NA NA
dt_IF %>% inner_join(dt_MF, by = join_by(証券番号, 特約種類)) 証券番号 特約種類 保有保険金額 契約年齢 性別
<num> <num> <num> <num> <num>
1: 2 0 99 40 2
2: 2 0 30 40 2
3: 3 0 50 50 1
4: 3 10 25 50 1
dt_IF %>% right_join(dt_MF, by = join_by(証券番号, 特約種類)) 証券番号 特約種類 保有保険金額 契約年齢 性別
<num> <num> <num> <num> <num>
1: 2 0 99 40 2
2: 2 0 30 40 2
3: 3 0 50 50 1
4: 3 10 25 50 1
5: 1 0 NA 30 1
6: 1 10 NA 30 1
data.tableが大規模データに強い理由
data.tableが大規模データに強い理由のうち、代表的なものを挙げると次のようになります。
freadやfwriteは、標準の関数に比べて非常に高速変数[行, 列]の形式でデータ抽出・加工を行う場合、関係のないカラムをいちいち操作しない- メモリの利用が効率的
:=による代入やset*系関数により、メモリのデータを丸ごとコピー(ディープコピー)することなくテーブルの加工を行うことが出来る
- いわゆるインデックスの機能がある
dt[x == 10 & y == 20, ]のようなクエリが発行されたとき、対応するインデックスが作成されていればそれを使用して高速にレコードを抽出できる。data.frameから行idの機能が失われた代わりに、主キーとなる列(以下単に「キー列」)を複数持つことが出来る。キー列でソートされた形でテーブルを保持するため、キー列による検索は特に高速(いわゆるクラスター化インデックスに近い)- キー列とは別に、明示的にインデックスを作成することもできる(secondary index)
dt[x == 10 & y == 20, ]のようなクエリが発行された時点で、必要なインデックスを自動的に作成(auto indexing)
- マルチコア処理などの低レイヤの処理を最適化している
これらの特徴を把握することで、大規模データに強いコーディングを意図的に行うことも可能になります。
大規模データでの動作を確認するために、いくつか例を見てみます。
fread, fwriteの性能比較
R標準の関数と簡単に速度比較をしてみます。
環境・条件によっては、十数倍~数十倍程度の速度差が出ることもあるようです。
set.seed(1)
print(file.info("../data/lapse_study.csv")$size / 1024)#KB単位のファイルサイズ
system.time(dt_tmp_r1 <- fread("../data/lapse_study.csv"))
system.time(dt_tmp_r2 <- read.csv("../data/lapse_study.csv"))
all(mapply(all.equal, dt_tmp_r1, dt_tmp_r2))[1] 1949.819
ユーザ システム 経過
0.00 0.00 0.02
ユーザ システム 経過
0.11 0.00 0.11
[1] TRUE
大規模データ操作での性能比較
公開データでは規模の大きいnycflights13パッケージのflightデータを用いて、 data.tableのデータ操作の速度を比較してみましょう。
まず、簡単にデータの概要を表示します。
df_f <- flights
dt_f <- as.data.table(df_f)
nrow(df_f) #レコード数[1] 336776
set.seed(42)
df_f[sample(nrow(df_f), 5), ] #サンプル抽出# A tibble: 5 × 19
year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
<int> <int> <int> <int> <int> <dbl> <int> <int>
1 2013 11 7 600 600 0 826 825
2 2013 10 30 1252 1250 2 1356 1400
3 2013 12 18 1723 1715 8 2008 2020
4 2013 11 20 2029 2030 -1 2141 2205
5 2013 10 21 1620 1625 -5 1818 1831
# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
# hour <dbl>, minute <dbl>, time_hour <dttm>
summary(df_f) #サマリー year month day dep_time sched_dep_time
Min. :2013 Min. : 1.000 Min. : 1.00 Min. : 1 Min. : 106
1st Qu.:2013 1st Qu.: 4.000 1st Qu.: 8.00 1st Qu.: 907 1st Qu.: 906
Median :2013 Median : 7.000 Median :16.00 Median :1401 Median :1359
Mean :2013 Mean : 6.549 Mean :15.71 Mean :1349 Mean :1344
3rd Qu.:2013 3rd Qu.:10.000 3rd Qu.:23.00 3rd Qu.:1744 3rd Qu.:1729
Max. :2013 Max. :12.000 Max. :31.00 Max. :2400 Max. :2359
NA's :8255
dep_delay arr_time sched_arr_time arr_delay
Min. : -43.00 Min. : 1 Min. : 1 Min. : -86.000
1st Qu.: -5.00 1st Qu.:1104 1st Qu.:1124 1st Qu.: -17.000
Median : -2.00 Median :1535 Median :1556 Median : -5.000
Mean : 12.64 Mean :1502 Mean :1536 Mean : 6.895
3rd Qu.: 11.00 3rd Qu.:1940 3rd Qu.:1945 3rd Qu.: 14.000
Max. :1301.00 Max. :2400 Max. :2359 Max. :1272.000
NA's :8255 NA's :8713 NA's :9430
carrier flight tailnum origin
Length:336776 Min. : 1 Length:336776 Length:336776
Class :character 1st Qu.: 553 Class :character Class :character
Mode :character Median :1496 Mode :character Mode :character
Mean :1972
3rd Qu.:3465
Max. :8500
dest air_time distance hour
Length:336776 Min. : 20.0 Min. : 17 Min. : 1.00
Class :character 1st Qu.: 82.0 1st Qu.: 502 1st Qu.: 9.00
Mode :character Median :129.0 Median : 872 Median :13.00
Mean :150.7 Mean :1040 Mean :13.18
3rd Qu.:192.0 3rd Qu.:1389 3rd Qu.:17.00
Max. :695.0 Max. :4983 Max. :23.00
NA's :9430
minute time_hour
Min. : 0.00 Min. :2013-01-01 05:00:00
1st Qu.: 8.00 1st Qu.:2013-04-04 13:00:00
Median :29.00 Median :2013-07-03 10:00:00
Mean :26.23 Mean :2013-07-03 05:22:54
3rd Qu.:44.00 3rd Qu.:2013-10-01 07:00:00
Max. :59.00 Max. :2013-12-31 23:00:00
カテゴリの数が多いカテゴリ変数で抽出する場合、data.tableはインデックスの効果により抽出が高速になります。
digits_cur <- getOption("digits")#表示桁数を一時的に少なくしておく
options(digits=3)#tailnumは機体記号(tail number) 航空機ごとにつけられる固有の記号のこと
#本データセットには4000以上の機体記号が登録されている
nrow(dt_f[,.N, by=.(tailnum) ])
#microbenchmarkは同じ処理を複数回実行し、その実行時間を観察することができる関数
#例えばmeanの列に平均実行時間がミリ秒単位で格納される
#特定の機体記号のレコードを抽出するだけの処理
df_tmp <- microbenchmark(df_f[df_f$tailnum == "N449US", ], times=100, unit = "milliseconds") #R標準
df_tmp <- rbind(df_tmp, microbenchmark(dt_f[tailnum == "N449US", ], times=100, unit = "milliseconds")) #data.table:auto indexingによりインデックスを作成して使用するので早い
df_tmp <- rbind(df_tmp, microbenchmark(df_f %>% filter(tailnum == "N449US"), times=100, unit = "milliseconds")) #dplyr
df_tmp[1] 4044
Unit: milliseconds
expr min lq mean median uq max neval
df_f[df_f$tailnum == "N449US", ] 1.72 1.84 2.09 1.93 2.20 5.65 100
dt_f[tailnum == "N449US", ] 1.17 1.30 1.58 1.39 1.57 8.71 100
df_f %>% filter(tailnum == "N449US") 2.93 3.00 3.58 3.18 3.59 7.40 100
多めの行を抽出しつつ限られた列に対してなんらかの演算を行う場合、dplyr が最も低速な傾向です。
変数[行, 列]記法の場合、操作に関係のない列を自然に無視して処理できているためと考えられます。
#R標準
#行の抽出だけ
df_tmp <- microbenchmark(df_f[df_f$month == 12, ], times=100, unit = "milliseconds")
#上記に加え、いくつかの列を使用して計算
#文を2つに分けた都合上microbenchmarkの出力が2行に分かれるので、比較対象はその合計になる
rows_tmp <- df_f$month == 12
df_tmp <- rbind(df_tmp, microbenchmark(rows_tmp <- df_f$month == 12, df_f[rows_tmp, "hour"]*60 + df_f[rows_tmp, "minute"], times=100, unit = "milliseconds"))
df_tmpUnit: milliseconds
expr min lq mean
df_f[df_f$month == 12, ] 2.197 2.306 2.836
rows_tmp <- df_f$month == 12 0.583 0.586 0.791
df_f[rows_tmp, "hour"] * 60 + df_f[rows_tmp, "minute"] 0.913 0.950 1.048
median uq max neval
2.442 2.985 6.39 100
0.595 0.884 5.60 100
0.966 1.010 5.72 100
#data.table
df_tmp <- microbenchmark(dt_f[month == 12, ], times=100, unit = "milliseconds")
df_tmp <- rbind(df_tmp, microbenchmark(dt_f[month == 12, hour*60 + minute], times=100, unit = "milliseconds"))
df_tmpUnit: milliseconds
expr min lq mean median uq max neval
dt_f[month == 12, ] 3.17 3.37 3.95 3.51 3.88 9.08 100
dt_f[month == 12, hour * 60 + minute] 1.88 1.99 2.32 2.17 2.31 7.49 100
#dplyr
df_tmp <- microbenchmark(df_f %>% filter(month == 12) , times=100, unit = "milliseconds")
df_tmp <- rbind(df_tmp, microbenchmark(df_f %>% filter(month == 12) %>% mutate(m = hour*60 + minute), times=100, unit = "milliseconds"))
#mutate %>% select や transmute とするとさらに遅くなったのでそうしなかった
df_f_l <- df_f %>% select(month, hour, minute) #関係のない列をそぎ落としてもまだ若干低速
df_tmp <- rbind(df_tmp, microbenchmark(df_f_l %>% filter(month == 12) %>% mutate(m = hour*60 + minute), times=100, unit = "milliseconds"))
df_tmpUnit: milliseconds
expr min lq
df_f %>% filter(month == 12) 3.41 3.55
df_f %>% filter(month == 12) %>% mutate(m = hour * 60 + minute) 4.31 4.47
df_f_l %>% filter(month == 12) %>% mutate(m = hour * 60 + minute) 3.10 3.25
mean median uq max neval
4.25 3.66 4.31 8.55 100
6.06 4.58 4.94 104.21 100
4.13 3.60 4.37 9.91 100
なお、キーやインデックスの詳細については以下のvignettesを参照してください。
また、本稿ではあまり説明していませんが、set*系関数などのメモリを節約する機能についてはvignette “Reference semantics”を参照してください。
options(digits=digits_cur)#表示桁数を元に戻す添え字の発展的な使用方法
data.tableはdplyrでの操作にも対応しているため可読性の観点から併用されるケースが目立ちますが、 data.tableの変数[行, 列]記法はdata.frameのそれよりも大幅に拡張されており、 これをフルに活用することで複雑な処理をエレガントに記述することも可能です。 また前述のとおり、変数[行, 列]記法は大規模データでの実行速度向上の観点でもメリットがあります。 本節ではこの変数[行, 列]記法について少しだけ掘り下げてみます。
「列」の自在性
「列」の箇所は、列名のみならず式を書くこともできます。 式中の列名をテーブルのカラム全体を表すベクトルに変換して計算するような動作になります。 結果の長さが元のベクトルとは異なっていても問題ないため、 meanのような集計関数(ベクトルを引数にとり数値を吐き出すような関数)も自然に使用できます。
#.(*)はlist(*)の省略形で、listを与えたときはdata.tableで結果を返却する
head(dt[ ,.(Sepal.Length, Sepal.Width)]) Sepal.Length Sepal.Width
<num> <num>
1: 5.1 3.5
2: 4.9 3.0
3: 4.7 3.2
4: 4.6 3.1
5: 5.0 3.6
6: 5.4 3.9
#.(*)のなかにx = aの形で記述することで、列名をxで返却する
head(dt[ ,.(SL = Sepal.Length)]) SL
<num>
1: 5.1
2: 4.9
3: 4.7
4: 4.6
5: 5.0
6: 5.4
#一方、右辺のほうには式を書くことができる
head(dt[ ,.(Sepal.Rate = Sepal.Length / Sepal.Width)]) Sepal.Rate
<num>
1: 1.457143
2: 1.633333
3: 1.468750
4: 1.483871
5: 1.388889
6: 1.384615
#ベクトルを引数にとり数値を吐き出すような関数も使える
dt[ ,.(Sepal.Rate.Mean = mean(Sepal.Length / Sepal.Width))] Sepal.Rate.Mean
<num>
1: 1.953681
引数byを指定してグループ化した場合、上記の操作がグループごとに細分化されて行われるイメージです。
dt[ ,.(Sepal.Rate.Mean = mean(Sepal.Length / Sepal.Width)), by = Species] Species Sepal.Rate.Mean
<fctr> <num>
1: setosa 1.470188
2: versicolor 2.160402
3: virginica 2.230453
特殊記号(Special Symbols)
また、「列」の欄にはいくつかの特殊記号を用いることができます。
代表的なものは、そのグループの件数を表す.Nと、テーブル全体を表す.SDです。 このうち、.SDは引数.SDcolで抽出する列を指定することができます。
.SDの応用例についてはvignette “Using .SD for Data Analysis”にも解説があります。 また、他の特殊記号の例についてはhelp("special-symbols")を参照してください。
##.Nの使用例
dt[ ,.(count = .N), by = Species] Species count
<fctr> <int>
1: setosa 50
2: versicolor 50
3: virginica 50
##.SDの使用例
#行数, 列名リスト, 全要素の合計値
list(nrow(dt), paste(names(dt), collapse = ","), sum(sapply(dt, function(vec) if(is.numeric(vec)){sum(vec)}else{0})))[[1]]
[1] 150
[[2]]
[1] "Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species"
[[3]]
[1] 2078.7
#これをSpeciesのグループごとに行うイメージ
dt[ ,.(nrow = nrow(.SD),
colnames = paste(names(.SD), collapse=", "),
sum = sum(sapply(.SD, function(vec) if(is.numeric(vec)){sum(vec)}else{0}))), by = Species] Species nrow colnames sum
<fctr> <int> <char> <num>
1: setosa 50 Sepal.Length, Sepal.Width, Petal.Length, Petal.Width 507.1
2: versicolor 50 Sepal.Length, Sepal.Width, Petal.Length, Petal.Width 714.6
3: virginica 50 Sepal.Length, Sepal.Width, Petal.Length, Petal.Width 857.0
#.SDcolで指定した列だけを取り出し
dt[ ,.(nrow = nrow(.SD),
colnames = paste(names(.SD), collapse=", "),
sum = sum(sapply(.SD, function(vec) if(is.numeric(vec)){sum(vec)}else{0}))), by = Species, .SDcol = c("Sepal.Length")] Species nrow colnames sum
<fctr> <int> <char> <num>
1: setosa 50 Sepal.Length 250.3
2: versicolor 50 Sepal.Length 296.8
3: virginica 50 Sepal.Length 329.4
使用例:縦長への変形
さて、ここまでに説明した内容を活用して、以下のテーブルを使いやすい形に成形してみましょう。
このテーブルは特約ごとに列が分かれてしまっており、主契約と特約を合算した数値を集計したりする場合にやや使いづらい構造になってしまっています。
これを「縦長」に変形することで使いやすくしてみます。
dt_is 配当方式 商品種類コード 件数 特約1件数 特約2件数 主契約保険金額
<char> <int> <num> <num> <num> <num>
1: 有配 1 10 10 0 100
2: 有配 2 16 0 0 60
3: 準有配 3 48 24 0 240
4: 無配 4 176 110 0 69
5: 無配 5 190 30 14 1931
6: 無配 6 15 12 0 300
特約1保険金額 特約2保険金額
<num> <num>
1: 100 0
2: 0 0
3: 24 0
4: 59 0
5: 3140 156
6: 240 0
「縦長」に変形するには、例えば件数、特約1件数、特約2件数を「縦」に並べる必要があります。
すなわち以下のような操作をすることになります。
c(dt_is$件数, dt_is$特約1件数, dt_is$特約2件数) [1] 10 16 48 176 190 15 10 0 24 110 30 12 0 0 0 0 14 0
このベクトルを「件数」という列に格納して返却する式はこのようになります。
dt_is[, .(件数 = c(件数, 特約1件数, 特約2件数))] 件数
<num>
1: 10
2: 16
3: 48
4: 176
5: 190
6: 15
7: 10
8: 0
9: 24
10: 110
11: 30
12: 12
13: 0
14: 0
15: 0
16: 0
17: 14
18: 0
主契約や特約を区別できるように、新たに特約種類なる列を追加します。
もとのレコードの数と同じ長さのベクトルを3本用意する必要がありますが、ここで特殊記号.Nが活躍します。
ついでに保険金額の列も追加しておきます。
dt_is[, .(特約種類 = c(rep(0, .N), rep(1, .N), rep(2, .N)),
件数 = c(件数, 特約1件数, 特約2件数),
保険金額 = c(主契約保険金額, 特約1保険金額, 特約2保険金額))] 特約種類 件数 保険金額
<num> <num> <num>
1: 0 10 100
2: 0 16 60
3: 0 48 240
4: 0 176 69
5: 0 190 1931
6: 0 15 300
7: 1 10 100
8: 1 0 0
9: 1 24 24
10: 1 110 59
11: 1 30 3140
12: 1 12 240
13: 2 0 0
14: 2 0 0
15: 2 0 0
16: 2 0 0
17: 2 14 156
18: 2 0 0
グループ化の単位となる引数byにキー列を指定することで、これらを自然に補完することができます。
これで欲しい形式にテーブルを変換することができました。
dt_is[, .(特約種類 = c(rep(0, .N), rep(1, .N), rep(2, .N)),
件数 = c(件数, 特約1件数, 特約2件数),
保険金額 = c(主契約保険金額, 特約1保険金額, 特約2保険金額)),
by = .(配当方式, 商品種類コード)] 配当方式 商品種類コード 特約種類 件数 保険金額
<char> <int> <num> <num> <num>
1: 有配 1 0 10 100
2: 有配 1 1 10 100
3: 有配 1 2 0 0
4: 有配 2 0 16 60
5: 有配 2 1 0 0
6: 有配 2 2 0 0
7: 準有配 3 0 48 240
8: 準有配 3 1 24 24
9: 準有配 3 2 0 0
10: 無配 4 0 176 69
11: 無配 4 1 110 59
12: 無配 4 2 0 0
13: 無配 5 0 190 1931
14: 無配 5 1 30 3140
15: 無配 5 2 14 156
16: 無配 6 0 15 300
17: 無配 6 1 12 240
18: 無配 6 2 0 0
ちなみに、[,]は複数回つなげて記述することができます。 例えば主契約・特約を合算しつつ配当方式別に集計したテーブルはこのように作れます。
dt_is[, .(特約種類 = c(rep(0, .N), rep(1, .N), rep(2, .N)),
件数 = c(件数, 特約1件数, 特約2件数),
保険金額 = c(主契約保険金額, 特約1保険金額, 特約2保険金額)),
by = .(配当方式, 商品種類コード)][
,.(件数合計 = sum(件数), 保険金額合計 = sum(保険金額)), by = .(配当方式)] 配当方式 件数合計 保険金額合計
<char> <num> <num>
1: 有配 36 260
2: 準有配 72 264
3: 無配 547 5895
なお、変数[行, 列] 記法はSQL文との関連で次のように対応付けられることがあります。
SELECT aaa
FROM bbb
WHERE ccc
GROUP BY ddd
ORDER BY eee
↓
bbb[ccc/eee, aaa, by = ddd]
※uPDATE文の場合は .(xxx := aaa) の形
今回の例からも、変数[行, 列] 記法は単なる添え字にとどまらず、SQLクエリと同等あるいはそれ以上の自在性を持つことがわかります。
このようなdata.table独特の哲学については以下のvignettesにも解説があります。
NSEの問題
..について
前述のとおり変数[行, 列]記法は自在性が高い一方、その自在性を実現するために添え字の処理が通常とは異なっており、 例えば以下のように列名を変数に格納して列を抽出するようなコードがエラーとなってしまいます。
colname <- "Sepal.Length"
head(df[, colname]) #OK
head(dt[, colname]) #ERROR
#Error in `[.data.table`(dt, , colname) :
#j (the 2nd argument inside [...]) is a single symbol but column name 'colname' is not found. Perhaps you intended DT[, #..colname]. This difference to data.frame is deliberate and explained in FAQ 1.1.これは「列」部分のシンボルが1つであると処理が特殊なためで、エラー文のとおり..colnameとすれば解決できます。
colname <- "Sepal.Length"
head(dt[, ..colname]) Sepal.Length
<num>
1: 5.1
2: 4.9
3: 4.7
4: 4.6
5: 5.0
6: 5.4
シンボルが2個以上であればいいのでは?とばかりにcolnameの後ろに空文字列を連結するという意味のない処理を加えると…
head(dt[, paste0(colname, "")])[1] "Sepal.Length"
SQLでいえばSELECT 'Sepal.Length'を実行したかのような結果になってしまいます。
dt[, "Sepal.Length"]では期待通りSepal.Lengthの列を抽出できていたわけですが、 これは文字列だけが「列」の箇所に記述されたときの特殊仕様。 前述の添え字の動作原理を把握しているならば、むしろ上記の実行結果のほうが整合的です。
ともかく、添え字の自在性と引き換えに、 列名などを文字列で与えて処理を行うには工夫が必要になってしまっているということです。
[[ ]]の利用
より複雑な例として、以下のように既存の特徴量を2乗した特徴量を追加する操作について、 これを他の特徴量でも簡単に使いまわせるように関数化することを考えます。
dt_tmp <- copy(dt)
dt_tmp[,Sepal.Length_Squared := Sepal.Length ^ 2][,Sepal.Width_Squared := Sepal.Width ^ 2]
head(dt_tmp) Sepal.Length Sepal.Width Petal.Length Petal.Width Species
<num> <num> <num> <num> <fctr>
1: 5.1 3.5 1.4 0.2 setosa
2: 4.9 3.0 1.4 0.2 setosa
3: 4.7 3.2 1.3 0.2 setosa
4: 4.6 3.1 1.5 0.2 setosa
5: 5.0 3.6 1.4 0.2 setosa
6: 5.4 3.9 1.7 0.4 setosa
Sepal.Length_Squared Sepal.Width_Squared
<num> <num>
1: 26.01 12.25
2: 24.01 9.00
3: 22.09 10.24
4: 21.16 9.61
5: 25.00 12.96
6: 29.16 15.21
dt_tmp[, .(sum(Sepal.Length_Squared), sum(Sepal.Width_Squared)), by = Species] #後で実行結果を検証するためのもの Species V1 V2
<fctr> <num> <num>
1: setosa 1259.09 594.60
2: versicolor 1774.86 388.47
3: virginica 2189.90 447.33
この操作を、例えば add_square(dt_tmp, c("Sepal.Length","Sepal.Width")) のように呼び出せるようにしたいとしましょう。
これをシンプルに変数に置き換えた以下の形では残念ながらエラーとなってしまいます。
add_square <- function(dt_tmp, cols){
for(col in cols)
dt_tmp[,paste0(col, "_Squared") := col ^ 2]
}
dt_tmp <- copy(dt)
add_square(dt_tmp, c("Sepal.Length","Sepal.Width"))#ERROR
#Error in col^2 : non-numeric argument to binary operator一番シンプルな解決策は[[ ]]を用いることです。:=を使用することにこだわらないのであれば<-による代入でもよいでしょう。
add_square <- function(dt_tmp, cols){
for(col in cols)
dt_tmp[, paste0(col, "_Squared") := dt_tmp[[col]]^2 ] # := の左辺は文字列でもOK
}
dt_tmp <- copy(dt)
add_square(dt_tmp, c("Sepal.Length","Sepal.Width"))
dt_tmp[, .(sum(Sepal.Length_Squared), sum(Sepal.Width_Squared)), by = Species] Species V1 V2
<fctr> <num> <num>
1: setosa 1259.09 594.60
2: versicolor 1774.86 388.47
3: virginica 2189.90 447.33
.SDの利用
data.table特有の記法を用いた解決策もあります。 .SDは引数.SDcolで取り出す列を指定することができることを利用します。 簡潔な記述で複数列一度に処理できるためおすすめです。
add_square <- function(dt_tmp, cols){
dt_tmp[, paste0(cols, "_Squared") := (.SD ^ 2), .SDcol = cols]
}
dt_tmp <- copy(dt)
add_square(dt_tmp, c("Sepal.Length","Sepal.Width"))
dt_tmp[, .(sum(Sepal.Length_Squared), sum(Sepal.Width_Squared)), by = Species] Species V1 V2
<fctr> <num> <num>
1: setosa 1259.09 594.60
2: versicolor 1774.86 388.47
3: virginica 2189.90 447.33
このような例については以下のvignetteにも解説があります。
substituteの利用
この変数[行,列]記法のように、通常の評価とは異なる評価が行われることをNSE(Non Standard Evaluation)といい、R言語でしばしば現れる概念です。
R言語には遅延評価(lazy evaluation)という概念があり、 スクリプトに書かれた式はその値が必要になるタイミングまで評価(≒計算)されません。
たとえば関数の引数に式が書かれた場合、関数の処理に入る前に評価するのではなく、 関数の処理の中で必要となったタイミングで評価を行いますが、 NSEとはこの評価方法が特殊なもののことをいいます。 身近な例ではlmなどの引数に現れるy ~ .の形の式や、dplyrの記法などがあります。
data.table以外にもNSEが行われる場合はしばしば上記のような問題を抱えるため、 他のケースでも用いられる解決策も参考までに提示します。
add_square <- function(dt_tmp, cols){
for(col in cols){
eval(substitute(
dt_tmp[, var_col_Squared := var_col ^ 2],
env = list(var_col = as.name(col), var_col_Squared = paste0(col, "_Squared"))
))
#バージョン1.15以降では上記機能が[.data.tableの引数`env`に組み込まれました。
#https://rdatatable.gitlab.io/data.table/news/index.html#datatable-v1150-30-jan-2024
}
}
dt_tmp <- copy(dt)
add_square(dt_tmp, c("Sepal.Length","Sepal.Width"))
dt_tmp[, .(sum(Sepal.Length_Squared), sum(Sepal.Width_Squared)), by = Species] Species V1 V2
<fctr> <num> <num>
1: setosa 1259.09 594.60
2: versicolor 1774.86 388.47
3: virginica 2189.90 447.33
rlangパッケージの利用
このような状況ではrlangパッケージが使用されることも多いため、一例を示します。
add_square <- function(dt_tmp, cols){
for(col in cols){
col_sym <- rlang::sym(col) #文字列をシンボルに変換
rlang::inject(dt_tmp[, paste0(col, "_Squared") := (!!col_sym) ^ 2])
#rlang::injectで囲むと、!!col_symの部分がSepal.Lengthなどのシンボルに置換される
}
}
dt_tmp <- copy(dt)
add_square(dt_tmp, c("Sepal.Length","Sepal.Width"))
dt_tmp[, .(sum(Sepal.Length_Squared), sum(Sepal.Width_Squared)), by = Species] Species V1 V2
<fctr> <num> <num>
1: setosa 1259.09 594.60
2: versicolor 1774.86 388.47
3: virginica 2189.90 447.33