data.table 教程5-数据拆分和合并

目录:

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

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


数据拆分和合并

这一讲我们学习reshaping函数 melt 和 dcast 原本的用法,以及从R语言 v1.9.6版以后,函数 melt 和 dcast 新扩展的功能(它们能操作多个列)。


数据

我们在讲解的时候直接加载数据。

介绍

data.table的函数melt 和 dcast 是增强包reshape2里同名函数的扩展。
在这一讲,我们会:

  • 首先,简单看一下原先的函数 melt 和 dcast,它们是如何reshaping一个data.table。
  • 然后,了解一下当前的功能是如何变得冗长而且低效。
  • 最后,学习一下改进之后的函数 melt 和 dcast 如何同时处理多个列。

扩展后的功能符合data.table的设计哲学:运行高效,语法简明。

注意:

从R语言 v1.9.6版以后,你再也不需要加载增强包 reshape2 了,只需要加载 data.table。如果你已经加载了 reshape2 来处理矩阵或者data.frame,那么一定要确保在这之后再加载 data.table。

1.原生的melt/dcast

a) 函数melt

假设我们有下面这样的data.table:

DT = fread("melt_default.csv")
DT
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA
## dob stands for date of birth.

str(DT)
# Classes 'data.table' and 'data.frame':    5 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5
#  $ age_mother: int  30 27 26 32 29
#  $ dob_child1: chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ dob_child2: chr  "2000-01-29" NA "2004-04-05" "2009-08-27" ...
#  $ dob_child3: chr  NA NA "2007-09-02" "2012-07-21" ...
#  - attr(*, ".internal.selfref")=<externalptr>

-转化DT,使得每个小孩的出生信息都独占一条数据
我们可以对函数 melt() 指定参数 id.vars 和 measure.vars 来实现

DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"),
        measure.vars = c("dob_child1", "dob_child2", "dob_child3"))
DT.m1
#     family_id age_mother   variable      value
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA
str(DT.m1)
# Classes 'data.table' and 'data.frame':    15 obs. of  4 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "dob_child1","dob_child2",..: 1 1 1 1 1 2 2 2 2 2 ...
#  $ value     : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  - attr(*, ".internal.selfref")=<externalptr>

说明:

  • 参数 measure.vars 指定了想要拆分(或合并)的列。
  • 我们也可以指定索引而不是列名。
  • 默认的,variable列是 factor(因子)类型的。如果你想返回一个字符型的向量,可以将参数 variable.factor 设为 FALSE。参数 variable.factor 是data.table的函数melt() 里独有的,增强包reshape2 里面没有这个参数。
  • 默认的,转化果的列被自动命名为 variable 和 value。
  • 在结果里,函数melt() 保持了原来列的属性。

-分别将 variable列和 value列重命名为 child 和 dob

DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"),
           variable.name = "child", value.name = "dob")
DT.m1
#     family_id age_mother      child        dob
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA

说明:

  • 默认的,参数id.vars 或 measure.vars 中的一个省略了,剩余的列自动被赋值给省略的那个参数。
  • 如果参数id.vars 和 measure.vars 都没有指定,所有不是numberic/integer/logical的列都会被赋值给 id.vars。另外,系统还会输出一条警告消息,提示那些列被认为是 id.vars。

b) 函数cast

在前面一节,我们知道如何分拆数据。这一节,我们学习相反的操作。
-如何将刚刚分拆的 DT.m 还原成 DT
也就是,我们想把每个家庭/母亲的所有小孩,都合并到同一行里。我们可以像下面这样使用函数 dcast()。

dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA

说明:

  • 函数 dcast() 使用了操作符“~”,左边是作为 id.vars 的列,右边是作为 measure.vars 的列。
  • 参数 value.var 指定了需要被分拆扩张的列。
  • 函数 dcast() 也会在结果中尽量保持原来的属性。

-对于 DT.m,如何知道每个家庭有几个小孩
可以给函数 dcast() 的参数 fun.aggregate 传递一个函数。当操作符“~”不方便指定列名的时候,这个功能特别有用。

dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob")
#    family_id .
# 1:         1 2
# 2:         2 1
# 3:         3 3
# 4:         4 3
# 5:         5 2

输入 ?dcast 可以查看其他参数和例子的说明。

2.原生的melt/dcast的局限

到目前为止,我们学习了函数 melt 和 dcast 的功能,它们是基于增强包 reshape2 的。但是因为使用了data.table的内部机制(快速排序,二分法搜索等),所以能有效地对data.table实行。
然而,也有一些情况,我们想做的操作无法写得很简洁。比如,考虑下面这个data.table:

DT = fread("melt_enhanced.csv")
DT
#    family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# 1:         1         30 1998-11-26 2000-01-29         NA             1             2            NA
# 2:         2         27 1996-06-22         NA         NA             2            NA            NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
# 5:         5         29 2000-12-05 2005-02-28         NA             2             1            NA
## 1 = female, 2 = male

如果你想用我们到目前为止学过的知识,将每个孩子的 dob 和 gender 合并到一行中,得这样做:

DT.m1 = melt(DT, id = c("family_id", "age_mother"))
# Warning in melt.data.table(DT, id = c("family_id", "age_mother")): 'measure.vars' [dob_child1,
# dob_child2, dob_child3, gender_child1, gender_child2, gender_child3] are not all of the same
# type. By order of hierarchy, the molten data value column will be of type 'character'. All measure
# variables not of type 'character' will be coerced to. Check DETAILS in ?melt.data.table for more on
# coercion.
DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)]
DT.c1 = dcast(DT.m1, family_id + age_mother + child ~ variable, value.var = "value")
DT.c1
#     family_id age_mother  child        dob gender
#  1:         1         30 child1 1998-11-26      1
#  2:         1         30 child2 2000-01-29      2
#  3:         1         30 child3         NA     NA
#  4:         2         27 child1 1996-06-22      2
#  5:         2         27 child2         NA     NA
#  6:         2         27 child3         NA     NA
#  7:         3         26 child1 2002-07-11      2
#  8:         3         26 child2 2004-04-05      2
#  9:         3         26 child3 2007-09-02      1
# 10:         4         32 child1 2004-10-10      1
# 11:         4         32 child2 2009-08-27      1
# 12:         4         32 child3 2012-07-21      1
# 13:         5         29 child1 2000-12-05      2
# 14:         5         29 child2 2005-02-28      1
# 15:         5         29 child3         NA     NA

str(DT.c1) ## gender column is character type now!
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 1 1 2 2 2 3 3 3 4 ...
#  $ age_mother: int  30 30 30 27 27 27 26 26 26 32 ...
#  $ child     : chr  "child1" "child2" "child3" "child1" ...
#  $ dob       : chr  "1998-11-26" "2000-01-29" NA "1996-06-22" ...
#  $ gender    : chr  "1" "2" NA "2" ...
#  - attr(*, ".internal.selfref")=<externalptr> 
#  - attr(*, "sorted")= chr  "family_id" "age_mother" "child"

问题:

  1. 我们想做的是,分别将每个孩子的 dob 和 gender 合并到一行。但是我们先把所有的东西都拆分开了,再将它们合并。很容易看出,这太过迂回和低效了。
    类似的,想想你的壁橱里有4架子的衣服,你想把第1架和第2架的衣服全都放到第1架上,把第3架和第4架的衣服全都放到第3架上。我们刚刚做的事情,就像把4架衣服都放一起,再分开放到第1架和第3架上!
  2. 需要被整合的列可能是不同的类型,在这个例子里面,是字符型和整型。使用函数melt 的时候,这些列被硬塞到结果里面,正如str(DT.c1)的警告消息所提示的,gender列被转化成了字符型。
  3. 我们将variable拆分成了两列,因此额外多了一列,这样做的目的真是非常模糊。我们这么做是因为下一步我们需要转化这一列。
  4. 最后,我们整合了数据。但是问题是我们引入很多操作。特别是,必须要计算等式中变量的顺序,代价太大。

事实上,base::reshape 有简单的写法来实现这个操作。它非常有用,而且经常被低估。你应该试试!

3.增强的新功能

a) 增强的melt

既然我们希望简单地实现同样的操作,我们实现了一个额外的功能,这样就可以同时操作多个列。
-用函数melt 同时拆分多个列
这个办法很简单。我们给参数 measure.vars 传递一个列表,这个列表的每个元素包含需要被合并的列。

colA = paste("dob_child", 1:3, sep = "")
colB = paste("gender_child", 1:3, sep = "")
DT.m2 = melt(DT, measure = list(colA, colB), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

str(DT.m2) ## col type is preserved
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "1","2","3": 1 1 1 1 1 2 2 2 2 2 ...
#  $ dob       : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ gender    : int  1 2 2 1 2 2 NA 2 1 1 ...
#  - attr(*, ".internal.selfref")=<externalptr>

-函数 patterns()
通常,我们想整合的这些列的列名都有共通的格式。我们可以用函数patterns()指定正则表达式,让语法更简洁。上面的操作还可以这样写:

DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

就是这样!

  • 如果需要,我们可以去掉 variable列。
  • 这个功能是用C实现的,因此效率高,节省内存,而且简洁。

b) 增强的dcast

非常好!现在我们可以同时拆分多个列了。现在我们如何将上面的 DT.m2 再恢复成原来的样子呢?
如果我们使用原生的函数dcast(),我们需要做两次,然后将结果合并在一起。但是这样做太麻烦,一点也不简洁和有效。
-同时合并多个 value.vars
我们可以对函数dcast()指定多个 value.var参数,这样操作就在内部进行,而且高效。

## new 'cast' functionality - multiple value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender"))
DT.c2
#    family_id age_mother      dob_1      dob_2      dob_3 gender_1 gender_2 gender_3
# 1:         1         30 1998-11-26 2000-01-29         NA        1        2       NA
# 2:         2         27 1996-06-22         NA         NA        2       NA       NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02        2        2        1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21        1        1        1
# 5:         5         29 2000-12-05 2005-02-28         NA        2        1       NA

说明:

  • 在结果中,原先的属性会尽量保持。
  • 所有的事情都在内部高效处理。快速并且节省内存。

参数fun.aggregate可以指定多个函数:

你可以给函数dcast()的参数fun.aggregate可以指定多个函数。详细内容请执行 ?dcast 来查看示例。