主键
什么是主键
在前面我们讨论了如何用“i”进行提取子集的方法,这节我们采用另一种方法,用主键(key)来提取子集。
开始,我们先看一个data.frame,每个data.frame都有一个行名称,先看下面一个data.frame DF :
> set.seed(1L)
> DF = data.frame(ID1 = sample(letters[1:2], 10, TRUE),
+ ID2 = sample(1:3, 10, TRUE),
+ val = sample(10),
+ stringsAsFactors = FALSE,
+ row.names = sample(LETTERS[1:10]))
> DF
ID1 ID2 val
C a 3 5
D a 1 6
E b 2 4
G a 1 2
B b 1 10
H a 2 8
I b 1 9
F b 2 1
J a 3 7
A b 2 3
> rownames(DF)
[1] "C" "D" "E" "G" "B" "H" "I" "F" "J" "A"
我们可以通过它的行名提取这一行,如下:
> DF["C", ]
ID1 ID2 val
C a 3 5
行名或多或少就像数据框中行的一个变量,但是:
1 每行只能有一个行名,但是,打个比方,每个人至少有两个名字,first name 和second name,就如查找一个电话簿,先查second name 再查first name ,这是非常有用的。用数据框就做不到这个,因为他只能支持一个行名称。
2 行名还是唯一的。不能出现重复的行名。
> rownames(DF) = sample(LETTERS[1:5], 10, TRUE) #行的名称不能重复
Error in `row.names<-.data.frame`(`*tmp*`, value = value) :
不允许有重复的'row.names'
In addition: Warning message:
non-unique values when setting 'row.names': ‘C’, ‘D’
现在我们将DT转化为data.table:
> DT = as.data.table(DF)
> DT
ID1 ID2 val
1: a 3 5
2: a 1 6
3: b 2 4
4: a 1 2
5: b 1 10
6: a 2 8
7: b 1 9
8: b 2 1
9: a 3 7
10: b 2 3
> rownames(DT)
[1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10"
1 现在行名已经被重置
2 Data.table不使用行名。因为data.table是衍化自data.frame,它仍旧保留着行名的特性,但是从来不使用行名,一会儿再探讨其中的原因。如果你想保留行名,可以使用keep.rownames = TRUE 在转化过程中,as.data.table()。它会产生一个新的列rn,内容是列名。
主键和它的特性
1.我们可以设置多个主键,在多维数据中,而且数据类型可以是字符型、整型、数字型、因子型,但不支持列表和混合类型
2.不强制主键的唯一性。复制主键是被允许的。
3.设置主键,可以做两件事:
a.对列进行升序排列
b.将这些列设置为主键列,data.table根据主键列进行排序,sorted()
因为行是重新排序的,所以至多只有一个主键,因为不能进行排序。
设置、取得和使用主键
1.如何设置data.table flights中的列origin为主键
> setkey(flights, origin) #这在交互界面下使用,飞铲更方便
> head(flights[,-(1:6)])
arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: 0 0 AA N3DEAA 119 EWR LAX 339 2454 18 24
2: -17 0 AA N5CFAA 172 EWR MIA 161 1085 16 55
3: 185 0 AA N471AA 300 EWR DFW 214 1372 16 11
4: -2 0 AA N4WNAA 320 EWR DFW 214 1372 14 49
5: -10 0 AA N5DMAA 1205 EWR MIA 154 1085 6 7
6: -17 0 AA N491AA 1223 EWR DFW 215 1372 9 49
另一种方法:
setkeyv(flights, "origin") #在函数中使用比较方便
设置完主键后,flights被自动修改,并不会产生中间data.table,因此,非常的有效率,且节省空间。
在建立data.table时也可以直接设置主键,采用参数key,内容是列名字,字符型向量。
设置“*”和“:=”
设置origin为主键以后,可以按照行提取子集:
> flights[.("JFK"),-(1:6)]
arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: 13 0 AA N338AA 1 JFK LAX 359 2475 9 14
2: 13 0 AA N335AA 3 JFK LAX 363 2475 11 57
3: 9 0 AA N327AA 21 JFK LAX 351 2475 19 2
4: 1 0 AA N319AA 117 JFK LAX 350 2475 13 47
5: -18 0 AA N323AA 185 JFK LAX 338 2475 21 33
---
81479: -21 0 UA N596UA 512 JFK SFO 337 2586 17 5
81480: -37 0 UA N568UA 514 JFK SFO 344 2586 18 27
81481: -33 0 UA N518UA 535 JFK LAX 320 2475 17 53
81482: -38 0 UA N512UA 541 JFK SFO 343 2586 9 24
81483: -38 0 UA N590UA 703 JFK LAX 323 2475 11 24
flights[J("JFK")]
flights[list("JFK")]
flights["JFK"]
flights[c("JFK", "LGA")]
如何返回data.table中被设置为主键的列?
> key(flights)
[1] "origin"
如何同时设置两列为主键?
> setkey(flights, origin, dest)
> head(flights[,-(1:6)])
arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: -25 0 EV N11547 4373 EWR ALB 30 143 7 24
2: 79 0 EV N18120 4470 EWR ALB 29 143 23 13
3: 211 0 EV N11184 4373 EWR ALB 32 143 15 26
4: 19 0 EV N14905 4551 EWR ALB 32 143 7 55
5: 42 0 EV N19966 4470 EWR ALB 26 143 8 17
6: 62 0 EV N19966 4682 EWR ALB 31 143 23 1
> key(flights)
[1] "origin" "dest"
它首先根据列"origin"进行排序,然后再根据列"dest"进行排序。比如origin第一个值是EWR,则对应的dest下面的值依次是ALB,B…,C…,一直到z,然后origin取值B,dest取值ewr,b,c等等。
根据主键列origin=JFK且dest=MIA筛选所有的行
> flights[.("JFK", "MIA"),-(1:6)]
arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: -17 0 AA N5FJAA 145 JFK MIA 161 1089 15 9
2: -8 0 AA N5DWAA 1085 JFK MIA 166 1089 9 17
3: -1 0 AA N635AA 1697 JFK MIA 164 1089 12 27
4: 3 0 AA N5CGAA 2243 JFK MIA 157 1089 5 46
5: -12 0 AA N397AA 2351 JFK MIA 154 1089 17 36
---
2746: -22 0 AA N5FNAA 2351 JFK MIA 148 1089 16 59
2747: -20 0 AA N5EYAA 1085 JFK MIA 146 1089 8 26
2748: -17 0 AA N5BTAA 1101 JFK MIA 150 1089 6 47
2749: -12 0 AA N3ETAA 2299 JFK MIA 150 1089 5 42
2750: 4 0 AA N5FSAA 2387 JFK MIA 146 1089 19 44
这个语句内部是如何工作的呢?
首先匹配JFK,筛选出子集,然后再匹配MIA,得出两者都匹配的数据集。
选择满足第一个主键origin="JFK"的所有行
> key(flights)
[1] "origin" "dest"
> flights[.("JFK")] ## or in this case simply flights["JFK"], for convenience
arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: 4 0 B6 N766JB 65 JFK ABQ 280 1826 20 11
2: 161 0 B6 N507JB 65 JFK ABQ 252 1826 22 15
3: 6 0 B6 N652JB 65 JFK ABQ 269 1826 20 6
4: -15 0 B6 N613JB 65 JFK ABQ 259 1826 20 9
5: 32 0 B6 N598JB 65 JFK ABQ 267 1826 20 39
---
81479: -18 0 DL N915AT 2165 JFK TPA 142 1005 8 0
81480: -8 0 B6 N516JB 225 JFK TPA 149 1005 19 32
81481: -22 0 B6 N334JB 325 JFK TPA 145 1005 14 43
81482: -5 0 B6 N637JB 925 JFK TPA 149 1005 9 57
81483: -18 0 B6 N595JB 1025 JFK TPA 145 1005 8 31
选择满足第二个主键dest="MIA"的所有行
> flights[.(unique(origin), "MIA")]
因为不能跳过第一个主键,所以,需要把第一个主键的值设置为唯一。----目前,还没有想的通其中过的道理。
结合主键,j\by
a.选择j
选择满足主键分别等于“LGA”,“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
也可以使用以下代码,效果与上面一样:
> flights[.("LGA","TPA"),"arr_delay",with=FALSE]
b.链接
在a结果的基础上,对列进行排序结果如下:
> 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
c.对j进行计算
在满足origin = "LGA" and dest = "TP"的子集上,计算arr_delay的最大值
> flights[.("LGA","TPA"),max(arr_delay)]
[1] 486
d.通过引用进行子集替代,在 j 上使用:=
查找所有不同的时间(hour)
> 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个不同时间,0和23都在,现在用0代替23。这次的方法是,用主键实现:
> setkey(flights, hour)
> key(flights)
[1] "hour"
> flights[.(23), hour := 0L]
> key(flights)
NULL
> 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
当用0代替23后,hour列已经不再按照顺序排列,所以,key(flights),返回值为NULL
e.使用“by”分组
先设置主键:
> setkey(flights,origin,dest)
> key(flights)
[1] "origin" "dest"
计算origin=“JFK”,每月dep_delay的最大值,并按月排序
> ans <- flights["JFK", max(dep_delay), keyby = month]
> head(ans)
month V1
1: 1 881
2: 2 1014
3: 3 920
4: 4 1241
5: 5 853
6: 6 798
> key(ans)
[1] "month"
结果显示,主键变为month.
3)附加的参数“mult”、“nomatch”
a.mult参数
返回满足条件的第一行
> flights[.("JFK", "MIA"), mult = "first"]
year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin dest air_time
1: 2014 1 1 546 6 853 3 0 AA N5CGAA 2243 JFK MIA 157
返回满足条件的最后一行
> flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last"]
year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin dest air_time distance hour min
1: 2014 5 23 1803 163 2003 148 0 MQ N515MQ 3553 LGA XNA 158 2: NA NA NA NA NA NA NA NA NA NA NA JFK XNA NA
3: 2014 2 3 1208 231 1516 268 0 EV N14148 4419 EWR XNA 184
没有满足JFK 和 XNA 的行,故返回值都是NA
b.nomatch参数
> flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", nomatch = 0L]
year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin dest air_time 1: 2014 5 23 1803 163 2003 148 0 MQ N515MQ 3553 LGA XNA 158 2: 2014 2 3 1208 231 1516 268 0 EV N14148 4419 EWR XNA 184
nomatch 默认值是NA,当设置nomatch=0L后,不匹配的列都被跳过,不予显示。因此,只返回了两行结果。
4)二进制检索与矢量扫描
> flights[.("JFK", "MIA")]
> flights[origin == "JFK" & dest == "MIA"]
比较两种检索方法,
> t1 <- system.time(ans1 <- DT[x == "g" & y == 877L])
> t1
用户 系统 流逝
0.33 0.28 10.16
> dim(t1)
NULL
> dim(ans1)
[1] 761 3
> t2 <- system.time(ans2 <- DT[.("g", 877L)])
> t2
用户 系统 流逝
0.01 0.00 0.13
> dim(ans2)
[1] 761 3
第二种比第一种速度提高了78倍。很明显了!
原因:
第一种即矢量扫描,所有的数据都要遍历一遍,且返回逻辑值,TRUE or FALSE ,两列,然后再比较两列是否相等。速度很长的慢。
第二种,即二进制检索,以以下的例子为证:
1, 5, 10, 19, 22, 23, 30
数据已经被排序了,同主键的特征,如果要寻找值等于1 的行,
(1)从中位数19开始,19==1?否,19>1
(2)所以大于19的值都不再进行检索
(3)选取1到19的中位数5,5==1?否,5>1
(4)选取1-5的数,1==1?是,完成。
遍历次数比第一种方法少很多,因此速度提升很快。
所以,通过使用关键词进行检索,是非常高效的。