书名:R语言高效能实战:更多数据和更快速度
ISBN:978-7-115-58440-3
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 刘艺非
责任编辑 秦 健
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书将目标设定为“在一台笔记本电脑上使用R语言处理较大的数据集”,从单机大型数据集处理策略、提升计算性能、其他工具和技巧3个方面介绍了使用R语言处理数据时的实用方法,包括减少数据占用空间、善用data.table处理数据、数据分块处理、提升硬盘资源使用效率、并行编程技术、提升机器学习性能,以及其他资源管理和提高性能的实用策略,以帮助读者处理较大的数据集、挖掘R的开发潜能。
本书适合有一定R语言基础的读者阅读,也适合作为程序员的R语言实践工具书。
我的工作离不开数据科学。在工作中,为了解决问题,我会到网上查询或者购买一些图书寻找解决方案。不过多年来我发现,很多教程或图书,都爱使用那个鼎鼎大名的iris数据集作为操作案例,那是一个特别小、特别规整又特别均衡的数据集。正因如此,人们总喜欢对它“为所欲为”[1]。但在实践中,我经常碰到的是一些比较大的用户特征和业务信息数据,而我的工具可能就只是一台普通的笔记本电脑。当要进行比较复杂的计算,例如训练机器学习模型或做超参数调优时,就会耗费很多时间。虽然也可以通过合理的抽样缩小数据规模,但这也在一定程度上造成了数据的浪费,有些数据由于太大甚至不能直接导入分析软件中。当数据不像iris那样小而美的时候,我们对数据的处理就不会那么容易,计算起来也不再那么快了!
我也看到很多教学资源在面对大型数据的处理和计算问题时,会教导人们如何采用各种听起来很“高端”的策略,这些策略中往往包含了像“云”“集群”“分布式”“××架构”“××平台”这样的术语。每次听到这些术语,我都不禁打个寒战:“你们这些策略是讲给老板听的吧?”对于一般用户来说,不论学习还是实际运用这些策略都需要很高的成本,因为可能要购置多台计算机或者租用云资源才能实现。要解决一些企业级的数据生产和存储问题当然确实需要实施这些策略,不过我也想知道,在真正要动用那些“重型武器”之前,对很多“轻型武器”无法解决的数据处理和计算问题,是否存在一些对我们普通单机用户来说可望又可及的“中小型武器”?我们手上这台价值几千元甚至上万元的笔记本电脑,已经被我们充分利用了吗?于是,我急切地希望整理一些在单机上处理大型数据集以及提升数据计算效能的策略。目前市面上的个人笔记本电脑内存大概为4 GB~8 GB,硬盘容量大概为500 GB~1 TB,台式电脑配置可能会更高,我认为,它们的数据处理和计算潜力还没有被充分挖掘出来,既然是这样,那么我们就要想办法把这种潜力挖掘出来!
“榨干”你的笔记本电脑,让它物有所值!
那么,怎样完成这个事情呢?
没有什么特别的原因,纯粹因为我比较喜欢R而已。我在近年的工作过程中其实使用Python的机会更多。实际上我更喜欢把不同类型的工具混在一起使用,我认为在数据科学领域不存在所谓的“世上最好的语言”,所有分析工具之间的壁垒都应该打破。数据工作者的使命在于利用数据并选择最合适的工具解决问题。本质上,他们是解决问题的高手,而不是或不仅仅是使用某一种工具的高手。不被工具左右,这才是数据工作者应有的专业素养。我读书和后来工作时,出于各种原因,例如客观环境、团队协作需要、要解决特殊问题等等,曾混搭过很多不同的分析工具,包括Excel、SPSS、STATA、SAS、R、Python……
不过在可以选择的情况下,我还是会首选R作为数据整理、分析、建模的工具。在众多工具之中,R并不一定性能最强大或最流行,但是只有用R的时候我感觉自己是在“玩”数据,心情非常舒畅,而用其他工具的时候,我都觉得自己是在“工作”,好像有人在背后盯着我。做个不太恰当的比喻,使用R就像到电影院看电影,使用Python就像是你用自己的笔记本电脑看电影:使用笔记本电脑也能看到高清版本的电影,而且除了看电影,你也能用笔记本电脑做很多别的事情,而在电影院,你就只能看电影了。但相比之下,哪种看电影的体验更好呢?就我个人而言,到电影院看电影的观影体验是无与伦比的。当然,你也可以有自己的喜好。
另外我还想为R鸣个不平。记得有一次我和同事交流各种数据科学工具的优劣时,一位同事说:“R不是个拿来画图的软件吗?”这让我非常诧异。R闻名于世的可视化功能掩盖了它在数据科学中的其他优势,以及曾经不足但后来已经逐渐得到完善的方面。可能有人告诉你,R是基于单线程运作的,R只能处理加载入内存的数据,R对深度学习支持不足云云。稍微对R有了解的用户可能会听说过有个叫data.table的工具可以比较快地处理大型数据集,但也仅此而已。而实际上,R社区中一批活跃的开发者早就开始突破这些局限,开发出各种高效能的工具,以弥补R单核运作、只能处理内存数据的先天缺陷,并且提升R数据处理和计算的效能,以紧随当前数据科学技术发展的趋势,曾经的短板现在对于R来说可能都不再是短板了。不过遗憾的是,目前以这些方面为主题的书并不多,至少远不及以统计分析、可视化等为主题的书,而且作者大多为外国人[2],其中一些作品如表1所示。也是出于这样的目的,我便着手以R为对象去研究和整理上面提到的那些策略。
人生苦短,我用Python,也用R!
表1 一些以大型数据集处理或计算效能提升为主题的R图书
英文名称 |
中译本名称 |
作者 |
---|---|---|
Parallel R |
暂无 |
Q. Ethan McCallum和Stephen Weston |
Big Data Analytics with R |
《R大数据分析实用指南》 |
Simon Walkowiak |
Mastering Parallel Programming with R |
《R并行编程实战》 |
Simon Chapple |
Efficient R Programming |
《高效R语言编程》 |
Colin Gillespie和Robin Lovelace |
R High Performance Programming |
《R高性能编程》 |
Aloysius Lim和William Tjhi |
Parallel Computing for Data Science:with Examples in R,C++ and CUDA |
《数据科学中的并行计算:以R、C++和CUDA为例》 |
Norman Matloff |
本书以单机环境为背景,针对过往业内普遍认为的R的“短板”,结合单机数据分析挖掘工作中较主要的几个工作环节,即数据载入—数据处理及探索—数据建模[3],在“大型数据集处理”和“计算效能提升”这两个方面,尝试挖掘R在单机环境下的潜能。对于数据集比较大的情况,我们将探究如何使用R对这些数据集实现有效的分割、载入、处理和探索等操作,让R能处理得“更多”。而对于在数据分析挖掘过程中执行一些计算量比较大的操作,特别是在机器学习工作流中模型训练、性能评估和比较、超参数调优等计算密集型环节,则探究R如何充分利用单机的资源及其自身的优势提升这些环节的处理效能,让R能处理得“更快”。寻找“更多”和“更快”的策略,正是本书的主要目标。
实际上,还有一个思路是优化R的代码,例如尽可能以向量化方式操作、对一些常用的计算使用内置函数而非自定义函数、使用C或者C++等编译型语言编写部分程序,以及对R代码进行性能监控等,但为了使本书的讨论更具有针对性,本书并不打算过多讨论代码优化方面的内容,有兴趣的读者可以阅读表1列举的书目或寻找线上相关资源。
本书所讨论的“数据集”,可以很直观地理解为数据科学领域中最常见的行列式结构化数据,即R和Python的pandas库支持的“数据框”(data.frame)格式。本书中所指的“大型数据集”,用英文来说,可以是“large dataset”或“large dataframe”,而不是现在人们常用的“big data”,因为后者除了表明数据的体量大以外,更强调数据的异质性强、更新速度快等特点。所以这里也需要特别说明,从这个意义上看,本书并不是一本讨论“大数据”的书。
对于本书的第一个目标,什么样的数据集才算是“大型数据集”?我认为这是一个相对概念,没有客观标准,全因你手头上的计算资源而异,例如10 GB的数据对于低配置的计算机可能是很大的数据集,但对于高配置的计算机则不一定。通俗点来说,这里说的“大型数据集”可以这样描述:它能够塞进一台一般配置的笔记本电脑,但做相应的处理或计算时,计算机不会立即给出结果,而是需要“转一下”甚至“转很久”才可以。也可以用具体的使用场景来说明:当我们在单台计算机上使用R或者Python等工具处理数据时,按照数据规模和单机的资源情况,有以下几种可能。
场景1:计算机的内存容量限制远大于要处理的数据集大小,对该数据集进行任何处理、分析、建模等工作都不会出现明显的资源紧张现象(但可能在某些时候接近瓶颈),这也是传统上R以及很多专业统计软件最擅长处理的场景,也是我们希望能够最终达到的状态。
场景2:要处理的数据集大小与计算机内存容量限制非常接近,但数据集本身比较大或者计算过程中生成的临时数据集总体占用内存比较大,使得数据虽然可以放入内存,但不能再进行一些较复杂的处理和计算,或者计算机处理和计算速度明显放缓。
场景3:要处理的数据集大小超过计算机内存的容量限制,直接读入数据会产生内存不足的错误,但计算机的硬盘能够装下这些数据。
场景4:要处理的数据集非常大,单台计算机的硬盘也无法容纳。
粗略来说,场景2、场景3、场景4中的数据都可以算作本书所说的“大型数据集”。本书的关注点在于场景2、场景3,以及场景1中的某些情况,而第4种场景暂不在本书的讨论范围之内,因为我们的前提条件是单机环境。如果真的遇到第4种场景,可能需要添置更多的计算机和掌握一些基于集群的分布式存储和计算技术(如Hadoop、Spark等)。
对于场景2,有以下解决思路。
对于场景3,有以下几种主要的解决思路。
通过种种努力,将数据以一种合适的大小放入R后(即回到上面提到的场景1中),我们希望进一步提高R的数据处理和计算效能,这就进入了本书的第二个目标。在这里我不打算在“效能”这个词上咬文嚼字,只想直接地用“效能”一词指代两个方面:效率和性能。高效率主要表现为通过编写尽可能少的代码(或在一些软件中通过菜单操作)来完成尽可能多的任务,而高性能则主要表现为能充分利用计算机的资源顺利、快速地完成计算。事实上,对于R来说,效率恰恰是其一贯的优势,而性能则是其劣势。概言之,R的特色就是“写得快、跑得慢”。R社区中极有影响力的数据科学家Hadley Wickham也认为,R不是一门速度快的语言,因为它的设计目的是使得数据分析和统计变得容易[4]。不过正如前面提到,R社区已经在不断做出改变,以各种并行计算框架为代表的高性能工具的出现使得R在计算速度方面有了极大的改善,但更高效率工具的开发也依然活跃,例如一些新的机器学习框架的出现。因此,我们希望找到让R“写得快,跑得也快”的方法。在效能提升方面,比较典型的需求是涉及循环处理的编程,以及机器学习工作流中的某些环节。对此,我们将讨论以下策略。
从上述的这些场景出发,本书系统地梳理了R中相关策略和工具,是一本实用的技术推荐指南,其知识结构如图1所示。
图1 本书知识结构
第1章至第5章的主要内容为单机大型数据集高效处理策略,探究如何让R能处理“更多”的数据,包括以下内容。
第6章至第11章主要介绍面对已经读入的数据使用R提升数据处理和计算效能的方法,特别是探究如何让R“更快”地处理数据和执行机器学习中的高密度计算任务,包括以下内容。
此外,本书第12章介绍R的其他大型数据集处理和计算效能提升技巧,主要是整理一些未能归入前面章节的内容,例如介绍R中一些资源管理和提高性能的实用策略和小工具,简单介绍微软开发的两个R的增强型版本:Microsoft R Open和Microsoft R Client,以及除了R以外的一些适合处理大型数据集的常用数据科学工具。
本书希望通过对大型数据集处理、效能提升等各种策略的整理和归纳,使你(也包括我自己)除了在“术”的层面有所收获以外,更能或多或少在上述方面形成一些“道”。“术”主要指具体的技术,而“道”则是解决问题的思路。换句话说,通过阅读本书,你除了知道如何在R上通过代码实现本书介绍的策略之外,在使用其他数据科学工具遇到了类似的问题时,也能马上借用类似的思路和策略解决这些问题。这才是本书希望达到的效果。
本书的内容安排综合考虑了内容的广度和深度,希望读者能够快速了解相关技术并将这些技术投入实践。但同时,本书又不仅仅停留在R代码实现的表层,而是致力于形成一个解决方案的逻辑框架。你如果符合以下描述,那么可能会对本书的内容感兴趣。
当然,你即使完全不具有上述特征,如仅仅是对R或者对大型数据集处理或效能提升感兴趣,也不妨将本书作为一个实用的参考指南。
本书不希望成为那种“百科全书”式的R语言教材,因此假定你对R已具有一定的使用经验。对于R的前世今生、R软件、相关第三方库的下载安装、基本对象类型和操作等内容,本书不打算单独介绍。另外,本书也不是专门介绍数据挖掘方法或机器学习算法方面的图书,因此基本不会涉及各类算法的原理及其在R中的具体实现,必须提及时也尽可能简略。你如果对以上内容有兴趣,可以阅读更加专业的图书或搜索其他线上资源。
在调试本书代码时,我使用的笔记本电脑使用6核、12线程处理器,16 GB内存,1 TB硬盘空间,64位Windows操作系统。
本书使用的R版本为4.0.3,RStudio版本为1.3.1093。
刘艺非
2021年7月16日
[1] 还有很多和iris类似的小型经典机器学习示例数据集,有兴趣可详见UCI机器学习工具库网站。
[2] 一些国内研究者的出版物也会有部分章节讨论大型数据处理或者效能提升的内容,但并非整本书都以此为主题。
[3] 在这个流程之前还有数据采集、数据存储环节,流程之后还有生产应用部署等环节。对于这些环节,本书暂不讨论,在实际生产中这些环节往往不是单机环境能解决的。
[4] WICKHAM.高级R语言编程指南[M].北京:机械工业出版社,2016:214.
本书由异步社区出品,社区(www.epubit.com)为您提供相关资源和后续服务。
本书提供配套代码文件。
请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作以获取配套资源。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
现在你要处理一个数据集。这个数据集有点大,虽然也可以整个读入计算机内存,但是你预计后面还要读入其他数据,可能很快就会耗尽内存,或者你将要对这个数据集进行一些处理或计算,期间会产生一些比较大的临时数据或结果数据,而这些数据也可能会瞬间“撑爆”内存。因此你想从一开始就将这个数据集变得尽可能小,以节省内存并为后续操作留出更多空间。
作为本书的开端,让我们先从常见的场景和直接的解决方案开始。面对这样的场景和需求,我们将在本章讨论以下主要应对策略:
保留必要的数据,分别从结构型数据的“列”和“行”的角度,对数据进行必要的裁剪;
设置合适的数据类型,避免不必要的内存消耗。
本章及接下来几章主要使用的示例数据来自2018年纽约市出租车服务载客记录,包含纽约市几种主要类型出租车服务全年的数据,包括green、yellow和fhv(即for-hire vehicle)三大类,每月数据为一个文件,每一条记录为出租车的一次载客记录,包括上下车时间、区域、费用、载客量、支付方式等信息。出于示例需要,笔者已经预先将各类出租车服务数据分别合并为一个大的csv文件,合并后的3个csv文件大小分别约为0.9 GB(green_2018.csv)、9.0 GB(yellow_2018.csv)和16.0 GB(fhv_2018.csv),不论使用哪个数据,对于一般的笔记本电脑而言,数据量均非常可观。
对于体积较大但仍然能够读入内存的数据集,可以在读入后进行各种各样的“缩小”处理,在此不做详细讨论。需要注意的是,在缩小原数据后,我们可以及时使用R的rm()函数删除原数据,以释放内存资源。我们重点关注那种体积处于内存大小的临界点,不能直接读入,但通过一定的处理后可以被内存所容纳的数据集。这需要在数据导入的源头就保留后续需要的信息。这包括了两种策略:保留必要的列和保留必要的行。
对于必要的列,如果能通过数据字典或者相关业务知识预先判断哪些列是有用的,就可以直接在数据导入时指定需要保留的列。另一种方法是,在导入数据前,先通过如Windows操作系统的预览窗格功能,或使用Ultra Edit、EmEditor等辅助工具,观察数据中有哪些列是有分析价值的,或者哪些列是明显缺乏有效信息的(如取值单一、存在大量缺失、数据含义不明等),对所需的列进行保留。
使用Ultra Edit或EmEditor可以很快地打开体积很大的数据文件,比用Excel打开更好。
先载入后续会用到的库,并利用readr的read_csv()读入数据green_2018.csv:
# 载入相关的库
library(readr)
library(dplyr)
library(tibble)
library(pryr)
# 读入数据
path <- "D:/K/DATA EXERCISE/big data/TLC Trip Record Data"
file <- "green_2018.csv"
green_2018 <- read_csv(file.path(path, file))
# 查看结果
green_2018
## # A tibble: 8,807,303 x 19
## VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~ RatecodeID
## <dbl> <dttm> <dttm> <chr> <dbl>
## 1 2 2018-01-01 00:18:50 2018-01-01 00:24:39 N 1
## 2 2 2018-01-01 00:30:26 2018-01-01 00:46:42 N 1
## 3 2 2018-01-01 00:07:25 2018-01-01 00:19:45 N 1
## 4 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 5 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 6 2 2018-01-01 00:38:35 2018-01-01 01:08:50 N 1
## 7 2 2018-01-01 00:18:41 2018-01-01 00:28:22 N 1
## 8 2 2018-01-01 00:38:02 2018-01-01 00:55:02 N 1
## 9 2 2018-01-01 00:05:02 2018-01-01 00:18:35 N 1
## 10 2 2018-01-01 00:35:23 2018-01-01 00:42:07 N 1
## # ... with 8,807,293 more rows, and 14 more variables: PULocationID <dbl>,
## # DOLocationID <dbl>, passenger_count <dbl>, trip_distance <dbl>,
## # fare_amount <dbl>, extra <dbl>, mta_tax <dbl>, tip_amount <dbl>,
## # tolls_amount <dbl>, ehail_fee <lgl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <dbl>, trip_type <dbl>
该数据完整导入后所占的内存空间约为1.3 GB。
# 数据所占内存大小
object_size(green_2018)
## 1.3 GB
由于readr库的read_csv()没有在导入数据的同时保留列的功能,因此可以在数据导入后使用select()保留或删除部分列。假定VendorID、store_and_fwd_flag、RatecodeID、ehail_fee这几列对于后续要解决的问题没有分析价值,需要将它们剔除,并将处理后的数据保存在green_2018_s中:
# 删除不需要的列
green_2018_s <- green_2018 %>% select(-c(VendorID, store_and_fwd_flag, RatecodeID, ehail_fee))
# 查看结果
green_2018_s
## # A tibble: 8,807,303 x 15
## lpep_pickup_dateti~ lpep_dropoff_datet~ PULocationID DOLocationID
## <dttm> <dttm> <dbl> <dbl>
## 1 2018-01-01 00:18:50 2018-01-01 00:24:39 236 236
## 2 2018-01-01 00:30:26 2018-01-01 00:46:42 43 42
## 3 2018-01-01 00:07:25 2018-01-01 00:19:45 74 152
## 4 2018-01-01 00:32:40 2018-01-01 00:33:41 255 255
## 5 2018-01-01 00:32:40 2018-01-01 00:33:41 255 255
## 6 2018-01-01 00:38:35 2018-01-01 01:08:50 255 161
## 7 2018-01-01 00:18:41 2018-01-01 00:28:22 189 65
## 8 2018-01-01 00:38:02 2018-01-01 00:55:02 189 225
## 9 2018-01-01 00:05:02 2018-01-01 00:18:35 129 82
## 10 2018-01-01 00:35:23 2018-01-01 00:42:07 226 7
## # ... with 8,807,293 more rows, and 11 more variables: passenger_count <dbl>,
## # trip_distance <dbl>, fare_amount <dbl>, extra <dbl>, mta_tax <dbl>,
## # tip_amount <dbl>, tolls_amount <dbl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <dbl>, trip_type <dbl>
剔除了上述几列后,可以看到green_2018_s所占内存有明显的减小。可以利用rm()删除处理前的原数据green_2018,释放一定内存空间:
# 数据所占内存大小
object_size(green_2018_s)
## 1.06 GB
# 删除原数据
# rm(green_2018)
对于必要的行,可以根据分析挖掘的目标,按照特定的条件进行筛选,仅保留必要的数据子集,也可以通过数据抽样的方法保留必要的行。过去,很多人都说在大数据时代用的都是“全数据”“全样本”,但是,使用“全数据”“全样本”往往有一些隐含的前提:要么你真的有条件收集到分析对象的全体样本(或接近全体样本),要么你手头上的计算资源能够存储或者计算全数据,然而在实践中,这两者在大多数情况下都很难做到,特别是对个体研究者而言。
那么,在这些隐含的前提都得不到满足的情况下,被认为源自“小数据时代”、以统计学理论为基础的数据抽样技术,在大数据时代实际上还有着很大的用武之地。你如果是统计学或者社会科学专业背景的用户,可能已经比较熟悉这部分,但也不妨回顾一下;如果是计算机、软件开发等工程专业背景较强的用户,对抽样技术可能比较陌生,可以了解一下这部分。虽然“大数据”总是强调可以拿研究对象的“全样本”来做分析挖掘,但就正如开篇所言,有时用全样本数据集,你的计算机就是装不下,或者即使能装得下,很多机器学习模型在运算过程中会在内存里产生体积巨大的临时数据集,也会导致内存溢出之类的错误。所以,面对实践中林林总总的约束,数据抽样技术非但不会落伍,甚至在很多资源受到制约的场景下还能发挥奇效。
抽样的一个基本思路是构造一个结构与分布和原数据集相同、保留了原数据集的所有信息,但比原数集体积小一点(或者小很多)的数据子集。那么问题的关键就转变为,使用什么方法能够保证这个“小一点”的数据子集和原数据的结构与分布类似?在社会科学领域,由于社会调查研究或实验研究的现实约束非常多,因此抽样方法也往往非常复杂。在此,我们介绍几种常用的抽样方法及其实现。使用抽样设计常见的场景如下:
需要分析研究对象的比例结构而不是绝对数量,例如了解办理不同产品的用户比例,但不需要知道具体的用户量;
机器学习模型训练(当然这也不是绝对的,如果条件允许的话,用更多数据训练一个简单模型可能会比用少量数据训练一个复杂模型效果更好)。
若有兴趣了解更复杂的抽样方法,可阅读Graham Kalton的《抽样调查方法简介》一书。
简单随机抽样的确是“简单”的解决方法,可以按照预设的样本数量或者样本占总体的比例进行。同时,可以进一步选择有放回或无放回两种方式。如果目标是要抽取样本构造一个训练数据集,无放回随机抽样更常用,而有放回抽样(有时也会称为自助抽样)则更多在一些集成机器学习算法(例如随机森林)中或者模型性能估计时用到。
在以下例子中,我们通过指定样本数的方式进行简单随机抽样。假设要抽取一个规模为100000的样本,则有两种实现形式。
一是使用dplyr库的sample_n()直接实现:
# 方法1:使用sample_n()
# 设置随机种子
set.seed(100)
# 抽样
green_2018_sample_n_1 <- green_2018 %>% sample_n(size = 100000)
# 按时间顺序观察结果
green_2018_sample_n_1 %>% arrange(lpep_pickup_datetime)
## # A tibble: 100,000 x 19
## VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~ RatecodeID
## <dbl> <dttm> <dttm> <chr> <dbl>
## 1 2 2008-12-31 23:02:52 2008-12-31 23:04:27 N 1
## 2 2 2008-12-31 23:10:46 2008-12-31 23:24:17 N 1
## 3 2 2008-12-31 23:26:54 2008-12-31 23:36:55 N 1
## 4 2 2008-12-31 23:31:42 2008-12-31 23:36:53 N 1
## 5 2 2009-01-01 00:06:29 2009-01-01 00:44:10 N 1
## 6 2 2009-01-01 00:42:38 2009-01-01 01:02:25 N 1
## 7 2 2009-01-01 02:39:10 2009-01-01 02:55:18 N 1
## 8 2 2009-01-01 05:59:25 2009-01-01 06:04:31 N 1
## 9 2 2010-09-23 01:38:06 2010-09-23 01:42:25 N 1
## 10 2 2010-09-23 23:37:45 2010-09-24 00:01:55 N 1
## # ... with 99,990 more rows, and 14 more variables: PULocationID <dbl>,
## # DOLocationID <dbl>, passenger_count <dbl>, trip_distance <dbl>,
## # fare_amount <dbl>, extra <dbl>, mta_tax <dbl>, tip_amount <dbl>,
## # tolls_amount <dbl>, ehail_fee <lgl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <dbl>, trip_type <dbl>
二是使用tibble库的rowid_to_column()先增加一列行ID,再使用dplyr库的slice_sample()对行ID进行随机抽样。在R中为了能使随机的结果重现,可在程序执行之前先通过set.seed()设置随机种子(这个函数在后面会经常用到)。具体处理如下:
# 方法2:使用rowid_to_column()和slice_sample()
# 设置随机种子
set.seed(100)
# 抽样
green_2018_sample_n_2 <- green_2018 %>%
rowid_to_column() %>% # 生成一列行ID,该函数来自tibble库
slice_sample(n = 100000)
# 按ID顺序观察前10行
green_2018_sample_n_2 %>% arrange(rowid)
## # A tibble: 100,000 x 20
## rowid VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~
## <int> <dbl> <dttm> <dttm> <chr>
## 1 93 2 2018-01-01 01:02:36 2018-01-01 01:26:46 N
## 2 152 2 2018-01-01 00:58:25 2018-01-01 01:24:46 N
## 3 187 2 2018-01-01 00:40:39 2018-01-01 00:48:24 N
## 4 190 2 2018-01-01 00:51:02 2018-01-01 01:03:11 N
## 5 266 2 2018-01-01 00:41:44 2018-01-01 01:07:57 N
## 6 381 2 2018-01-01 01:00:07 2018-01-01 01:19:52 N
## 7 472 2 2018-01-01 00:15:47 2018-01-01 00:20:15 N
## 8 491 2 2018-01-01 00:19:49 2018-01-01 00:20:19 N
## 9 556 2 2018-01-01 00:31:39 2018-01-01 00:47:24 N
## 10 628 1 2018-01-01 00:54:05 2018-01-01 00:56:20 N
## # ... with 99,990 more rows, and 15 more variables: RatecodeID <dbl>,
## # PULocationID <dbl>, DOLocationID <dbl>, passenger_count <dbl>,
## # trip_distance <dbl>, fare_amount <dbl>, extra <dbl>, mta_tax <dbl>,
## # tip_amount <dbl>, tolls_amount <dbl>, ehail_fee <lgl>,
## # improvement_surcharge <dbl>, total_amount <dbl>, payment_type <dbl>,
## # trip_type <dbl>
除了通过样本量确定抽样规模外,简单随机抽样的另一种方式是按照比例抽取样本。假定需要抽取全体数据中的10%进行后续分析,可以使用dplyr库的sample_frac(),或者rowid_to_column()结合slice_sample()的方式实现。
使用sample_frac()的具体方法如下:
# 方法1:使用sample_frac()
# 设置随机种子
set.seed(100)
# 抽样
green_2018_sample_prop_1 <- green_2018 %>% sample_frac(size = 0.1)
# 按时间顺序观察结果
green_2018_sample_prop_1 %>% arrange(lpep_pickup_datetime)
## # A tibble: 880,730 x 19
## VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~ RatecodeID
## <dbl> <dttm> <dttm> <chr> <dbl>
## 1 2 2008-12-31 23:02:37 2008-12-31 23:02:44 N 1
## 2 2 2008-12-31 23:02:52 2008-12-31 23:04:27 N 1
## 3 2 2008-12-31 23:03:48 2008-12-31 23:17:40 N 1
## 4 2 2008-12-31 23:03:53 2008-12-31 23:05:29 N 1
## 5 2 2008-12-31 23:09:15 2008-12-31 23:23:14 N 1
## 6 2 2008-12-31 23:10:46 2008-12-31 23:24:17 N 1
## 7 2 2008-12-31 23:26:54 2008-12-31 23:36:55 N 1
## 8 2 2008-12-31 23:31:42 2008-12-31 23:36:53 N 1
## 9 2 2008-12-31 23:35:43 2008-12-31 23:45:21 N 1
## 10 2 2008-12-31 23:50:31 2009-01-01 18:39:11 N 1
## # ... with 880,720 more rows, and 14 more variables: PULocationID <dbl>,
## # DOLocationID <dbl>, passenger_count <dbl>, trip_distance <dbl>,
## # fare_amount <dbl>, extra <dbl>, mta_tax <dbl>, tip_amount <dbl>,
## # tolls_amount <dbl>, ehail_fee <lgl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <dbl>, trip_type <dbl>
使用rowid_to_column()结合slice_sample()的具体方法如下:
# 方法2:rowid_to_column()结合slice_sample()
# 设置随机种子
set.seed(100)
# 抽样
green_2018_sample_prop_2 <- green_2018 %>%
rowid_to_column() %>%
slice_sample(prop = 0.1)
# 按ID顺序观察结果
green_2018_sample_prop_2 %>% arrange(rowid)
## # A tibble: 880,730 x 20
## rowid VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~
## <int> <dbl> <dttm> <dttm> <chr>
## 1 21 1 2018-01-01 00:25:08 2018-01-01 00:28:27 N
## 2 29 2 2018-01-01 00:41:36 2018-01-01 00:56:36 N
## 3 37 2 2018-01-01 01:03:50 2018-01-01 01:09:36 N
## 4 39 2 2018-01-01 00:39:21 2018-01-01 00:48:02 N
## 5 47 2 2018-01-01 00:47:29 2018-01-01 00:56:32 N
## 6 53 2 2018-01-01 00:26:40 2018-01-01 01:03:38 N
## 7 65 1 2018-01-01 00:04:29 2018-01-01 00:23:48 N
## 8 70 2 2018-01-01 00:51:16 2018-01-01 00:58:18 N
## 9 71 2 2018-01-01 00:12:10 2018-01-01 00:14:57 N
## 10 76 1 2018-01-01 00:30:21 2018-01-01 00:36:48 N
## # ... with 880,720 more rows, and 15 more variables: RatecodeID <dbl>,
## # PULocationID <dbl>, DOLocationID <dbl>, passenger_count <dbl>,
## # trip_distance <dbl>, fare_amount <dbl>, extra <dbl>, mta_tax <dbl>,
## # tip_amount <dbl>, tolls_amount <dbl>, ehail_fee <lgl>,
## # improvement_surcharge <dbl>, total_amount <dbl>, payment_type <dbl>,
## # trip_type <dbl>
有时候,数据集中可能有某些变量在分析或者建模中特别重要,我们希望抽取的样本在这些变量上的分布结构能够和总体保持一致,例如在构建机器学习分类模型中划分训练集和测试集时,要保证表示分类结果的因变量各个取值的比例分布和整体相同,这种情况下,简单随机抽样并不一定能达到这个效果(当然,如果数据量足够大,简单随机抽样得到的结果也有非常接近的分布),需要使用更精确的分层随机抽样方法,即按照某个或某些重要变量的比例分布进行抽样,保证样本中这些变量的分布结构和总体保持一致。
在以下例子中,我们希望保证样本和整体中支付类型(payment_type)字段的比例分布一致。要达到这个目标,先使用group_by()确定作为分组参考的变量,再使用sample_frac()实现抽样,并将结果保存在green_2018_pt_sample_frac中:
# 设定随机种子
set.seed(100)
# 根据payment_type进行抽样
green_2018_pt_sample_frac <- green_2018 %>%
group_by(payment_type) %>%
sample_frac(size = 0.1) %>%
ungroup()
通过比较我们可以发现,在green_2018_pt_sample_frac中,5种支付类型的比例分布与整体完全一致,而在上文中通过简单随机抽样得到的green_2018_sample_prop_1中,5种支付类型的比例分布同样与整体接近,但依然略有差异。如果对某个或某些变量在抽样中的分布与整体的一致性有较高要求的话,使用分层抽样是个更优的选择。
# 在整体中payment_type的整体比例
green_2018 %>%
count(payment_type) %>%
mutate(prop = round(n / sum(n), 4))
## # A tibble: 5 x 3
## payment_type n prop
## <dbl> <int> <dbl>
## 1 1 5006775 0.568
## 2 2 3740376 0.425
## 3 3 40688 0.0046
## 4 4 19089 0.0022
## 5 5 375 0
# 分层抽样中payment_type的分布
green_2018_pt_sample_frac %>%
count(payment_type) %>%
mutate(prop = round(n / sum(n), 4))
## # A tibble: 5 x 3
## payment_type n prop
## <dbl> <int> <dbl>
## 1 1 500678 0.568
## 2 2 374038 0.425
## 3 3 4069 0.0046
## 4 4 1909 0.0022
## 5 5 38 0
# 简单随机抽样中payment_type的分布
green_2018_sample_prop_1 %>%
count(payment_type) %>%
mutate(prop = round(n / sum(n), 4))
## # A tibble: 5 x 3
## payment_type n prop
## <dbl> <int> <dbl>
## 1 1 500106 0.568
## 2 2 374643 0.425
## 3 3 4088 0.0046
## 4 4 1865 0.0021
## 5 5 28 0
还有一种抽样场景:研究对象按照某种特定的顺序(例如时间)排列,我们希望根据这种顺序依照固定的间隔抽取样本,例如对某个产品按照一定间隔抽取批次进行质量检查。这就需要使用系统抽样方法。系统抽样的基本逻辑是,对整体样本按照某种顺序进行排列,先随机选取起始样本的编号,然后根据相同的间隔抽取样本,直到要抽取样本的编号大于总体编号的最大值为止。也可以预先确定抽样规模,然后计算在该样本量下的抽样间隔,进而按照间隔进行样本抽取。R中没有直接用于系统抽样的函数,我们可以根据系统抽样的逻辑自行编写程序。
第一种思路的实现方式具体如下:
# 方法1:预先设定抽样间隔进行抽样
# 设定随机种子
set.seed(100)
# 设定抽样间隔,假设每隔100条样本抽1条
sep <- 100
# 设定起始样本
start <- sample(1:sep, 1)
# 确定需要抽取的样本ID
sample_id_1 <- seq(start, nrow(green_2018), sep)
# 按照样本ID抽取样本
green_2018_sys_sample_1 <- green_2018 %>%
arrange(lpep_pickup_datetime) %>% # 按照上车时间排序
rowid_to_column() %>% # 增加一列样本ID
slice(sample_id_1) # 选择ID在抽样ID范围内的样本,等同于filter(rowid %in% sample_id)
# 查看结果
green_2018_sys_sample_1
## # A tibble: 88,073 x 20
## rowid VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~
## <int> <dbl> <dttm> <dttm> <chr>
## 1 74 2 2008-12-31 23:50:31 2009-01-01 18:39:11 N
## 2 174 2 2009-01-01 03:08:37 2009-01-01 03:10:06 N
## 3 274 2 2010-09-23 00:13:51 2010-09-23 00:16:19 N
## 4 374 2 2010-09-23 01:43:13 2010-09-23 01:54:04 N
## 5 474 2 2017-12-31 17:31:04 2017-12-31 17:31:09 N
## 6 574 2 2018-01-01 00:04:43 2018-01-01 00:28:55 N
## 7 674 2 2018-01-01 00:08:30 2018-01-01 00:28:34 N
## 8 774 2 2018-01-01 00:11:39 2018-01-01 00:23:23 N
## 9 874 2 2018-01-01 00:13:52 2018-01-01 00:18:10 N
## 10 974 2 2018-01-01 00:16:20 2018-01-01 00:20:43 N
## # ... with 88,063 more rows, and 15 more variables: RatecodeID <dbl>,
## # PULocationID <dbl>, DOLocationID <dbl>, passenger_count <dbl>,
## # trip_distance <dbl>, fare_amount <dbl>, extra <dbl>, mta_tax <dbl>,
## # tip_amount <dbl>, tolls_amount <dbl>, ehail_fee <lgl>,
## # improvement_surcharge <dbl>, total_amount <dbl>, payment_type <dbl>,
## # trip_type <dbl>
第二种思路的实现方式具体如下:
# 方法2:预先设定抽样规模进行抽样
# 设定抽样规模
sample_size <- ceiling(nrow(green_2018) * 0.01)
# 计算该规模下的抽样间隔
interval <- nrow(green_2018) %/% sample_size
# 要抽取的样本编号
sample_id_2 <- seq_len(sample_size) * interval
# 按照样本ID抽取样本
green_2018_sys_sample_2 <- green_2018 %>%
arrange(lpep_pickup_datetime) %>%
rowid_to_column() %>%
slice(sample_id_2)
# 观察结果
green_2018_sys_sample_2
## # A tibble: 88,074 x 20
## rowid VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~
## <int> <dbl> <dttm> <dttm> <chr>
## 1 99 2 2009-01-01 00:14:36 2009-01-01 14:59:07 N
## 2 198 2 2009-01-01 13:54:36 2009-01-01 14:39:36 N
## 3 297 2 2010-09-23 00:43:38 2010-09-23 00:52:15 N
## 4 396 2 2010-09-23 02:19:03 2010-09-23 02:27:21 N
## 5 495 2 2017-12-31 23:36:17 2017-12-31 23:48:07 N
## 6 594 1 2018-01-01 00:05:52 2018-01-01 00:26:12 N
## 7 693 2 2018-01-01 00:09:02 2018-01-01 00:20:55 N
## 8 792 1 2018-01-01 00:11:56 2018-01-01 00:26:35 N
## 9 891 2 2018-01-01 00:14:15 2018-01-01 00:19:41 N
## 10 990 1 2018-01-01 00:16:39 2018-01-01 00:36:12 N
## # ... with 88,064 more rows, and 15 more variables: RatecodeID <dbl>,
## # PULocationID <dbl>, DOLocationID <dbl>, passenger_count <dbl>,
## # trip_distance <dbl>, fare_amount <dbl>, extra <dbl>, mta_tax <dbl>,
## # tip_amount <dbl>, tolls_amount <dbl>, ehail_fee <lgl>,
## # improvement_surcharge <dbl>, total_amount <dbl>, payment_type <dbl>,
## # trip_type <dbl>
在某些场景下,抽样不一定是以1条样本(数据集的1行)为单位,而可能是以具有某种共同特征的整个群体作为抽样单位,抽取群体中的所有个体。如在社会科学研究的抽样方案设计上,出于研究成本的考虑,经常以整个群体而非个人作为抽样单位,例如以街道、学校为抽样对象,抽取某个街道的全体居民,或某个学校的全体学生。一般来说,当不同群体样本之间的差异较大,而同一群体内部样本之间差异较小时,比较适合使用整群抽样方法。
下面的例子中,我们以不同的上车地点(PULocationID)作为抽样对象,将同一上车地点的所有行车记录均抽取为样本。此时,问题就从随机抽取行车记录变为随机抽取上车地点。例如随机抽取50个PULocationID的具体实现方式如下:
# 整群抽样
# 设定随机种子
set.seed(100)
# 随机抽取50个PULocationID
sample_group <- green_2018 %>%
select(PULocationID) %>%
unique() %>%
sample_n(50) %>%
pull() # 将结果变为一个向量
# 所有抽取的上车地点的行车记录均作为样本
green_2018_group_sample <- green_2018 %>%
filter(PULocationID %in% sample_group)
# 查看结果
green_2018_group_sample
## # A tibble: 1,306,969 x 19
## VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~ RatecodeID
## <dbl> <dttm> <dttm> <chr> <dbl>
## 1 2 2018-01-01 00:30:26 2018-01-01 00:46:42 N 1
## 2 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 3 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 4 2 2018-01-01 00:38:35 2018-01-01 01:08:50 N 1
## 5 2 2018-01-01 00:35:23 2018-01-01 00:42:07 N 1
## 6 2 2018-01-01 00:11:48 2018-01-01 00:30:13 N 1
## 7 1 2018-01-01 00:25:08 2018-01-01 00:28:27 N 1
## 8 1 2018-01-01 00:11:41 2018-01-01 00:22:10 N 1
## 9 1 2018-01-01 00:40:32 2018-01-01 01:01:20 N 1
## 10 2 2018-01-01 00:35:44 2018-01-01 00:48:23 N 1
## # ... with 1,306,959 more rows, and 14 more variables: PULocationID <dbl>,
## # DOLocationID <dbl>, passenger_count <dbl>, trip_distance <dbl>,
## # fare_amount <dbl>, extra <dbl>, mta_tax <dbl>, tip_amount <dbl>,
## # tolls_amount <dbl>, ehail_fee <lgl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <dbl>, trip_type <dbl>
通过合理的抽样方法来压缩数据,可以避免数据中所包含的信息损失过量,也使得最终所得结果能在内存中处理,充分利用计算机内存处理速度快的优势。
完成数据抽样后,可以将原数据删除,以释放更多的内存进行后续工作。但通过系统的任务管理器可以看到,单单使用rm()删除对象不会马上释放内存空间,在删除比较大的数据集时,需要配合使用gc()函数才能更好地达到释放内存的目的。
另外,在数据挖掘场景下的抽样和社会科学调查研究的抽样场景会有些不同。多数情况下,数据挖掘场景下的抽样是一种事后抽样,即在获取数据(很多时候是全体样本)后,将数据存储在数据库中,并出于对计算资源的考虑,在数据库中抽取样本。在这种情况下,抽样的自由程度较高,错误成本较低,因为实施补救措施更容易,只要重新抽样即可。相比之下,传统的社会科学调查研究场景中,由于一般来说都不可能获取全样本,因此往往是花费相当长的时间,设计了完备的抽样方案再实施调查。这对抽样方案科学性、严谨性的要求非常高,一般来说,抽样方案也会非常复杂,因为样本如果不合理,就会为研究带来灾难性的后果。
抽样一般针对比较大,但仍能够读入计算机内存的数据集。如果数据集太大以致计算机内存不能读入,要怎么实施抽样呢?这就要结合第3章的数据分块思路。
数据类型是影响数据占用空间的一个重要因素。readr库的read_csv()等数据导入函数默认通过读取数据的前1000行来判断每个变量的类型,这种自动指定变量类型的方式能提高数据读取的速度,但从数据对象内存占用的角度来说,往往并非是最优的。
Aloysius Lim等人详细总结了每种数据类型的空间占用情况[1]:
64位版本的R使用40个字节作为向量的头,存储向量的信息,如长度和数据类型,使用其余空间存储向量中的数据。每个原始类型向量值占1个字节,每个逻辑型和整型值占4个字节,每个数值类型值占8个字节,每个复数类型值占16个字节。字符型向量和列表有些不同,并没有在向量内部存储实际数据,而是存储指向实际数据的其他向量的指针。在计算机内存中,字符型向量或列表的每个元素都是一个指针,该指针在32位R版本下占4个字节,在64位R版本下占8个字节。字符型向量需要的内存量取决于向量中的唯一字符串数。如果字符串向量中有很多重复字符串,那么将它转换为因子向量就能够减少内存占用量。因为因子实际上是对字符型向量中的唯一字符串进行索引的整型向量。
从上述信息中,我们可以大致总结出一些技巧,例如在条件允许的前提下,使用整型变量存储值比使用数值型变量更节省内存,使用因子形式存储字符型变量也相对更节省内存。
在对数据内容熟悉的前提下,可以考虑通过人工方法显式指定最合适的数据类型,在这方面,基础R、readr库,以及第2章介绍的data.table库等,都包含了带有可以指定变量类型的参数的数据导入函数。以readr库的read_csv()函数为例,将某些数值型变量直接指定为整型(integer)、将某些字符型变量指定为因子(factor)或者时间(datetime)等,均能够显著地提高内存使用的效率。当数据集比较大时,这种策略会带来很可观的回报。以下展示了设置变量类型的具体方式:
# 读入数据
green_2018_type <- read_csv(
file.path(path, file),
col_names = TRUE,
col_types = cols(
VendorID = col_integer(),
store_and_fwd_flag = col_factor(),
RatecodeID = col_integer(),
PULocationID = col_integer(),
DOLocationID = col_integer(),
passenger_count = col_integer(),
payment_type = col_integer()
)
)
# 查看结果
green_2018_type
## # A tibble: 8,807,303 x 19
## VendorID lpep_pickup_dateti~ lpep_dropoff_datet~ store_and_fwd_f~ RatecodeID
## <int> <dttm> <dttm> <fct> <int>
## 1 2 2018-01-01 00:18:50 2018-01-01 00:24:39 N 1
## 2 2 2018-01-01 00:30:26 2018-01-01 00:46:42 N 1
## 3 2 2018-01-01 00:07:25 2018-01-01 00:19:45 N 1
## 4 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 5 2 2018-01-01 00:32:40 2018-01-01 00:33:41 N 1
## 6 2 2018-01-01 00:38:35 2018-01-01 01:08:50 N 1
## 7 2 2018-01-01 00:18:41 2018-01-01 00:28:22 N 1
## 8 2 2018-01-01 00:38:02 2018-01-01 00:55:02 N 1
## 9 2 2018-01-01 00:05:02 2018-01-01 00:18:35 N 1
## 10 2 2018-01-01 00:35:23 2018-01-01 00:42:07 N 1
## # ... with 8,807,293 more rows, and 14 more variables: PULocationID <int>,
## # DOLocationID <int>, passenger_count <int>, trip_distance <dbl>,
## # fare_amount <dbl>, extra <dbl>, mta_tax <dbl>, tip_amount <dbl>,
## # tolls_amount <dbl>, ehail_fee <lgl>, improvement_surcharge <dbl>,
## # total_amount <dbl>, payment_type <int>, trip_type <dbl>
通过显式地指定部分变量的类型,与未指定相比,导入后的数据节省了约200 MB内存:
# 占用内存比较
object_size(green_2018)
## 1.3 GB
object_size(green_2018_type)
## 1.06 GB
关于变量类型设置的更多介绍,可参阅readr库的官方网站中的“Introduction to readr”一文。
数据太大怎么办?缩小啊!所谓“大道至简”,有时候最简单的方法可能也是最有效的。在数据科学项目中,深入地了解业务问题和所掌握的数据应该先于其他各项技术环节。对业务和数据有充分了解后,我们可能会发现手上的数据中有很多无用的部分——这有点像买榴:榴很大,壳也带刺,所以我们不会把榴连壳带回家,而是挖出里面的果肉带走,因为有价值的就是那么一点东西而已。对于大量的数据,在进行更复杂的操作之前,也不妨处理雕琢一番,去掉无用的部分。
[1] ALOYSIUS,TJHI.R高性能编程[M].北京:电子工业出版社,2015:74-77.