-->

Published on July 14, 2024 by DengKai

R语言 数据处理

阅读需要 76 分钟

前言

Static Badge data.table是R语言中一个非常重要的包,它可以极大地提高数据处理的效率。其对大型数据的处理速度不仅远远强于baseR的data.frame,也显著强于tidyverse中的tibble,同样强于python中的pandas,本文将介绍data.table的一些基本用法。

这篇笔记原始参考自知乎Dwzb,原文分为了两个章节,个人觉得写得非常好,获益匪浅,然而一些地方对于我来说有些绕,不太容易理解,做了一些修改和删除。

概述

data.table包是一个超高性能处理包,在数据处理上代码异常简洁,速度非常快。 data.table的使用基本上是基于[ ]的,它不仅能覆盖基础函数[ ]的功能,还有许多强大的其他功能。比如它们都使用[ ]来提取,而data.table中分组、计算等全都是基于[ ]的。

快速了解baseR中的[]与data.table中的[]区别

  • 要想使用data.table的功能,首先数据框的类型要转换为data.table,否则使用的就是data.frame的[]功能
  • dft[1,2]这样取一个值,dft中结果仍是DT数据框,而DF取出来自动降维成一个数(提取点)
  • dft[2]这样只接一个数时,这里取的是第二行,基础函数取的是第二列。这样改方便对行的排序和筛选(即不需要多加一个逗号)(见提取一行)
  • 使用列名可以直接用名称,不需要加引号(见按名称提取提取)
  • 这里在[]中使用这个数据框里的列名,不需要加$(见根据逻辑值提取和排序)
  • 增加了使用!反选的删除方法(见删除行列)

由以上可知,data.table的逻辑和tidyverse的逻辑类似,都是相较于baseR的[]更加简洁,更加方便。减少了$的使用,增加了!的删除功能,选择列的时候不需要加”“,使得代码更加简洁。

提取数据

创建data.table示例数据

library(data.table)
# 创建一个数据框
name1 <- c("Bob","Mary","Jane","Kim")
name2 <- c("Bob","Mary","Kim","Jane")
weight <- c(60,65,45,55)
height <- c(170,165,140,135)
birth <- c("1990-1","1980-2","1995-5","1996-4")
accept <- c("no","ok","ok","no")
dft <- data.table(name1,weight,height,accept)

提取点

# 提取点
dft[1,2] # data.table中这样提取后不会降维,仍是DT数据框,而DF取出来自动降维成一个数值。
# data.table中提取一个值
dft[[1,2]]

运行代码后输出如下:

> dft
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok
3:   Jane     45    140     ok
4:    Kim     55    135     no

> dft[1,2] %>% class()
[1] "data.table" "data.frame"
> dft[[1,2]] %>% class()
[1] "numeric"

提取行

dft[1] # 只有数字,没有逗号分隔时默认是提取行
dft[1,] # 也可以加逗号
dft[1:2] # 提取多行
dft[c(1,3)] # 提取指定多行

输出如下:

> dft[1] # 只有数字,没有逗号分隔时默认是提取行
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no

> dft[1,] # 也可以加逗号
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no

> dft[1:2] # 提取多行
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok

> dft[c(1,3)] # 提取指定多行
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Jane     45    140     ok

提取列

dft[,2] # 这里返回的是一个DT,与基础函数中的向量不同
dft[[3]] # 这样只接一个数时提取列,会降维,返回一个向量
dft[["weight"]] 
dft$weight # 以上都是返回一个向量

dft[,"weight"]
dft[,weight] # 注意区别,配合下面例子做出解释
dft[,c("weight","height")]
dft[,weight:accept] # 返回一个数

小结:在data.table中提取单列时,列名带”“时不会降维,而不带”“时会降维。如果是提取多列则不会降维; 如果不想加引号,可以在提取时将列名转换为list(),list()又可以简写为一个点.(),所以dft[,”weight”]等价于dft[,list(“weight”)],等价于dft[,.(weight)]。依次运行上述代码输出如下:

> dft[,"weight"]
   weight
    <num>
1:     60
2:     65
3:     45
4:     55

> dft[,list(weight)]
   weight
    <num>
1:     60
2:     65
3:     45
4:     55

> dft[,.(weight)]
   weight
    <num>
1:     60
2:     65
3:     45
4:     55

with参数

data.table[]中引用列名可以不加引号,但是这也产生了如何区分我们输入的字符串是代表列名还是代表变量名的问题,此时引入with参数。当with=FALSE时,输入的字符串将被视为变量名,当with=TRUE时,输入的字符串将被视为列名。

> dft <- data.table(name1,weight,height,accept)
> name1 <- "weight"
> dft[,name1] # 提取出来的是name1列
[1] "weight" "weight" "weight" "weight"
> dft[,name1,with=F] # 提取出来的是weight列
   weight
    <num>
1:     60
2:     65
3:     45
4:     55

> k <- "weight" # 如果使用的变量名不是dft中的列名
> dft[,k] # 报错,因为此处默认寻找变量名空间为这个dft
Error: j (the 2nd argument inside [...]) is a single symbol but column name 'k' is not found. If you intended to select columns using a variable in calling scope, please try DT[, ..k]. The .. prefix conveys one-level-up similar to a file system path.

> dft[,k,with=F] # 提取出来的是weight列
   weight
    <num>
1:     60
2:     65
3:     45
4:     55

除了用with参数,还可以用双中括号[[]]来提取作为变量的列。在[[]]中不加引号时,会将输入的字符串视为变量名,加引号时视为列名。如果字符串没有加引号而且不存在这个变量,则会报错。

> dft # 数据框
    name1 weight height accept
   <char>  <num>  <num> <char>
1: weight     60    170     no
2: weight     65    165     ok
3: weight     45    140     ok
4: weight     55    135     no

> dft[[name1]] # 双括号中字符串默认视为变量名,提取后会降维
[1] 60 65 45 55
> dft[['weight']] # 双括号中加“”则视为列名,好理解
[1] 60 65 45 55
> dft[[weight]] # 如果将列名不加引号放在双括号中则报错(没有这个变量)
Error in .subset2(x, i, exact = exact) : no such index at level 1
> name1 <- c("Bob","Mary","Jane","Kim") # 恢复数据,不要干扰到后面的代码

如何理解with参数?个人理解是,with的意思是询问你是否把字符串当作列名,默认为True,所以当with=FALSE时,输入的字符串将被视为变量名,当with=TRUE时,输入的字符串将被视为列名。

其次,提取列时,可以用索引值,也可以直接指明列名,索引值不会降维,但是直接指明列名的值则会降维,除非加上引号则不降维,如dft[,2]代表第二列,返回数据框,dft[,weight]也代表第二列,返回向量,dft[,”weight”]也代表第二列,返回数据框,等价于dft[,2]。理解这一点的关键在于提取(或叫做索引)的结果是否会降维,而是否降维则取决于提取所用的条件是索引值还是实际的值。不加引号的weiht相当于条件列名 == weight,而加引号的相当于返回列名 == "weight"的索引值。

根据逻辑值提取行

逻辑值返回的是TRUE或FALSE,也就是索引值,按照前面的理解则可以知道不降维,所以返回的依旧是数据框。

> dft[weight > 40] # 不需要dft$weight
    name1 weight height accept
   <char>  <num>  <num> <char>
1: weight     60    170     no
2: weight     65    165     ok
3: weight     45    140     ok
4: weight     55    135     no

> dft[weight>40&height<170]
    name1 weight height accept
   <char>  <num>  <num> <char>
1: weight     65    165     ok
2: weight     45    140     ok
3: weight     55    135     no

> dft[c(T,F,T,T)]
    name1 weight height accept
   <char>  <num>  <num> <char>
1: weight     60    170     no
2: weight     45    140     ok
3: weight     55    135     no

> dft[,c(T,F,T,F)] # 列不可以根据逻辑值提取
[1]  TRUE FALSE  TRUE FALSE

on参数

使用on参数提取某一列是某一个值的行。 或者简单理解为on就是作用于列,而“,”作为[ ]中行和列的分隔也很好理解,更符合直觉和习惯。

dft["Bob",on="name1"]  # name1列中值为Bob的行
dft["Bob",on=.(name1)] # 等价于上面的写法
dft[name1=="Bob"] # 等价于上面的写法,类似于dplyr中的filter
dft[c("Bob","Mary"),on="name1"] # 多列查找,也是类似于dplyr中的filter,多个条件筛选。
dft[!"Bob",on="name1"] # 删除name1列中值为Bob的行
dft[.("Bob",60),on=.(name1,weight)] # 查找name1列中值为Bob且weight列中值为60的行,相当于以字典的形式设置多个筛选条件。

小结:on参数相当于显式的指定了列名,整体类似于dplyr中的filter,可以指定多个条件筛选。 比较特殊的是字符串前直接加!表示取反,以及字符串前加.()表示以字典的形式设置多个筛选条件。

dft[.("Bob",c(55,60)),on=.(name1,weight)] # 找不到这样的行则创造一个满足这两列,其他列设为NA
dft[.("Bob",c(55,60)),on=.(name1,weight),nomatch=0] # 找不到也不返回缺失值
dft[.("Mary",c(65,55)),on=.(name1,weight),roll=-Inf] # 回滚到上一个可能值填充

删除和排序

删除行和列

dft
dft[-c(2,3)] # 删除第23行
dft[,-c(2,3)] # 删除第23列
dft[,c(2,3)] <- NULL;dft # 删除第23列

以下是data.table比较特殊的功能

# 用!和- 删除索引值的多列以及列名的单列
dft[!2:3]
dft[,!"weight"] 
dft[,-"weight"]
# 用!和- 删除列名多列
dft[,-c("weight","height")]
dft[,!c("weight","height")]

排序

dft <- data.table(name1,weight,height,accept)
dft[order(weight)] # 注意,不需要dft$weight
dft[order(weight),] # 加一个逗号指明排序针对行,相同效果
setcolorder(dft,rev(names(dft))) # 接受重新排列的列名将列排序

这里的rev(names(dft))是将列名反转,而不是数据框,setcolorder()函数则按照列名的顺序排序将数据框进行了真正的排序。

计算

DT中的计算也是在[]中完成的,包括分组也只是通过加了一个参数. data.table在计算方面的使用思路。在[]中接三个参数

  • 第一个指定哪些行要加入计算, 即 i
  • 第二个指定要进行什么样的计算, 即j
  • 第三个指定按照哪个变量来分组计算,即by

<img src = “https://picx.zhimg.com/70/v2-9a53ab2eabd91f5f2962de98ee6568d3_1440w.avis?source=172ae18b&biz_tag=Post” alt = “dt” width = 50% />

普通计算

dft <- data.table(name1,weight,height,accept)
dft[,sum(weight)] # 在第二个参数位置指明要对那一列做什么样的操作
dft[,weight] # 这一条提取操作其实也可以看成是使用它本身输出,不进行其他计算
dft[,c(summary(weight),mean(weight))] # 用向量方式展示结果
dft[,.(wm=mean(weight),ws=sum(weight))] # 对同一列计算多种,并指定计算结果列名
dft[1:2,summary(weight)] # 对前两行的weight做描述性统计
dft["Bob", weight-10, on="name1"] # 筛选计算

data.table的计算仅在[]中完成,不需要额外的函数,直接使用运算符即可。

w <- dft[,c("weight","accept")]
dft[,w[,sum(weight)],by=accept]

里面的w在计算,展示时虽然看起来按照accept分组,实际上计算结果没有按照by分组,只是按照w的行进行了计算。即只是在内部的[]中完成计算,并没有真正的分组。 以上代码输出如下:

> w <- dft[,c("weight","accept")]
> dft[,w[,sum(weight)],by=accept]
   accept    V1
   <char> <num>
1:     no   225   #这里的225是根据weight列的总和计算的,并没有按照accept分组
2:     ok   225

分组计算

dft <- data.table(name1,weight,height,accept)
dft[,sd(weight),by=accept]
dft[,sd(weight),keyby=accept] # 按照accept的顺序排列
dft[,sd(weight),by=accept][order(accept)] # 与上面等价
dft[,mean(weight),by=height>150] # 对计算之后的变量分组

按照多列分组

DT = data.table(x=rep(c("b","a","c"),each=6), y=c(1,3,6), v=1:18)
DT[,sum(v),by=x]
DT[,sum(v),by=y]
DT[,sum(v),by=.(x,y)]
DT[,sum(v),by=c("x","y")]
DT[,sum(v),by=.(x,y)][,sum(V1),by=x]

合并计算

> dt1
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok
3:   Jane     45    140     ok

> dt2
    name1     V2 friend
   <char> <char> <char>
1:    Bob 1990-1   Mary
2:   Mary 1980-2    Kim
3:    Kim 1995-5   Jane

> dt1[dt2,on="name1"] # 按照相同列名融合两个数据框,保留dt2中name1的所有值
    name1 weight height accept     V2 friend
   <char>  <num>  <num> <char> <char> <char>
1:    Bob     60    170     no 1990-1   Mary
2:   Mary     65    165     ok 1980-2    Kim
3:    Kim     NA     NA   <NA> 1995-5   Jane

> dt2[dt1,on="name1"] # 这里保留dt1的,dt2中没有的填上NA
    name1     V2 friend weight height accept
   <char> <char> <char>  <num>  <num> <char>
1:    Bob 1990-1   Mary     60    170     no
2:   Mary 1980-2    Kim     65    165     ok
3:   Jane   <NA>   <NA>     45    140     ok

> dt2[dt1,on="name1",nomatch=0] # 取交叉部分
    name1     V2 friend weight height accept
   <char> <char> <char>  <num>  <num> <char>
1:    Bob 1990-1   Mary     60    170     no
2:   Mary 1980-2    Kim     65    165     ok
> dt1[!dt2,on="name1"] # 取dt2没有的部分
    name1 weight height accept
   <char>  <num>  <num> <char>
1:   Jane     45    140     ok

> dt1[dt2,on=.(name1==friend)] # 当要融合的内容列名不相同时,用==匹配在一起
    name1 weight height accept i.name1     V2
   <char>  <num>  <num> <char>  <char> <char>
1:   Mary     65    165     ok     Bob 1990-1
2:    Kim     NA     NA   <NA>    Mary 1980-2
3:   Jane     45    140     ok     Kim 1995-5

> dt1[dt2,on="name1==friend"] # 与上等价
    name1 weight height accept i.name1     V2
   <char>  <num>  <num> <char>  <char> <char>
1:   Mary     65    165     ok     Bob 1990-1
2:    Kim     NA     NA   <NA>    Mary 1980-2
3:   Jane     45    140     ok     Kim 1995-5

小结:以上代码类似于left_join()函数、inner_join()、merge()等函数的功能。

Key

> dft  #DT不能通过行名来提取,只能通过索引值即行号来提取
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok
3:   Jane     45    140     ok
4:    Kim     55    135     no

> rownames(dft) # 显示似乎有“行名”,其实只是索引值
[1] "a" "b" "c" "d"
> rownames(dft) <- letters[1:4] # 通过赋值方法尝试赋予行名
> rownames(dft) # 看似赋值了行号
[1] "a" "b" "c" "d"
> dft # 但是这样输出发现还没有变
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok
3:   Jane     45    140     ok
4:    Kim     55    135     no

DT把data.frame的行名当成一列读进去,通过设置key来指定该列为行名。这样做的好处是,不止可以指定这一列,任意一列都可以,还可以指定多列。

ndt <- copy(dft) # 为了和原数据框对比,创建一个新的
setkey(ndt,name1)
ndt # 发现数据框自动按照name1这一列进行排序了
# 如果想去掉key,则setkey(ndt,NULL)

# 实现通过行名提取
ndt["Bob",weight] 
ndt["Bob","weight"]
ndt["Bob",2]
dft["Bob",2,on="name1"] # key 相当于使用了on
ndt["Bob"]

# 注意,提取数字时不同于字符串,因为DT[60]这样的语法是提取第60行,而不是weight为60的行,需要用J()函数
setkey(ndt, weight)
ndt[60] # 认为是提取第60行
ndt[.(60)] # 下面两个才是提取weight为60的行,
ndt[J(60)] # 注意J()函数的用法,它可以把数字转化为向量,然后作为索引值

当我们把data.frame数据框转化成data.table时,默认抛弃行名,不过我们也可以用一个参数保留行名成为新的一列

> df1 <- data.frame(weight,height,row.names = name1)
> dt1 <- as.data.table(df1)
> dt2 <- as.data.table(df1,keep.rownames=T) # 将原来数据框中的行名当成一列,列名为rn
> dt1;dt2
   weight height
    <num>  <num>
1:     60    170
2:     65    165
3:     45    140
4:     55    135
       rn weight height
   <char>  <num>  <num>
1:    Bob     60    170
2:   Mary     65    165
3:   Jane     45    140
4:    Kim     55    135

> as.data.table(df1,keep.rownames = "rownames") # 自己指定新增列的列名
   rownames weight height
     <char>  <num>  <num>
1:      Bob     60    170
2:     Mary     65    165
3:     Jane     45    140
4:      Kim     55    135

setkey()与setkeyv()

name1 <- "weight"
setkey(ndt,name1) # 设置name1这一列为key 
ndt # 观察这里的排序和下面一个的区别
setkeyv(ndt,name1) # 设置name1这个变量指向的weight这一列为key 
setkey(ndt,name1,weight) # 设置两个key,setkey()针对的是列名
setkeyv(ndt,c("name1","weight")) #setkeyv()针对的是变量(向量)

小结:setkey()与setkeyv()的作用都是设置DT的key,但作用的对象类型不同。setkey()针对的是列名,setkeyv()针对的是变量。

检查Key的函数

haskey(ndt) # 返回TF值,检查是否有Key
key(ndt) # 检查它的key是什么

使用key来辅助计算

> ndt
Key: <name1, weight>
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Jane     45    140     ok
3:    Kim     55    135     no
4:   Mary     65    165     ok

> setkey(ndt,accept)
> ndt["ok",sum(weight)] # 指定计算accept为ok的weight之和
[1] 110
> ndt[c("ok","no"),sum(weight)] # 全部合在一起算
[1] 225
> ndt[c("ok","no"),sum(weight),by=.EACHI] # 分类算
   accept    V1
   <char> <num>
1:     ok   110
2:     no   115

> ndt[accept,sum(weight),by=.EACHI] # 每一类计算完,保留所有行输出
   accept    V1
   <char> <num>
1:     no   115
2:     ok   110
3:     ok   110
4:     no   115

> ndt[unique(accept),sum(weight),by=.EACHI] # 只显示和类数相同的行
Key: <accept>
   accept    V1
   <char> <num>
1:     no   115
2:     ok   110
> ndt[,sum(weight),by=accept] # 上面等价于分组计算
Key: <accept>
   accept    V1
   <char> <num>
1:     no   115
2:     ok   110
3:     ok   110
4:     no   115

我们可以发现,key的作用相当于设定on参数。计算时使用key,再指定计算哪些行,by=.EACHI,可以实现和分组计算一样的功能。

设置多个key

setkey(ndt,name1,weight)
ndt[.("Kim",50:60)] # 不匹配的全部显示NA
ndt[.("Kim",50:60),nomatch=0] # 不匹配的删除掉
ndt[.("Kim",50:60),roll=T] # 没有信息的用上面的添
ndt[.("Kim",50:60),roll=Inf] # 和上一条一样
ndt[.("Kim",50:60),roll=-Inf] # 没有信息的用下面的添
ndt[!"Kim"]
ndt[!.("Kim",56)]

融合重铸

data.table包改写了reshape2包中的融合重铸功能。melt和dcast函数都有改进。 这一部分的内容对应于tidyr包中的pivot_longer()和pivot_wider()函数。

值得一提的是,在reshape2和data.table包中都有melt()函数,这取决于接收的数据是data.frame还是data.table。二者存在的区别主要在于几个方面:

  1. 得到的数据是否转化为因子factor。
    • reshape2中的factorsAsStrings参数可以控制value列是否为因子型,而variable列则自动为因子型
    • data.table中通过variable.factor和value.factor分别控制两列是否为因子型
  2. 多种数据类型融合问题 下面以示例数据说明,具体的示例数据的构建略过。
> DT
     i_1   i_2    f_1   f_2    c_1        d_1        d_2
   <int> <num> <fctr> <ord> <char>     <Date>     <Date>
1:     1    NA      a     z      c 2013-09-02 2012-01-07
2:     2     6      c     a      c 2013-09-03 2012-01-06
3:     3     7      b     x   <NA> 2013-09-04 2012-01-05
4:     4     8   <NA>     c      c       <NA> 2012-01-04
5:     5     9      c     x   <NA> 2013-09-05 2012-01-03
6:    NA    10      b     x      c 2013-09-06 2012-01-02

我们可以看到,这个数据框中各列的数据类型是不相同的,总结一下是这样

  • i前缀的列是数值型
  • f前缀的列是因子型
  • c前缀的列是字符型
  • d前缀的列是时间型
  • l前缀的列是列表

选择相同类型的数据融合在一起,也就是相同的前缀融合在一起。两个包都是使用了measure.vars参数来控制要被融合的列

> DT # 相当于tidyr中的pivort_longer(),宽数据变长数据原教程这里写得太绕。
     i_1   i_2    f_1   f_2    c_1        d_1        d_2       l_1       l_2
   <int> <num> <fctr> <ord> <char>     <Date>     <Date>    <list>    <list>
1:     1    NA      a     z      c 2013-09-02 2012-01-07         1     c,c,c
2:     2     6      b     a   <NA> 2013-09-03 2012-01-06 2,2,2,2,2        NA
3:     3     7      c     x      b 2013-09-04 2012-01-05       3,3 b,b,b,b,b
4:     4     8      b     c      a       <NA> 2012-01-04   4,4,4,4     a,a,a
5:     5     9      b     x      b 2013-09-05 2012-01-03         5     b,b,b
6:    NA    10      c     x      a 2013-09-06 2012-01-02     NA,NA       a,a

> melt(DT, id=3:4, measure=c("d_1", "d_2")) # measure参数指定需要变换为长数据的列
       f_1   f_2 variable      value
    <fctr> <ord>   <fctr>     <Date>
 1:      a     z      d_1 2013-09-02
 2:      b     a      d_1 2013-09-03
 3:      c     x      d_1 2013-09-04
 4:      b     c      d_1       <NA>
 5:      b     x      d_1 2013-09-05
 6:      c     x      d_1 2013-09-06
 7:      a     z      d_2 2012-01-07
 8:      b     a      d_2 2012-01-06
 9:      c     x      d_2 2012-01-05
10:      b     c      d_2 2012-01-04
11:      b     x      d_2 2012-01-03
12:      c     x      d_2 2012-01-02

> melt(DT, id=1:2, measure=list(3:4, c("d_1", "d_2"))) #原教程说这里用3:4不能用列名其实不对
      i_1   i_2 variable value1     value2
    <int> <num>   <fctr> <char>     <Date>
 1:     1    NA        1      a 2013-09-02
 2:     2     6        1      b 2013-09-03
 3:     3     7        1      c 2013-09-04
 4:     4     8        1      b       <NA>
 5:     5     9        1      b 2013-09-05
 6:    NA    10        1      c 2013-09-06
 7:     1    NA        2      z 2012-01-07
 8:     2     6        2      a 2012-01-06
 9:     3     7        2      x 2012-01-05
10:     4     8        2      c 2012-01-04
11:     5     9        2      x 2012-01-03
12:    NA    10        2      x 2012-01-02

> melt(DT, id=1:2, measure=list(c("f_1","f_2"), c("d_1", "d_2"))) # 一样可以用列名,这和list无关
      i_1   i_2 variable value1     value2
    <int> <num>   <fctr> <char>     <Date>
 1:     1    NA        1      a 2013-09-02
 2:     2     6        1      b 2013-09-03
 3:     3     7        1      c 2013-09-04
 4:     4     8        1      b       <NA>
 5:     5     9        1      b 2013-09-05
 6:    NA    10        1      c 2013-09-06
 7:     1    NA        2      z 2012-01-07
 8:     2     6        2      a 2012-01-06
 9:     3     7        2      x 2012-01-05
10:     4     8        2      c 2012-01-04
11:     5     9        2      x 2012-01-03
12:    NA    10        2      x 2012-01-02

> # DT可以搭配使用正则表达式快速选择需要变换的列
> melt(DT, id=1:2, measure=patterns("^f_", "^d_"), value.factor=TRUE)
      i_1   i_2 variable value1     value2
    <int> <num>   <fctr>  <ord>     <Date>
 1:     1    NA        1      a 2013-09-02
 2:     2     6        1      b 2013-09-03
 3:     3     7        1      c 2013-09-04
 4:     4     8        1      b       <NA>
 5:     5     9        1      b 2013-09-05
 6:    NA    10        1      c 2013-09-06
 7:     1    NA        2      z 2012-01-07
 8:     2     6        2      a 2012-01-06
 9:     3     7        2      x 2012-01-05
10:     4     8        2      c 2012-01-04
11:     5     9        2      x 2012-01-03
12:    NA    10        2      x 2012-01-02

> melt(DT, id=1:2, measure=patterns("l_", "c_"), na.rm=TRUE) # 移除NA值,得到5行,原本是12行
     i_1   i_2 variable  value1 value2
   <int> <num>   <fctr>  <list> <char>
1:     1    NA        1       1      c
2:     3     7        1     3,3      b
3:     4     8        1 4,4,4,4      a
4:     5     9        1       5      b
5:    NA    10        1   NA,NA      a

在melt()两个关键参数id.vars和measure.vars中,如果缺省,则默认选取所有列。

新列的命名

融合之后得到一列自动命名为variable和value,如果我想自己指定名字,就使用variable.name和value.name两个参数,在这个功能上,两个包没有区别。类似于tidyr中的value_name和variable_name参数。

DT
# 这里value.var参数需要指定,默认value=guess(data),也就是函数会从DT的列名中猜测。
# 最好一次到位直接指定更好。
dcast(DT,v1~v2,mean,value.var = "v4") # 注意参数值需要加引号的列名
# margin参数实测无论是赋予布尔运算值T或F,还是0,1,或者列名,均无变化,参数说明里也似乎说还没有发挥作用。
dcast(DT,v1~v2,mean,margins=T, value.var = "v4") # 3*4的矩阵,多出了对每行每列求的均值
dcast(DT,v1~v2,mean,margins=F,value.var = "v4") # margins参数目前无区别
dcast(DT,v1~v2,mean,margins=1,value.var = "v4") # margins参数目前无区别
dcast(DT,v1~v2,mean,margins="v1",value.var = "v4") # margins参数目前无区别

drop参数

可以理解为将 v1 和 v3 的所有唯一值视为一个集合,生成所有可能的组合,并在结果数据表中显示这些组合,当drop为F的时候则去除原数据中不存在的组合

> dcast(DT,v1+v3~v2,drop=F,value.var = "v4") 
Key: <v1, v3>
      v1     v3         1        2          3
   <int> <fctr>     <num>    <num>      <num>
1:     1      1 0.4360063 1.539886  2.1829996
2:     1      2        NA       NA         NA
3:     1      3 0.7571990 1.539394 -0.9953758
4:     2      1 0.4360063 1.539886  2.1829996
5:     2      2        NA       NA         NA
6:     2      3 0.7571990 1.539394 -0.9953758

data.table同时变换多列

dcast(DT,v1~v3,mean,value.var=c("v4","v2")) # v3中的元素分别和v2和v4组合,生成四列
dcast(DT,v1~v3,fun=list(sum, mean),value.var="v2") # 同时使用两种计算函数生成四列
dcast(DT,v1~v3,fun=list(sum, mean),value.var=c("v4","v2")) # 二者结合,生成8列
dcast(DT,v1~v3,fun=list(sum, mean),value.var=list("v4","v2")) # v4的使用sum,v2的使用mean,生成4列

小结:这个功能只是比baseR强一些(具体不是特别了解baseR是否能实现多列操作),但是tidyr中的pivot_longer()同样实现了这个功能,而且更容易理解。

特殊符号

添加、更新和删除 := 符号

这个符号可以实现在本身直接更改,而无需产生一个新的数据框,再赋值给原本相同的变量名

> dft
    name1 weight height accept
   <char>  <num>  <num> <char>
1:    Bob     60    170     no
2:   Mary     65    165     ok
3:   Jane     45    140     ok
4:    Kim     55    135     no

> dft[,u:=1] # 添加一个全是1的列
> dft[,height:=1:4] # 更改height列
> dft
    name1 weight height accept     u
   <char>  <num>  <int> <char> <num>
1:    Bob     60      1     no     1
2:   Mary     65      2     ok     1
3:   Jane     45      3     ok     1
4:    Kim     55      4     no     1
dft[,`:=`(m=1:4,n=3:6)] # 使用:=函数的真正调用方式
dft[,weight:=NULL] # 删除weight列
dft[,c("m","n"):=NULL] # 删除多列
dft[2,height:=22][] # 只修改一个值,加一个[]返回得到的数据框
dft["Bob",accept:="yes",on="name1"] # 通过逻辑判断修改
dft[,m:=mean(height),by=accept] # 增加一个列,这个列根据分组计算得出

注意

# 注意一点
dft[name1=="Bob"][,height:=13][] # :=作用在提取之后的数据框,所以对原数据框没有改变
dft
# 使用一个指向字符串的变量作为新名称
a <- "aa"
dft[,a:=1][] # 使用a作为列名
dft[,(a):=2][] # 使用aa作为列名

.N 符号

.N 代表行的数量,用by参数分组时则是每一组的行数量

dft[.N-1] # 返回倒数第二行
dft[,.N] # 返回数据框一共有几行(放在第二个参数位置表示计算并输出结果)
dft[,.N,by=accept] # 分组计算行数

.SD 符号

.SD 代表整个数据框,用by参数分组时则是每一组的数据框

dft <- data.table(name1,weight,height,accept)
dft[,print(.SD),by=accept]
dft[,head(.SD,1),by=accept]
dft[,.SD[2],by=accept]
dft[,lapply(.SD[,c(-1,-4)],sum),by=accept] # 分组多列计算

.SDcols 符号

.SDcols 指定.SD 代表的数据框包括哪些列

dft[,lapply(.SD[,c(-1,-4)],sum),by=accept]
# 下面4条命令和上面那条有相同的效果
dft[,lapply(.SD,sum),by=accept,.SDcols=c("weight","height")] #.SD中只包含这两列
dft[,lapply(.SD,sum),by=accept,.SDcols=weight:height] #用:指定这列到这列之间的所有列
dft[,lapply(.SD,sum),by=accept,.SDcols=2:3]
dft[,lapply(.SD,sum),by=accept,.SDcols=-c(1,4)]

.I 符号

.I 代表行的索引,用by参数分组时则是每一组的索引

dft[,.I[2],by=accept]

.GRP 符号

如果不使用by参数,则为1。使用by,则是组的计数(第一组的值是1,第二组是2).GRP 代表分组的名称,用by参数分组时则是每一组的名称

dft[,grp:=.GRP][]
dft[,grp:=.GRP,by=accept][]

串联操作,避免多余中间变量

dft[weight>50][height>100][order(height)]
# 等价于
tmp <- dft[weight>50]
tmp <- tmp[height>100]
tmp[order(height)]

%between% 范围操作符

# 以下6个等价
dft[weight>=50&weight<=60]
dft[weight %between%c(50,60)]
dft[weight %inrange%c(50,60)]
dft[weight %between% list(rep(50,4),rep(60,4))]
dft[between(weight,50,60)]
dft[inrange(weight,50,60)]

%like% 字符串中含有某个字符

dft[name1%like%"a"]

读写文件

data.table包中的fread和fwrite读写文件的速度非常快,可以处理.txt.csv.dat等文件,下面是最基本的使用方法。

# 读入文件
dataw <- data.table(a=1:10,b=2:11)
fwrite(dataw,"dataw.csv")
fwrite(dataw,"dataw.txt")
fwrite(dataw,"dataw.dat")

fread("dataw.csv") # 读取文件直接用字符串,赋予参数input
fread(file="dataw.csv") # 也可以赋予参数file

fread函数的一些参数

fread(data) # 第一行作为列名,生成一个data.table
fread(data,col.names = letters[1:4]) # 改变列名
fread(data,data.table=F) # 生成data.frame
fread(data,showProgress = T) # 显示进度条
# data.table和showProgress都可以再options里面设置默认值

fread(data, header=F) # header第一行不作为行名
fread(data1,header=T,check.names = T) # 检查,避免数字作为列名,避免两列名重复

# nrow控制读取第几行,默认-1全部读取,0取列名,1只取第一行
fread(data, nrow=0)
fread(data, nrow=1) # 取第一行,一般用这个来检测读进来的形式是否正确
fread(data, nrow=3) # 取前三行


fread(data, select="A") # 只取A这列
fread(data, select=c("A","B")) # 取AB列
fread(data, drop="A") # 取除了A这列