Matt Dowle 演讲节选(二)

时间:2022-07-28
本文章向大家介绍Matt Dowle 演讲节选(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

写在前面

也许很多小伙伴都注意到了,这一期的文章和往期的排版有所不同,因为从这一期开始,大猫将使用markdown来进行写作,并在最后用css来进行渲染输出。原来大猫使用的是秀米等富文本编辑器,最然可以实现很花哨的效果,但是每次编辑的时间可能都比写作的时间长,而且富文本编辑器对于代码块的支持极弱,语法高亮没有就算了,但是代码块无法水平滚动就不能忍。相比之下,markdown不仅对于代码有着先天的支持,而且只要在第一次设定好css,以后每次编辑的时间几乎为零,直接复制粘贴到公众号平台就可以渲染出非常漂亮的网页,简直美滋滋!

上期回顾

上次讲到 Matt 在转移到 R 阵营之后,开始思考下面那个无法在 S-PLUS 上面实现的命令,能否在 R 中实现呢?

> DF[2:3, sum(B)]

注:在 S-PLUS 中,以上命令必须要用一种非常不直观的方式写出来,如下:

> sum(DF[2:3, "B"])

2004:data.table诞生

2004-Day 1: 实现 j

所以 Matt 对 Pat(Matt 在所罗门兄弟的上司,S-PLUS 的坚定支持者)这么说到:

Matt:嗨伙计,既然 R 是开源的,我是不是能自己写一个包来实现上面提到的那个命令呢? Pat:祝你好运。

在2004年的第一天,Matt 离开了所罗门兄弟,也离开了 S-PLUS。他开始不断尝试,终于用自己的方式重写了[.data.frame这个函数,从而让sum(B)在 R 中也能得以运行。Matt 将这个包取名为data.table意味源于data.frame,但又不仅仅是data.frame

千万不要小看了DF[2:3, sum(B)])这行代码,因为这行代码体现了 R 的与众不同之处—— lazy evaluation. 在这行代码中,B 的值来自于 DF 这个表,而不是 global environment。换句话说,哪怕在 global environment 中存在一个叫做 B 的变量,那么data.talbe在运行的时候也会“认” DF 中的那个叫做 B 的列,而不是 global environment 中那个叫做 B 的全局变量。这种非常独特的行为可以让使用者大大减少敲击键盘的次数,并且也是 Python 等语言无法实现的。

2004 Day2: 实现 i

既然在j的部分实现了 lazy evaluation,Matt接着想,那么在i的部分能不能也实现 lazy evaluation 呢?

注:Matt 把 data.table的语法归纳为 DF[i, j, by]。例如代码DF[2:3, sum(B), by = group],其中i的部分为2:3,表示对行的选择;j的部分为sum(B),表示对列进行运算;by的部分对应by = group,表示按照变量group进行分组。

Matt 是这样想的:在data.frame中,如果我们想要选择region这个变量为特定值的关泽,那么代码就会是下面这样:

> DF[DF$region == "US", sum(population)]

但是重复输入DF$毕竟是一件很烦心的事,如果活用 R 的 lazy evaluation 机制,那么完全可以把上面的代码改成下面这个样子:

> DF[region == "US", sum(population)]

这就相当于默认开启了data.framewith参数。

2004 Day 3: 实现 by

Matt 接着想,如果我还想要将数据集按照特定变量分组呢?何不把分组这个命令也一块给整合进去?就这样,data.table的雏形就诞生了:

> DF[region == "US", sum(population), by = state]

2004 Day4: 把它们都串起来!

Matt 还不满意,“如果我希望把上面代码得到的数据集按照population排列呢?难道还要另起一行?这样就生成太多无用的中间数据集了啊……”于是 Matt 心生一计:“把他们都串起来!”

# 计算每个state的人口,并将结果按照人口从多到少排序
> DF[region == "US", sum(population), by = state
   ][order(-population)]

大猫:这就是大猫对 SAS 狂吐槽的原因。在 SAS 中,每对数据集排序就要运行一遍proc sort,代码一多到最后自己究竟要干啥都不知道了,这能忍?!

选择、运算、分组,三个截然不同的命令被完美的整合到了DF[i, j, by]的语法中,更妙的是,上一步运算的结果可以直接作为下一步的输入数据集!虽然在dplyr包中可以用 pipe 符号%>%实现类似的功能,但是小伙伴不觉得用[进行 pipe 要显得 neat 很多么?

最终,data.table诞生了。

2004-2012: data.table不断进化

一开始的data.table只是 Matt 为了方便自己工作而创作的,到了2008年,Matt 在 GPL 开源协议下发布了data.table

2011年,在 v1.6.3 版本中,data.table加入了可以说是发布以来最重要的功能:assignment by reference,也即:=符号。虽然听起来很 fancy,但是 Matt 却用两行代码说明了一切:

> for ( i in 1:1000) DF[, v1] <- i # 591 s 
> for ( i in 1:1000) DF[, v1 := i] # 1 s

上面两行代码做的都是同一件事:把变量v1从第1行到第1000行的值分别设置为1至1000。但是第一种方法用了 591 s,第二种方法(assignment by reference)只用了 1 s。这里的关键在于,在第一种方法中,每为新的一行赋值,data.table就要重新复制一遍DT,也就是说,第一种方法的运行过程中,DF被复制了1000遍!而在第二种方法中,由于采用了 assignment by reference,data.table仅对内存中v1所在的地址进行修改,其他地方则不变!事实上,DF 在第二种方法中一遍都没有被复制!

这就是 assignment by reference 的伟大之处。设想一下,假如我们的内存为 4G,而数据集为 3.9G,这就意味着我们几乎不能对数据集进行任何修改!因为任何对列的处理都必须导致数据集在内存中的复制,也即假如我们的内存是 4G,那么在使用data.frame的情况下,我们最大就只能处理 2G 的数据集!但是有了data.table,我们就可以处理 3.9999G 的数据集!

一个更极端的例子是,加入你在 4G 内存中 装下了一个 3G 的数据集,这时你想要删去其中的一列都是不可能的,因为在data.frame中,哪怕删除操作都会导致数据集的复制!(大猫:在最新版本的 R 中,这个问题已经明显缓解,但是这时已经过去了5年多)而在data.table中,一切都是那么自然:

> DF[, colToDelete := NULL]

哪怕你的数据集有 20G,所用时间也不会超过 0.01 秒。

data.table带来的不仅是全新的、人性化的语法,更是无可匹敌的性能。在演讲中,Matt 引用了一个在 StackOverflow 论坛中的真实例子。在这个2012年注意dplyr的最早版本在2016年!)的帖子中,一个用户需要处理以下数据集(这里只显示前6行)

他想首先按照gene_id分组,然后分别计算特定变量的极值和均值。这个用户一开始使用lapplydo.call函数,不仅计算时间很长(30 min!),而且代码特别难看:

而使用data.table,则简直是一阵春风:

最终要的是,原来要30分钟才计算完成的任务,现在3秒钟就够了!!!

Matt 在最后总结到:

“我们在这里讨论的是时间,宝贵的时间。30分钟足够你用来享受下午茶(不愧是腐国人Orz)或是享用午餐……这是一个严肃的问题,从5秒降低到1秒?没什么人会在意。从10秒降低到1秒?同样不稀奇。但是我们讨论的却是从30分钟降低到3秒!” “这只是一个任务,我们还没考虑到loop。试想一下,如果这是一个循环,我们能节约多少时间?”

2014:data.table的现在

fread函数

在演讲的最后(演讲在2014年),Matt 提到了当时他正在给data.table添加的新功能:fast read,也即fread函数。顾名思义,fread函数大大提高了 R 读取文本文件的性能。在演讲中 Matt说到:

假设我们现在有个 50 MB 的文件,100万行,6列,如果用传统的read.csv("test.csv")的方法,需要大约 30-60 秒。 但我知道你们肯定要大呼小叫了,“什么?!读一个50 MB 的 csv 竟然要一分钟?果然 R 的性能就是不行啊”。这时你们肯定会去 StackOverflow 上发帖询问,而得到的回答大多数是让你指定read.csv的一大堆的参数。于是你老老实实做了,把代码改成了read.csv("test.csv", colClass =, nrows =, etc...),然后你发现运算时间减少到 10 秒。 True,时间是缩短不少,但那意味着许多枯燥的输入。假设你有100列,难道你要每列的class都指定一遍? 这时你就需要fread("test.csv")!不需要输入任何其他的参数,你猜要运行多久? 3秒! 现在我们再玩得大点,假设你有 20G 的 csv 文件,2亿行,16列,哪怕你为每个列都指定了class,read.csv("test.csv")也需要好几个小时才能运行完,而fread只要—— 8 分钟。

data.tablesupport

S-PLUS 是商业软件,我可以花钱让人解决问题;而 R 是开源软件,出了问题找谁呢?Matt 用 StackOverflow (全球最大的编程问答网站) 的数据给出了答案:

在过去的 7 天中,有 21 条关于 data.table 的问题没有被回答,占 19%; 在过去的 30 天中,有 85 条关于data.table的问题没有被回答,占 15.3%; 所有关于data.table的历史问题中,1542条没有被回答,占 8.6%

(完)