data.table 教程4-二级索引和自动索引

目录:

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

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


感谢G_天星的提醒,貌似现在版本的data.table中还没有setindex()函数,所以可能应该使用set2key()函数。或者通过代码安装data.table试试。


二级索引和自动索引

本教程假定读者已经熟悉data.table的[i, j, by]语法、懂得如何基于二分法的选取了。如果你对这些不熟悉,请学习上面三讲 data.table 介绍语义引用主键、基于快速二分法搜索的subset


数据

我们继续使用已经保存到本地的航班信息flights。

flights <- fread("flights14.csv")
head(flights)
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1        -8       -26      AA    LGA  PBI      157     1035    7
# 5: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 6: 2014     1   1         4         0      AA    EWR  LAX      339     2454   18
dim(flights)
# [1] 253316     11

介绍

在这一讲,我们会:

  • 讨论二级索引。
  • 再次演示快速subset,但这次我们使用新的参数on,它能自动设置二级索引。
  • 最后进一步的,来看一下自动索引,它也能自动设置索引,但是它是基于R的原生语法来做subset的。

1.二级索引

a) 什么是二级索引

二级索引和data.table的主键类似,但有以下两点不同:

  • 它不会再内存里将整个data.table重新排序。它只会计算某列的顺序,将这个顺序向量保存在一个额外的,叫做index的属性里面。
  • 一个data.table可以有多个二级索引,这是我们下面要演示的。

b) 设置和获取二级索引

-如何将origin列设置为该data.table的二级索引

setindex(flights, origin)
head(flights)
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1        -8       -26      AA    LGA  PBI      157     1035    7
# 5: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 6: 2014     1   1         4         0      AA    EWR  LAX      339     2454   18

## alternatively we can provide character vectors to the function 'setindexv()'
# setindexv(flights, "origin") # useful to program with

# 'index' attribute added
names(attributes(flights))
# [1] "names"             "row.names"         "class"             ".internal.selfref"
# [5] "index"

说明:

  • 函数setindex 和 setindexv()可以对data.table添加一个二级索引。
  • 注意flights实际上没有按照origin列的升序重新排列。还记得吗?setkey()会重新排序!
  • setindex(flights, NULL)会删除所有的二级索引。

-如何取得flights的二级索引

indices(flights)
# [1] "origin"

setindex(flights, origin, dest)
indices(flights)
# [1] "origin"       "origin__dest"

说明:

  • 函数indices()返回一个data.table所有的二级索引。如果该data.table没有二级索引,那么返回NULL。
  • 注意我们对 origin列,dest列创建了另一个二级索引的时候,我们没有丢掉之前创建的第一个二级索引。也就是说,我们可以创建多个二级索引。

c) 为什么使用二级索引

-对一个data.table重新排序成本太高
考虑一下这种情况,当你想用主键origin列来subset所有“JFK”的时候,我们得这么做:

## not run
setkey(flights, origin)
flights["JFK"] # or flights[.("JFK")]

说明:

setkey()需要:
a.计算得出origin列的排序向量,并且
b.基于刚刚的排序向量,对整个data.table重新排序

排序并不是最花时间的,因为data.table使用对整型、字符型、数值型的向量进行radix排序。然而重新排序却很花时间。
除非我们需要对某一列重复地进行subset,否则二分法快速subset的高效可能被重新排序抵消。

-为添加/更新列而对整个data.table重新排序并不理想
-最多只能有一个主键
现在我们如果想对dest列是“LAX”的行,重复地进行某个特定的操作,那么我们必须再调用函数setkey() 设置一次主键。

## not run
setkey(flights, dest)
flights["LAX"]

这样,flights又再次按dest列重新排序了。其实我们真正想做的是,快速地subset同时又不必重新排序。
这时候,二级索引就派上用场了!

-二级索引可以被重用
既然一个data.table中可以有多个二级索引,并且创建一个二级索引就和将一个排序向量保存为属性一样简单,那么创建二级索引后,我们可以省下重新排序的时间。
-参数on使得语法更简洁,并且能自动创建并重用二级索引
我们下面一节会说明参数on的几个优点:

参数on

  • 通过创建索引进行subset。每次都能节省setindex()的时间。
  • 通过检查属性,可以简单地重用已经存在的二级索引。
  • 语法简单。
    注意参数on也可以用来指定主键。事实上,为了更佳的可读性,我们鼓励在参数on里面指定主键。

2.使用参数on和索引进行快速subset

a) 参数i里的subset

-subset所有origin是“JFK”的行

flights["JFK", on = "origin"]
#        year month day dep_delay arr_delay carrier origin dest air_time distance hour
#     1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
#     2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
#     3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
#     4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
#     5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21
#    ---
# 81479: 2014    10  31        -4       -21      UA    JFK  SFO      337     2586   17
# 81480: 2014    10  31        -2       -37      UA    JFK  SFO      344     2586   18
# 81481: 2014    10  31         0       -33      UA    JFK  LAX      320     2475   17
# 81482: 2014    10  31        -6       -38      UA    JFK  SFO      343     2586    9
# 81483: 2014    10  31        -6       -38      UA    JFK  LAX      323     2475   11

## alternatively
# flights[.("JFK"), on = "origin"] (or) 
# flights[list("JFK"), on = "origin"]

说明:

  • 这段语句执行的subset也是通过创建二级索引,基于快速二分法搜索的。但记住,它不会把这个二级索引自动创建为data.table的一个属性。当然后面我们也会教你如何将它设置为一个属性。
  • 如果我们已经添加了一个二级索引了,那么参数on就可以直接使用这个二级索引,而不是再对整个航班信息flights进行计算。

我们来看下面 verbose = TRUE 的用法:

setindex(flights, origin)
flights["JFK", on = "origin", verbose = TRUE][1:5]
# names(on) = NULL. Assigning 'on' to names(on)' as well.
# Looking for existing (secondary) index... found. Reusing index.
# Starting bmerge ...done in 0 secs
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21

-如何对origin列和dest列进行subset
举个例子,如果我们想选取所有从“JFK”起飞到达“LAX”的所有航班:

flights[.("JFK", "LAX"), on = c("origin", "dest")][1:5]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21

说明:

  • 在参数i里面指定取值,在参数on里面指定列名。参数on必须是一个字符型的向量。
  • 因为计算索引非常快,所以我们不需要使用setindex()。除非你需要对某一列重复地进行subset操作。

b) 参数j里的select

下面我们将要讨论的所有操作,跟我们在上一讲里面学习的类似。只是我们现在使用参数on。
-返回满足条件 origin = “LGA” and dest = “TPA”的 arr_delay列的值

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

c) Chaining

-在上面的基础上,使用chaining来将结果降序排列

flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")][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

d) 参数j里的计算

-找出满足条件 origin = “LGA” and dest = “TPA”的 arr_delay列的最大值(航班到达的最长延误时间)

flights[.("LGA", "TPA"), max(arr_delay), on = c("origin", "dest")]
# [1] 486

e) 参数j里使用操作符”:=”进行sub-assign

在上一讲中,我们学习过了类似的功能。同样地,现在我们看看如何找到在flights里面,hours列所有可能的取值:

# 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

可以看到,一共有25种不同的取值。但是0点和24点其实是同样的。下面我们把24全部替换成0,但是这次我们使用参数on。

flights[.(24L), hour := 0L, on = "hour"]

现在我们再来看看24是不是都被替换成0了:

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

说明:

  • 这真是二级索引的一大优点。以前,只是为了更新一些行的hour列的取值,我们不得不调用函数setkey()将hour列设置为主键,这必须对整个data.table进行重新排序。但是现在,用参数on,原数据的顺序并没有改变,操作反而更快了!而代码还是如此简洁。

f) 通过参数by聚合

-找到每月从“JFK”起飞的航班起飞的最长延误时间,并按照月份排序

ans <- flights["JFK", max(dep_delay), keyby = month, on = "origin"]
head(ans)
#    month   V1
# 1:     1  881
# 2:     2 1014
# 3:     3  920
# 4:     4 1241
# 5:     5  853
# 6:     6  798

说明:

  • 如果我们不使用二级索引,也就是不在参数on里面指定,那么我们就必须把origin设置为主键。

g) 参数mult

参数mult和上一讲一样。它的默认值是“all”。我们可以选择是第一条还是最后一条符合条件的行被返回。
-subset满足条件dest = “BOS” 和 “DAY”的第一行

flights[c("BOS", "DAY"), on = "dest", mult = "first"]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1         3         1      AA    JFK  BOS       39      187   12
# 2: 2014     1   1        25        35      EV    EWR  DAY      102      533   17

-subset满足条件 origin = “LGA” 或者 “JFK” 或者 “EWR”,并且 dest = “XNA” 的最后一行

flights[.(c("LGA", "JFK", "EWR"), "XNA"), on = c("origin", "dest"), mult = "last"]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014    10  31        -5       -11      MQ    LGA  XNA      165     1147    6
# 2:   NA    NA  NA        NA        NA      NA    JFK  XNA       NA       NA   NA
# 3: 2014    10  31        -2       -25      EV    EWR  XNA      160     1131    6

h) 参数nomatch

如果查询语句没有找到任何匹配的数据,通过指定参数nomatch,我们可以选择是返回 NA,还是忽略。
-在上面这个列子中,忽略没有实际意义的数据

flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", on = c("origin", "dest"), nomatch = 0L]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014    10  31        -5       -11      MQ    LGA  XNA      165     1147    6
# 2: 2014    10  31        -2       -25      EV    EWR  XNA      160     1131    6

说明:

  • 没有航班从“JFK”起飞到达“XNA”,所以结果里面,这一行被忽略了。

3.自动索引

回顾一下,我们先学习如何通过主键使用快速二分法搜索进行subset。接着,我们学习了使用二级索引,它带来更好的效果,而且语法也更简洁。
等等,有没有更好的方法?有!优化R的原生语法,使用内置的索引。这样我们毋需使用新的语法,就能得到同样的效果。
这就是自动索引。
目前,它只支持操作符 == 和 %in% 。而且只对一列起作用。某一列会被自动创建为索引,并且作为data.table的属性保存起来。这跟参数on不同,参数on会每次创建一个临时索引,所以才会被叫做“二级索引”。

让我们创建一个极大的data.table来凸显它的优势。

set.seed(1L)
dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
print(object.size(dt), units = "Mb")
# 114.4 Mb

当我们第一次对某一列使用 == 或者 %in% 的时候,会自动创建一个二级索引,它会被用来进行subset。

# have a look at all the attribute names
names(attributes(dt))
# [1] "names"             "row.names"         "class"             ".internal.selfref"

## run thefirst time
(t1 <- system.time(ans <- dt[x == 989L]))
#    user  system elapsed 
#   0.235   0.013   0.249
head(ans)
#      x         y
# 1: 989 0.5372007
# 2: 989 0.5642786
# 3: 989 0.7151100
# 4: 989 0.3920405
# 5: 989 0.9547465
# 6: 989 0.2914710

## secondary index is created
names(attributes(dt))
# [1] "names"             "row.names"         "class"             ".internal.selfref"
# [5] "index"

indices(dt)
# [1] "x"

第一次subset的时候,就是创建索引的时候。因为创建二级索引只会引入一个排序向量,在很多情况下,这种操作符的方式会比扫描向量快得多。所以,从第二次subset开始,自动索引的优势就非常明显了:

## successive subsets
(t2 <- system.time(dt[x == 989L]))
#    user  system elapsed 
#   0.001   0.000   0.001
system.time(dt[x %in% 1989:2012])
#    user  system elapsed 
#   0.001   0.000   0.001

说明:

  • 第一次subset花了0.228秒,但是第二次只花了0.001秒!
  • 可以通过设置全局参数关闭自动索引:options(datatable.auto.index = FALSE)。

我们正在将二分法搜索扩展到其它的操作符,比如 <, <= 和 >=。完成之后,就能直接用在其他操作符上了。
在将来,我们计划将自动索引扩展到参数中的其它列。

在下一讲“结合和滚动结合”中,我们会学习使用主键和二级索引进行快速subset。