概述

data.table是一个十分有效的数据处理包,它是data.frame的一个扩展,能够快速的对数据进行分片,分组,聚合等操作。data.table在继承data.frame基础数据类型的同时,还具备很多独特而出色的性质,与其他数据类型(如data.frame、tibble/tbl_df)相比具有很多优势。

  • 高效:轻松、快速处理GB级别的大数据,并且融合了SQL数据库的语法风格
  • 极简:只需很短的代码就能完成数据的行、列、分组、合并、重塑等相关操作
  • 丰富:数据类型自带筛选、计算、分组、合并等多种方法,无需借助其他函数

data.table的语法结构:DT[i, j, by]

即对于DT这个data.table,使用 i来处理行,然后计算 j,最后用 by分组。

创建datatable

从向量创建

library(data.table)
options (warn = -1)

name <- rep(c("apple","banana","orange"),each=2)
number <- c(1,3,6)
money <- 1:6
fruit_cp <- data.table(name,number,money)
fruit_cp
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: banana      6     3
## 4: banana      1     4
## 5: orange      3     5
## 6: orange      6     6

从dataframe转换而来

  • setDT()

setDT()适用于对’list’, ‘data.table’,’data.frame’这三种类型,它比as.data.table要快,是以传地址的方式直接修改对象。函数的具体形式如下: setDT(x, keep.rownames=FALSE, key=NULL, check.names=FALSE)

常见属性 描述
x 类型为’list’, ‘data.table’,’data.frame’的数据
keep.rownames data.table没有行名,对于data.frames而言, keep.rownames = TRUE会保留data.frame的行名保存在在新列’‘rn’’中。keep.rownames = “id”,将列名”rn”称改为”id”。
key 传递给setkeyv的一个或多个列名的字符向量。它可以是一个用逗号分隔的字符串,比如key=“x,y,z”,或者是一个由名字组成的向量,比如key=c(“x”,“y”,“z”)。
check.names 逻辑值。如果为TRUE,则检查数据帧中的变量名,以确保它们是语法有效的变量名,并且没有重复。
fruit <- data.frame(name, money, number)
setDT(fruit)# fruit本身变成了data.table,没有复制
class(fruit)
## [1] "data.table" "data.frame"
setDF(fruit_cp) #转回dataframe格式
  • as.data.table()

与setDT()不同,as.data.table()并不会改变数据本身的类型。

fruit <- data.frame(name, number, money)
fruit_cp <- as.data.table(fruit) # 经过了复制
class(fruit) # 其本身类型没有改变
## [1] "data.frame"
class(fruit_cp)
## [1] "data.table" "data.frame"

复制其他datatable

复制数据并命名,因为data.table的部分函数在使用的过程中会直接对原来的数据进行改写,为了防止原来的数据被改变,使用拷贝的文件。

fruit_copy <- data.table::copy(fruit_cp)
fruit_copy
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: banana      6     3
## 4: banana      1     4
## 5: orange      3     5
## 6: orange      6     6

对行进行操作

提取行

  • 普通提取
fruit_cp[1] # 只接一个数是提取行,提取第一行
##     name number money
## 1: apple      1     1
fruit_cp[1:3,] # 也可以加一个逗号,提取第1至3行
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: banana      6     3
fruit_cp[c(1,3),]# 提取第一和第三行
##      name number money
## 1:  apple      1     1
## 2: banana      6     3
  • 条件提取
fruit_cp[name == 'banana',] #name这一列,值为banana的行,逗号可以去掉
##      name number money
## 1: banana      6     3
## 2: banana      1     4
fruit_cp[money > 2 & number <3]#money这一列,数值大于3的行
##      name number money
## 1: banana      1     4
  • like模糊提取用法 like(vector, pattern, ignore.case = FALSE, fixed = FALSE) vector %like% pattern#其他用法

    常见属性 描述
    vector 字符或因子向量
    pattern 匹配的字符
    ignore.case 是否忽略大小写
    fixed logica 是否应该解释为文本字符串(即忽略正则表达式)
fruit_cp[name %like% "e",] #选择name中名字含有e的行 
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: orange      3     5
## 4: orange      6     6
fruit_cp[!(name %like% "e")]#选择name中名字不包含e的行
##      name number money
## 1: banana      6     3
## 2: banana      1     4
  • between提取用法
between(x, lower, upper)
x %between% y

| 常见属性 | 描述                |
|----------|---------------------|
| x        | 任何可排序向量      |
| lower    | 下限范围            |
| upper    | 上限范围            |
| y        | 长度为2的向量或列表 |
fruit_cp[number %between% c(3,7),] #选择number列中数值在3和7之间的行 
##      name number money
## 1:  apple      3     2
## 2: banana      6     3
## 3: orange      3     5
## 4: orange      6     6
fruit_cp[number %between% c(3,7) & money%between% c(3,7)] #同时两列满足条件的行 
##      name number money
## 1: banana      6     3
## 2: orange      3     5
## 3: orange      6     6

删除行

  • 删除行
fruit_cp[-1] #删除第一行
##      name number money
## 1:  apple      3     2
## 2: banana      6     3
## 3: banana      1     4
## 4: orange      3     5
## 5: orange      6     6
fruit_cp[-c(1,3),]#删除第一、三行,逗号可省略
##      name number money
## 1:  apple      3     2
## 2: banana      1     4
## 3: orange      3     5
## 4: orange      6     6
fruit_cp[!(name == 'banana'),]#按条件删除行
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: orange      3     5
## 4: orange      6     6

对列进行操作

修改列名

setnames(x,old,new)

常见属性 描述
x 需要修改列名的datatable
old 要修改的列名
new 新的列名,必须和old中元素个数相同
setnames(fruit_cp,c('name','money'),c('x','z'))#修改第一列和第三列的列名
fruit_cp
##         x number z
## 1:  apple      1 1
## 2:  apple      3 2
## 3: banana      6 3
## 4: banana      1 4
## 5: orange      3 5
## 6: orange      6 6
setnames(fruit_cp,c('x','z'),c('name','money'))#将列名还原

修改列的顺序

setcolorder(x, neworder)

常见属性 描述
x 需要排序的datatable
neworder 新列名排序的字符向量,也可以是列编号
  • 根据列名修改
setcolorder(fruit_cp, c("name", "money", "number"))#新列名顺序
fruit_cp
##      name money number
## 1:  apple     1      1
## 2:  apple     2      3
## 3: banana     3      6
## 4: banana     4      1
## 5: orange     5      3
## 6: orange     6      6
#只选择部分列时,默认将该部分列提至最前
setcolorder(fruit_cp, c( "number", "money"))
fruit_cp
##    number money   name
## 1:      1     1  apple
## 2:      3     2  apple
## 3:      6     3 banana
## 4:      1     4 banana
## 5:      3     5 orange
## 6:      6     6 orange
  • 根据位置修改
setcolorder(fruit_cp, c(3:1))#根据列的位置修改列的顺序
fruit_cp
##      name money number
## 1:  apple     1      1
## 2:  apple     2      3
## 3: banana     3      6
## 4: banana     4      1
## 5: orange     5      3
## 6: orange     6      6
  • 调换两列的位置
DT<-data.table::copy(fruit_cp)
DT[,c('x','y','z'):=list(1,2:7,9)]
DT
##      name money number x y z
## 1:  apple     1      1 1 2 9
## 2:  apple     2      3 1 3 9
## 3: banana     3      6 1 4 9
## 4: banana     4      1 1 5 9
## 5: orange     5      3 1 6 9
## 6: orange     6      6 1 7 9
#调换第1列和第6列的位置
#赋值法
DT1 <- DT[,c(6,2:5,1)]
DT1
##    z money number x y   name
## 1: 9     1      1 1 2  apple
## 2: 9     2      3 1 3  apple
## 3: 9     3      6 1 4 banana
## 4: 9     4      1 1 5 banana
## 5: 9     5      3 1 6 orange
## 6: 9     6      6 1 7 orange
#setcolorder
setcolorder(DT, c(6,2:5,1))
DT
##    z money number x y   name
## 1: 9     1      1 1 2  apple
## 2: 9     2      3 1 3  apple
## 3: 9     3      6 1 4 banana
## 4: 9     4      1 1 5 banana
## 5: 9     5      3 1 6 orange
## 6: 9     6      6 1 7 orange

按列的值排序

  • setorderv(x, cols = colnames(x), order=1L, na.last=FALSE)
常见属性 描述
x 需要排序的datatable
cols 要排序的x列名的字符向量
order 如果为1即升序,-1降序
na.last 如果为TRUE,则将数据中缺少的值放在最后
setorder(fruit_cp, number) #对number进行升序
setorder(fruit_cp, number, -money) #先对number进行升序,然后对money降序
setorderv(fruit_cp, c("number", "money"), c(1, -1)) #效果同上一行

提取列

  • 根据位置提取
#输出结果为data.table
fruit_cp[,2]
##    money
## 1:     4
## 2:     1
## 3:     5
## 4:     2
## 5:     6
## 6:     3
fruit_cp[,c(1,3)]
##      name number
## 1: banana      1
## 2:  apple      1
## 3: orange      3
## 4:  apple      3
## 5: orange      6
## 6: banana      6
#提取多列输出为矩阵
as.matrix(fruit_cp[,c(1,2)])
##      name     money
## [1,] "banana" "4"  
## [2,] "apple"  "1"  
## [3,] "orange" "5"  
## [4,] "apple"  "2"  
## [5,] "orange" "6"  
## [6,] "banana" "3"
fruit_cp[, -2] #显示除第2列以外的数据 
##      name number
## 1: banana      1
## 2:  apple      1
## 3: orange      3
## 4:  apple      3
## 5: orange      6
## 6: banana      6
#输出结果为向量
fruit_cp[[2]]
## [1] 4 1 5 2 6 3
  • 根据列名提取
fruit_cp[,'name']#输出为data.table
##      name
## 1: banana
## 2:  apple
## 3: orange
## 4:  apple
## 5: orange
## 6: banana
fruit_cp[,c(name,number)]#不加引号,输出结果为向量
##  [1] "banana" "apple"  "orange" "apple"  "orange" "banana" "1"      "1"     
##  [9] "3"      "3"      "6"      "6"
fruit_cp[,list(name,money)]#不加引号时用list(),输出结果仍为data.table
##      name money
## 1: banana     4
## 2:  apple     1
## 3: orange     5
## 4:  apple     2
## 5: orange     6
## 6: banana     3
#list()等同于.(),故也可表示为  fruit_cp[,.(name,money)]

#提取单个列,输出结果为向量
fruit_cp[['money']]
## [1] 4 1 5 2 6 3
fruit_cp$money
## [1] 4 1 5 2 6 3
name <- 'number'
fruit_cp[,name]#提取结果为name列
## [1] "banana" "apple"  "orange" "apple"  "orange" "banana"
fruit_cp[,name,with=F]#提取结果为'number'列
##    number
## 1:      1
## 2:      1
## 3:      3
## 4:      3
## 5:      6
## 6:      6
  • 提取值
fruit_cp[2,3]#返回data.table
##    number
## 1:      1
fruit_cp[[1,2]]#返回向量
## [1] 4
fruit_cp[c(1,2,3),c(1,2,3)]#提取前三行、前三列,返回data.table
##      name money number
## 1: banana     4      1
## 2:  apple     1      1
## 3: orange     5      3
fruit_cp[c(1,3),money] #返回向量
## [1] 4 5
fruit_cp[c(1,3),"money"]#返回data.table
##    money
## 1:     4
## 2:     5

添加/删除/更新列/修改列的数据类型

操作符”:=“

操作符”:=“用于添加/更新/删除列。

操作符”:=“,通过引用更新列,会直接改变原始数据。

  • 添加列
#添加一列,数值为3,列名为x
fruit_cp[,x:=3]

#有条件添加列
fruit_cp[name == "apple", y := 7]
fruit_cp[, z := ifelse(money <4, "cheap", "expensive")]

#添加多个列
fruit_cp[, c("m", "n") := list(1, 2)] 

fruit_cp
##      name money number x  y         z m n
## 1: banana     4      1 3 NA expensive 1 2
## 2:  apple     1      1 3  7     cheap 1 2
## 3: orange     5      3 3 NA expensive 1 2
## 4:  apple     2      3 3  7     cheap 1 2
## 5: orange     6      6 3 NA expensive 1 2
## 6: banana     3      6 3 NA     cheap 1 2
  • 删除列

将一列赋值为 NULL,就会删除那一列。删除立即生效。

fruit_cp[,x := NULL]
fruit_cp[,c('y','z','m','n') := list(NULL,NULL,NULL,NULL)]
fruit_cp
##      name money number
## 1: banana     4      1
## 2:  apple     1      1
## 3: orange     5      3
## 4:  apple     2      3
## 5: orange     6      6
## 6: banana     3      6
  • 更新列

操作符”:=“没有返回值,可以在查询语句的最后加一对方括号[],来查看运行的结果。

fruit_cp[name == 'apple', money := 0][]
##      name money number
## 1: banana     4      1
## 2:  apple     0      1
## 3: orange     5      3
## 4:  apple     0      3
## 5: orange     6      6
## 6: banana     3      6
#恢复原始数据
fruit_cp <- data.table::copy(fruit_copy)
  • 修改列的数据类型

as.integer()、as.numeric()、as.character()分别是将数据取整、转换为数字、字符

fruit_cp[, c("e", "f") := list(1, rnorm(6))] #增加一列为1的常数列名为e,增加一列列名为f的随机数
fruit_cp
##      name number money e          f
## 1:  apple      1     1 1 -1.8991547
## 2:  apple      3     2 1 -0.1311543
## 3: banana      6     3 1  1.4181249
## 4: banana      1     4 1 -1.2314630
## 5: orange      3     5 1 -2.5768268
## 6: orange      6     6 1  1.1266451
fruit_cp[, f := as.integer(f)] #让f列变成整数类型
fruit_cp
##      name number money e  f
## 1:  apple      1     1 1 -1
## 2:  apple      3     2 1  0
## 3: banana      6     3 1  1
## 4: banana      1     4 1 -1
## 5: orange      3     5 1 -2
## 6: orange      6     6 1  1
#恢复原始数据
fruit_cp <- data.table::copy(fruit_copy)

计算

DT[ i, j, by]

DT中的计算也是在[]中完成的,包括分组也只是通过加了一个参数,这样做我们可以非常简单地一步实现基础函数或者其他包的很多步才能实现的功能。

i指定哪些行要加入计算

j指定要进行什么样的计算

by指定按照哪个变量来分组计算

普通计算

fruit_cp[, sum(number)] # 在第二个参数位置指明要对那一列做什么样的操作
## [1] 20
fruit_cp[,number]#可以看成是使用它本身输出,不进行其他计算
## [1] 1 3 6 1 3 6
fruit_cp[,c(sum(number),mean(money))] # 进行多种计算,用向量方式展示结果
## [1] 20.0  3.5
fruit_cp[,.(sum(number),mean(money))] # 多种计算,list返回data.table
##    V1  V2
## 1: 20 3.5
fruit_cp[,.(avg=mean(money),s=sum(money))] # 对同一列进行多种计算,并指定计算结果列名
##    avg  s
## 1: 3.5 21
summary(fruit_cp)#描述统计
##      name               number          money     
##  Length:6           Min.   :1.000   Min.   :1.00  
##  Class :character   1st Qu.:1.500   1st Qu.:2.25  
##  Mode  :character   Median :3.000   Median :3.50  
##                     Mean   :3.333   Mean   :3.50  
##                     3rd Qu.:5.250   3rd Qu.:4.75  
##                     Max.   :6.000   Max.   :6.00
fruit_cp[1:5,summary(money)] # 对前5行的weight做描述性统计
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##       1       2       3       3       4       5
fruit_cp['apple', number-1, on="name"] #根据取值,选择特定的行进行计算
## [1] 0 2

分组计算

输出结果均为data.table

  • 按照单列分组
fruit_cp[, name, by = number]#提取name列,根据number排序
##    number   name
## 1:      1  apple
## 2:      1 banana
## 3:      3  apple
## 4:      3 orange
## 5:      6 banana
## 6:      6 orange
fruit_cp[, sd(money),by = name]#根据name分组计算money的标准差
##      name        V1
## 1:  apple 0.7071068
## 2: banana 0.7071068
## 3: orange 0.7071068
fruit_cp[,.(sd=sd(money)),by = name]#对计算后的设置列名需要用list进行封装
##      name        sd
## 1:  apple 0.7071068
## 2: banana 0.7071068
## 3: orange 0.7071068
fruit_cp[,.(mean=mean(money)),by=number>2] # 对计算之后的变量分组
##    number mean
## 1:  FALSE  2.5
## 2:   TRUE  4.0
  • 按照多列分组
fruit_cp[,sum(money),by=number] 
##    number V1
## 1:      1  5
## 2:      3  7
## 3:      6  9
fruit_cp[,sum(money),by=name]
##      name V1
## 1:  apple  3
## 2: banana  7
## 3: orange 11
fruit_cp[,sum(money),by=.(number,name)]#根据两列分组求和 
##    number   name V1
## 1:      1  apple  1
## 2:      3  apple  2
## 3:      6 banana  3
## 4:      1 banana  4
## 5:      3 orange  5
## 6:      6 orange  6
fruit_cp[,money+5,by=.(number,name)][,sum(V1),by=name]#根据两列分组计算后,再将计算结果根据name分组求和
##      name V1
## 1:  apple 13
## 2: banana 17
## 3: orange 21

读写文件

  • fwrite()

fwrite(x, file, append = FALSE, row.names = FALSE, col.names = TRUE)

导出data.table和data.frame。

常见属性 描述
x 任意一组等长向量,可以为data.frame和data.table。如果为矩阵,会被强制删除行名转换为data.table。
file 导出文件的路径名称。
append 如果为TRUE,则在相同文件中添加数据,并自动省略列名。
row.names 如果为TRUE,则导出行名。
col.names 如果为TRUE,则导出列名。
fwrite(fruit_cp,file = 'fruit_cp.csv',row.names = F,col.names = T)
fwrite(fruit_cp,file = 'fruit_cp.csv',append = T)
  • fread()

fread(file, select=NULL, drop=NULL, header=“auto”, data.table = TRUE)

从文件中导入数据。

常见属性 描述
file 导入文件的路径及文件名,也可以是网站的URL。
header 导入的第一行数据是否作为列名,默认为FALSE。
select 包含列名或数字的向量,保留对应的列,并排序。
drop 包含列名或数字的向量,删除对应的列。
data.table TRUE 设置返回为data.table ,FALSE设置返回为 data.frame。
nrows 指定导入前多少行
fread('fruit_cp.csv', select=c('money','name','number'), data.table = TRUE)
##     money   name number
##  1:     1  apple      1
##  2:     2  apple      3
##  3:     3 banana      6
##  4:     4 banana      1
##  5:     5 orange      3
##  6:     6 orange      6
##  7:     1  apple      1
##  8:     2  apple      3
##  9:     3 banana      6
## 10:     4 banana      1
## 11:     5 orange      3
## 12:     6 orange      6
fread('fruit_cp.csv', drop =  c(1,3), data.table = TRUE)
##     number
##  1:      1
##  2:      3
##  3:      6
##  4:      1
##  5:      3
##  6:      6
##  7:      1
##  8:      3
##  9:      6
## 10:      1
## 11:      3
## 12:      6
fread('fruit_cp.csv', nrows = 3) #只导入前3行数据
##      name number money
## 1:  apple      1     1
## 2:  apple      3     2
## 3: banana      6     3

索引

提取时没有使用行名提取这个方法,这是因为data.table没有行名,如果硬要说有,那就是1234,而且不能修改,也不能根据行名来做提取等操作。这不是data.table的漏洞,而是因为它有更强大的操作,根本就不需要使用行名。具体的使用方法是,把data.frame的行名当成一列读进去,通过设置key来指定该列为行名。这样做的好处是,不止可以指定这一列,任意一列都可以,还可以指定多列。

设置索引

setkey(x, verbose=getOption(“datatable.verbose”), physical = TRUE)

常见属性 描述
x A data.table
verbose=getOption(“datatable.verbose”) 列名的字符向量
physical TRUE更改RAM中数据的顺序。FALSE添加索引。
haskey(fruit_cp) #查看是否有索引,如果返回true那么说明存在索引
key(fruit_cp) #查看索引的名称
setkey(fruit_cp, NULL) #将索引设置为null,即去除索引,
haskey(fruit_cp) #查看是否有索引
setkey(fruit_cp, name)  #将索引设置为name
fruit_cp["banana", ] #将索引设置为name后可以通过索引来取值
setkey(fruit_cp, number) #将索引设置为number(列名)
fruit_cp[3,] #取出满足索引为3的行的第一行
fruit_cp[.(3)] ##取出满足索引为3的所有行

setkey(fruit_cp, name, number) #可以设置多个索引 
fruit_cp[.("banana", 1),] #然后取出同时满足索引的行,注意条件要对应着索引写

以索引来计算

setkey(fruit_cp, name)
fruit_cp["apple", sum(number)] #计算索引为apple的行的number的和
## [1] 4

查看出现了几次重复值

rowid(…, prefix=NULL) rowidv(x, cols=seq_along(x), prefix=NULL)

常见属性 描述
x A data.table
长度相同的数字、整数64、字符或逻辑向量序列。
cols x的列名(或数字)的字符向量。
prefix NULL(默认)或长度为1的字符向量,该字符向量以行ID为前缀,返回字符向量(而不是整数向量)。

一个组合出现第几次就显示为几

DT = data.table(x=c(20,10,10,30,30,20), y=c("a", "a", "a", "b", "b", "b"), z=1:6)
DT
rowid(DT$x) # 只看x这一列,第一次出现就为1,第二次出现为2
rowidv(DT, cols="x") # 同上
rowid(DT$x, prefix="group") # 数字前面加"group"
# 返回  "group1" "group1" "group2" "group1" "group2" "group2"
rowid(DT$x, DT$y) # 同时看x,y两列,第一次出现就为1,第二次出现为2
rowidv(DT, cols=c("x","y")) #功能同上一行

提取时间格式中的年月日信息

分割并且高效地转置生成的列表

tstrsplit(x, …, fill=NA, type.convert=FALSE, keep, names=FALSE)

常见属性 描述
x 要拆分(和转置)的向量
要传递给strsplit的所有参数,即以什么符号分割
name <- 1:3
dates <- c("2016-3-4","2016-3-14","2016-3-24")
nd <- data.table(name,dates)
strsplit(dates,"-")
tstrsplit(dates,"-") # 把strsplit得到的结果转置
nd[,c("year","month","day"):=tstrsplit(dates,"-")] # 实现拆分
nd

浅复制和深复制(shallow vs deep copy)

使用R语言基础函数进行数据处理时,常常默认使用的是深复制的方法,当处理数据集较大时,运行速度就会很慢,data.table在一些地方使用了浅复制,极大提高了运行效率。不过浅复制也会有一些副作用,本节后面会进行介绍。

比如我们要修改一个数据框中某一列的值,用R基础函数的[]处理,其实处理之后得到的数据框已经完全不是最初的数据框本身,它是把原有数据框复制出一个完整的备份,再在这个备份上进行修改,修改的过程中,还可能多次复制,这样的复制不仅极大增加了运行时间,同时也非常消耗内存。这就是所谓的深复制。

而data.table在处理的时候,会使用改变后的新值,而其他没改变的内容还是用原来那些,没有重新复制出来使用。虽然也是一个新的数据框,但是只是新创建了一个指针,指向原有的内容。这样不需要把大量数据全部复制一遍,会大大缩短运行时间,这就是浅复制。

而浅复制有一个弊端,就是新数据框合旧数据框都指向同一个内容,只要在一个数据框中把这个内容改变,另外的数据框也会受到影响。这就是copy函数存在的意义,这样深复制一下可以让两个数据框之间互不影响。下面我们用具体的例子来解释

R语言中可以用tracemem函数来跟踪一个变量名指向的地址。地址是变量名指向的内容的存放位置,如果改变数据框时地址发生变化,说明在其他位置复制出了一个一模一样的数据框,新的数据框则使用新产生的那个。因为每次复制数据框,都要分配给它一个新的地址来储存,所以我们可以通过地址变化的次数来反映数据框被复制的次数。

tracemem函数作用在一个变量名上,如果这个变量名指向的地址发生改变,就会print出一条信息。

DF <- data.frame(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18)

# 先测试基础函数的复制情况
tracemem(DF) # 打印出此时地址 "<  >"
DF$c <- 18:13 # 修改数据框,打印出三条更改信息,说明这个过程中,数据框被复制了三次
DF$c[DF$ID == "b"] <- 15:13 # 这样改变则复制了四次
untracemem(DF) # 结束检测

接下来我们测试一下data.table

DT <- as.data.table(DF)
tracemem(DT)
DT[,c:=18:13]
DT["b",c:=15:13,on="ID"]
untracemem(DT)

修改的过程中一次信息都没有print出来,说明没有进行过一次深复制,这是data.table处理高效的原因之一。

浅复制的副作用

上面我们已经说明了data.table的处理方式是浅复制,下面我们用例子说明浅复制中相互影响带来的负面影响。

DT <- data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18)
DD <- DT[,c:=18:13][]
DT;DD # 二者相同
DT["b",c:=15:13,on="ID"]
DT;DD # 二者仍相同,说明改变DT的同时也改变了DD
rm(DT,DD) # 删除变量重新试验

使用copy函数实现复制,不影响原来数据框

DT = data.table(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c = 13:18)
assign_DT <- DT 
copy_DT  <- copy(DT)
DT;assign_DT;copy_DT # 此时三者一样
DT[,c:=18:13] # 改变其中一个
DT;assign_DT # 通过普通赋值符号产生的数据框也跟着改变了
copy_DT # 通过copy深复制才没有被影响
rm(DT,assign_DT,copy_DT)

缺失值的处理

DT <- fread('E:/读书笔记/课程——金融大数据分析/BostonHousing.csv')
head(DT) #查看前几行

anyNA(DT) #判断是否有缺失值,如果显示true说明存在缺失值
# 这个数据集中有缺失值
DT <- fread('E:/读书笔记/课程——金融大数据分析/Cars93.csv')
anyNA(DT)

print(is.na(DT[16])) # Cars93 数据集中第16行数据有否存在缺失值
#获取缺失值所在的位置(行、列值)
output <- c()
for(i in 1:nrow(DT)){
  for(j in 1:ncol(DT)){
    # 获取当前元素
    curr_value <- DT[i, j,with=F][[1]]
    if(is.na(curr_value)){
      # 如果是缺失值,将其位置放入 output 中
      position <- paste0('(', paste(i, j, sep=', '), ')')
      output   <- c(output, position)
    }
  }
}

output

DT[ is.na(DT)] <- 0 #替换缺失值为 0

replace_with_mean <- function(x){ifelse(is.na(x),mean(x, na.rm = TRUE),x)}
output <- DT[,lapply(.SD, replace_with_mean)] #替换缺失值为均值

特殊符号

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

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

dft <- data.table(name1,weight,height,accept)
dft[,u:=1] # 添加一个全是1的列
dft[,height:=1:4] # 更改height列
dft[,c("accept","height"):=.(1:4,2:5)] # 作用于多个列
dft[,`:=`(m=1:4,n=3:6)] # 使用:=函数的真正调用方式
dft[,weight:=NULL] # 删除weight列
dft[,c("m","n"):=NULL] # 删除多列
dft[2,height:=22][] # 只修改一个值,加一个[]返回得到的数据框

dft <- data.table(name1,weight,height,accept)
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] # 分组多列计算

如果想返回多个矩阵,那就使用嵌套list,像把矩阵压缩成一个元素一样,放在DT中。这里要用分组计算,返回矩阵。

下面这个例子是要对分组之后的每个数据框求covariance,计算得到的是列与列两两对应协方差矩阵。

dft[,cov(.SD[,c(-1,-4)]),by=accept] # 矩阵被变成向量
l <- dft[,.(.(cov(.SD[,c(-1,-4)]))),by=accept]
l[[1,2]]

.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 表示(分组后)每一行在原数据框中是第几行

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

.GRP

如果不使用by参数,则为1。使用by,则是组的计数(第一组的值是1,第二组是2)

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

上下合并数据框

使用rbindlist函数,先将数据框转化为list再进行合并

DT1 = data.table(A=1:3,B=letters[1:3])
DT2 = data.table(A=4:5,B=letters[4:5])
DT3 = data.table(B=letters[4:5],A=4:5)
DT4 = data.table(B=letters[4:5],C=factor(1:2))
l1 = list(DT1,DT2)
l2 = list(DT1,DT3)
l3 = list(DT1,DT4)
rbindlist(l1)
rbindlist(l1,idcol=T) # 多出一列,对数据框分组(来自不同数据框)
rbindlist(l2) # 不同列名直接合并
rbindlist(l2,use.names=T) # 将相同列名的合并在一起
rbindlist(l3) # 不同列名直接合并
rbindlist(l3,fill=T) # 选择相同列名合并,不匹配的填入NA

options设置

在控制台中输入options()会打印出一个list,这是当前的options设置值,比如显示保留几位小数等。加载data.table包之后,这里新增了一些data.table专用的参数,可以用下面的命令查看

ops <- options() # ops就是一个list,参数和值的一一对应
# ops$  这样输入在rstudio中就会自动提示后面的参数
# 由于data.table专用参数都是以datatable为前缀,使用我们输入时可以这样
# ops$datatable.  这样输入提示的会都是以datatable为前缀的参数,当然当你打出da的时候就已经差不多全是data.table的参数了
ops$datatable.print.nrows # 查看这个参数,返回100
getOption("datatable.print.topn") # 也可以这样查看,返回5

拿打印行数来举例子,看这样两个参数datatable.print.topn和datatable.print.nrows

d <- data.table(a=1:200, b=2:201)
d # 200行数据自动只输出前5行和后5行
op <- options(datatable.print.topn=10) # 设置打出前10行和后10行
d # 打出前10行和后10行
options(op) # 恢复默认值5

f <- data.table(a=1:50, b=2:51)
f # 50行全打了出来
op <- options(datatable.print.nrows = 30) # 设置行数超过30行时就省略打出
f # 只打出前5行和后5行
options(op) # 恢复默认值100

options设置的内部运行机制:上面打印的参数设置其实调用了print函数,options里面设置的参数被print函数自动调用

?print.data.table # 可以查看打印data.table的函数的帮助文档,发现函数参数设置如下
print(x,
    topn=getOption("datatable.print.topn"),          # default: 5
    nrows=getOption("datatable.print.nrows"),        # default: 100
    class=getOption("datatable.print.class"),  # default: FALSE
    row.names=getOption("datatable.print.rownames"), # default: TRUE
    quote=FALSE,...)
# 所以我们之前在options里面设置的参数都在这里被调用
# 所以我们也可以直接使用print函数来实现和options设置相同的功能
print(d)
print(d,topn=10)
print(f)
print(f,nrows=30)

性能之Secondary indices and auto indexing

使用setkey设置键值方便以后提取,但是它会自动按照键将整个数据框排序,这是是非常耗费时间的。我们可以选择用setindex函数省去这部分时间,同时不损失提取效率。

dft <- data.table(name1,weight,height,accept)
setindex(dft, name1) # 设置按照name1列来索引,但不进行排序
names(attributes(dft)) # 多出了属性index
indices(dft) # 查看现有的index,"name1"
setindex(dft,accept) # 增加一个index
indices(dft) # "name1"  "accept"
setindex(dft,NULL) # 去掉index

dft[name1=="Bob"] # 用==判断提取
indices(dft) # 自动生成index为name1
dft[weight==45] # 这样之后就有两个index了
setindex(dft,NULL) # 去掉index

dft[.(60),on="weight"] # 使用on判断提取
indices(dft) # 不会创建index

使用==进行提取时就已经自动创建了index,所以一般没有必要提前用setindex去设置

而创建index的好处主要是提高运行速度

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

system.time(ans <- dt[.(988L),on="x"]) # 有一定的时间消耗,多次运行这条命令,实现消耗几乎没有区别
system.time(ans <- dt[x == 989L]) # 时间消耗与使用on基本相同
system.time(ans <- dt[x == 1L]) # 几乎没有时间消耗
system.time(ans <- dt[.(988L),on="x"]) # 这时使用on也不耗费了
system.time(ans <- dt[y == 989L]) # 有较大时间消耗
system.time(ans <- dt[y == 9]) # 几乎没有时间消耗
setindex(dt,NULL)
system.time(ans <- dt[x == 1L]) # 仍有一定的时间消耗

# 看普通数据框
df = data.frame(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
system.time(ans <- df[df$x == 1L,]) # 时间消耗比较小,但是每次运行时间相同

可以看到,使用==提取创建了index耗费了一些时间后,第二次提取就几乎不耗费时间了,而用on提取每次都要创建index。下面我们来看一下设置index的耗时,和index与key的对比

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
head(dt)
system.time(setindex(dt,x)) # 0.28 
setindex(dt,NULL) # 这样删除之后再重新加,时间不变
system.time(setindex(dt,x)) # 0.28 

# setkey
system.time(setkey(dt,x)) # setkey多了排序,时间要长一些,0.72
setkey(dt,NULL)
head(dt) # 即使删除后,依然按照x排序
system.time(setkey(dt,x)) # 因为排序仍然保留,所以再重新加时间缩短了非常多,0.03
system.time(setkey(dt,y)) # 时间还是很多
system.time(setkey(dt,x)) # 因为按y排序,x被打乱了,所以这一次时间也延长了

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
system.time(dt[x==2]) # 有一定的时间消耗
setkey(dt,x)
system.time(dt[x==2]) # 几乎不耗费时间
system.time(dt[.(1),on="x"]) # 几乎不耗费时间

总结

也可以通过设置options参数来禁止index的使用,主要有两个参数

dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
op <- options(datatable.auto.index = F) # 使用==时不会自动创建index
system.time(ans <- dt[x == 989L]) # 多次运行,每次消耗时间相同
indices(dt) # NULL
setindex(dt,x)
system.time(ans <- dt[x == 989L]) # 特意设置index还是可以不消耗时间
options(op)op <- options(datatable.use.index = F) # 使用==时不会自动创建index
setindex(dt,x)
system.time(ans <- dt[x == 989L]) # 特意设置index也要消耗时间
indices(dt) # 虽然有index:”x”
options(op)

长表变宽表

dcast()函数可以用于长表变宽表。与宽表变长表相反,长表变宽表将分类变量的若干水平值变成变量(列名)。

dcast()函数用法如下:dcast(data, formula, fun.aggregate = NULL, fill = NULL,drop = TRUE, value.var = guess(data))

set.seed(45)
library(data.table)
DT <- data.table(n_1=rep(c(1:4,NA),each=4),
                 n_2=rep(-2:2,4),
                 n_3=sample(1:100,20),
                 c_1=rep(letters[3:4],each=10),
                 c_2=rep(LETTERS[1:5],4),
                 c_3=sample(letters[1:4],20,replace=T),
                 d_1=sample(as.Date(c(0:5,NA),origin="2021-01-01"),
                            20,replace=T))
#法一:formula,重铸公式的形式为LHS~RHS
#公式左边为单个变量
dcast(DT,c_3~c_1,value.var="n_3")     #聚合函数缺失,默认的聚合函数为计数

#公式左边为多个变量的组合,注意变量组合的顺序
dcast(DT,c_3+c_2~c_1,value.var="n_3")
dcast(DT,c_2+c_3~c_1,value.var="n_3")

#.和...的用法
#.表示没有变量
DT[,c("d_1","n_1","n_2"):=NULL]     #:=通过引用方式增加、修改列
dcast(DT,c_3~.,value.var="n_3")
dcast(DT,c_3+c_2~.,value.var="n_3")
#...表示公式中未提及的所有变量
dcast(DT,...~c_1,value.var="n_3")     #类似于c_2+c_3~c_1,value.var="n_3"


#方法二:参数value.var,指定某列的值将被填充(fill)用来重铸(cast)数据。
dcast(DT,c_3~c_1,value.var="n_3")
dcast(DT,c_3~c_1,value.var="n_2")


#方法三:参数fun.aggregate,用来指定数据的聚合方式。
#默认的聚合方式为计数
dcast(DT,c_3~c_1,value.var="n_3") 

#其他聚合方式
dcast(DT,c_3~c_1,value.var="n_3",fun=sum)
dcast(DT,c_3~c_1,value.var="n_3",fun=mean)
dcast(DT,c_3~c_1,value.var="n_3",fun=max)
dcast(DT,c_3~c_1,value.var="n_3",fun=min)

#方法四:参数drop,用于是否删除未使用的分组-值组合。参数fill,用于填充缺失单元格的值。#默认参数drop=T
dcast(DT,c_3+c_2~c_1,value.var="n_3",fun.aggregate=sum)
#参数drop=F
dcast(DT,c_3+c_2~c_1,value.var="n_3",fun.aggregate=sum,drop=F)

#参数fill
dcast(DT,c_3+c_2~c_1,value.var="n_3",fun.aggregate=sum,drop=F,fill=1000)

宽表变长表

melt()函数,可以用于宽表变长表,将变量(列名)变成分类变量的若干水平值。

melt()函数用法如下:melt(data, id.vars,measure.vars,variable.name= “variable”,value.name= “value”,na.rm=T)

set.seed(45)
library(data.table)
DT <- data.table(n_1=rep(c(1:4,NA),each=4),
                 n_2=rep(-2:2,4),
                 n_3=sample(1:100,20),
                 c_1=rep(letters[3:4],each=10),
                 c_2=rep(LETTERS[1:5],4),
                 c_3=sample(letters[1:4],20,replace=T),
                 d_1=sample(as.Date(c(0:5,NA),origin="2021-01-01"),
                            20,replace=T))

#方法一:参数id指定不变形的列,参数measure指定要变形的列。
#不变形的列为单个,变形的列为一个或者多个
melt(DT,id="c_1",measure="n_1")
melt(DT,id="c_1",measure=c("n_1","n_2","n_3"))

#不变形的列为多个,变形的列为一个或者多个
melt(DT,id=c("c_1","c_2"),measure="n_1")
melt(DT,id=c("c_1","c_2","d_1"),measure=c("n_1","n_2","n_3"))

#方法二:参数variable.name修改"variable"名,参数value.name修改"value"名。
melt(DT,id="c_1",measure=c("n_1","n_2","n_3"),
     variable.name = "var.name",value.name = "val.name")

#方法三:参数na.rm=T,移除value为NA的值。
melt(DT,id="c_1",measure="n_1")
melt(DT,id="c_1",measure="n_1",na.rm=T)

#方法四:melt()和patterns()函数的结合,patterns()函数可以获取模式对应的匹配索引。同时融化以c开头的列和以n开头的列。
melt(DT,id="d_1",measure=patterns("^c_","^n_"))

多表连接

实际操作中,我们经常需要引入其他表中的列,即将其他表中列加入到表中,需要把两个或者更多的表合并成一个,R语言中有几种常用的几个合并函数。这里主要介绍merge()函数、data.table包中的合并数据方法。

##创建学生信息表
student <- data.frame("name" = c("张三", "李四", "王五", "马六"),
                      "score" = c(65, 70, 61, 98))

##创建学生年级信息表
class <- data.frame(name = c("张三", "王五", "马六", "小明"),
                    "grade" = c("三年级", "二年级", "四年级", "四年级"))

##转换数据类型
library(data.table)
student.dt <- data.table(student)
class.dt <- data.table(class)

##转换数据类型
library(dplyr)
student.df <- tbl_df(student)
class.df <- tbl_df(class)

merge()函数

merge(x, y, by, by.x, by.y, all, all.x, all.y…)

参数解释:

x,y:需要合并的数据集

by:用于连接两个数据集的列,当两个数据集公共列名相同,可以写成by = “公共列名”

by.x、by.y:用于指定依据哪个列合并,常用于当两个数据集公共列名不一样的时候;

all、all.x、all.y:指定x和y的行是否应该全部输出

sort:是否需要排序

data.table中的方法

首先创建公共键,可以理解为两个数据表连接的列。

setkey(x, column_name)

setkey(y, column_name)

然后基本语法:

x[y] ##注意x和y数据类型为data.table

内连接

##merge函数实现
merge(student, class, by = "name")

##通过data.table实现
###创建键
setkey(student.dt, name)
setkey(class.dt, name)
###内连接实现,nomatch = 0限制是内连接
student.dt[class.dt,nomatch = 0]

##通过dplyr实现,与merge函数语句基本相同
inner_join(student.df, class.df, by = "name")

左连接

##merge函数实现
merge(student, class, by = "name", all.x = T)

###data.table中实现,哪个是主表,写在[]里面
class.dt[student.dt]

右连接

##merge函数实现
merge(student, class, by = "name", all.y = T)

##data.table实现,哪个是主表,写在[]内
student.dt[class.dt]

全连接

##merge函数实现
merge(student, class, by = "name",all = T)