更多深度文章,请关注云计算频道:https://yq.aliyun.com/cloud
在这篇文章中,我希望能够简洁地介绍一下关于pandas的一些关键问题,以及介绍如何处理这些问题有效的解决方案。
一.背景
对于我们现在所说的数据科学而言,Python用到的地方比较少。近几年Pandas还不是大家处理数据时候的首选,他们通常使用R,SAS,SPSS,Hadoop或MATLAB。
pandas的内部体系结构有一些不足,这并不奇怪。2011年夏季,我设计了一个被称为BlockManager的内存管理对象的装置,内部使用了NumPy数组来管理内部的数据列pandas.DataFrame。
尽管BlockManager和pandas耦合到NumPy上,整体的内部联系已经很好的为这个项目提供了服务,但如今这些同时也是导致pandas用户在大型数据集中工作的出现的问题的一个根本原因。
在2011年我并没有考虑分析100 GB或1 TB的数据集。如今,我对pandas的准则是,你的数据集的大小应该是RAM的5到10倍。所以如果你有一个10 GB的数据集,那么如果你想避免内存管理问题的话,你应该有大约64G、最好是128GB的RAM。
Pandas的首个准则:数据集的大小应该是RAM的5到10倍。
二.DataPad,Badger,我在Cloudera的时间
我与Chang She(我的老朋友也是pandas合作者)在2013年合作开始做DataPad。我们想要使用新兴的PyData堆栈为我们正在构建的可视化分析应用程序提供支持,但是我们遇到了一些严重的性能问题,特别是在云(cloud)这方面,因为来自DataPad应用程序的分析查询的响应并不适合pandas。
所以我把pandas的特征集简化为简单的要点,我们称之为Badger。我发现通过针对优化本地数据使用连续不变的柱状数据结构,我可以在优化过程中获得2-20倍的性能提升,最大的改善是在字符串处理上。
在2013年11月我做过一个主题为“我讨厌pandas的十个方面”的演讲。
这十个方面(其实是十一个)是:
1.内部构件离“metal”太远。
2.不支持内存映射数据集。
3.数据库和文件摄取/导出性能不佳。
4.Warty缺少数据支持。
5.缺乏内存使用的透明度和RAM管理。
6.对分类数据的弱支持。
7.复杂的组合操作笨拙而又缓慢。
8.将数据附加到DataFrame很繁琐而且成本非常高。
9.有限的,不可扩展的类型元数据。
10.急切的评估模式,无查询规划。
11.“慢”,算法不适用于处理较大数据集。
我已经开始解决Badger中的一些问题,但在解决DataPad问题的方案上能力有限。幸运的是,我搬到了Cloudera,那里有很多资料库和大数据系统开发人员的经验可供我学习。
在Cloudera,我开始关注Impala,Kudu,Spark,Parquet等大型数据存储和分析系统。由于Python和pandas从未涉及这些任何一个项目,因此与其建立集成很困难。最大问题是数据交换,特别是将大型表格数据集从一个进程的内存空间移动到另一个进程的内存空间。这是非常昂贵的,而且没有标准的解决办法。
到2015年初,我一直在研究所谓的“柱状数据中间设备(columnar data middleware)”,它提供零拷贝访问,对字符串、嵌套类型以及野生的类似于JSON数据类型的所有类型。像Badger的原型运行时一样,这种格式需要针对本地数据进行优化,以便我们可以以最大速度的进行评估查询。
我很幸运地遇到一群做过大数据项目的朋友,特别是来自Apache Drill,Impala,Kudu,Spark等的人。在2015年下半年,为了创建一个没有软件供应商关联的中立“安全空间”,我们与Apache软件基金会合作建立了Apache Arrow。
我坚定地认为,Arrow是下一代数据科学工具的关键技术。
同样在2015年底,我写了一整套设计文件,准备开始建立一个更快,更干净的pandas核心实现,我们可以称之为pandas2。Pandas是一个基于共识来管理自己的社区项目,我想看看其他的核心开发者是否同意我对pandas内部错误的评估,在这个问题上大家的意见基本一致,但是在不破坏现有的pandas用户社区情况下解决它们还是个问题。在这段时间里,我专注于构建计算基础设施,此过程pandas用户基本上看不见。
三.Arrow解决这“10件事”了吗?
Arrow的C++实现工具为pandas等项目提供必要的内存分析基础架构:
1.针对分析处理性能优化的运行时列式存储格式
2.零拷贝,面向块/流的数据层,旨在以最大速度移动和访问大型数据集
3.可扩展类型的元数据,用于描述现实世界系统中发生的各种平面和嵌套数据 类型,并支持用户自定义的类型
而目前Arrow C++项目所缺少的是(但这种情况不会持续太久):
1.一个全面的分析功能的“内核”库
2.图形数据流执行的逻辑运算符图(仅在数据帧方面,可以参考一下TensorFlow或PyTorch)
3.用于并行评估运算符图的多核心调度器
接下来,我将介绍这“10个方面”,以及如何通过Arrow项目来解决。
1.更接近“metal”
基于Arrow每列所有内存(无论是字符串类型,number类型还是嵌套类型)都排列在针对随机访问(单个值)和扫描(多个相邻的值)性能优化的连续内存缓冲区中。这个想法是,即使使用字符串或其他非数字类型,也可以在循环访问表列中的数据时尽量减少CPU或GPU高速缓存时未命中项。
在pandas里,字符串数组是数组PyObject的指针,而实际的字符串数据存在于PyBytes或PyUnicode整个堆结构进程中。作为开发人员,对于处理这些对象的内存限制,我们无能为力。在Python中,简单的字符串'wes'占用了52个字节的内存。''占用49字节。
在Arrow中,每个字符串紧邻内存中的前一个,因此你可以扫描字符串中的所有数据,而不会出现任何高速缓存未命中的问题。在“metal”上处理连续字节。
Arrow的C / C ++ API意味着对Python不了解的应用程序可以消耗或产生原始的Arrow表,并在进程中或通过共享内存/内存映射来共享它们。Pandas缺少用于数据帧的C或Cython API是另一个大问题。
2.内存映射巨大的数据集
或许pandas最大的内存管理问题是数据必须完全加载到RAM中才能处理的要求。Pandas的内部BlockManager太复杂了,无法在任何实际的内存映射设置中使用,因此,只要你创建pandas.DataFrame,都将执行不可避免的转换和复制。
Arrow序列化设计提供了一个“数据header”,它描述了表中所有列的所有内存缓冲区的确切位置和大小。这意味着你可以将内存映射到比RAM更大的数据集,并在其原状上评估pandas类型的算法,而不必将其加载到内存中,就像你现在必须使用pandas一样。你可以从1TB的桌面中间读取1MB,而只需支付总共1MB的随机读取内存。此技术采用现代固态驱动器,这是一个很好的策略。
3.高速数据采集和导出(数据库和文件格式)
Arrow的高效内存布局和丰富的元数据使其成为数据库和柱状存储格式(如Apache Parquet)的入站数据的理想容器。
Arrow的原始构造之一是“记录批处理流”的概念,聚在一起的一系列原子表包含了一个大型数据集。这种处理数据流模型是为数据库指针提供记录流的数据库的一个想法。
我们一直在研发一种采用Parquet格式的高速连接器。我们还看到了用以基于ODBC的数据库连接的turbodbc优化项目。
4.“正确”的丢失数据
Arrow中的所有缺失数据都表示为一个填充位数组,并与其他数据分开。这使得丢失数据处理在所有数据类型中都是简单和一致的。你还可以使用快速逐位内置的硬件运算符和SIMD对null bits(AND位图或计数位)进行分析。
数组中的空值也明确的存储在其元数据中,因此如果数据没有空值,我们可以选择更快的代码路径来跳过空检查。使用pandas,我们不能假设数组没有空标签值,因此大多数分析都有额外的空检查,虽然这会损害性能。如果没有空值,你甚至不需要分配位数组。
因为NumPy中本来就支持丢失数据,所以随着时间的推移,我们不得不实现我们自己的更为关键的性能关键算法的空闲版本。最好是从头开始将所有的算法和内存管理内置到null-handling中。
5.保持内存分配检查
在pandas中,所有的内存都是由NumPy或Python解释器所拥有,并且很难精确地测量给定的pandas.DataFrame的内存使用量。
在Arrow的C ++实现中,所有内存分配都在中央“内存池”中“小心的”跟踪,因此你可以确定任何给定时间内RAM中的Arrow内存数量。通过使用带有父子关系的“子池(subpools)”,你可以精确地测量算法中的“高水位”,以了解分析操作时的内存使用的峰值情况。这种技术在数据库中常见用于监视或限制内存在操作员评估中的使用。如果你知道你将要超出可用的RAM,则可以应用缓解策略,如溢出到磁盘(内存映射数据集到磁盘上的能力当然最为关键的)。
在Arrow里,其内存中是一成不变的或写入时复制的。在任何给定的时间,你可以知道另一个数组是否引用一个你可以看到的缓冲区。这可以使我们避免防御性的复制。
6.支持分类数据
当我在2013年发表演讲时,pandas还没有pandas.Categorical类型,这个类型是之后实现的。对于不在Numpy的数据类型,pandas的解决方法一直是有点“愚蠢”的。如果不用pandas,那你就不能使用pandas.Categorical。扩展dtypes的方法已经实现了,但由于pandas与NumPy的紧密耦合,有些束缚和局限。
在Arrow中,categories数据是最厉害的功能,同时在内存中,网络上或者共享内存中我们可以连续而又连续的优先体现。我们支持在多个数组之间共享categories(在Arrow中称为字典)。
Pandas有其他用户自定义的类型:datetime与时区和周期。我们打算能够支持Arrow中的逻辑数据类型,以便特定的系统可以使用Arrow完整的地传输其数据,而无需更改Arrow格式的文档。
7.更好的组合应用操作
Arrow之所以有用是因为能使groupby操作更容易并行化,由于这里也列出的有其他问题,所以不可能使df.groupby(...).apply(f)操作完全并行化。
有时候,我们也希望改进pandas复杂应用操作的API。
8.附加到数据帧
在pandas中,DataFrame(数据帧)中每列中的所有数据必须位于相同的NumPy数组中。这是一个限制性的要求,所以经常会导致内存加倍和额外的计算来连接Series和DataFrame对象。
Arrow C ++中的表中的列可以分块,因此附加到表是零复制操作,所以不需要独特的计算或内存分配。通过为流,分块表设计前端,附加到现有的内存中的表相对于pandas来说在计算上是便宜的。用于分块或流数据的设计对于实现out-of-core算法也是必不可少的,因此我们也为处理大于内存中的数据集奠定了基础。
9.添加新的数据类型
添加新数据类型有多层的复杂性:
1.添加新的元数据
2.为分析中运算符的实现创建动态调度规则
3.通过操作保留元数据
例如,“currency(通用)”类型可以包含当前类型的字符串,数据在物理上表示为float64或decimal。因此,你可以按照数字表示方式对currency(通用)类型进行计算,然后在数值操作中执行currency(通用)类型的元数据。
在Arrow中,我们已经将从计算的细节中将元数据表现层与元数据的保护层分离了。在C ++的实现中,我们一直在为用户自定义的类型进行规划,因此当我们更多地关注构建一个分析引擎时,目标是创建用户自定义的运营商调度和元数据的提升规则。
10/11。查询计划,多核执行
当你写df[df.c < 0].d.sum()时,pandas创建一个临时名为df[df.c < 0]的DataFrame,然后对该d列的临时对象求和。如果 df 包含很多列,是非常浪费的。当然你可以写df.d[df.c < 0].sum(),但即使这样,也会产生一个临时的序列,然后将其相加。
显然,如果你知道你正在评估的整个表述,你可以做得更好,而且也可以避免这些临时分配。此外,许多算法(包括此示例)可以在计算机上的所有处理器核心之间并行化。
作为构建Arrow的分析引擎的一部分,我们还计划使用在进程中的多核调度程序构建一个轻量级的“查询计划程序”实体,以使多种算法能够并行化和有效地评估。在图形数据流执行的领域(特别是在最近的ML,如TensorFlow和PyTorch)中有大量现有技术,所以这相当于创建一个图形数据流引擎,其原始数据单位是一个Arrow表。
四.附录:On Dask
很多人问我关于Dask(和Spark,以及其他这样的项目)以及它如何帮助pandas展现更好的表现和可扩展性。其实在各个方面都有,比如:
1.将大型数据集拆分成单独的线程或单独的进程来工作
2.从RAM中消除不再需要的pandas数据
Dask通过并行运行pandas.read_csv然后在整个数据集上运行groupby,来轻松读取CSV文件目录。Dask模型的一个问题是使用pandas作为黑盒子,但dask.dataframe不会解决pandas的内在性能和内存使用问题,而是将它们分散在多个进程中,并小心地通过拒绝一次性处理太大的数据来减少问题,从而导致一个非预期的MemoryError错误。
此外,pandas的内存管理和IO的性能挑战使得Dask工作比通过更高效的内存运行要慢得多。
文章原标题:《Apache Arrow and the "10 Things I Hate About pandas"》
作者: Wes McKinney
译者:一只高冷的猫,审校:袁虎。
文章为简译,更为详细的内容,请查看原文