R语言学习笔记之——数据处理神器data.table
杜雨,EasyCharts团队成员,R语言中文社区专栏作者,兴趣方向为:Excel商务图表,R语言数据可视化,地理信息数据可视化。
个人公众号:数据小魔方(微信ID:datamofang) ,“数据小魔方”创始人。
精彩集锦·
数据处理在数据分析流程中的地位相信大家都有目共睹,也是每一个数据从业者面临的最为繁重的工作任务。
在实际应用场景下,虽然SQL(SQL类专业的etl语言)是数据处理的首选明星语言,性能佳、效率高、容易培养数据思维,但是SQL没法处理构建全流程的数据任务,之后仍然需要借助其他数据分析工具来对接更为深入的分析任务。
R语言作为专业的统计计算语言,数据处理是其一大特色功能,事实上每一个处理任务在R语言中都有着不止一套解决方案(这通常也是初学者在入门R语言时,感觉内容太多无从下手的原因),当然这些不同方案确实存在着性能和效率的绝大差异。
合理选择一套自己的数据处理工具组合算是挺艰难的选择,因为这个涉及到使用习惯和迁移成本的问题,比如你先熟知了R语言的基础绘图系统,在没有强大的驱动力的情况下,你可能不太愿意画大把时间去研究ggplot2,你用会写for/while循环,就不太愿意去掌握apply组函数,甚至那些性能逆天的并行算运算包;刚开始会用基础字符串处理,看到stringr包就面临着技能工具更新的问题……
太多的选择,让人眼花缭乱,我自己也遇到过这种困惑,为了避免注意力分散,我的做法是先做可能性罗列——罗列一个可以实现同类功能的所有工具清单并做一套功能卡(也算是初步了解)。然后根据自己掌握的现状选择最熟练的一套,随着时间的推移慢慢发现现有工具组合的不足,开始尝试往更加高效、简介的工具迁移,这样以需求为推动力的技能升级和迁移更为彻底和明确。
最典型的几个技能组合迁移如下:
基础字符串处理函数——stringr绘图系统:plot——ggplot2代码风格:函数嵌套——管道函数(`%>%`)列表处理:list(自建循环)——rlistjson处理: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仍然是支持的,因为他本来就继承了数据框,支持所有针对数据框的函数调用。
长宽转换:
长宽转换仍然支持plyr中的melt/dcast函数以及tidyr中的gather/spread函数。
本篇仅对data.table的基础常用函数做一个整理,如果想要学习期更为灵活高阶的用法,还请异步官方文档。
注:如果你想要深入的去学ggplot2,但是又苦于平时学习、工作太忙木有时间研究浩如烟海的源文档,那也没关系,本小编最近花了不少功夫,把我自己学习ggplot2过程中的一些心得体会、学习经验、仿入坑指南精心整理,现已成功上线了R语言ggplot2可视化的视频课程,由天善智能独家发行,希望这门课程可以给你的R语言数据可视化学习带来更加丰富的体验。
相关课程推荐
体系全面,最具调性!R语言可视化&商务图表实战课程:
点击“阅读原文”开启新姿势