UPDATE: 2022-10-28 19:13:01

はじめに

data.tableパッケージを頻繁に使うわけではないが、利用するたびに文法を忘れるので、N回目のdata.tableパッケージ入門を行う。このページを検索すればdata.tableの文法というか使い方を検索できるように真面目にまとめておく。これを最後にできるように。

章立てはdata.table cheatsheetsを参考にdata.tableの使い方をまとめておく。dplyrライクにdata.tableを利用できるdtplyrパッケージもあるが、ここではdata.tableパッケージの使い方に焦点を当てる。

準備

必要なライブラリは下記の通り。

# remove.packages("data.table")
# install.packages("data.table")
library(data.table)
library(fasttime)
library(tidyverse)
library(lubridate)
library(fuzzyjoin)
library(nycflights13)

# packageVersion("data.table")
# [1] ‘1.13.2’

data.tableの文法

data.tableの基本的な文法は下記の通りで、iで行を操作し(SQLのWHERE)、jで列を操作し(SQLのSELECT)、byでグループ化を行う(SQLのGROUP BY)。data.tableを使用する際は[i,j,by]をすべて指定する必要はなく、必要なものを指定すれば良いが、ここではカンマ,は操作を明示するために記載する。

dt[i, j, by]

data.tableを作る

data.tableを作成する場合は、data.table()を利用する。

dt <- data.table::data.table(
  intger = c(1L, 2L, 3L, 4L, 5L),
  double = c(1.0, 2.0, 3.0, 4.0, 5.0),
  character = c("a", "b", "c", "d", "e"),
  logical = c(TRUE, FALSE, T, F, NA),
  datetime = c(
    "2019-12-31 14:00:00",
    "2020-12-31 15:00:00",
    "2020-12-31 16:00:00",
    "2020-12-31 17:00:00",
    "2020-12-31 18:00:00")
)

dt
   intger double character logical            datetime
1:      1      1         a    TRUE 2019-12-31 14:00:00
2:      2      2         b   FALSE 2020-12-31 15:00:00
3:      3      3         c    TRUE 2020-12-31 16:00:00
4:      4      4         d   FALSE 2020-12-31 17:00:00
5:      5      5         e      NA 2020-12-31 18:00:00

str(dt)
Classes ‘data.table’ and 'data.frame':  5 obs. of  5 variables:
 $ intger   : int  1 2 3 4 5
 $ double   : num  1 2 3 4 5
 $ character: chr  "a" "b" "c" "d" ...
 $ logical  : logi  TRUE FALSE TRUE FALSE NA
 $ datetime : chr  "2019-12-31 14:00:00" "2020-12-31 15:00:00" "2020-12-31 16:00:00" "2020-12-31 17:00:00" ...
 - attr(*, ".internal.selfref")=<externalptr> 

クラスがdata.tableとdata.frameになっているので、data.tableクラスに対応する関数がない場合、data.frameクラスの関数のメソッドが呼びだされ、利用できる。データの変換については、as.data.table()でdata.frameを変換できる。

data.table::as.data.table(iris)
     Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
  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
 ---                                                            
146:          6.7         3.0          5.2         2.3 virginica
147:          6.3         2.5          5.0         1.9 virginica
148:          6.5         3.0          5.2         2.0 virginica
149:          6.2         3.4          5.4         2.3 virginica
150:          5.9         3.0          5.1         1.8 virginica

data.table::setDT()を使うことで、代入せずとも型変換が可能。

iris2 <- iris
class(iris2)
[1] "data.frame"

data.table::setDT(iris2)
class(iris2)
[1] "data.table" "data.frame"

テーブルの内容を確認するための関数も利用できる。

head(dt, 3);tail(dt, 3)
   order_id      order_datetime user_id branch_num price product_id
1:        2 2003-12-20 09:10:59   53605          1   200      33120
2:        2 2003-12-20 09:10:59   53605          2   110      28985
3:        2 2003-12-20 09:10:59   53605          3   410       9327
   order_id      order_datetime user_id branch_num price product_id
1:  3421083 2002-01-25 07:38:57  842528          8   260       4600
2:  3421083 2002-01-25 07:38:57  842528          9   350      24852
3:  3421083 2002-01-25 07:38:57  842528         10   400       5020

nrow(dt);ncol(dt);dim(dt)
[1] 32434489
[1] 6
[1] 32434489        6

names(dt)
[1] "order_id"       "order_datetime" "user_id"        "branch_num"     "price"          "product_id" 

欠損値の確認

データに欠損値NAが含まれているかどうかは、anyNA()で確認できる。

# NAなし
dt_iris <- as.data.table(iris)

# NAあり
dt_airquality <- as.data.table(airquality)

anyNA(dt_iris)
[1] FALSE

anyNA(dt_airquality)
[1] TRUE

# NAを0で埋めてよい場合はいつもどおり
dt_airquality[is.na(dt_airquality)] <- 0
sum(is.na(dt_airquality))
[1] 0

読み込みと書き出し

読み込みと書き出しに対応する関数はfread()fwrite()fread()は他にも引数があるが、ここでは必要そうなもの例として記載しておく。data.tableには他にはないkeyという引数がある。これはデータベースのテーブルのインデックスに当たるもので、データを検索する場合に、検索速度を高める事ができるので、必要なカラムをインデックスとして指定しておくと、処理のスピードを向上させることができる。setkey(dt, key1,key2,...)と指定して、後から変更できる。

cols_dt <- c(
  order_id = "integer",
  order_datetime = "character", #日付型はないので文字型
  user_id = "integer",
  branch_num = "integer",
  price = "integer",
  product_id = "integer"
)

dt <- data.table::fread(
  file = "~/Desktop/orders_30m.csv",
  key = c("order_id", "order_datetime"),
  colClasses = cols_dt,
  header = TRUE,
  encoding = "UTF-8",
  sep = ",",
  na.strings = ",,",
  skip = 0
)

dt
          order_id      order_datetime user_id branch_num price product_id
       1:        2 2003-12-20 09:10:59   53605          1   200      33120
       2:        2 2003-12-20 09:10:59   53605          2   110      28985
       3:        2 2003-12-20 09:10:59   53605          3   410       9327
       4:        2 2003-12-20 09:10:59   53605          4   230      45918
       5:        2 2003-12-20 09:10:59   53605          5   330      30035
      ---                                                                 
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020

必要は列だけを読み込む場合はselect引数を利用する。

dt2 <- data.table::fread(
  file = "~/Desktop/orders_30m.csv",
  select = c("order_id", "order_datetime")
)

dt2
          order_id      order_datetime
       1:        2 2003-12-20 09:10:59
       2:        2 2003-12-20 09:10:59
       3:        2 2003-12-20 09:10:59
       4:        2 2003-12-20 09:10:59
       5:        2 2003-12-20 09:10:59
      ---                             
32434485:  3421083 2002-01-25 07:38:57
32434486:  3421083 2002-01-25 07:38:57
32434487:  3421083 2002-01-25 07:38:57
32434488:  3421083 2002-01-25 07:38:57
32434489:  3421083 2002-01-25 07:38:57

fwrite()にも多くの引数があるが、ここでは必要そうなもの例として記載しておく。

data.table::fwrite(
  x = dt,
  file = "~/Desktop/fwrite.csv",
  append = FALSE,
  quote = "auto", # surrounded by double quotes
  sep = ",",
  na = "", # NA is a blank string ""
  dec = ".", # decimal separator
  row.names = FALSE,
  col.names = TRUE,
  dateTimeAs = "ISO"
)

速度の比較(簡易版)

簡易的ではあるが、fread()がどの程度速いのか確認しておく。読み込むデータは100万行100列のCSVデータ。

set.seed(1989)
mat <- data.frame(matrix(runif(1e7), nrow=1e8))
dim(mat)
[1] 100000000         1

fwrite(mat, "~/Desktop/mat.csv", row.names = FALSE)

読み込み速度を検証する。read.csv()よりも10倍、read_csv()より5倍くらい速くなった。時間あるときにでもbench::mark()でやってみよう。

system.time(read.csv("~/Desktop/mat.csv"))
   ユーザ   システム       経過  
   108.951      5.395    116.763 

system.time(readr::read_csv("~/Desktop/mat.csv"))
   ユーザ   システム       経過  
    40.533      5.944     59.838 

system.time(data.table::fread("~/Desktop/mat.csv"))
   ユーザ   システム       経過  
     4.474      2.602     12.534 

日付型(POSIXct)に変換する

data.tableでは、日付型として読み込むことができず、文字型として読み込まれるので、fasttimeパッケージのfastPOSIXct()で日付に変換する。注意せずにそのままfastPOSIXct()を利用し、タイムゾーンを考慮する必要があるような分析を行う場合、場合によっては痛い目にあうかもしれない。

dt <- data.table::data.table(
  datetime = c(
    "2019-12-31 14:00:00",
    "2020-12-31 15:00:00",
    "2020-12-31 16:00:00",
    "2020-12-31 17:00:00",
    "2020-12-31 18:00:00")
)

引数tzをつけない場合、Sys.timezone()がデフォルトで利用されるので、日本時間で2020-12-31 15:00:00としたい場合、勝手にGMTからAsia/Tokyoへの変換が行われるので、2020-12-31 15:00:00は+9時間の2021-01-01 00:00:00となってしまう。3列目は明示的に引数を設定した場合だが、2列目のデフォルト設定と同じである。そのため、日本時間2020-12-31 15:00:00としたければ、そこから9時間戻す必要がある。5列目は引数にGMTをつけた場合、2020-12-31 15:00:002020-12-31 15:00:00と変換されている。

# デフォルトのSys.timezone()が使用されて、tz = "Asia/Tokyo"
dt[, Default := fasttime::fastPOSIXct(datetime),]
dt[, Tokyo := fasttime::fastPOSIXct(datetime, tz = "Asia/Tokyo"),]
dt[, Coordinated_Tokyo := fasttime::fastPOSIXct(datetime, tz = "Asia/Tokyo") - lubridate::dhours(9),]
dt[, GMT := fasttime::fastPOSIXct(datetime, tz = "GMT"),]
dt
              datetime             Default               Tokyo   Coordinated_Tokyo                 GMT
1: 2019-12-31 14:00:00 2019-12-31 23:00:00 2019-12-31 23:00:00 2019-12-31 14:00:00 2019-12-31 14:00:00
2: 2020-12-31 15:00:00 2021-01-01 00:00:00 2021-01-01 00:00:00 2020-12-31 15:00:00 2020-12-31 15:00:00
3: 2020-12-31 16:00:00 2021-01-01 01:00:00 2021-01-01 01:00:00 2020-12-31 16:00:00 2020-12-31 16:00:00
4: 2020-12-31 17:00:00 2021-01-01 02:00:00 2021-01-01 02:00:00 2020-12-31 17:00:00 2020-12-31 17:00:00
5: 2020-12-31 18:00:00 2021-01-01 03:00:00 2021-01-01 03:00:00 2020-12-31 18:00:00 2020-12-31 18:00:00

タイムゾーンの確認もしておく。

attributes(dt$default)
NULL

attributes(dt$Tokyo)
$class
[1] "POSIXct" "POSIXt" 

$tzone
[1] "Asia/Tokyo"

attributes(dt$Coordinated_Tokyo)
$tzone
[1] "Asia/Tokyo"

$class
[1] "POSIXct" "POSIXt" 

attributes(dt$GMT)
$class
[1] "POSIXct" "POSIXt" 

$tzone
[1] "GMT"

ちなみに、Dockerでrockerイメージを使ってRstudioServerを起動すると、タイムゾーンはデフォルトでEtc/UTCなので、日本時間2020-12-31 15:00:00だと思って文字列をデフォルト設定で変換しても2020-12-31 15:00:00となる。実行環境がこのように変わる場合、デフォルトの挙動が変わるので、注意が必要ですね(完全に自戒)。

# Sys.timezone()
# [1] "Etc/UTC"

dt[, Default := fasttime::fastPOSIXct(datetime),]
dt[, Tokyo := fasttime::fastPOSIXct(datetime, tz = "Asia/Tokyo"),]
dt[, Coordinated_Tokyo := fasttime::fastPOSIXct(datetime, tz = "Asia/Tokyo") - lubridate::dhours(9),]
dt[, GMT := fasttime::fastPOSIXct(datetime, tz = "GMT"),]
dt

              datetime             Default               Tokyo   Coordinated_Tokyo                 GMT
1: 2019-12-31 14:00:00 2019-12-31 14:00:00 2019-12-31 23:00:00 2019-12-31 14:00:00 2019-12-31 14:00:00
2: 2020-12-31 15:00:00 2020-12-31 15:00:00 2021-01-01 00:00:00 2020-12-31 15:00:00 2020-12-31 15:00:00
3: 2020-12-31 16:00:00 2020-12-31 16:00:00 2021-01-01 01:00:00 2020-12-31 16:00:00 2020-12-31 16:00:00
4: 2020-12-31 17:00:00 2020-12-31 17:00:00 2021-01-01 02:00:00 2020-12-31 17:00:00 2020-12-31 17:00:00
5: 2020-12-31 18:00:00 2020-12-31 18:00:00 2021-01-01 03:00:00 2020-12-31 18:00:00 2020-12-31 18:00:00

行の操作

数値を指定して行を操作

iに数値を指定することで行を操作できる。

dt[1:5, , ]
   order_id      order_datetime user_id branch_num price product_id
1:        2 2003-12-20 09:10:59   53605          1   200      33120
2:        2 2003-12-20 09:10:59   53605          2   110      28985
3:        2 2003-12-20 09:10:59   53605          3   410       9327
4:        2 2003-12-20 09:10:59   53605          4   230      45918
5:        2 2003-12-20 09:10:59   53605          5   330      30035

dt[seq(0,100000,30000)*5, , ]
   order_id      order_datetime user_id branch_num price product_id
1:    15807 2005-11-11 20:16:48   20286          7   410      31769
2:    31628 2001-10-16 05:09:42  906869          9   420      26139
3:    47638 2004-08-21 01:48:01   12774          3   290      29820

比較演算子を指定して行を操作

==><>=<=is.na()!is.na()%in%%like%%between%などの比較演算子を利用して行を操作できる。

dt[order_id == 3421080, , ]
   order_id      order_datetime user_id branch_num price product_id
1:  3421080 2004-06-22 23:21:36  911407          1   240      27845
2:  3421080 2004-06-22 23:21:36  911407          2   360       4932
3:  3421080 2004-06-22 23:21:36  911407          3   250      18811
4:  3421080 2004-06-22 23:21:36  911407          4   320      41950
5:  3421080 2004-06-22 23:21:36  911407          5   400      31717
6:  3421080 2004-06-22 23:21:36  911407          6   300      12935
7:  3421080 2004-06-22 23:21:36  911407          7   150      25122
8:  3421080 2004-06-22 23:21:36  911407          8   510      10667
9:  3421080 2004-06-22 23:21:36  911407          9   450      38061

dt[order_id > 3421081, , ]
    order_id      order_datetime user_id branch_num price product_id
 1:  3421082 2002-06-30 19:21:21  362401          1   450      17279
 2:  3421082 2002-06-30 19:21:21  362401          2   320      12738
 3:  3421082 2002-06-30 19:21:21  362401          3   450      16797
【長いので略】
15:  3421083 2002-01-25 07:38:57  842528          8   260       4600
16:  3421083 2002-01-25 07:38:57  842528          9   350      24852
17:  3421083 2002-01-25 07:38:57  842528         10   400       5020

複数の比較演算子を指定して行を操作

and条件の&または、or条件の|で比較演算子をつなげればよい。

dt[order_id == 3421083 & branch_num == 10, , ]
   order_id      order_datetime user_id branch_num price product_id
1:  3421083 2002-01-25 07:38:57  842528         10   400       5020

dt[order_id >= 3421080 & branch_num == 5, , ]
   order_id      order_datetime user_id branch_num price product_id
1:  3421080 2004-06-22 23:21:36  911407          5   400      31717
2:  3421081 2000-09-25 14:36:25  185767          5   200      20539
3:  3421082 2002-06-30 19:21:21  362401          5   430      32700
4:  3421083 2002-01-25 07:38:57  842528          5    50      35211

%付き演算子で行を操作

あいまい検索(like)を行う場合は%like%を利用する。

flights2 <- data.table::copy(flights)
data.table::setDT(flights2)
flights2[tailnum %like% "N1110"]

flights2[tailnum %like% "N1110", "tailnum", ]
     tailnum
  1:  N11107
  2:  N11107
  3:  N11107
  4:  N11106
  5:  N11106
 ---        
421:  N11109
422:  N11106
423:  N11109
424:  N11109
425:  N11109

否定演算子と組み合わせることもできる。%nin%はデフォルトではないので、!%in%を組み合わせれば同じことができる。

flights2[!tailnum %like% "N11", "tailnum", ]
        tailnum
     1:  N14228
     2:  N24211
     3:  N619AA
     4:  N804JB
     5:  N668DN
    ---        
331178:    <NA>
331179:    <NA>
331180:  N535MQ
331181:  N511MQ
331182:  N839MQ

ちなみにカラム名をダブルクオーテーションで囲まなければ、1列の指定の場合ベクトルで取り出せる。

flights2[tailnum %like% "N1110", tailnum, ][1:10]
 [1] "N11107" "N11107" "N11107" "N11106" "N11106" "N11109" "N11109" "N11107" "N11107" "N11107"

%between%で区間を指定する場合。ここでは、“2002-01-25 07:38:55”から”2002-01-25 07:38:59”までの4秒間で発生したトランザクションを検索する。

# 日付型に変換
dt[, order_datetime := fasttime::fastPOSIXct(order_datetime, tz = "Asia/Tokyo") - lubridate::dhours(9),]

# dt["2002-01-25 07:38:59" >= order_datetime & order_datetime >= "2002-01-25 07:38:55", ,] と同じ
dt[order_datetime %between% c("2002-01-25 07:38:55", "2002-01-25 07:38:59"), ,]
    order_id      order_datetime user_id branch_num price product_id
 1:  3421083 2002-01-25 07:38:57  842528          1   330       7854
 2:  3421083 2002-01-25 07:38:57  842528          2   490      45309
 3:  3421083 2002-01-25 07:38:57  842528          3   480      21162
 4:  3421083 2002-01-25 07:38:57  842528          4   360      18176
 5:  3421083 2002-01-25 07:38:57  842528          5    50      35211
 6:  3421083 2002-01-25 07:38:57  842528          6   360      39678
 7:  3421083 2002-01-25 07:38:57  842528          7   240      11352
 8:  3421083 2002-01-25 07:38:57  842528          8   260       4600
 9:  3421083 2002-01-25 07:38:57  842528          9   350      24852
10:  3421083 2002-01-25 07:38:57  842528         10   400       5020

dt[order_id %between% c(700, 780), ,]
     order_id      order_datetime user_id branch_num price product_id
  1:      700 2003-07-01 13:01:33  478908          1   260      21903
  2:      700 2003-07-01 13:01:33  478908          2   280       8571
  3:      700 2003-07-01 13:01:33  478908          3   380      46149
  4:      700 2003-07-01 13:01:33  478908          4   260        196
  5:      700 2003-07-01 13:01:33  478908          5   220      14715
 ---                                                                 
792:      778 2005-12-31 23:57:25   35254         17   260      43789
793:      779 2006-05-14 16:27:44  810647          1   450      16797
794:      779 2006-05-14 16:27:44  810647          2   310      31915
795:      780 2006-07-15 15:58:22  354652          1   290      19348
796:      780 2006-07-15 15:58:22  354652          2   300      14905

基本的には、%付き演算子のほうが検索速度が速い。これも要検証。

system.time(dt[order_id %between% c(1, 780), ,])
   ユーザ   システム       経過  
     0.094      0.001      0.094 

system.time(dt[780 >= order_id & order_id >= 1, ,])
   ユーザ   システム       経過  
     0.195      0.001      0.199 

行の並び替え

行の並び替えを行う場合はsetorder()を利用する。デフォルトは昇順の並び替えになる。

dt[1:10, , ]
    order_id      order_datetime user_id branch_num price product_id
 1:        2 2003-12-20 09:10:59   53605          1   200      33120
 2:        2 2003-12-20 09:10:59   53605          2   110      28985
 3:        2 2003-12-20 09:10:59   53605          3   410       9327
 4:        2 2003-12-20 09:10:59   53605          4   230      45918
 5:        2 2003-12-20 09:10:59   53605          5   330      30035
 6:        2 2003-12-20 09:10:59   53605          6   120      17794
 7:        2 2003-12-20 09:10:59   53605          7   410      40141
 8:        2 2003-12-20 09:10:59   53605          8   460       1819
 9:        2 2003-12-20 09:10:59   53605          9   320      43668
10:        3 2006-12-29 09:21:27  561254          1   400      33754

data.table::setorder(dt, order_datetime)
dt[1:10, , ]
    order_id      order_datetime user_id branch_num price product_id
 1:  1140762 2000-01-01 00:00:41  906591          1   230      40486
 2:  1140762 2000-01-01 00:00:41  906591          2   300      36810
 3:  1140762 2000-01-01 00:00:41  906591          3   430      15945
 4:  1140762 2000-01-01 00:00:41  906591          4   280       8424
 5:  1140762 2000-01-01 00:00:41  906591          5   380      40586
 6:  1140762 2000-01-01 00:00:41  906591          6   380      48364
 7:  1140762 2000-01-01 00:00:41  906591          7   240      15100
 8:  1948161 2000-01-01 00:01:20  955985          1   460      34209
 9:  1948161 2000-01-01 00:01:20  955985          2   210      23362
10:  1948161 2000-01-01 00:01:20  955985          3   230      13712

複数のカラムを基準に並び替える際は、複数指定し、降順にする場合は-を付与する。

setorder(dt, order_datetime, -branch_num)
dt[1:10, , ]
    order_id      order_datetime user_id branch_num price product_id
 1:  1140762 2000-01-01 00:00:41  906591          7   240      15100
 2:  1140762 2000-01-01 00:00:41  906591          6   380      48364
 3:  1140762 2000-01-01 00:00:41  906591          5   380      40586
 4:  1140762 2000-01-01 00:00:41  906591          4   280       8424
 5:  1140762 2000-01-01 00:00:41  906591          3   430      15945
 6:  1140762 2000-01-01 00:00:41  906591          2   300      36810
 7:  1140762 2000-01-01 00:00:41  906591          1   230      40486
 8:  1948161 2000-01-01 00:01:20  955985         11   250      40604
 9:  1948161 2000-01-01 00:01:20  955985         10   320       1090
10:  1948161 2000-01-01 00:01:20  955985          9   200      28050

# もとの並び順に戻す
setorder(dt, order_id, branch_num)

列の操作

位置の番号で列を操作

数値で列を操作

jに数値を指定することで列を操作できる。

dt[, c(1, 2, 3), ]
          order_id      order_datetime user_id
       1:        2 2003-12-20 09:10:59   53605
       2:        2 2003-12-20 09:10:59   53605
       3:        2 2003-12-20 09:10:59   53605
       4:        2 2003-12-20 09:10:59   53605
       5:        2 2003-12-20 09:10:59   53605
      ---                                     
32434485:  3421083 2002-01-25 07:38:57  842528
32434486:  3421083 2002-01-25 07:38:57  842528
32434487:  3421083 2002-01-25 07:38:57  842528
32434488:  3421083 2002-01-25 07:38:57  842528
32434489:  3421083 2002-01-25 07:38:57  842528

文字で列を操作

jに文字を指定することで列を操作できる。

dt[, c("price", "product_id"), ]
          price product_id
       1:   200      33120
       2:   110      28985
       3:   410       9327
       4:   230      45918
       5:   330      30035
      ---                 
32434485:   360      39678
32434486:   240      11352
32434487:   260       4600
32434488:   350      24852
32434489:   400       5020

他にもlist.を使う方法がある。

dt[, list(price, product_id), ]
          price product_id
       1:   200      33120
       2:   110      28985
       3:   410       9327
       4:   230      45918
       5:   330      30035
      ---                 
32434485:   360      39678
32434486:   240      11352
32434487:   260       4600
32434488:   350      24852
32434489:   400       5020

dt[, .(price, product_id), ]
          price product_id
       1:   200      33120
       2:   110      28985
       3:   410       9327
       4:   230      45918
       5:   330      30035
      ---                 
32434485:   360      39678
32434486:   240      11352
32434487:   260       4600
32434488:   350      24852
32434489:   400       5020

余事象で列を操作

カラム数が多く、特定の列だけを除きたい場合は!を利用する。もちろん、さきに除外したいカラムをdrop <- c("x", "y", "z")として、dt[, , !drop, ]としても同じ。

dt[, !c("order_id"), ]
               order_datetime user_id branch_num price product_id
       1: 2003-12-20 09:10:59   53605          1   200      33120
       2: 2003-12-20 09:10:59   53605          2   110      28985
       3: 2003-12-20 09:10:59   53605          3   410       9327
       4: 2003-12-20 09:10:59   53605          4   230      45918
       5: 2003-12-20 09:10:59   53605          5   330      30035
      ---                                                        
32434485: 2002-01-25 07:38:57  842528          6   360      39678
32434486: 2002-01-25 07:38:57  842528          7   240      11352
32434487: 2002-01-25 07:38:57  842528          8   260       4600
32434488: 2002-01-25 07:38:57  842528          9   350      24852
32434489: 2002-01-25 07:38:57  842528         10   400       5020

dt[, !c("order_id", "order_datetime"), ]
          user_id branch_num price product_id
       1:   53605          1   200      33120
       2:   53605          2   110      28985
       3:   53605          3   410       9327
       4:   53605          4   230      45918
       5:   53605          5   330      30035
      ---                                    
32434485:  842528          6   360      39678
32434486:  842528          7   240      11352
32434487:  842528          8   260       4600
32434488:  842528          9   350      24852
32434489:  842528         10   400       5020

ヘルパー関数で列の操作

is.*系のヘルパー関数で列の操作を行う場合は、which()sapply()を組み合わせて利用する。

# POSIXctだけ
dt[, .SD, .SDcols = which(sapply(dt, is.POSIXct))]
               order_datetime
       1: 2003-12-20 09:10:59
       2: 2003-12-20 09:10:59
       3: 2003-12-20 09:10:59
       4: 2003-12-20 09:10:59
       5: 2003-12-20 09:10:59
      ---                    
32434485: 2002-01-25 07:38:57
32434486: 2002-01-25 07:38:57
32434487: 2002-01-25 07:38:57
32434488: 2002-01-25 07:38:57
32434489: 2002-01-25 07:38:57

# POSIXct以外
dt[, .SD, .SDcols = !which(sapply(dt, is.POSIXct))]
          order_id user_id branch_num price product_id
       1:        2   53605          1   200      33120
       2:        2   53605          2   110      28985
       3:        2   53605          3   410       9327
       4:        2   53605          4   230      45918
       5:        2   53605          5   330      30035
      ---                                             
32434485:  3421083  842528          6   360      39678
32434486:  3421083  842528          7   240      11352
32434487:  3421083  842528          8   260       4600
32434488:  3421083  842528          9   350      24852
32434489:  3421083  842528         10   400       5020

正規表現で列を操作

正規表現で列を操作する場合、%like%grep()が利用できる。withは位置だけでその列を取得したい場合、with=FALSEという引数を追加する必要がある。

dt[,names(dt) %like% "order", with=FALSE]
          order_id      order_datetime
       1:        2 2003-12-20 09:10:59
       2:        2 2003-12-20 09:10:59
       3:        2 2003-12-20 09:10:59
       4:        2 2003-12-20 09:10:59
       5:        2 2003-12-20 09:10:59
      ---                             
32434485:  3421083 2002-01-25 07:38:57
32434486:  3421083 2002-01-25 07:38:57
32434487:  3421083 2002-01-25 07:38:57
32434488:  3421083 2002-01-25 07:38:57
32434489:  3421083 2002-01-25 07:38:57

dt[, grep("^order", names(dt)), with = FALSE]
          order_id      order_datetime
       1:        2 2003-12-20 09:10:59
       2:        2 2003-12-20 09:10:59
       3:        2 2003-12-20 09:10:59
       4:        2 2003-12-20 09:10:59
       5:        2 2003-12-20 09:10:59
      ---                             
32434485:  3421083 2002-01-25 07:38:57
32434486:  3421083 2002-01-25 07:38:57
32434487:  3421083 2002-01-25 07:38:57
32434488:  3421083 2002-01-25 07:38:57
32434489:  3421083 2002-01-25 07:38:57

列名を変更

列名を変更する場合は、setnames()を利用しベクトルを渡すことで名前を変更できる。

data.table::setnames(dt, c("order_id", "user_id"), c("ORDER_ID", "USER_ID"))
dt
          ORDER_ID      order_datetime USER_ID branch_num price product_id
       1:        2 2003-12-20 09:10:59   53605          1   200      33120
       2:        2 2003-12-20 09:10:59   53605          2   110      28985
       3:        2 2003-12-20 09:10:59   53605          3   410       9327
       4:        2 2003-12-20 09:10:59   53605          4   230      45918
       5:        2 2003-12-20 09:10:59   53605          5   330      30035
      ---                                                                 
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020

# 上書きされるのでもとに戻す
data.table::setnames(dt, c("ORDER_ID", "USER_ID"), c("order_id", "user_id"))

ベクトルを渡せば良いので、全カラムを大文字、小文字に変換もできる。

data.table::setnames(dt, names(dt), stringr::str_to_upper(names(dt)))
dt
          ORDER_ID      ORDER_DATETIME USER_ID BRANCH_NUM PRICE PRODUCT_ID
       1:        2 2003-12-20 09:10:59   53605          1   200      33120
       2:        2 2003-12-20 09:10:59   53605          2   110      28985
       3:        2 2003-12-20 09:10:59   53605          3   410       9327
       4:        2 2003-12-20 09:10:59   53605          4   230      45918
       5:        2 2003-12-20 09:10:59   53605          5   330      30035
      ---                                                                 
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020

# もとに戻す
setnames(dt, names(dt), stringr::str_to_lower(names(dt)))

列を集計

sum()mean()sd()var()median()min()max()などの集計関数を利用する場合は下記のように記述する。チートシートにあるようにdt[, .(name = sum(x)), ]という書き方が一般的の模様。

dt[, sum(price), ]
[1] 9912645520

dt[, sum_price = sum(price), ]
 `[.data.table`(dt, , sum_price = sum(price), ) でエラー: 
   使われていない引数 (sum_price = sum(price)) 

dt[, .(sum_price = sum(price)), ]
    sum_price
1: 9912645520

複数の集計を同時に行う場合は、そのまま.()内に記述すればよい。

dt[, .(
  mean = mean(price, na.rm = TRUE),
  median = median(price, na.rm = TRUE),
  min = min(price, na.rm = TRUE),
  max = max(price, na.rm = TRUE)
)]
       mean median min max
1: 305.6205    300   0 780

重複を削除する場合は、unique()を利用する。ここでは、flights2データのc("year", "month", "day")のユニークな行にしている。そのため、1日ごとのデータに変わっている。また、ユニークな組み合わせの数を求める際はuniqueN()を利用する。

# 特に計算に意味はない
unique(flights2, by = c("year", "month", "day"))
     year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum origin dest air_time distance hour minute           time_hour
  1: 2013     1   1      517            515         2      830            819        11      UA   1545  N14228    EWR  IAH      227     1400    5     15 2013-01-01 05:00:00
  2: 2013     1   2       42           2359        43      518            442        36      B6    707  N580JB    JFK  SJU      189     1598   23     59 2013-01-02 23:00:00
  3: 2013     1   3       32           2359        33      504            442        22      B6    707  N763JB    JFK  SJU      193     1598   23     59 2013-01-03 23:00:00
  4: 2013     1   4       25           2359        26      505            442        23      B6    707  N554JB    JFK  SJU      194     1598   23     59 2013-01-04 23:00:00
  5: 2013     1   5       14           2359        15      503            445        18      B6    739  N592JB    JFK  PSE      201     1617   23     59 2013-01-05 23:00:00
 ---                                                                                                                                                                        
361: 2013     9  26      451            500        -9      619            648       -29      US   1877  N155UW    EWR  CLT       72      529    5      0 2013-09-26 05:00:00
362: 2013     9  27      453            500        -7      627            648       -21      US   1877  N161UW    EWR  CLT       76      529    5      0 2013-09-27 05:00:00
363: 2013     9  28      451            500        -9      627            648       -21      US   1877  N181UW    EWR  CLT       77      529    5      0 2013-09-28 05:00:00
364: 2013     9  29      521            520         1      739            800       -21      UA    322  N564UA    EWR  IAH      179     1400    5     20 2013-09-29 05:00:00
365: 2013     9  30      453            500        -7      621            648       -27      US   1877  N186US    EWR  CLT       76      529    5      0 2013-09-30 05:00:00

uniqueN(flights2, by = c("year", "month", "day"))
[1] 365

組み合わせ集計

この表現が正しいかはさておき、データの組み合わせを集計できるrollup()cube()という便利関数がある。複数の合計を生成するグループ化の様々なレベルでの集計を計算してくれる便利な関数。SQLでいうところのGROUPING SETS。

dt_roll_cube <- data.table::data.table(
    c1 = c("A", "B", "A", "B", "A", "B", "B"),
    c2 = c("a", "b", "a", "a", "b", "c", "c"),
    x = 1:7)
setorder(dt_roll_cube, c1, c2)

dt_roll_cube
   c1 c2 x
1:  A  a 1
2:  A  a 3
3:  A  b 5
4:  B  a 4
5:  B  b 2
6:  B  c 6
7:  B  c 7

このデータをもとにrollup()cube()を使うことで、A-aA-bB-aB-bB-cA-NAB-NANA-aNA-bNA-bNA-NAという組み合わせで集計できる。cube()のほうがより多くの組み合わせを返す。グループ化計算でも似たようなことができるが、それはあくまでグループの組み合わせ数しか計算できない。

data.table::rollup(dt_roll_cube, sum(x), by = c("c1", "c2"), id = TRUE)
   grouping   c1   c2 V1
1:        0    A    a  4
2:        0    A    b  5
3:        0    B    a  4
4:        0    B    b  2
5:        0    B    c 13
6:        1    A <NA>  9
7:        1    B <NA> 19
8:        3 <NA> <NA> 28

data.table::cube(dt_roll_cube,   sum(x), by = c("c1", "c2"), id = TRUE)
    grouping   c1   c2 V1
 1:        0    A    a  4
 2:        0    A    b  5
 3:        0    B    a  4
 4:        0    B    b  2
 5:        0    B    c 13
 6:        1    A <NA>  9
 7:        1    B <NA> 19
 8:        2 <NA>    a  8
 9:        2 <NA>    b  7
10:        2 <NA>    c 13
11:        3 <NA> <NA> 28

列の計算

dplyrパッケージのmutate()のように列の計算を行う場合はj:=セイウチ演算子を利用し、計算式を記述する。代入しなくても既存のdata.tableが上書きされる。

dt[, price_with_tax := price * 1.08,]
dt
          order_id      order_datetime user_id branch_num price product_id price_with_tax
       1:        2 2003-12-20 09:10:59   53605          1   200      33120          216.0
       2:        2 2003-12-20 09:10:59   53605          2   110      28985          118.8
       3:        2 2003-12-20 09:10:59   53605          3   410       9327          442.8
       4:        2 2003-12-20 09:10:59   53605          4   230      45918          248.4
       5:        2 2003-12-20 09:10:59   53605          5   330      30035          356.4
      ---                                                                                
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678          388.8
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352          259.2
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600          280.8
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852          378.0
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020          432.0

ちなみに、列の計算を新しいオブジェクトにだけ適用しようとしても、既存のテーブルも更新されるので注意。set*:=はメモリアドレスをそのまま利用するため。

d <- dt[, price_with_tax := price * 1.08,]
identical(d,dt)
[1] TRUE

条件付きの列の計算

data.tableでは条件をつけて列の計算がシンプルに記述できる。例えばbranch_numが3より小さければ、priceには0をかける、という条件付きの計算ができる。もちろん、if文を使って同じこともできる。

dt[branch_num < 3, price_with_tax := price * 0,] 
dt
          order_id      order_datetime user_id branch_num price product_id price_with_tax
       1:        2 2003-12-20 09:10:59   53605          1   200      33120            0.0
       2:        2 2003-12-20 09:10:59   53605          2   110      28985            0.0
       3:        2 2003-12-20 09:10:59   53605          3   410       9327          442.8
       4:        2 2003-12-20 09:10:59   53605          4   230      45918          248.4
       5:        2 2003-12-20 09:10:59   53605          5   330      30035          356.4
      ---                                                                                
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678          388.8
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352          259.2
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600          280.8
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852          378.0
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020          432.0

# 4より小さい場合にして違いがわかるようにする
dt[, price_with_tax := ifelse(branch_num < 4, price * 0, price),] 
dt
          order_id      order_datetime user_id branch_num price product_id price_with_tax
       1:        2 2003-12-20 09:10:59   53605          1   200      33120              0
       2:        2 2003-12-20 09:10:59   53605          2   110      28985              0
       3:        2 2003-12-20 09:10:59   53605          3   410       9327              0
       4:        2 2003-12-20 09:10:59   53605          4   230      45918            230
       5:        2 2003-12-20 09:10:59   53605          5   330      30035            330
      ---                                                                                
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678            360
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352            240
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600            260
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852            350
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020            400

列を削除

列を削除する場合は、カラム名に対してNULLを代入することで、指定したカラムをドロップできる。

dt[, price_with_tax := NULL, ]
dt
          order_id      order_datetime user_id branch_num price product_id
       1:        2 2003-12-20 09:10:59   53605          1   200      33120
       2:        2 2003-12-20 09:10:59   53605          2   110      28985
       3:        2 2003-12-20 09:10:59   53605          3   410       9327
       4:        2 2003-12-20 09:10:59   53605          4   230      45918
       5:        2 2003-12-20 09:10:59   53605          5   330      30035
      ---                                                                 
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020

複数列の計算

複数列の計算を行う場合は:=をバックティックで囲んで、複数の式を記述する。

dt[, `:=`(price10 = price * 10, price100 =  price * 100),]
dt
          order_id      order_datetime user_id branch_num price product_id price10 price100
       1:        2 2003-12-20 09:10:59   53605          1   200      33120    2000    20000
       2:        2 2003-12-20 09:10:59   53605          2   110      28985    1100    11000
       3:        2 2003-12-20 09:10:59   53605          3   410       9327    4100    41000
       4:        2 2003-12-20 09:10:59   53605          4   230      45918    2300    23000
       5:        2 2003-12-20 09:10:59   53605          5   330      30035    3300    33000
      ---                                                                                  
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678    3600    36000
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352    2400    24000
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600    2600    26000
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852    3500    35000
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020    4000    40000

複数の列計算を行う場合は条件付き計算ができないので注意。

dt[branch_num < 3, `:=`(price10 = price * 10, price100 =  price * 100),]
dt
          order_id      order_datetime user_id branch_num price product_id price10 price100
       1:        2 2003-12-20 09:10:59   53605          1   200      33120    2000    20000
       2:        2 2003-12-20 09:10:59   53605          2   110      28985    1100    11000
       3:        2 2003-12-20 09:10:59   53605          3   410       9327    4100    41000
       4:        2 2003-12-20 09:10:59   53605          4   230      45918    2300    23000
       5:        2 2003-12-20 09:10:59   53605          5   330      30035    3300    33000
      ---                                                                                  
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678    3600    36000
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352    2400    24000
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600    2600    26000
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852    3500    35000
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020    4000    40000

列のデータ型を変換

列の計算と同じように型変換関数を利用すればよい。ここでは小数点型のprice_with_taxを整数型に変換する。これによって、小数点が丸まり、データ型が変換されていることがわかる。

dt[, price_with_tax := price * 1.08,]
dt
          order_id      order_datetime user_id branch_num price product_id price_with_tax
       1:        2 2003-12-20 09:10:59   53605          1   200      33120          216.0
       2:        2 2003-12-20 09:10:59   53605          2   110      28985          118.8
       3:        2 2003-12-20 09:10:59   53605          3   410       9327          442.8
       4:        2 2003-12-20 09:10:59   53605          4   230      45918          248.4
       5:        2 2003-12-20 09:10:59   53605          5   330      30035          356.4
      ---                                                                                
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678          388.8
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352          259.2
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600          280.8
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852          378.0
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020          432.0

dt[, price_with_tax := as.integer(price_with_tax)]
dt
          order_id      order_datetime user_id branch_num price product_id price_with_tax
       1:        2 2003-12-20 09:10:59   53605          1   200      33120            216
       2:        2 2003-12-20 09:10:59   53605          2   110      28985            118
       3:        2 2003-12-20 09:10:59   53605          3   410       9327            442
       4:        2 2003-12-20 09:10:59   53605          4   230      45918            248
       5:        2 2003-12-20 09:10:59   53605          5   330      30035            356
      ---                                                                                
32434485:  3421083 2002-01-25 07:38:57  842528          6   360      39678            388
32434486:  3421083 2002-01-25 07:38:57  842528          7   240      11352            259
32434487:  3421083 2002-01-25 07:38:57  842528          8   260       4600            280
32434488:  3421083 2002-01-25 07:38:57  842528          9   350      24852            378
32434489:  3421083 2002-01-25 07:38:57  842528         10   400       5020            432

# もとに戻す
dt[, price_with_tax := NULL, ]

グループ化の計算

グループ化計算は、byにカラムを指定することでグループ化されるので、それにより行う。まずは、このトランザクションデータに年order_yaerと月order_monthを追加する。

dt[, order_yaer := lubridate::year(order_datetime),]
dt[, order_month := lubridate::month(order_datetime),]
data.table::setkey(dt, "order_yaer", "order_month")

1つのグループ化と1つの集計関数

グループ化集計は下記のように記述する。

# 1つのグループ化と1つの集計関数
dt[, .(sum_price = sum(price)), by = order_yaer]
    order_yaer sum_price
 1:       2000 990548810
 2:       2001 986052220
 3:       2002 992225870
 4:       2003 991483970
 5:       2004 993533550
 6:       2005 990051130
 7:       2006 990772120
 8:       2007 989867690
 9:       2008 992720950
10:       2009 992566890
11:       2010   2822320

.()をつけないと、エラーになるので注意。

dt[, sum_price = sum(price), by = order_yaer]
 `[.data.table`(dt, , sum_price = sum(price), by = order_yaer) でエラー: 
   使われていない引数 (sum_price = sum(price)) 

2つのグループ化と1つの集計関数

グループ化の単位を複数指定することも可能。その場合はbyにベクトルを渡す。

dt[, .(sum_price = sum(price)), by = c("order_yaer","order_month")]

     order_yaer order_month sum_price
  1:       2000           1  83586700
  2:       2000           2  77677530
  3:       2000           3  83966190
  4:       2000           4  81226780
  5:       2000           5  84364940
 ---                                 
117:       2009           9  81803670
118:       2009          10  84297040
119:       2009          11  82029220
120:       2009          12  84424100
121:       2010           1   2822320

2つのグループ化と2つの集計関数

グループ化の単位を複数指定し、集計関数を複数指定することも可能。その場合は、.()内に複数の計算式を記述する。

dt[, 
   .(sum_price = sum(price),
     avg_price = mean(price)
     ),
   by = c("order_yaer","order_month")]

     order_yaer order_month sum_price avg_price
  1:       2000           1  83586700  305.4109
  2:       2000           2  77677530  305.5849
  3:       2000           3  83966190  305.4404
  4:       2000           4  81226780  305.7109
  5:       2000           5  84364940  305.5162
 ---                                           
117:       2009           9  81803670  305.8927
118:       2009          10  84297040  305.8019
119:       2009          11  82029220  305.8817
120:       2009          12  84424100  305.7382
121:       2010           1   2822320  306.1085

.SDを使った複数列と1つの集計関数

集計関数は1つだが、複数の列に適用したい場合、.SDという演算子が利用できる。.SDは’Subset of Data’の略で、データのサブセットを作ってくれる。下記の例では、サブセットにc("order_id","order_datetime","user_id","branch_num", "price", "product_id")を指定し、それに対し、lapply(.SD, min)とすることで、各サブセットのカラムに対し、min()を適用できる。

dt[,
   lapply(.SD, min),
   .SDcols = c("order_id","order_datetime","user_id","branch_num", "price", "product_id")]

   order_id      order_datetime user_id branch_num price product_id
1:        2 2000-01-01 00:00:41       1          1     0          1

複数列と1つのグループ化と1つの集計関数

.SDを使った計算でも、グループ化することは可能。通常通りbyに指定する。

dt[,
   lapply(.SD, sum),
   by = order_yaer,
   .SDcols = c("branch_num", "price")]

    order_yaer branch_num     price
 1:       2000   27078348 990548810
 2:       2001   26919797 986052220
 3:       2002   27090914 992225870
 4:       2003   27083785 991483970
 5:       2004   27160601 993533550
 6:       2005   27110321 990051130
 7:       2006   27040760 990772120
 8:       2007   27017839 989867690
 9:       2008   27134445 992720950
10:       2009   27148734 992566890
11:       2010      77323   2822320

もちろん、複数列と複数のグループ化と1つの集計関数という組み合わせも可能。

dt[,
   lapply(.SD, sum),
   by = c("order_yaer","order_month"),
   .SDcols = c("branch_num", "price")]

     order_yaer order_month branch_num    price
  1:       2000           1    2273233 83586700
  2:       2000           2    2147128 77677530
  3:       2000           3    2291413 83966190
  4:       2000           4    2232009 81226780
  5:       2000           5    2309220 84364940
 ---                                           
117:       2009           9    2265171 81803670
118:       2009          10    2293936 84297040
119:       2009          11    2239036 82029220
120:       2009          12    2309258 84424100
121:       2010           1      77323  2822320

集計関数は1つしか指定できないので注意。

dt[,
   lapply(.SD, sum, max),
   by = c("order_yaer","order_month"),
   .SDcols = c("branch_num", "price")]

 sum(branch_num, max) でエラー:  引数 'type' (builtin) が不正です 

エラー文をみるとsum(branch_num, max)となっており、maxsumに内包されている。つまり、sum()の引数であれば動く。

dt[,
   lapply(.SD, sum, na.rm = TRUE),
   by = c("order_yaer","order_month"),
   .SDcols = c("branch_num", "price")]

     order_yaer order_month branch_num    price
  1:       2000           1    2273233 83586700
  2:       2000           2    2147128 77677530
  3:       2000           3    2291413 83966190
  4:       2000           4    2232009 81226780
  5:       2000           5    2309220 84364940
 ---                                           
117:       2009           9    2265171 81803670
118:       2009          10    2293936 84297040
119:       2009          11    2239036 82029220
120:       2009          12    2309258 84424100
121:       2010           1      77323  2822320

グループ化単位の集計関数を追加

dplyrパッケージのgroup_by()mutate()を組み合わせ、グループ化した単位で集計した結果を追加するような計算をしたい場合、下記のように記述する。

col <- "price"
dt[order_id == 154 | order_id == 387,
   paste0(col, "_sum") := lapply(.SD, sum),
   .SDcols = col,
   by = order_id]

dt[order_id == 154 | order_id == 387,,]
    order_id      order_datetime user_id branch_num price product_id order_yaer order_month price_sum
 1:      154 2000-01-09 10:13:00  351476          1   190      30489       2000           1      1150
 2:      154 2000-01-09 10:13:00  351476          2   260      25246       2000           1      1150
 3:      154 2000-01-09 10:13:00  351476          3   360      41801       2000           1      1150
 4:      154 2000-01-09 10:13:00  351476          4   340      11806       2000           1      1150
 5:      387 2000-01-18 17:09:56  912079          1   300      12916       2000           1      3710
 6:      387 2000-01-18 17:09:56  912079          2   310       2457       2000           1      3710
 7:      387 2000-01-18 17:09:56  912079          3   420      13032       2000           1      3710
 8:      387 2000-01-18 17:09:56  912079          4   440      40688       2000           1      3710
 9:      387 2000-01-18 17:09:56  912079          5   570      39275       2000           1      3710
10:      387 2000-01-18 17:09:56  912079          6   450      16797       2000           1      3710
11:      387 2000-01-18 17:09:56  912079          7   210      32538       2000           1      3710
12:      387 2000-01-18 17:09:56  912079          8   240      35561       2000           1      3710
13:      387 2000-01-18 17:09:56  912079          9   200      19380       2000           1      3710
14:      387 2000-01-18 17:09:56  912079         10   250      32403       2000           1      3710
15:      387 2000-01-18 17:09:56  912079         11   320      19887       2000           1      3710

特別なシンボルや関数

data.tableにはいくつかの特別なシンボルがある。例えば、.SD.BY.N.I.GRPand.NGRPなどである。データの最後の行を確認したい際は.Nを利用する。

dt[.N, , ]  

   order_id      order_datetime user_id branch_num price product_id order_yaer
1:  2834347 2009-12-21 13:23:33 1000000         17   400      21137       2009

行数のカウントを行う際は.N関数を利用する。

# カラム名が必要ない場合は dt[, .N, by = order_yaer] でもよい
dt[, .(cnt = .N), by = order_yaer]
    order_yaer     cnt
 1:       2001 3226029
 2:       2003 3243705
 3:       2006 3242308
 4:       2000 3242456
 5:       2004 3251308
 6:       2008 3247951
 7:       2002 3246198
 8:       2005 3238845
 9:       2007 3239304
10:       2009 3247165
11:       2010    9220

.SDcolsで補集合を作り、それを列に.SDとして適用できる。

dt[, .SD, .SDcols = user_id:order_yaer]
          user_id branch_num price product_id order_yaer
       1:       1          1   330       9894       2001
       2:       1          2   180      10912       2001
       3:       1          3   150      34466       2001
       4:       1          4   190      13733       2001
       5:       1          5   330      27307       2001
      ---                                               
32434485: 1000000         13   400      31717       2009
32434486: 1000000         14   120      14235       2009
32434487: 1000000         15   290      38530       2009
32434488: 1000000         16   330      43122       2009
32434489: 1000000         17   400      21137       2009

グループでカウンターを作る場合は、.GRPを利用する。.GRPはグループ単位で数字をインクリメントして付与してくれる。それにグループの数をカウントして付与する.NGRPを組み合わせると、グループの割合が計算できる。

DT = data.table(x=rep(c("b","a","c"),each=3), v=c(1,1,1,2,2,1,1,2,2), y=c(1,3,6), a=1:9, b=9:1)
DT[, grp := .GRP, by = x]  
DT[, ngrp := .NGRP, by = x]
DT[, grp_pct := .GRP/.NGRP, by = x]
DT
   x v y a b grp ngrp   grp_pct
1: b 1 1 1 9   1    3 0.3333333
2: b 1 3 2 8   1    3 0.3333333
3: b 1 6 3 7   1    3 0.3333333
4: a 2 1 4 6   2    3 0.6666667
5: a 2 3 5 5   2    3 0.6666667
6: a 1 6 6 4   2    3 0.6666667
7: c 1 1 7 3   3    3 1.0000000
8: c 2 3 8 2   3    3 1.0000000
9: c 2 6 9 1   3    3 1.0000000

set(dt, i, j, value)は、値を代入する際に使える関数で、for-loopでも非常に高速に動作する。data.tableだけではなく、data.frameでも使える。

m = matrix(1,nrow=100000,ncol=100)
DF = as.data.frame(m)
DT = as.data.table(m)    

system.time(
  for (i in 1:10000){
    DF[i,1] <- i
    }
  )
#> 13.483 seconds

system.time(
  for (i in 1:10000) {
    DT[i, V1:= i, ]
    }
  )
#> 3.559 seconds

system.time(
  for (i in 1:10000) {
    set(DT, i, 1L, i)
    }
  )
#> 0.039 seconds

後片付けを実行する。

# もとに戻す
dt[,price_sum := NULL,]
dt[,c("order_yaer", "order_month") := NULL,]

テーブルのマージ

テーブルの紐付け(Join)

data.tableのJoinは、個人的には馴染めない部分が多い。InnerJoinしかしないのであれば、それほど気にする必要はない。下記のような簡単例を使ってユニークな点を検証する。tbl1はトランザクション、tbl2は会員マスタみたいなイメージ。

tbl1 <- data.table::data.table(id1 = c("X001", "X001", "X002", "X003", "X004"), x = 0:4)
tbl2 <- data.table::data.table(id2 = c("X001", "X002", "X003", "X005"), gender = c("M", "F", "M", NA), y = c(1,3,4,5))

tbl1
    id1 x
1: X001 0
2: X001 1
3: X002 2
4: X003 3
5: X004 4

tbl2
    id2 gender y
1: X001      M 1
2: X002      F 3
3: X003      M 4
4: X005   <NA> 5

トランザクションをプライマリーテーブルとしてLeftJoinするというケースを考えると、tbl1X004は表示され、tbl2X005は紐付かないはずであるが、下記のように記述すると、テーブルの主従関係が逆になる。感覚的には、LeftJoinなので、プライマリーテーブルtbl1を左に記述したいところである。

# leftjoinを想定するとテーブルが逆
tbl1[tbl2, on = .(id1 = id2),]
    id1  x gender y
1: X001  0      M 1
2: X001  1      M 1
3: X002  2      F 3
4: X003  3      M 4
5: X005 NA   <NA> 5

トランザクションをプライマリーテーブルとしてLeftJoinするというケースは下記のように記述しなければいけない。これであれば想定通りの紐付けが可能。文法はこうなっているleft[right, on = "key",]

# leftjoin
tbl2[tbl1, on = .(id2 = id1),]
    id2 gender  y x
1: X001      M  1 0
2: X001      M  1 1
3: X002      F  3 2
4: X003      M  4 3
5: X004   <NA> NA 4

ただ、紐付けはうまくいくが、カラム名を見ると、tbl1id1tbl2id2というカラムを持っており、プライマリーテーブルをtbl1としたのであれば、キーとして表示されるカラムはid2ではなく、id1であってほしい。追加の引数などで解決できるのかまでは調べてない。baseのmerge()やdplyrの*_joinでJoinする場合は下記のように記述する。data.tableでInnerJoinする場合はleft[right, on = "key", nomatch = 0]nomatchをつける。

# leftjoin
merge(x = tbl1, y = tbl2, by.x = "id1", by.y = "id2", all.x = TRUE)
dplyr::left_join(x = tbl1, y = tbl2, by = c("id1" = "id2"), keep = TRUE)

# innerjoin

merge(x = tbl1, y = tbl2, by.x = "id1", by.y = "id2", all = FALSE)
dplyr::inner_join(x = tbl1, y = tbl2, by = c("id1" = "id2"), keep = TRUE)

# outerjoin
merge(x = tbl1, y = tbl2, by.x = "id1", by.y = "id2", all = TRUE)
dplyr::full_join(x = tbl1, y = tbl2, by = c("id1" = "id2"), keep = TRUE)

ただ、data.tableのJoinには、baseのmerge()やdplyrの*_joinとは異なっている点がある。それは「不等号」を使ってJOINできる点である。tbl2yと、tbl1xとをy > xという条件で比較して、一致するものを紐付けるというJoinが可能。2行目はy > xという関係がFALSEなので、genderNAになっている。

# leftjoinで「一致」と「不等号」の複数条件
tbl2[tbl1, on = .(id2 = id1),]
    id2 gender  y x
1: X001      M  1 0
2: X001      M  1 1
3: X002      F  3 2
4: X003      M  4 3
5: X004   <NA> NA 4

tbl2[tbl1, on = .(id2 = id1, y > x),]
    id2 gender y
1: X001      M 0
2: X001   <NA> 1
3: X002      F 2
4: X003      M 3
5: X004   <NA> 4

分析系のSQLを記述する際には頻繁にJoinで不等号は使用するので、data.tableの非常にありがたい点であるのは間違いないが、テーブルの位置が少し馴染めない。かといって、baseのmerge()やdplyrの*_joinでは不等号のJoinを使用できない。ここをみると、バージョンによっては出来たのかもしれない。

# 今は動かない
dplyr::left_join(x = tbl1, y = tbl2,
                 join_by(id1 == id2)
)
 join_by(id1 == id2) でエラー: 
   関数 "join_by" を見つけることができませんでした 

ということで、fuzzyjoinパッケージを利用するのが妥当かもしれない(fuzzyjoin一択というわけではない)。fuzzyjoinパッケージの説明は省くが、このパッケージは文字列のあいまい検索結合や不等号などの結合もサポートしている非常に便利なパッケージである。

fuzzyjoin::fuzzy_left_join(x = tbl1, y = tbl2,
                           by = c("id1" = "id2", "x" = "y"),
                           match_fun = list(`==`, `<`))

   id1 x  id2 gender  y
1 X001 0 X001      M  1
2 X001 1 <NA>   <NA> NA
3 X002 2 X002      F  3
4 X003 3 X003      M  4
5 X004 4 <NA>   <NA> NA

このように不等号の条件を増やしても実行可能である。広告の配信履歴と購買データを紐付けるが、会員、広告は一致していて、注文日と広告の配信日などの期間を条件付ける場合などのケースを想定している。

ad_dt <- tibble(cu_id = c(paste0("U00",1:3)),
                ad_id = c(paste0("X00",1:3)),
                ad_dt = lubridate::ymd("2018-03-01")) %>%
  mutate(end_dt = (ad_dt + lubridate::days(3))) %>% setDT()

tran_dt <- data.table::data.table(cu_id = c(paste0("U00",1:5)),
                                  ad_id = c(paste0("X00",1:3),paste0("X00",1:2)),
                                  order_dt = lubridate::ymd(c("2018-02-28", "2018-03-01", "2018-03-05",
                                                              "2018-03-05", "2018-03-06")),
                                  total = seq(1000, 5000, 1000)) %>% setDT()

res <- fuzzyjoin::fuzzy_left_join(x = ad_dt, y = tran_dt,
                                  by = c("cu_id" = "cu_id",
                                         "ad_id" = "ad_id",
                                         "ad_dt" = "order_dt",
                                         "end_dt" = "order_dt"),
                                  match_fun = list(`==`, `==`, `<=`, `>=`))

res
   cu_id.x ad_id.x      ad_dt     end_dt cu_id.y ad_id.y   order_dt total
1:    U001    X001 2018-03-01 2018-03-04    <NA>    <NA>       <NA>    NA
2:    U002    X002 2018-03-01 2018-03-04    U002    X002 2018-03-01  2000
3:    U003    X003 2018-03-01 2018-03-04    <NA>    <NA>       <NA>    NA

これまで使用してきたトランザクションのデータにマージするための会員マスタデータを読み込んで準備しておく。

cols_du <- c(
  user_id = "integer",
  gender = "character", 
  area = "character",
  age = "integer"
)

du <- data.table::fread(
  file = "~/Desktop/cust_mst_1m.csv",
  key = c("user_id"),
  colClasses = cols_du,
  header = TRUE,
  encoding = "UTF-8",
  sep = ",",
  na.strings = ",,",
  skip = 0
)

du
        user_id gender   area age
     1:       1   Male area_B  27
     2:       2 Female area_C  37
     3:       4 Female area_C  30
     4:       5   Male area_C  23
     5:       6   Male area_B  41
    ---                          
959672:  999996   Male         27
959673:  999997   Male area_C  49
959674:  999998 Female area_C  32
959675:  999999 Female area_C  NA
959676: 1000000   Male area_A  34

かといってfuzzyjoin一択というわけにもいかない。簡単なJoinでも内部の実装の違いの問題か、サイズの大きいデータでは、PCのメモリとの関係で、メモリが足りなくなる場合もある。

data.table::setkey(dt, "user_id")
system.time(du[dt, on = .(user_id = user_id),])
   ユーザ   システム       経過  
     3.922      1.641      5.910 

system.time(merge(x = dt, y = du, by.x = "user_id", by.y = "user_id", all.x = TRUE))
   ユーザ   システム       経過  
     3.919      0.981      5.508 

system.time(dplyr::left_join(x = dt, y = du, by = c("user_id" = "user_id"), keep = TRUE))
   ユーザ   システム       経過  
     3.168      1.191      4.893 

system.time(fuzzyjoin::fuzzy_left_join(x = dt, y = du, by = c("user_id" = "user_id"), match_fun = list(`==`)))
 エラー:  ベクトルのメモリを使い切りました (上限に達した?) 
Timing stopped at: 142.2 2.299 145.5

スペックなどにもよるので、参考までにちなみに同じデータで、同じことをpostgreでやってみると1分ちょいかかるので、やはりインメモリってめちゃ速い。postgresの設定はこちら

CREATE INDEX user_id_idx ON public.orders (user_id);
CREATE INDEX order_user_id_idx ON public.users (user_id);

SELECT * 
FROM public.orders as o
LEFT JOIN public.users as u
ON o.user_id = u.user_id;

Successfully run. Total query runtime: 1 min 23 secs.
32434489 rows affected.

なんだかんだでdata.tableの記法になれるしかないかも。

テーブルの紐付け(cbind, rbind)

バインドを行う場合の例を見ていく。行をバインドする場合は、rbind()を利用する。この場合、列名が異なるとバインドできないので注意。

t1 <- data.table::data.table(x = c("a", "b"), x1 = 1:2)
t2 <- data.table::data.table(x = c("c", "d"), x1 = 3:4)
t <- rbind(t1, t2)
t
   x x1
1: a  1
2: b  2
3: c  3
4: d  4

t3 <- data.table::data.table(x = c("e", "f"), x2 = 5:6)
rbind(t, t3)
 rbindlist(l, use.names, fill, idcol) でエラー: 
  Column 2 ['x2'] of item 2 is missing in item 1. Use fill=TRUE to fill with NA (NULL for list columns), or use.names=FALSE to ignore column names.

列をバインドする際はcbind()を利用する。cbind()で行が足りていない場合はリサイクル規則が働いてバインドされる。

t1 <- data.table::data.table(x = c("a", "b"), x1 = 1:2)
t2 <- data.table::data.table(x = c("c", "d"), x1 = 3:4)
t <- cbind(t1, t2)
t
   x x1 x x1
1: a  1 c  3
2: b  2 d  4

t3 <- data.table::data.table(x = "c", x1 = 3)
cbind(t, t3)
   x x1 x x1 x x1
1: a  1 c  3 c  3
2: b  2 d  4 c  3

テーブルの変形(Reshape)

data.tableにもdplyrパッケージのpivot_*()(昔であればgather(), spread())に相当するdcast()melt()がある。reshapeパッケージの名前と同じである。dcast()はLong to Wide変換に対応し、melt()はWide to Long変換に対応する。

Wide to Long変換

サンプルのテーブルを用意する。

t <- data.table::data.table(pref = c("A", "B", "C"), 
                            y2000 = 1:3, x2000 = letters[1:3], 
                            y2005 = 4:6, x2005 = letters[4:6],
                            y2010 = 7:9, x2010 = letters[7:9],
                            y2015 = 10:12, x2015 = letters[10:12],
                            y2020 = 13:15, x2020 = letters[13:15])
t
   pref y2000 x2000 y2005 x2005 y2010 x2010 y2015 x2015 y2020 x2020
1:    A     1     a     4     d     7     g    10     j    13     m
2:    B     2     b     5     e     8     h    11     k    14     n
3:    C     3     c     6     f     9     i    12     l    15     o

これをprefyeary*カラムの値valsのLong型データに変形するには、下記のように記述する。

m1 <- data.table::melt(
  data = t,
  id.vars = "pref",
  measure.vars = patterns("^y"),
  variable.name = "year",
  value.name = "vals"
)

m1
    pref  year vals
 1:    A y2000    1
 2:    B y2000    2
 3:    C y2000    3
 4:    A y2005    4
 5:    B y2005    5
 6:    C y2005    6
 7:    A y2010    7
 8:    B y2010    8
 9:    C y2010    9
10:    A y2015   10
11:    B y2015   11
12:    C y2015   12
13:    A y2020   13
14:    B y2020   14
15:    C y2020   15

もちろん、prefyearx*カラムの値lettersのLong型データにも変形できる。

m2 <- data.table::melt(
  data = t,
  id.vars = "pref",
  measure.vars = patterns("^x"),
  variable.name = "year",
  value.name = "letters"
)

m2
    pref  year letters
 1:    A x2000       a
 2:    B x2000       b
 3:    C x2000       c
 4:    A x2005       d
 5:    B x2005       e
 6:    C x2005       f
 7:    A x2010       g
 8:    B x2010       h
 9:    C x2010       i
10:    A x2015       j
11:    B x2015       k
12:    C x2015       l
13:    A x2020       m
14:    B x2020       n
15:    C x2020       o

分けて書くこともできるが、同時に実行することも可能。この場合、yearカラムにはy*x*かは指定はできないので、カラム数の番号が付与される。

m3 <- data.table::melt(
  data = t,
  id.vars = "pref",
  measure.vars = patterns("^y", "^x"),
  variable.name = "year",
  value.name = c("vals", "letters")
)

m3
    pref year vals letters
 1:    A    1    1       a
 2:    B    1    2       b
 3:    C    1    3       c
 4:    A    2    4       d
 5:    B    2    5       e
 6:    C    2    6       f
 7:    A    3    7       g
 8:    B    3    8       h
 9:    C    3    9       i
10:    A    4   10       j
11:    B    4   11       k
12:    C    4   12       l
13:    A    5   13       m
14:    B    5   14       n
15:    C    5   15       o

そのため、わけて実行し、バインドするほうが場合によっては良いのかもしれない。

data.table::data.table(cbind(m1, m2), check.names = TRUE)

    pref  year vals pref.1 year.1 letters
 1:    A y2000    1      A  x2000       a
 2:    B y2000    2      B  x2000       b
 3:    C y2000    3      C  x2000       c
 4:    A y2005    4      A  x2005       d
 5:    B y2005    5      B  x2005       e
 6:    C y2005    6      C  x2005       f
 7:    A y2010    7      A  x2010       g
 8:    B y2010    8      B  x2010       h
 9:    C y2010    9      C  x2010       i
10:    A y2015   10      A  x2015       j
11:    B y2015   11      B  x2015       k
12:    C y2015   12      C  x2015       l
13:    A y2020   13      A  x2020       m
14:    B y2020   14      B  x2020       n
15:    C y2020   15      C  x2020       o

Long to Wide変換

さきほどのm1m2テーブルを使って、Long to Wide変換を行う。つまり、変形前のもとの形に戻す。

d1 <- data.table::dcast(
  data = m1,
  formula = pref ~ year,
  value.var = "vals"
  )

d1
   pref y2000 y2005 y2010 y2015 y2020
1:    A     1     4     7    10    13
2:    B     2     5     8    11    14
3:    C     3     6     9    12    15

d2 <- data.table::dcast(
  data = m2,
  formula = pref ~ year,
  value.var = "letters"
)

d2
   pref x2000 x2005 x2010 x2015 x2020
1:    A     a     d     g     j     m
2:    B     b     e     h     k     n
3:    C     c     f     i     l     o

こちらの同時に変形することができる。さきほど同じく、名前の問題が出てきてしまう。

d3 <- data.table::dcast(
  data = m3,
  formula = pref ~ year,
  value.var = c("vals", "letters")
)

d3
   pref vals_1 vals_2 vals_3 vals_4 vals_5 letters_1 letters_2 letters_3 letters_4 letters_5
1:    A      1      4      7     10     13         a         d         g         j         m
2:    B      2      5      8     11     14         b         e         h         k         n
3:    C      3      6      9     12     15         c         f         i         l         o

名前の問題を回避するためには、pref.1となってしまうが分けて実行しバインドするのが手っ取り早い。

data.table::data.table(cbind(d1, d2), check.names = TRUE)
   pref y2000 y2005 y2010 y2015 y2020 pref.1 x2000 x2005 x2010 x2015 x2020
1:    A     1     4     7    10    13      A     a     d     g     j     m
2:    B     2     5     8    11    14      B     b     e     h     k     n
3:    C     3     6     9    12    15      C     c     f     i     l     o

ピボットテーブル変換

単純に変形するのではなく、集計関数を使って集計しながら集計テーブルに変換できる。俗に言うピボットテーブルである。例えば、user_idごとに、各年で購入があったかどうかのフラグを立てたい場合や、機械学習でおなじみのワンホットエンコーディングしたい場合に使うと便利。

dt_unq <- unique(dt, by = c("user_id", "order_yaer"))
dt_unq[,flg := 1,]

data.table::dcast.data.table(dt_unq,
                             user_id ~ order_yaer,
                             fun.aggregate = max,
                             value.var = "flg",
                             fill = 0)

        user_id 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010
     1:       1    0    1    0    1    0    0    1    0    0    0    0
     2:       2    1    1    0    1    1    0    0    0    1    0    0
     3:       4    0    1    1    1    1    1    1    1    0    0    0
     4:       5    0    0    0    1    0    0    0    1    1    0    0
     5:       6    0    1    0    0    0    0    1    0    1    0    0
    ---                                                               
959672:  999996    0    0    0    1    0    0    0    0    1    0    0
959673:  999997    1    1    0    1    1    1    0    0    0    0    0
959674:  999998    0    0    0    1    0    0    0    0    0    0    0
959675:  999999    1    0    0    0    0    1    0    0    0    0    0
959676: 1000000    0    0    0    1    0    0    1    1    0    1    0

後述するが、data.tableでもパイプ%>%は使えなくはない。.は前の関数の結果を受け取るものなので、dt[i,j,by]dtの部分を.で置き換える。他にもdata.tableの用のパイプの記述があるので、そこで再度紹介する。

unique(dt, by = c("user_id", "order_yaer")) %>% 
  .[,flg := 1,] %>% 
  data.table::dcast.data.table(.,
                               user_id ~ order_yaer,
                               fun.aggregate = max,
                               value.var = "flg",
                               fill = 0)

        user_id 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010
     1:       1    0    1    0    1    0    0    1    0    0    0    0
     2:       2    1    1    0    1    1    0    0    0    1    0    0
     3:       4    0    1    1    1    1    1    1    1    0    0    0
     4:       5    0    0    0    1    0    0    0    1    1    0    0
     5:       6    0    1    0    0    0    0    1    0    1    0    0
    ---                                                               
959672:  999996    0    0    0    1    0    0    0    0    1    0    0
959673:  999997    1    1    0    1    1    1    0    0    0    0    0
959674:  999998    0    0    0    1    0    0    0    0    0    0    0
959675:  999999    1    0    0    0    0    1    0    0    0    0    0
959676: 1000000    0    0    0    1    0    0    1    1    0    1    0

このトランザクションテーブルのproduct_idはユニークで5万近くの商品があるが、これをワンホットエンコーディングすると、.Machine$integer.max=2147483647の制約でエラーが返される。単純計算で、総要素数が47,673,824,652になるので、Rの限界(正確な数値は忘れた)を超えてしまうので、こんなことをカジュアルにやってはいけない。そもそもRでは2,147,483,647行または列(=2^31 - 1= 2147483647)が限界。こちらを参照。

length(unique(dt$product_id))
[1] 49677

unique(dt, by = c("user_id", "product_id")) %>% 
  .[,flg := 1,] %>% 
  data.table::dcast.data.table(.,
                               user_id ~ product_id,
                               fun.aggregate = max,
                               value.var = "flg",
                               fill = 0)
 CJ(1:959676, 1:49677) でエラー: 
  Cross product of elements provided to CJ() would result in 47673824652 rows which exceeds .Machine$integer.max == 2147483647
Timing stopped at: 56.1 5.56 71.55

分割して小さくしたなら出来なくもないが、おすすめはしない…。こんなことをする必要かどうかを考えて、もう一度必要かを考えて、さらにもう一度考えて、必要なのであれば、Rを諦めて、他のプログラミング言語やシステムで考えるべきな気がする。もしくはRstudio社のThree Strategies for Working with Big Data in Rにあるように、(私の仕事の分析要件としても)サプリングが妥当に思える。

ゲノムとか、自然言語処理とか、画像処理、レコメンド系からしたら小さいもん?なのかもしれないが、そっちの知見はない・・・。もしRでいくならビックデータ系のff bigmemoryパッケージ、ffパッケージとかスパース系のパッケージを組み合わせたらいけるのかな…行は問題ないと思うけど列はどうなんだろうか。というかこんなデータを機械学習のアルゴリズムで計算するなんて、スペック的にどんなもんが必要なのか想像もつかん。

# dt_unq <- unique(dt, by = c("user_id", "order_yaer"))
# dt_unq[,flg := 1,]
data.table::setorder(dt_unq, user_id)
dt_unq1 <- dt_unq[user_id %between% c(1,50000), ,]
system.time(
data.table::dcast.data.table(dt_unq1,
                             user_id ~ product_id,
                             fun.aggregate = max,
                             value.var = "flg",
                             fill = 0)
)
   ユーザ   システム       経過  
    27.557     12.559     52.522 

ウインドウ関数

行番号をふる

data.tableにもウインドウ関数が用意されている。例えば、行番号を振るdplyr::row_number()に相当するものは下記のように記述すれば実現できる。ここではわかりやすいように、user_idを限定する。この書き方をすると、条件付けられた計算になるので、他のuser_idNULLになることに注意。グループ単位で行番号を振りたい場合は、byでグループ化を行う。

dt[user_id == 351476,
   rowid := 1:.N,]

dt[user_id == 351476,
   rowid_by := 1:.N,
   by = order_id]

dt[user_id == 351476,,]  
    order_id      order_datetime user_id branch_num price product_id order_yaer rowid rowid_by
 1:      154 2000-01-09 10:13:00  351476          1   190      30489       2000     1        1
 2:      154 2000-01-09 10:13:00  351476          2   260      25246       2000     2        2
 3:      154 2000-01-09 10:13:00  351476          3   360      41801       2000     3        3
 4:      154 2000-01-09 10:13:00  351476          4   340      11806       2000     4        4
 5:   690175 2002-05-26 19:03:18  351476          1   360      21586       2002     5        1
 6:   690175 2002-05-26 19:03:18  351476          2   310      36186       2002     6        2
 7:   690175 2002-05-26 19:03:18  351476          3   310      12013       2002     7        3
 8:   690175 2002-05-26 19:03:18  351476          4   410      21394       2002     8        4
 9:   690175 2002-05-26 19:03:18  351476          5   270      15424       2002     9        5
10:  1260215 2004-01-22 15:17:58  351476          1   250      12732       2004    10        1
11:  1260215 2004-01-22 15:17:58  351476          2   340      14032       2004    11        2

# もとに戻す
dt[,rowid := NULL,]
dt[,rowid_by := NULL,]

行をずらす

行をずらすdplyr::lead()dplyr::lag()と同様の操作を行いたい場合は、shift()を利用する。グループ単位で行をずらしたい場合は、byでグループ化を行う。

dt[user_id == 351476,
   lag := shift(branch_num, 1, type = "lag"),]

dt[user_id == 351476,
   lead := shift(branch_num, 1, type = "lead"),]

dt[user_id == 351476,
   lag_by := shift(branch_num, 1, type = "lag"),
   by = order_id]

dt[user_id == 351476,
   lead_by := shift(branch_num, 1, type = "lead"),
   by = order_id]

dt[user_id == 351476,,]  

    order_id      order_datetime user_id branch_num price product_id order_yaer lag lead lag_by lead_by
 1:      154 2000-01-09 10:13:00  351476          1   190      30489       2000  NA    2     NA       2
 2:      154 2000-01-09 10:13:00  351476          2   260      25246       2000   1    3      1       3
 3:      154 2000-01-09 10:13:00  351476          3   360      41801       2000   2    4      2       4
 4:      154 2000-01-09 10:13:00  351476          4   340      11806       2000   3    1      3      NA
 5:   690175 2002-05-26 19:03:18  351476          1   360      21586       2002   4    2     NA       2
 6:   690175 2002-05-26 19:03:18  351476          2   310      36186       2002   1    3      1       3
 7:   690175 2002-05-26 19:03:18  351476          3   310      12013       2002   2    4      2       4
 8:   690175 2002-05-26 19:03:18  351476          4   410      21394       2002   3    5      3       5
 9:   690175 2002-05-26 19:03:18  351476          5   270      15424       2002   4    1      4      NA
10:  1260215 2004-01-22 15:17:58  351476          1   250      12732       2004   5    2     NA       2
11:  1260215 2004-01-22 15:17:58  351476          2   340      14032       2004   1   NA      1      NA

# もとに戻す
dt[,lag := NULL,]
dt[,lead := NULL,]
dt[,lag_by := NULL,]
dt[,lead_by := NULL,]

累計合計を計算する

累計合計を計算する場合はcumsum()を利用する。

dt[user_id == 351476,
   cumsum := cumsum(price),]

dt[user_id == 351476,
   cumsum_by := cumsum(price),
   by = order_id]

dt[user_id == 351476,,]
    order_id      order_datetime user_id branch_num price product_id order_yaer cumsum cumsum_by
 1:      154 2000-01-09 10:13:00  351476          1   190      30489       2000    190       190
 2:      154 2000-01-09 10:13:00  351476          2   260      25246       2000    450       450
 3:      154 2000-01-09 10:13:00  351476          3   360      41801       2000    810       810
 4:      154 2000-01-09 10:13:00  351476          4   340      11806       2000   1150      1150
 5:   690175 2002-05-26 19:03:18  351476          1   360      21586       2002   1510       360
 6:   690175 2002-05-26 19:03:18  351476          2   310      36186       2002   1820       670
 7:   690175 2002-05-26 19:03:18  351476          3   310      12013       2002   2130       980
 8:   690175 2002-05-26 19:03:18  351476          4   410      21394       2002   2540      1390
 9:   690175 2002-05-26 19:03:18  351476          5   270      15424       2002   2810      1660
10:  1260215 2004-01-22 15:17:58  351476          1   250      12732       2004   3060       250
11:  1260215 2004-01-22 15:17:58  351476          2   340      14032       2004   3400       590

# もとに戻す
dt[,cumsum := NULL,]
dt[,cumsum_by := NULL,]

ランク付けを行う

ランク付けを行う場合は、frank()を利用する。ties.methodがたくさんあるので、必要に応じて使い分ける。デフォルトはaveragena.lastは最後にNAにランクをつけるか、最初につけるかの違い。

dt_rank <- data.table::data.table(val = c(1, 2, 2, 5, 6, 7, NA_integer_))
dt_rank[,.(val,
        rank_average = frank(x = val, na.last = TRUE, ties.method = "average"),
        rank_first   = frank(x = val, na.last = TRUE, ties.method = "first"),
        rank_last    = frank(x = val, na.last = TRUE, ties.method = "last"),
        rank_random  = frank(x = val, na.last = TRUE, ties.method = "random"),
        rank_max     = frank(x = val, na.last = TRUE, ties.method = "max"),
        rank_min     = frank(x = val, na.last = TRUE, ties.method = "min"),
        rank_dense   = frank(x = val, na.last = TRUE, ties.method = "dense")
        )
        ,]

   val rank_average rank_first rank_last rank_random rank_max rank_min rank_dense
1:   1          1.0          1         1           1        1        1          1
2:   2          2.5          2         3           2        3        2          2
3:   2          2.5          3         2           3        3        2          2
4:   5          4.0          4         4           4        4        4          3
5:   6          5.0          5         5           5        5        5          4
6:   7          6.0          6         6           6        6        6          5
7:  NA          7.0          7         7           7        7        7          6