Arrow文件格式的理论和实践介绍
上周,我看到了韦斯·麦金尼(Wes McKinney)的一条推文,该推文可能是最棒的Pandas软件包的创建者而闻名:
因此,当我看到他引用威廉·吉布森(William Gibson)的讲话时,我认为一定会有令人惊奇的事情发生。 我没有失望。
链开始时的推文是有关自然语言处理库Hugging Face的。 该项目收集可用于模型训练和基准测试的数据集。 其中一些数据集非常庞大。 在最初的推文中,Thomas Wolf指出,通过一种特殊的文件格式,他和Quentin Lhoest现在能够在不到一分钟的时间内遍历17GB数据,而RAM占用9MB
我想到的第一个问题是:如何? 这里发生了什么魔术?
这就是韦斯·麦金尼所谈论的未来。
由于在线上没有很多实际示例,因此我决定写一篇介绍性博客文章,其中包含有关我到目前为止所学知识的动手示例。 我与Hugging Face或PyArrow项目无关。 在本文的结尾,您将找到所有资源的链接。
任何足够先进的技术都无法与魔术区分开。 -亚瑟·克拉克(Arthur C. Clarke),第三定律

> Photo by Joshua Sortino on Unsplash
顺利交换数据
第一个线索是托马斯·沃尔夫(Thomas Wolf)指的是Apache Arrow。 Apache Arrow是由Wes McKinney启动的一个项目,旨在创建一个用于数据交换的接口:
Apache Arrow是用于内存数据的跨语言开发平台。 它为平面和分层数据指定了一种与语言无关的标准化列式存储格式,该格式组织用于在现代硬件上进行有效的分析操作。 它还提供计算库和零复制流式消息传递和进程间通信。 [1]
这是什么意思?
在Arrow之前,在任何应用程序或库之间交换数据的标准方法是将数据以一种或另一种方式存储到磁盘。 因此,如果.NET Core库希望将数据传递给Python进行数据分析,则很可能有人会写出一个文件(例如csv,json,Parquet等),然后使用Python重新读取。 写入(序列化)和读取(反序列化)这两个步骤都是昂贵且缓慢的-数据集越大,完成每个步骤所花费的时间就越长。

如果可以通过握手和零复制直接交换数据该怎么办? 它可能看起来像这样:.NET将开始与Python聊天,指向内存中的一堆数据,然后像:嘿,朋友,这是您的了。 而且Python可以直接跳转到它,而无需将它从一个地方拖到另一个地方。 那不是很棒吗?
这就是Apache Arrow的目的。

Parquet是秘密吗?
那让我想知道-我如何使用Arrow? 通过查看Hugging Face的源代码,我了解到该项目使用PyArrow读取数据。 在此之前,我将PyArrow与Parquet(一种高度压缩的列式存储格式)相关联。 那么,Parquet是Arrow如何交换数据的方式吗? (剧透:不是)
传统上,数据以逐行的方式存储在磁盘上。 列式存储的诞生是出于分析大型数据集并对其进行有效聚合的需要。 数据分析对数据行(例如,一项客户交易,一个呼叫日志等)的兴趣较小,但对其聚合(例如,客户花费的总金额,按区域划分的总呼叫分钟数,等等)的兴趣较小。

> Row vs column oriented storage (Adapted from [4] with Palmer Station Penguin dataset)
这引起了方向的改变:列式存储不是逐行存储,而是逐列地排列数据。
Parquet是一种列存文件格式,具有两个主要优点[4]:
· 高度可压缩:默认情况下,.json或.csv文件未压缩,而Parquet压缩数据,因此节省了大量磁盘空间。 表通常混合使用具有大量唯一值(高基数;考虑唯一的用户ID)的列和仅具有少数唯一值(低基数;考虑国家/地区)的列。 基数越低,压缩(可能)的效果越好-下一节将对此进行更多说明
· 文件查询/过滤器下推:在读入之前修剪不必要的数据。这可以缩短加载时间并优化资源消耗。 如果您只需要一千个表格中的两列,则无需扫描所有行即可获得这两个属性-您可以直接获取整个列
压缩
为了更好地理解Parquet和Arrow之间的区别,我们将需要绕道而行,以进行压缩。 文件压缩本身就是一个巨大的课题。 以下是一个简化的叙述,通过我自己对该主题的理解而过滤掉了。 题外话将帮助回答以下两个问题:
· Parquet如何将文件缩小到如此小的尺寸?
· Parquet与Avro有何不同?
掷硬币
假设您掷硬币十次并记录结果:
[Head, Head, Head, Head, Tail, Tail, Tail, Head, Tail, Tail]
现在,尝试大声说出结果吗? 可能会缩短它,并说" 4头,3尾,头和2尾"。
[4 x Head, 3 x Tail, Head, 2 x Tail]
这就是实际的压缩(所描述的算法称为游程长度编码[8])。 我们趋向于自然地看到一种模式并且缩写。 压缩算法也可以做到这一点-只是具有更多的原始计算能力和复杂的规则。 但是此示例足以帮助我们理解一个关键的区别:.csv采用文字方法并拼写出每条记录时,Parquet则缩写(不丢失任何信息)。
这个简单的例子足以说明为什么压缩率会大不相同。 例如,如果排序顺序无关紧要,而您只是对头与尾的总出现次数感兴趣,则可以先对列表进行排序,然后压缩版本将如下所示:
[5 x Head, 5 x Tail]
言外之意,如果在将数据集保存到Parquet之前先按所有列对数据集进行排序,则文件大小将比未排序的数据集小。 基数越低,压缩率越高。 预期每列的压缩率将按排序顺序进一步缩小。
Arrow被压缩了吗?
这样,我们就可以理解为什么Parquet文件与未压缩的.csv文件相比如此之小。 但这与Arrow的关系如何?
事实证明,这恰恰是关键区别之一。 Parquet以高效的方式存储在磁盘上。 借助过滤器下推功能,您可以减少读入的数据量(即,仅选择您实际需要的列)。 但是,当您要对数据执行操作时,计算机仍需要解压缩压缩后的信息并将其带入内存。 [2]
另一方面,Arrow是一种内存映射格式。 Wes McKinney在博客文章中总结如下:
"Arrow序列化设计提供了一个'数据头',它描述了表中所有列的所有内存缓冲区的确切位置和大小。 这意味着您可以在内存中映射比RAM更大的大型数据集,并就其评估Pandas式算法,而无需像现在对Pandas那样将其加载到内存中。 您可以从1 TB的表的中间读取1兆字节,而您只需支付执行总计1兆字节的那些随机读取的费用。" [6]
简而言之,应用程序可以直接对磁盘上存储的数据集进行操作,而无需将其完全加载到内存中。 如果您还记得最初的Tweet,那就恰好在这里。
动手:性能比较
现在,让我们探索这些数据格式。 作为示例数据集,我使用的是Palmer Station Penguin数据集。 由于它仅包含350行,因此我将其重新采样为100万行,以使性能差异更加明显:
写文件
下一步,我将文件以三种格式写入磁盘:
· csv(缺少值的DataFrame)
· Parquet(缺少值的DataFrame)
· Arrow(具有&缺少值的DataFrame)
请注意,在某些情况下,Arrow只能转换为Pandas,而无需分配内存(=零副本)。 其中之一:必须没有NaN值。 为了比较使用零复制和不使用零复制之间的性能,我编写了一次包含和不包含缺失数值的Arrow文件。
结果文件大小为:

> File size comparison
正如预期的那样,Parquet是最小的文件-尽管是随机序列(在将文件写出之前未进行排序),但其压缩率高达80%。 Arrow仅比csv小。 原因是csv甚至将数值存储为字符串,这会占用更多磁盘空间。 在所有情况下,具有缺失值和没有缺失值的文件之间的大小差异都很小(<0.5 MB)。
阅读时间
现在的关键部分:阅读性能。 计算平均长度需要多长时间?
· CSV
· Parquet
· 带有文件API的Arrow(OSFile(…))
· 带有缺失值/ NaN的Arrow作为内存映射API(memory_map(…))
· Arrow作为内存映射API(memory_map(…)),没有缺少值
对这三个功能中的每一个进行计时,会产生以下结果:

> Performance comparison: Time required to read column & calculate average
毫不奇怪,csv是最慢的选择。 它要求读取200MB,解析文本,丢弃除长度以外的所有列,然后计算平均值。
由于不需要解析整个文件,因此Parquet的速度提高了约60倍-仅读入了所需的列。
缺少值的Arrow比Parquet快约3倍,比csv快约200倍。 与Parquet一样,Arrow可以将自身限制为仅读取指定的列。 使速度更快的原因是无需解压缩列。
请注意,使用零复制和不使用零复制来读取内存映射的Arrow文件之间的差异意味着性能又提高了约3倍(即零复制的总速度比csv快600倍,比Parquet快9倍)。
令人惊讶的是:带有文件API的Arrow甚至比Parquet慢。 这是怎么回事?
内存消耗
为了回答这个问题,让我们看一下内存消耗。 如果我们只读一列,每个文件消耗多少RAM?
结果如下:

> Performance comparison: Memory consumed to read column
最值得注意的是:带有文件API的Arrow占用189 MB的空间-几乎是整个文件的大小(即使我们只读取一列?!)。 答案在于文档:
" […] OSFile在每次读取时分配新的内存,就像Python文件对象一样。" [3]
通过使用OSFile,整个文件首先被读取到内存中。 现在显而易见,为什么此操作要比Parquet慢并且占用最多的内存!
但是,通过使用内存映射功能和填充的NaN值,可直接在存储的Arrow文件顶部创建pandas DataFrame。 没有复制:0 MB的RAM! 毫不奇怪,这是最快的选择。
您可以在这里找到整个Jupyter Notebook
结论
对我来说,有很多关于Arrow的知识。 到目前为止,我了解到的是:您也不能吃蛋糕也不能吃它。 在[7]之间需要权衡:
· 优化磁盘空间/在磁盘上长期存储→Parquet
· 优化数据交换和快速检索→Arrow
与csv相比,Parquet和Arrow的性能提升都非常显着。 当将Arrow存储到磁盘时,它消耗的存储空间比Parquet更多。 但是,在读取性能方面,Avro在时间和内存消耗方面都超过了Parquet。 所提供的示例(计算一列/读取列的平均值)只是表面上的划痕-我的期望是,随着查询的复杂度和数据集的增加,Arrow会更加发光。
只要使用内存映射功能读取Arrow,读取性能就令人难以置信。 最好的情况是数据集没有缺失值/ NaN。 然后,PyArrow可以发挥其神奇作用,并允许您在表上进行操作,而几乎不占用任何内存。
未来的确已经来了-真是太神奇了!
在推特上关注我
我将继续撰写有关Python,数据和技术的文章-很高兴在Twitter上与您会面✨
非常感谢Yingying的全面审查和好评!
资料来源
[1] Apache Arrow,着陆页(2020),Apache Arrow网站
[2] Apache Arrow,常见问题解答(2020),Apache Arrow网站
[3] Apache Arrow,磁盘和内存映射文件(2020年),Apache Arrow Python绑定文档
[4] J. LeDem,Apache Arrow和Apache Parquet:为什么我们需要用于列数据的不同项目,在磁盘和内存中(2017年),KDnuggets
[5] J. LeDem,专栏路线图:Apache Parquet和Apache Arrow(2017),Dremio
[6]麦金尼(W. McKinney),阿帕奇箭(Apache Arrow)和"我讨厌熊猫的十件事"(2017),博客
[7]麦金尼(W. McKinney),丹尼尔·阿巴迪(Daniel Abadi)关于Apache Arrow的博客的一些评论(2017),博客
[8]维基百科,游程编码(2020),维基百科
(本文翻译自Dejan Simic的文章《Apache Arrow: Read DataFrame With Zero Memory》,参考:https://towardsdatascience.com/apache-arrow-read-dataframe-with-zero-memory-69634092b1a)