R语言学习笔记之——数据处理神器data.table

2,800 阅读9分钟
原文链接: zhuanlan.zhihu.com

数据处理在数据分析流程中的地位相信大家都有目共睹,也是每一个数据从业者面临的最为繁重的工作任务。

在实际应用场景下,虽然SQL(SQL类专业的etl语言)是数据处理的首选明星语言,性能佳、效率高、容易培养数据思维,但是SQL没法处理构建全流程的数据任务,之后仍然需要借助其他数据分析工具来对接更为深入的分析任务。

R语言作为专业的统计计算语言,数据处理是其一大特色功能,事实上每一个处理任务在R语言中都有着不止一套解决方案(这通常也是初学者在入门R语言时,感觉内容太多无从下手的原因),当然这些不同方案确实存在着性能和效率的绝大差异。

合理选择一套自己的数据处理工具组合算是挺艰难的选择,因为这个涉及到使用习惯和迁移成本的问题,比如你先熟知了R语言的基础绘图系统,在没有强大的驱动力的情况下,你可能不太愿意画大把时间去研究ggplot2,你用会写for/while循环,就不太愿意去掌握apply组函数,甚至那些性能逆天的并行算运算包;刚开始会用基础字符串处理,看到stringr包就面临着技能工具更新的问题……

太多的选择,让人眼花缭乱,我自己也遇到过这种困惑,为了避免注意力分散,我的做法是先做可能性罗列——罗列一个可以实现同类功能的所有工具清单并做一套功能卡(也算是初步了解)。然后根据自己掌握的现状选择最熟练的一套,随着时间的推移慢慢发现现有工具组合的不足,开始尝试往更加高效、简介的工具迁移,这样以需求为推动力的技能升级和迁移更为彻底和明确。

最典型的几个技能组合迁移如下:

基础字符串处理函数——stringr
绘图系统:plot——ggplot2
代码风格:函数嵌套——管道函数(`%>%`)
列表处理:list(自建循环)——rlist
json处理:Rjson+RJSONIO——jsonlite
数据抓取:RCurl+XML——httr+xml2
循环任务:for/while——apply——plyr::a_ply——并行运算(foreach、parallel)
切片索引:subset——dplyr::select+filter
聚合运算:aggregate——plyr::ddply+mutate——dplyr::group_by+summarize
数据联结:merge——plyr::join——dplyr::left/right/inner/outer_join
数据塑型:plyr::melt/dcast——tidyr::gather/spread

……

其实还有很多类型的同类功能组合技能升级的路径,不一给出,虽然工具迁移确实面临着很高昂的代价,特别是时间成本、学习成本,但是迁移之后获得的高效、代码简洁的体验还是很爽的,以上特别是管道函数的迁移感触最深,再也不存在自己写完的东西间歇性懵逼的场景了。

说了这么多,绕了这么大的弯子想干啥呢,没错今天又要给自己升级新技能啦,这次的主角儿是

data.table

一个R语言高性能数据处理包,一个包可以涵盖以上所说的数据处理的大部分内容,而且操作高度抽象化话(抽象化就意味着代码量少的可怕)。

其实很早就接触过data.table,之所以一直没有深入应用,因为它的理念与其他数据处理包偏离太远,可以说迁移成本很高,几乎就是技能重构而非迁移。

不过随着视野的开阔,发现确实有必要深入了解这个高性能包,尽管有点儿颠覆R的传统风格,但是性能和效率的提升可以弥补这一点。

data.table

1、I/O性能:

data.table的被推崇的重要原因就是他的IO吞吐性能在R语言诸多包中首屈一指,这里以一个1.6G多的2015年纽约自行车出行数据集为例来检验其性能到底如何,希望我的小米本能扛得住折腾~_~

#清空内存
rm(list=ls())
gc()
#使用传统的I/O函数read_csv2进行导入:
setwd("D:/Python/citibike-tripdata/")
system.time(
     mydata1 <- read.csv("2015-citibike-tripdata.csv",stringsAsFactors = FALSE,check.names = FALSE) 
     )

  用户   系统   流逝 
197.34   2.56 200.75 
object.size(mydata1)
1914019808 bytes
数据量还是很大的,将近1.6G,900多万记录,16个字段。

可怜的机器呀,内存和磁盘要撑爆了~

使用data.table内的I/O函数进行导入:

rm(list=ls())
gc()

library("data.table")

system.time(
     mydata1 <- fread("2015-citibike-tripdata.csv") 
     )

Read 9937969 rows and 16 (of 16) columns from 1.606 GB file in 00:00:45
 用户  系统  流逝 
43.44  0.48 44.43

五倍效率,45秒钟900万1.606G的数据,还是很有说服力的(虽然没有传说中的十倍性能)。

rm(list=ls())
gc()

2、索引切片聚合

data.table中提供了将行索引、列切片、分组功能于一体的数据处理模型。

DT[i,j,by]

如果这个过程是SQL中是由select …… from …… where …… groupby …… having 来完成的,在R的其他基础包中起码也是分批次完成的。

dplyr::fliter() %>% select() %>% group_by() %>% summarize()

虽然可以借助管道函数进行代码优化,但是仍然无法与data.table的简洁想抗衡。

mydata <- fread("https://raw.githubusercontent.com/wiki/arunsrinivasan/flights/NYCflights14/flights14.csv")

这里使用一个在线数据集,包含2014年纽约机场发出的所有航班信息。

class(mydata)

[1] "data.table" "data.frame"

使用fread函数导入之后便会自动转化为data.table对象,这是data.table所特有的高性能数据对象,同时继承了data.frame传统数据框类,也意味着他能囊括很多数据框的方法和函数调用。

str(mydata)
一共253316条记录,17个字段。

“year” 航班日期——年
“month” 航班日期——月
“day” 航班日期——天
“dep_time” 航班起飞时间
“dep_delay” 航班延误时长
“arr_time” 航班到达时间
“arr_delay” 航班到达延误时间
“cancelled” 航班是否取消
“carrier”
“tailnum”
“flight”
“origin” 起飞地
“dest” 目的地
“air_time”
“distance” 距离
“hour”
“min”

data.table行索引

carrier <- unique(mydata$carrier)
[1] "AA" "AS" "B6" "DL" "EV" "F9" "FL" "HA" "MQ" "VX" "WN" "UA" "US" "OO"

tailnum <- sample(unique(mydata$tailnum),5)
[1] "N332AA" "N813MQ" "N3742C" "N926EV" "N607SW"

origin  <- unique(mydata$origin)
[1] "JFK" "LGA" "EWR"

dest    <- sample(unique(mydata$dest),5)
[1] "BWI" "OAK" "DAL" "ATL" "ALB"``

mydata[carrier == "AA" ]
#等价于
mydata[carrier == "AA",]
#行索引可以直接引用列表,无需加表明前缀,这一点儿数据框做不到,而且i,j,by三个参数对应的条件支持模糊识别,无论加“,”与否都可以返回正确结果。

mydata[carrier %in% c("AA","AS"),]

支持在行索引位置使用%in% 函数。

data.table列索引

列索引与数据框相比操作体验差异比较大,data.table的列索引摒弃了data.frame时代的向量化参数,而使用list参数进行列索引。

mydata[,list(carrier,tailnum)]

为了操作体验更佳,这里的list可以简化为一个英文句点符号。即:

mydata[,.(carrier,tailnum)]
#但心里要清楚列索引接受的条件是含有列表的列表,而且这里的列表作为变量给出,而非data.frame时代的字符串向量。

行列同时索引毫无压力。

mydata[carrier %in% c("AA","AS"),.(carrier,tailnum)]

列索引的位置不仅支持列名索引,可以直接支持内建函数操作。

mydata[,.(flight/1000,carrier,tailnum)]

支持直接在列索引位置新建列,赋值符号为:=。

mydata[,delay_all := dep_delay+arr_delay]
#销毁某一列:
mydata[,delay_all := NULL]

批量新建列:

mydata[,c("delay_all","delay_dif") := .((dep_delay+arr_delay),(dep_delay-arr_delay))]
等价于写法2:
mydata[,`:=`(delay_all = dep_delay+arr_delay,delay_dif =dep_delay-arr_delay )]
#销毁新建列:
mydata[,c("delay_all","delay_dif") := NULL]

注意以上新建列时,如果只有一列,列名比较自由,写成字符串或者变量都可以,但是新建多列,必须严格按照左侧列名为字符串向量,右侧为列表的模式,当然你也可以使用第二种写法。

DT[,`:=`(varname1 = statement1 ,varname1 = statement2)]

可以直接使用data.table内建的函数。

mydata[carrier %in% c("AA","AS"),.N]
[1] 26876
.N是一个计数函数,相当于plyr中的count,或者基础函数中的length。

基本的统计函数都可以直接支持。

mydata[carrier %in% c("AA","AS"),.(sum(dep_delay),mean(arr_delay))]
       V1       V2
1: 228913 5.263841

mydata[carrier %in% c("AA","AS"),.(dep_delay,mean(arr_delay))]

mydata[carrier %in% c("AA","AS") & dep_delay %between% c(500,1000),.(dep_delay,arr_delay)]

当整列和聚合的单值同时输出时,可以支持自动补齐操作。

当聚合函数与data.table中的分组参数一起使用时,data.table的真正威力才逐渐显露。

mydata[,.(sum(dep_delay),mean(arr_delay)),by = carrier]

多分组聚合。

mydata[,.(sum(dep_delay),mean(arr_delay)),by = .(carrier,origin)]

多分组计数。

mydata[,.N,by = .(carrier,origin)]

自定义名称:

mydata[,.(dep_delay_sum = sum(dep_delay),arr_delay_mean = mean(arr_delay)),by = carrier]
mydata[,.(dep_delay_sum = sum(dep_delay),arr_delay_mean = mean(arr_delay)),by = .(carrier,origin)]
mydata[,.(carrier_n = .N),by = .(carrier,origin)]

数据排序:

排序行:

setorder(mydata,carrier,-arr_delay)

setorder函数作用于mydata本身,运行无输出。如果想要运行的同时进行输出则可以在结尾加上[]

setorder(mydata,carrier,-arr_delay)[]

这个功能有点儿类似于基础函数中,在语句外部加上圆括号。(a <- 1+1)

排序列:

sample(names(mydata),length(names(mydata)))
 [1] "arr_time"  "air_time"  "distance"  "dep_time"  "dest"      "arr_delay" "month"     "min"       "tailnum"   "origin"   
[11] "year"      "hour"      "cancelled" "flight"    "day"       "carrier"   "dep_delay"

setcolorder(mydata,sample(names(mydata),length(names(mydata))))

mydata[carrier == "AA",
        lapply(.SD, mean),
        by=.(carrier,origin,dest),
        .SDcols=c("arr_delay","dep_delay")
        ]

以上语法加入了新的参数.SDcols和.SD,咋一看摸不着头脑,其实是在按照carrier,origin,dest三个维度分组的基础上,对每个子块特定列进行均值运算。

这里的执行逻辑是这样的:

by=.(carrier,origin,dest) 先按照三个维度进行全部的分组;
.SDcols=c("arr_delay","dep_delay")则分别在筛选每一个子数据块儿上的特定列;
lapply(.SD, mean)则将各个子块的对应列应用于均值运算,并返回最终的列表。

数据合并:

data.table的数据合并方式非常简洁;

DT <- data.table(x=rep(letters[1:5],each=3), y=runif(15))
DX <- data.table(z=letters[1:3], c=runif(3))

设置各自的主键:

setkey(DT,x)
setkey(DX,z)

DT[DX]

就是如此简单,连接的执行逻辑是,内侧是左表,外侧是右表,所以是DX left join DT

如果没有设置主键,需要显式声明内部的on参数,指定连接主键,单主键必须在左右表中名称一致。

当然你要是特别不习惯这种用法,还是习惯使用merge的话,data.table仍然是支持的,因为他本来就继承了数据框,支持所有针对数据框的函数调用。

左手用R右手Python系列——数据合并与追加

长宽转换:

长宽转换仍然支持plyr中的melt/dcast函数以及tidyr中的gather/spread函数。

本篇仅对data.table的基础常用函数做一个整理,如果想要学习期更为灵活高阶的用法,还请异步官方文档。

左手用R右手Python系列——数据塑型与长宽转换