13 min read

data.table学习笔记3

  • data.table的特点:减小计算复杂度,降低计算时间

1.主键

利用主键进行subset,速度会更加快。随着data.table的不断升级,很多行为譬如根据条件提取子集,已经进行了优化,设置和不设置主键的性能差异已经不是太大了。

设置主键

通过函数setkey()来设置主键。下边代码将origin列设置为主键。

setkey(flights,origin)
setkey(flights,"origin")
head(flights)
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     1   1     1824         4     2145         0         0      AA
## 2: 2014     1   1     1655        -5     2003       -17         0      AA
## 3: 2014     1   1     1611       191     1910       185         0      AA
## 4: 2014     1   1     1449        -1     1753        -2         0      AA
## 5: 2014     1   1      607        -3      905       -10         0      AA
## 6: 2014     1   1      949         4     1243       -17         0      AA
##    tailnum flight origin dest air_time distance hour min
## 1:  N3DEAA    119    EWR  LAX      339     2454   18  24
## 2:  N5CFAA    172    EWR  MIA      161     1085   16  55
## 3:  N471AA    300    EWR  DFW      214     1372   16  11
## 4:  N4WNAA    320    EWR  DFW      214     1372   14  49
## 5:  N5DMAA   1205    EWR  MIA      154     1085    6   7
## 6:  N491AA   1223    EWR  DFW      215     1372    9  49

origin列设置为主键后,一个明显特征,其中的元素从小到大排序了。主键列中元素并不唯一,可以重复。 列被setkey后,速度提升很快。利用microbenchmark包进行测试。设置为主键后,的确快一些。

require(microbenchmark)
## Loading required package: microbenchmark
microbenchmark(flights[.("JFK")],flights.copy[origin == "JFK"])
## Unit: milliseconds
##                           expr     min       lq     mean  median       uq
##              flights[.("JFK")] 11.0625 11.97405 14.82183 12.6537 17.50760
##  flights.copy[origin == "JFK"] 11.7488 12.97150 17.33587 13.7778 20.57225
##      max neval
##  26.2247   100
##  91.2646   100
  • 因为已经将主键设置为 origin列了,所以只要直接指定“JFK”就可以了。这里 .()用来在data.table的主键(也就是flights 的 origin列)里,查找“JFK”。
  • 首先,满足“JFK”条件的行的索引都被获取到。然后,这些行的哪些信息是必要的呢。既然参数j里没有指定任何表达式,这些行的所有列都被返回了。
  • 如果主键是字符型的列,那么可以省略 .(),就像用行名subset一个data.frame的行的时候。 flights[“JFK”] ## same as flights[.(“JFK”)]

  • 我们可以根据需要指定多个值 flights[c(“JFK”, “LGA”)] ## same as flights[.(c(“JFK”, “LGA”))] 这返回所有 origin列是“JFK” 或者 “LGA”的所有行。

flights[c("JFK", "LGA")]
##         year month day dep_time dep_delay arr_time arr_delay cancelled
##      1: 2014     1   1      914        14     1238        13         0
##      2: 2014     1   1     1157        -3     1523        13         0
##      3: 2014     1   1     1902         2     2224         9         0
##      4: 2014     1   1     1347         2     1706         1         0
##      5: 2014     1   1     2133        -2       37       -18         0
##     ---                                                               
## 165912: 2014    10  31      609        24      843        -5         0
## 165913: 2014    10  31     1459         1     1747       -30         0
## 165914: 2014    10  31     1102        -8     1311        16         0
## 165915: 2014    10  31     1106        -4     1325        15         0
## 165916: 2014    10  31      824        -5     1045         1         0
##         carrier tailnum flight origin dest air_time distance hour min
##      1:      AA  N338AA      1    JFK  LAX      359     2475    9  14
##      2:      AA  N335AA      3    JFK  LAX      363     2475   11  57
##      3:      AA  N327AA     21    JFK  LAX      351     2475   19   2
##      4:      AA  N319AA    117    JFK  LAX      350     2475   13  47
##      5:      AA  N323AA    185    JFK  LAX      338     2475   21  33
##     ---                                                              
## 165912:      UA  N16709   1714    LGA  IAH      198     1416    6   9
## 165913:      UA  N23708   1744    LGA  IAH      201     1416   14  59
## 165914:      MQ  N827MQ   3591    LGA  RDU       83      431   11   2
## 165915:      MQ  N511MQ   3592    LGA  DTW       75      502   11   6
## 165916:      MQ  N813MQ   3599    LGA  SDF      110      659    8  24
flights[.(c("JFK", "LGA"))]
##         year month day dep_time dep_delay arr_time arr_delay cancelled
##      1: 2014     1   1      914        14     1238        13         0
##      2: 2014     1   1     1157        -3     1523        13         0
##      3: 2014     1   1     1902         2     2224         9         0
##      4: 2014     1   1     1347         2     1706         1         0
##      5: 2014     1   1     2133        -2       37       -18         0
##     ---                                                               
## 165912: 2014    10  31      609        24      843        -5         0
## 165913: 2014    10  31     1459         1     1747       -30         0
## 165914: 2014    10  31     1102        -8     1311        16         0
## 165915: 2014    10  31     1106        -4     1325        15         0
## 165916: 2014    10  31      824        -5     1045         1         0
##         carrier tailnum flight origin dest air_time distance hour min
##      1:      AA  N338AA      1    JFK  LAX      359     2475    9  14
##      2:      AA  N335AA      3    JFK  LAX      363     2475   11  57
##      3:      AA  N327AA     21    JFK  LAX      351     2475   19   2
##      4:      AA  N319AA    117    JFK  LAX      350     2475   13  47
##      5:      AA  N323AA    185    JFK  LAX      338     2475   21  33
##     ---                                                              
## 165912:      UA  N16709   1714    LGA  IAH      198     1416    6   9
## 165913:      UA  N23708   1744    LGA  IAH      201     1416   14  59
## 165914:      MQ  N827MQ   3591    LGA  RDU       83      431   11   2
## 165915:      MQ  N511MQ   3592    LGA  DTW       75      502   11   6
## 165916:      MQ  N813MQ   3599    LGA  SDF      110      659    8  24

使用函数 key(),获得被设置为主键的列名。

key(flights)
## [1] "origin"
key(flights.copy)
## NULL
  • 函数 key() 返回主键列名的字符型向量。
  • 如果data.table没有设置过主键,返回 NULL。

2. 更多主键

可以把主键理解为功能更强大的行名。可以设置多列为主键。

setkey(flights,origin,dest)
head( flights)
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     1   2      724        -2      810       -25         0      EV
## 2: 2014     1   3     2313        88        9        79         0      EV
## 3: 2014     1   4     1526       220     1618       211         0      EV
## 4: 2014     1   4      755        35      848        19         0      EV
## 5: 2014     1   5      817        47      921        42         0      EV
## 6: 2014     1   5     2301        66        2        62         0      EV
##    tailnum flight origin dest air_time distance hour min
## 1:  N11547   4373    EWR  ALB       30      143    7  24
## 2:  N18120   4470    EWR  ALB       29      143   23  13
## 3:  N11184   4373    EWR  ALB       32      143   15  26
## 4:  N14905   4551    EWR  ALB       32      143    7  55
## 5:  N19966   4470    EWR  ALB       26      143    8  17
## 6:  N19966   4682    EWR  ALB       31      143   23   1

从返回结果看,origin和dest这两列被重新排序了。

提取满足orgin==“JFK”,dest== “MIA”条件的数据集。

flights[.("JFK","MIA")]
##       year month day dep_time dep_delay arr_time arr_delay cancelled
##    1: 2014     1   1     1509        -1     1828       -17         0
##    2: 2014     1   1      917         7     1227        -8         0
##    3: 2014     1   1     1227         2     1534        -1         0
##    4: 2014     1   1      546         6      853         3         0
##    5: 2014     1   1     1736         6     2043       -12         0
##   ---                                                               
## 2746: 2014    10  31     1659        -1     1956       -22         0
## 2747: 2014    10  31      826        -3     1116       -20         0
## 2748: 2014    10  31      647         2      941       -17         0
## 2749: 2014    10  31      542        -3      834       -12         0
## 2750: 2014    10  31     1944        29     2232         4         0
##       carrier tailnum flight origin dest air_time distance hour min
##    1:      AA  N5FJAA    145    JFK  MIA      161     1089   15   9
##    2:      AA  N5DWAA   1085    JFK  MIA      166     1089    9  17
##    3:      AA  N635AA   1697    JFK  MIA      164     1089   12  27
##    4:      AA  N5CGAA   2243    JFK  MIA      157     1089    5  46
##    5:      AA  N397AA   2351    JFK  MIA      154     1089   17  36
##   ---                                                              
## 2746:      AA  N5FNAA   2351    JFK  MIA      148     1089   16  59
## 2747:      AA  N5EYAA   1085    JFK  MIA      146     1089    8  26
## 2748:      AA  N5BTAA   1101    JFK  MIA      150     1089    6  47
## 2749:      AA  N3ETAA   2299    JFK  MIA      150     1089    5  42
## 2750:      AA  N5FSAA   2387    JFK  MIA      146     1089   19  44

当有两个主键时,对第一个主键,这样写是ok的。

flights[.("JFK")]
##        year month day dep_time dep_delay arr_time arr_delay cancelled
##     1: 2014     1   1     2011        10     2308         4         0
##     2: 2014     1   2     2215       134      145       161         0
##     3: 2014     1   7     2006         6     2314         6         0
##     4: 2014     1   8     2009        15     2252       -15         0
##     5: 2014     1   9     2039        45     2339        32         0
##    ---                                                               
## 81479: 2014    10  31      800         0     1040       -18         0
## 81480: 2014    10  31     1932         1     2228        -8         0
## 81481: 2014    10  31     1443        -2     1726       -22         0
## 81482: 2014    10  31      957        -8     1255        -5         0
## 81483: 2014    10  31      831        -4     1118       -18         0
##        carrier tailnum flight origin dest air_time distance hour min
##     1:      B6  N766JB     65    JFK  ABQ      280     1826   20  11
##     2:      B6  N507JB     65    JFK  ABQ      252     1826   22  15
##     3:      B6  N652JB     65    JFK  ABQ      269     1826   20   6
##     4:      B6  N613JB     65    JFK  ABQ      259     1826   20   9
##     5:      B6  N598JB     65    JFK  ABQ      267     1826   20  39
##    ---                                                              
## 81479:      DL  N915AT   2165    JFK  TPA      142     1005    8   0
## 81480:      B6  N516JB    225    JFK  TPA      149     1005   19  32
## 81481:      B6  N334JB    325    JFK  TPA      145     1005   14  43
## 81482:      B6  N637JB    925    JFK  TPA      149     1005    9  57
## 81483:      B6  N595JB   1025    JFK  TPA      145     1005    8  31

如何只对第二个主键进行过滤?

flights[.("MIA")]
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1:   NA    NA  NA       NA        NA       NA        NA        NA    <NA>
##    tailnum flight origin dest air_time distance hour min
## 1:    <NA>     NA    MIA <NA>       NA       NA   NA  NA

像上边这样是行不通的。必须要给出第一个主键的所有值,通过unique(origin)来实现。

flights[.(unique(origin),"MIA")]
##       year month day dep_time dep_delay arr_time arr_delay cancelled
##    1: 2014     1   1     1655        -5     2003       -17         0
##    2: 2014     1   1      607        -3      905       -10         0
##    3: 2014     1   1     1125        -5     1427        -8         0
##    4: 2014     1   1     1533        43     1840        42         0
##    5: 2014     1   1     2130        60       29        49         0
##   ---                                                               
## 9924: 2014    10  31     1348       -11     1658        -8         0
## 9925: 2014    10  31      950        -5     1257       -11         0
## 9926: 2014    10  31      658        -2     1017        10         0
## 9927: 2014    10  31     1913        -2     2212       -16         0
## 9928: 2014    10  31     1530         1     1839       -11         0
##       carrier tailnum flight origin dest air_time distance hour min
##    1:      AA  N5CFAA    172    EWR  MIA      161     1085   16  55
##    2:      AA  N5DMAA   1205    EWR  MIA      154     1085    6   7
##    3:      AA  N3AGAA   1623    EWR  MIA      157     1085   11  25
##    4:      UA  N491UA    244    EWR  MIA      155     1085   15  33
##    5:      UA  N476UA    308    EWR  MIA      162     1085   21  30
##   ---                                                              
## 9924:      AA  N3AMAA   2283    LGA  MIA      157     1096   13  48
## 9925:      AA  N3LFAA   2287    LGA  MIA      150     1096    9  50
## 9926:      AA  N3HNAA   2451    LGA  MIA      156     1096    6  58
## 9927:      AA  N3LFAA   2455    LGA  MIA      156     1096   19  13
## 9928:      US  N768US   1715    LGA  MIA      164     1096   15  30

3. 和参数j, by一起使用

提取满足origin=“LGA”和dest=“TPA”这两个条件的数据集,并且只返回arr_delay列。

key(flights)
## [1] "origin" "dest"
flights[.("LGA","TPA"),.(arr_delay)]
##       arr_delay
##    1:         1
##    2:        14
##    3:       -17
##    4:        -4
##    5:       -12
##   ---          
## 1848:        39
## 1849:       -24
## 1850:       -12
## 1851:        21
## 1852:       -11

chaining表达式,链式表达式。

对上边的表达式生成的数据集,进一步对arr_delay按照降序排列。

flights[.("LGA","TPA"),.(arr_delay)][order(-arr_delay)]
##       arr_delay
##    1:       486
##    2:       380
##    3:       351
##    4:       318
##    5:       300
##   ---          
## 1848:       -40
## 1849:       -43
## 1850:       -46
## 1851:       -48
## 1852:       -49

j参数运算

找出从LGA到TPA的到达航班最长延迟时间。

flights[.("LGA","TPA"),.(max(arr_delay))]
##     V1
## 1: 486

可以跟降序排列的第一行结果对比验证。两者应该是一致的。

利用主键,把hour列中的24替换为0。注意,替换后,由于主键列的内容发生变化,hour的主键特性消失。

setkey(flights,hour)
key(flights)
## [1] "hour"
flights[.(24),hour:=0L]
flights[,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
## [24] 23
key(flights)
## NULL

用参数by进行聚合

每个月从“JFK”起飞航班的最大起飞延误时间,按照月排序。注意用到了keyby参数。

setkey(flights,origin,dest)
flights[.("JFK")]
##        year month day dep_time dep_delay arr_time arr_delay cancelled
##     1: 2014     7   2       40       280      336       282         0
##     2: 2014     1  13     1957         3     2254       -13         0
##     3: 2014     1  14     1955        -5     2226       -42         0
##     4: 2014     1  16     1953        -1     2232       -35         0
##     5: 2014     1  17     1946        -8     2208       -59         0
##    ---                                                               
## 81479: 2014     8   9     2201        31       55        34         0
## 81480: 2014     8  10     2211        41       40        19         0
## 81481: 2014     8  21     2243        73      152        91         0
## 81482: 2014     4  15     2347       257      245       250         0
## 81483: 2014     8  16     2323       113      159        98         0
##        carrier tailnum flight origin dest air_time distance hour min
##     1:      B6  N520JB     65    JFK  ABQ      244     1826    0  40
##     2:      B6  N583JB     65    JFK  ABQ      262     1826   19  57
##     3:      B6  N641JB     65    JFK  ABQ      254     1826   19  55
##     4:      B6  N828JB     65    JFK  ABQ      248     1826   19  53
##     5:      B6  N531JB     65    JFK  ABQ      240     1826   19  46
##    ---                                                              
## 81479:      B6  N591JB    725    JFK  TPA      128     1005   22   1
## 81480:      B6  N585JB    725    JFK  TPA      128     1005   22  11
## 81481:      B6  N564JB    725    JFK  TPA      133     1005   22  43
## 81482:      B6  N588JB    225    JFK  TPA      156     1005   23  47
## 81483:      B6  N590JB    725    JFK  TPA      131     1005   23  23
flights.max.dep_delay.per.month <- flights[.("JFK"),max(dep_delay),keyby=month]
head(flights.max.dep_delay.per.month)
##    month   V1
## 1:     1  881
## 2:     2 1014
## 3:     3  920
## 4:     4 1241
## 5:     5  853
## 6:     6  798

4. 两个重要的参数mult和nomatch

mult参数

用来设置返回的行数, first只返回第一行,last返回最后一行,all返回所有行。

flights[.("JFK","MIA"),mult="first"]
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     1   1      546         6      853         3         0      AA
##    tailnum flight origin dest air_time distance hour min
## 1:  N5CGAA   2243    JFK  MIA      157     1089    5  46
flights[.("JFK","MIA"),mult="last"]
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     2   3     2301       331      210       315         0      AA
##    tailnum flight origin dest air_time distance hour min
## 1:  N360AA   2351    JFK  MIA      146     1089   23   1
flights[.("JFK","MIA"),mult="all"]
##       year month day dep_time dep_delay arr_time arr_delay cancelled
##    1: 2014     1   1      546         6      853         3         0
##    2: 2014     1   2      544         4      915        25         0
##    3: 2014     1   4      554        14      902        12         0
##    4: 2014     1   9      538        -2      837       -13         0
##    5: 2014     1  10      539        -1      842        -8         0
##   ---                                                               
## 2746: 2014    10   4     2137       142       33       125         0
## 2747: 2014    10  16     2145       150       38       130         0
## 2748: 2014     8  10     2231       302      126       277         0
## 2749: 2014     8  30     2231       196      112       164         0
## 2750: 2014     2   3     2301       331      210       315         0
##       carrier tailnum flight origin dest air_time distance hour min
##    1:      AA  N5CGAA   2243    JFK  MIA      157     1089    5  46
##    2:      AA  N5DTAA   2243    JFK  MIA      167     1089    5  44
##    3:      AA  N5ENAA   2243    JFK  MIA      162     1089    5  54
##    4:      AA  N640AA   2243    JFK  MIA      161     1089    5  38
##    5:      AA  N5CKAA   2243    JFK  MIA      158     1089    5  39
##   ---                                                              
## 2746:      AA  N5EWAA   2387    JFK  MIA      151     1089   21  37
## 2747:      AA  N5EKAA   2387    JFK  MIA      142     1089   21  45
## 2748:      AA  N5FSAA   2351    JFK  MIA      138     1089   22  31
## 2749:      AA  N5BSAA   2387    JFK  MIA      133     1089   22  31
## 2750:      AA  N360AA   2351    JFK  MIA      146     1089   23   1

nomatch参数

指定在没有找到符合条件的数据的情况下,是返回NA呢,还是跳过(不返回)。默认是返回NA,如果想跳过,设置nomatch=0L。JFK+XNA不匹配数据库中的任何一条记录,因此在第一种方式中跳过,在第二种方式中用NA补充。

flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult="last", nomatch = 0L]
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     5  23     1803       163     2003       148         0      MQ
## 2: 2014     2   3     1208       231     1516       268         0      EV
##    tailnum flight origin dest air_time distance hour min
## 1:  N515MQ   3553    LGA  XNA      158     1147   18   3
## 2:  N14148   4419    EWR  XNA      184     1131   12   8
flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult="last", nomatch = NA]
##    year month day dep_time dep_delay arr_time arr_delay cancelled carrier
## 1: 2014     5  23     1803       163     2003       148         0      MQ
## 2:   NA    NA  NA       NA        NA       NA        NA        NA    <NA>
## 3: 2014     2   3     1208       231     1516       268         0      EV
##    tailnum flight origin dest air_time distance hour min
## 1:  N515MQ   3553    LGA  XNA      158     1147   18   3
## 2:    <NA>     NA    JFK  XNA       NA       NA   NA  NA
## 3:  N14148   4419    EWR  XNA      184     1131   12   8

5. 二分法搜索vs向量搜索

探讨一下为什么设置主键后,速度会提升。
创建一个2000万行三列的数据。

set.seed(1234)
N <- 2e7L
DT <-  data.table(x=sample(letters,N,TRUE),
                  y=sample(1000L,N,TRUE),
                  val=runif(N),key=c("x","y"))
print(object.size(DT),units="Mb")
## 381.5 Mb
key(DT)
## [1] "x" "y"
head(DT)
##    x y       val
## 1: a 1 0.5370339
## 2: a 1 0.8148401
## 3: a 1 0.5479537
## 4: a 1 0.2060227
## 5: a 1 0.1166678
## 6: a 1 0.3066754

上边代码产生了一个380M的数据集,用来测试和比较设置主键后的性能优化效果。

现在要提取x=a 和 y=100的行。来比较直接向量提取和利用主键来做的性能差异。

require(microbenchmark)
microbenchmark(DT[x=="a" & y==100L], DT[.("a",100L)])
## Unit: milliseconds
##                      expr    min      lq     mean  median      uq    max
##  DT[x == "a" & y == 100L] 2.3902 2.53205 2.747706 2.64780 2.91935 3.5221
##          DT[.("a", 100L)] 1.3863 1.50180 1.699601 1.62685 1.80665 4.3145
##  neval
##    100
##    100

看起来,设置主键后的确是快一些。 看看给出的解释:中文地址在这里

向量扫描

  • 在所有两千条数据中,逐行搜索 x列里值为“g”的行。这会生成一个有两千行的逻辑向量,根据和x列的批评结果,它每个元素的取值可能是TRUE, FALSE 以及 NA。
  • 相似的,在所有两千条数据中,逐行搜索 y列里值为“877”的行,再保存在另一个逻辑向量里面。
  • 操作符“&”对上面两个逻辑向量进行“且”运算,返回结果为TRUE的行 这就是所谓的“向量扫描”。效率非常低,特别是数据量很大、需要重复subset的时候。因为它每次不得不对整个数据全盘扫描。

二分法搜索

这里有一个简单的示例。看看下面这组排过序的数字: 1, 5, 10, 19, 22, 23, 30 假设我们希望找到数字1的位置,用二分法搜索(因为这组数字是排过序的),我们是这么做的:

  • 从中间的数开始,它是19,不是1,而且 1<19。
  • 既然我们要找的数字1小于19,那它应该排在19前面。所以我们可以无视19后面的那一半数据,因为它们都大于19.
  • 现在我们的数据只剩下1, 5, 10。再找到中间的数5,它不是1,而且 1<5。
  • 现在我们的数据只剩下1。符合条件。这就是我们要找的数。 相反的,向量扫描需要扫描所有的数字,在这个例子中是7。

用主键也就是二分法搜索的时间开销是O(logn),而用向量扫描,时间开销是O(n)。当n(数据行数)非常大时,性能会差别很大。