data.table 教程2-语义引用

目录:

  1. data.table 介绍
  2. 语义引用
  3. 主键、基于二分法搜索的subset
  4. 二次索引和自动索引
  5. 数据拆分和合并

原文地址:
data.table/wiki/Getting-started


本教程讨论data.table的语义引用,它允许通过引用来add/update/delete列,然后通过参数i和by结合。它主要给那些熟悉data.table语法、知道如何subset行/select列/分组的人使用。如果你对这些不熟悉,请学习上一讲 data.table 介绍


数据

我们继续使用上一讲中使用的航班信息flights。

flights <- fread("https://raw.githubusercontent.com/wiki/arunsrinivasan/    flights/NYCflights14/flights14.csv")
flights
#         year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
#      1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1
#      2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3
#      3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21
#      4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29
#      5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117
#     ---                                                                                      
# 253312: 2014    10  31     1459         1     1747       -30         0      UA  N23708   1744
# 253313: 2014    10  31      854        -5     1147       -14         0      UA  N33132   1758
# 253314: 2014    10  31     1102        -8     1311        16         0      MQ  N827MQ   3591
# 253315: 2014    10  31     1106        -4     1325        15         0      MQ  N511MQ   3592
# 253316: 2014    10  31      824        -5     1045         1         0      MQ  N813MQ   3599
#         origin dest air_time distance hour min
#      1:    JFK  LAX      359     2475    9  14
#      2:    JFK  LAX      363     2475   11  57
#      3:    JFK  LAX      351     2475   19   2
#      4:    LGA  PBI      157     1035    7  22
#      5:    JFK  LAX      350     2475   13  47
#     ---                                       
# 253312:    LGA  IAH      201     1416   14  59
# 253313:    EWR  IAH      189     1400    8  54
# 253314:    LGA  RDU       83      431   11   2
# 253315:    LGA  DTW       75      502   11   6
# 253316:    LGA  SDF      110      659    8  24
dim(flights)
# [1] 253316     17

介绍

在这一讲,我们会:

* 简要讨论“语义引用”,然后比较操作符“:=”的两种不同的形式。
* 学习如何在参数j里面使用操作符“:=”来add/update/delete列,如何与参数i和by相结合。
* 了解操作符“:=”的副作用,并学习如何用 copy() 来避免这些副作用。

1. 语义引用

到目前为止,我们学习到的所有的操作都会生成一个新的数据集。接下来,我们会学习如何在原来数据集的基础上,添加/更新/删除那些已经存在的列。

a) 背景

在学习语义引用之前,我们先来看下面这个data.frame:

DF = data.frame(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c=13:18)
DF
#   ID a  b  c
# 1  b 1  7 13
# 2  b 2  8 14
# 3  b 3  9 15
# 4  a 4 10 16
# 5  a 5 11 17
# 6  c 6 12 18

当我们执行下面的命令:

DF$c <- 18:13               # (1) -- replace entire column
# or
DF$c[DF$ID == "b"] <- 15:13 # (2) -- subassign in column 'c'

在R语言V3.1之前的版本里,上面这两种方法都会导致对整个data.frame的深度拷贝[^1]。而且还会拷贝多次[^2]。为了提高效率避免冗余操作,data.tabel使用了操作符”:=”。R里面本来就有定义了这个操作符,但却没有使用[^3]。
[^1]:Speeding up perception
[^2]:Is data really copied four times in R’s replacement functions?
[^3]:Why has data.table defined := rather than overloading <-?

在R语言V3.1之前的版本里,方法(1)只做影子拷贝,处理性能有了很大提升。然而,方法(2)还是会做深度拷贝。这就意味着,对于同样的查询语句,想要选取的列越多,需要做的深度拷贝就越多。

影子拷贝 vs 深度拷贝
影子拷贝,只是一份指向列的指针向量的拷贝,它会随着data.frame或者data.table的变化而变化。但在内存里,数据不是真的被复制了。   
深度拷贝,正相反,它会复制整个数据,并且保存在内存里。

如果使用操作符”:=”,不管在R语言的什么版本里,不管是方法(1)还是方法(2),都不会再拷贝。这是因为,操作符”:=”通过引用更新列。

b) 操作符“:=”

在参数j中,操作符“:=”有两种使用方法:
a.左右等式的形式

DT[, c("colA", "colB", ...) := list(valA, valB, ...)]

# when you have only one column to assign to you 
# can drop the quotes and list(), for convenience
DT[, colA := valA]

b.函数形式

DT[, `:=`(colA = valA, # valA is assigned to colA
      colB = valB, # valB is assigned to colB
      ...
)]

注意:

上面的两个例子只是用来说明使用的形式,并不是实际可以运行的代码示例。我们会在下一节中,用航班信息flight的data.table来举例说明。

说明:

* 形式(a)比较容易编码,特别是,事先不知道需要被赋值的列的时候。
* 相对而言,形式(b)更加趁手,如果你愿意追加点注释😄。
* 操作符“:=”没有返回值。
* 既然参数j里面可以使用操作符“:=”,那么,就像上一讲中学习到的内容,我们可以和参数i和参数by一起,做些聚合的运算。

在上面两种形式里,注意我们没有把运算的结果赋值给一个变量。因为完全没必要。我们直接更新data.table。让我们通过一些例子来说明。
在接下来的教程里,我们对航班信息flight,这个data.table来示例。

2. 添加/更新/删除列

a) 添加列

-如何对每次航班,添加 speed 和 total delay 两列

flights[, `:=`(speed = distance / (air_time/60), # speed in km/hr
           delay = arr_delay + dep_delay)]       # delay in minutes
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed delay
# 1:  LAX      359     2475    9  14 413.6490    27
# 2:  LAX      363     2475   11  57 409.0909    10
# 3:  LAX      351     2475   19   2 423.0769    11
# 4:  PBI      157     1035    7  22 395.5414   -34
# 5:  LAX      350     2475   13  47 424.2857     3
# 6:  LAX      339     2454   18  24 434.3363     4

## alternatively, using the 'LHS := RHS' form
# flights[, c("speed", "delay") := list(distance/(air_time/60), arr_delay + dep_delay)]

注意:

* 我们不需要将结果赋值给 flights。
* flights 现在包含了刚刚追加的两列。这就是我们说的“添加列”。
* 我们用函数形式,这样就可以在旁边写注释了。当然也可以用等式的形式。

b) 更新列(sub-assign)

现在留意一下 fligths 里的 hour列。

# get all 'hours' in flights
flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

可以看到,hour列有25种不同的取值。但是0点和24点应该是一样的,我们来把24点全部替换成0点。
-将 hour=24 替换成0

# subassign by reference
flights[hour == 24L, hour := 0L]

说明:

* 就像在上一讲中学习的一样,我们可以使用参数i 和 参数j里的操作符“:=”一起使用。
* 只有满足了参数i 中指定的条件 hour == 24L 的那些列,它们的值会被替换成0。
* 操作符“:=”没有返回值。有时候需要查看运行的结果,我们可以在查询语句的最后加一对方括号[],来达到这个目的。
flights[hour == 24L, hour := 0L][]
#         year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
#      1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1
#      2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3
#      3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21
#      4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29
#      5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117
#     ---                                                                                      
# 253312: 2014    10  31     1459         1     1747       -30         0      UA  N23708   1744
# 253313: 2014    10  31      854        -5     1147       -14         0      UA  N33132   1758
# 253314: 2014    10  31     1102        -8     1311        16         0      MQ  N827MQ   3591
# 253315: 2014    10  31     1106        -4     1325        15         0      MQ  N511MQ   3592
# 253316: 2014    10  31      824        -5     1045         1         0      MQ  N813MQ   3599
#         origin dest air_time distance hour min    speed delay
#      1:    JFK  LAX      359     2475    9  14 413.6490    27
#      2:    JFK  LAX      363     2475   11  57 409.0909    10
#      3:    JFK  LAX      351     2475   19   2 423.0769    11
#      4:    LGA  PBI      157     1035    7  22 395.5414   -34
#      5:    JFK  LAX      350     2475   13  47 424.2857     3
#     ---                                                      
# 253312:    LGA  IAH      201     1416   14  59 422.6866   -29
# 253313:    EWR  IAH      189     1400    8  54 444.4444   -19
# 253314:    LGA  RDU       83      431   11   2 311.5663     8
# 253315:    LGA  DTW       75      502   11   6 401.6000    11
# 253316:    LGA  SDF      110      659    8  24 359.4545    -4

现在我们再来看下 hour列。

# check again for '24'
flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

c) 删除列

-删除 delay列

flights[, c("delay") := NULL]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed
# 1:  LAX      359     2475    9  14 413.6490
# 2:  LAX      363     2475   11  57 409.0909
# 3:  LAX      351     2475   19   2 423.0769
# 4:  PBI      157     1035    7  22 395.5414
# 5:  LAX      350     2475   13  47 424.2857
# 6:  LAX      339     2454   18  24 434.3363

## or using the functional form
# flights[, `:=`(delay = NULL)]

说明:

* 将一列赋值为 NULL,就会删除那一列。删除立即生效。
* 使用左右等式的形式的时候,除了指定列名,我们也可以指定列号。但还是忘记吧,指定列名是个好的编码习惯。
* 为了方便,如果只需要删除一列,可以去掉 c(""),只指定列名,像这样:
flights[, delay := NULL]
这和上面的方法是等效的。

d) “:=”和分组

我们已经在b)里面学习了如何跟参数i 一起使用,现在我们来学习如何跟参数by 一起使用。
-如何追加一列,用来保存某对起飞/到达机场间的最快飞行速度

flights[, max_speed := max(speed), by=.(origin, dest)]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed max_speed
# 1:  LAX      359     2475    9  14 413.6490  526.5957
# 2:  LAX      363     2475   11  57 409.0909  526.5957
# 3:  LAX      351     2475   19   2 423.0769  526.5957
# 4:  PBI      157     1035    7  22 395.5414  517.5000
# 5:  LAX      350     2475   13  47 424.2857  526.5957
# 6:  LAX      339     2454   18  24 434.3363  518.4507

说明:

* 我们用操作符“:=”追加了一列 max_speed。
* 和上一讲学习到的内容一样,我们将所有数据进行分组。对于每组数据,计算最快速度。对于一对机场,这个最快速度是唯一的。循环复制这个值到一个list,直到跟该组数据的行数一样多。航班信息flights会被就地更新,不会因拷贝浪费内存空间。
* 和上一讲学习到的内容一样,我们也可以对参数by指定一个字符型的向量,形式是这样:
by = c("origin", "dest")

e) “:=”和复数列

-如何再追加两列,用于保存每个月的最大起飞延误时间dep_delay 和到达延误时间arr_delay
小提示:要用到上一讲学习到的 .SD

in_cols  = c("dep_delay", "arr_delay")
out_cols = c("max_dep_delay", "max_arr_delay")
flights[, c(out_cols) := lapply(.SD, max), by = month, .SDcols = in_cols]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed max_speed max_dep_delay max_arr_delay
# 1:  LAX      359     2475    9  14 413.6490  526.5957           973           996
# 2:  LAX      363     2475   11  57 409.0909  526.5957           973           996
# 3:  LAX      351     2475   19   2 423.0769  526.5957           973           996
# 4:  PBI      157     1035    7  22 395.5414  517.5000           973           996
# 5:  LAX      350     2475   13  47 424.2857  526.5957           973           996
# 6:  LAX      339     2454   18  24 434.3363  518.4507           973           996

说明:

* 为了更好的可读性,我们使用了左右等式的形式。我们事先保存了输入的列名到变量in_cols,作为 .SDcols的参数。我们还事先保存了输出的列名到变量out_cols,作为左边的表达式。
* 注意一下,我们在c)里面讲过,如果只需要追加一列,那么可以省略双引号,只指定列名。但是这里我们需要指定 c(out_cols) 或者 (out_cols)。 
* 左右等式的形式,允许我们操作复数的列。在右边的表达式里,为了对指定在 .SDcols 里的列计算最大值,我们使用了R的基础函数 lapply()。这些我们在上一讲中都学习过了。它返回有两个元素的list,包含每组的 dep_delay 和 arr_delay 这两列的最大值。

在进行下一节的学习之前,让我们删除刚刚追加的几列:speed, max_speed, max_dep_delay 和 max_arr_delay。

# RHS gets automatically recycled to length of LHS
flights[, c("speed", "max_speed", "max_dep_delay", "max_arr_delay") := NULL]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
# 3:  LAX      351     2475   19   2
# 4:  PBI      157     1035    7  22
# 5:  LAX      350     2475   13  47
# 6:  LAX      339     2454   18  24

3. “:=”和copy()

操作符“:=”会更新原数据。和我们之前学过的功能不同,有时候,我们希望更新原数据。但有时候,我们不想更新原数据,这种情况下,我们可以用函数 copy()。

a) “:=”的副作用

如果我们想创建一个函数,用于返回每个月的最快速度。但是此时,我们也想对 flights 追加一列 speed。可以像下面这样做:

foo <- function(DT) {
  DT[, speed := distance / (air_time/60)]
  DT[, .(max_speed = max(speed)), by=month]
}
ans = foo(flights)
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed
# 1:  LAX      359     2475    9  14 413.6490
# 2:  LAX      363     2475   11  57 409.0909
# 3:  LAX      351     2475   19   2 423.0769
# 4:  PBI      157     1035    7  22 395.5414
# 5:  LAX      350     2475   13  47 424.2857
# 6:  LAX      339     2454   18  24 434.3363
head(ans)
#    month max_speed
# 1:     1  535.6425
# 2:     2  535.6425
# 3:     3  549.0756
# 4:     4  585.6000
# 5:     5  544.2857
# 6:     6  608.5714

说明:

* 注意一个新的列 speed 被追加到 flight 里了。这时因为我们用了操作符“:=”。既然 DT 和flights都指向内存中同一个对象,对 DT 的操作,也会对 flights 生效。
* 返回值 ans 包含了每月的最快速度。

b) 函数copy()

在前面一节,我们利用了操作符“:=”的副作用来更新原数据。但是不会一直希望这样又是,我们希望给函数传递data.table参数,使用操作符“:=”的功能,但是不想改变原数据。我们可以用函数 copy() 来做到这一点。

函数 copy() 对输入参数进行深度拷贝,因此对副本做的所有更新操作,都不会对原数据生效。

函数 copy() 有两个不可或缺的特点:
1.和前一节内容的情形相反,我们可能不希望传递的参数被修改。举个例子,考虑前一节中,我们不想修改 flights的内容。
我们先删掉前一节中,追加的 speed列:

flights[, speed := NULL]   

现在,我们可以像下面这样做:

foo <- function(DT) {
  DT <- copy(DT)                             ## deep copy
  DT[, speed := distance / (air_time/60)]    ## doesn't affect 'flights'
  DT[, .(max_speed = max(speed)), by=month]
}
ans <- foo(flights)
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
# 3:  LAX      351     2475   19   2
# 4:  PBI      157     1035    7  22
# 5:  LAX      350     2475   13  47
# 6:  LAX      339     2454   18  24
head(ans)
#    month max_speed
# 1:     1  535.6425
# 2:     2  535.6425
# 3:     3  549.0756
# 4:     4  585.6000
# 5:     5  544.2857
# 6:     6  608.5714

说明:

* 使用函数 copy() 不会更新 flights。它现在不包含 speed列。
* 返回值 ans 包含了每月的最快速度。
然而,我们可以使用影子拷贝来代替深度拷贝,来大幅度提高这个操作的效率。事实上,我们希望在 Data.Table的V1.9.8的版本里提供这个功能。我们会在data.table的设计里面继续讨论这个内容。

Data.Table V1.9.8 相关资料:
Copy-on-:= at column level, DT[,list(…)] shallow copy and add cols to shallow(DT, cols)

2.当我们将列名保存在变量里的时候,比如:DT_n = names(DT),然后再对 DT 添加/更新/删除列,操作符“:=”也会更新变量 DT_n,除非我们运行 copy(names(DT))。

DT = data.table(x=1, y=2)
DT_n = names(DT)
DT_n
# [1] "x" "y"

## add a new column by reference
DT[, z := 3]

## DT_n also gets updated
DT_n
# [1] "x" "y" "z"

## use `copy()`
DT_n = copy(names(DT))
DT[, w := 4]

## DT_n doesn't get updated
DT_n
# [1] "x" "y" "z"

总结

操作符“:=”
* 操作符“:=”用于添加/更新/删除列。
* 我们也学习了如何跟参数i和参数by一起使用,就像在第一讲中学习的那样。同样,我们也可以使用 keyby,可以用方括号 [] 将操作连结起来,可以给参数by 指定表达式。
* 我们可以利用操作符“:=”更新原数据,也可以用函数 copy() 来避免更新原数据。

到目前为止,我们学习了好多参数j相关的知识,知道了参数i、参数j和参数by如何一起使用。下一讲主键、基于二分法搜索的subset,我们将注意力回到参数i上,来做一些通过主键的超快速的排序。