实操 | 内存占用减少高达90%,还不用升级硬件?没错,这篇文章教你妙用Pandas轻松处理大规模数据

编译 | AI科技大本营(rgznai100)

参与 | 周翔

注:Pandas(Python Data Analysis Library) 是基于 NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。此外,Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。

相比较于 Numpy,Pandas 使用一个二维的数据结构 DataFrame 来表示表格式的数据, 可以存储混合的数据结构,同时使用 NaN 来表示缺失的数据,而不用像 Numpy 一样要手工处理缺失的数据,并且 Pandas 使用轴标签来表示行和列。

Pandas 通常用于处理小数据(小于 100Mb),而且对计算机的性能要求不高,但是当我们需要处理更大的数据时(100Mb到几千Gb),计算机性能就成了问题,如果配置过低就会导致更长的运行时间,甚至因为内存不足导致运行失败。

在处理大型数据集时(100Gb到几TB),我们通常会使用像 Spark 这样的工具,但是想要充分发挥 Spark 的功能,通常需要很高的硬件配置,导致成本过高。而且与 Pandas 不同,这些工具缺少可用于高质量数据清洗、勘测和分析的特征集。

因此对于中等规模的数据,我们最好挖掘 Pandas 的潜能,而不是转而使用其他工具。那么在不升级计算机配置的前提下,我们要怎么解决内存不足的问题呢?

在这篇文章中,我们将介绍 Pandas 的内存使用情况,以及如何通过为数据框(dataframe)中的列(column)选择适当的数据类型,将数据框的内存占用量减少近 90%。

棒球比赛日志

我们将要处理的是 130 年来的大型棒球联盟比赛数据,原始数据来源于 retrosheet。

最原始的数据是 127 个独立的 CSV 文件,不过我们已经使用 csvkit 合并了这些文件,并且在第一行中为每一列添加了名字。如果读者想亲自动手操作,可下载网站上的数据实践下:https://data.world/dataquest/mlb-game-logs

首先让我们导入数据,看看前五行:

import pandas as pdgl = pd.read_csv('game_logs.csv')gl.head()

我们总结了一些重要的列,但是如果你想查看所有的列的指南,我们也为整个数据集创建了一个数据字典:

我们可以使用 DataFrame.info() 的方法为我们提供数据框架的更多高层次的信息,包括数据大小、类型、内存使用情况的信息。默认情况下,Pandas 会占用和数据框大小差不多的内存来节省时间。因为我们对准确度感兴趣,所以我们将 memory_usage 的参数设置为 ‘deep’,以此来获取更准确的数字。

我们可以看到,这个数据集共有 171,907 行、161 列。Pandas 已经自动检测了数据的类型:83 列数字(numeric),78 列对象(object)。对象列(object columns)主要用于存储字符串,包含混合数据类型。为了更好地了解怎样减少内存的使用量,让我们看看 Pandas 是如何将数据存储在内存中的。

数据框的内部表示

在底层,Pandas 按照数据类型将列分成不同的块(blocks)。这是 Pandas 如何存储数据框前十二列的预览。

你会注意到这些数据块不会保留对列名的引用。这是因为数据块对存储数据框中的实际值进行了优化,BlockManager class 负责维护行、列索引与实际数据块之间的映射。它像一个 API 来提供访问底层数据的接口。每当我们选择、编辑、或删除某个值时,dataframe class 会和 BlockManager class 进行交互,将我们的请求转换为函数和方法调用。

每个类型在 pandas.core.internals 模块中都有一个专门的类, Pandas 使用 ObjectBlock class 来代表包含字符串列的块,FloatBlock class 表示包含浮点型数据(float)列的块。对于表示数值(如整数和浮点数)的块,Pandas 将这些列组合在一起,并存储为 NumPy ndarry 数组。NumPy ndarry 是围绕 C array 构建的,而且它们的值被存储在连续的内存块中。由于采用这种存储方案,访问这些值的地址片段(slice)是非常快的。

因为不同的数据都是单独存储的,所以我们将检查不同类型的数据的内存使用情况。我们先来看看所有数据类型的平均内存使用情况。

可以看到,大部分的内存都被 78 个对象列占用了。我们稍后再来分析,首先看看我们是否可以提高数字列(numeric columns)的内存使用率。

了解子类型

正如前面介绍的那样,在底层,Pandas 将数值表示为 NumPy ndarrays,并将它存储在连续的内存块中。该存储模型消耗的空间较小,并允许我们快速访问这些值。因为 Pandas 中,相同类型的值会分配到相同的字节数,而 NumPy ndarray 里存储了值的数量,所以 Pandas 可以快速并准确地返回一个数值列占用的字节数。

Pandas 中的许多类型包含了多个子类型,因此可以使用较少的字节数来表示每个值。例如,float 类型就包含 float16、float32、float64 等子类型。类型名称的数字部分代表了用于表示值类型的位数。例如,我们刚刚列出的子类型就分别使用了 2、4、8、16 个字节。下表显示了最常见的 Pandas 的子类型:

int8 使用 1 个字节(或者 8 位)来存储一个值,并且可以以二进制表示 256 个值。这意味着,我们可以使用这种子类型来表示从 -128 到 127 (包括0)的值。我们可以使用 numpy.iinfo class 来验证每个整数子类型的最小值和最大值,我们来看一个例子:

我们可以在这里看到 uint(无符号整数)和 int(有符号整数)之间的区别。这两种类型具有相同的存储容量,但如果只存储正数,无符号整数显然能够让我们更高效地存储只包含正值的列。

使用子类型优化数字列

我们可以使用函数 pd.to_numeric() 来 downcast(向下转型)我们的数值类型。我们将使用 DataFrame.select_dtypes 来选择整数列,然后优化这些列包含的类型,并比较优化前后内存的使用情况。

我们可以看到,内存的使用量从 7.9Mb 降到了 1.5 Mb,减少了 80% 以上。但这对原始数据框的影响并不大,因为本身整数列就非常少。

现在,让我们来对浮点型数列做同样的事情。

可以看到,我们所有的浮点型数列都从 float64 转换成 float32,使得内存的使用量减少了 50%。

让我们创建一个原始数据框的副本,然后分配这些优化后的数字列代替原始数据,并查看现在的内存使用情况。

虽然我们大大减少了数字列的内存使用量,但是从整体来看,我们只是将数据框的内存使用量降低了 7%。内存使用量降低的主要原因是我们对对象类型(object types)进行了优化。

在动手之前,让我们仔细看一下,与数字类型相比,字符串是怎样存在 Pandas 中的。

比较数字和字符串的存储方式

对象类型代表了 Python 字符串对象的值,部分原因是 NumPy 缺少对字符串值的支持。因为 Python 是一种高级的解释语言,它不能对数值的存储方式进行细粒度控制。

这种限制使得字符串以分散的方式存储在内存里,不仅占用了更多的内存,而且访问速度较慢。对象列表中的每一个元素都是一个指针(pointer),它包含了实际值在内存中位置的“地址”。

下面的图标展示了数字值是如何存储在 NumPy 数据类型中,以及字符串如何使用 Python 内置的类型存储。

你可能已经注意到,我们的图表之前将对象类型描述成使用可变内存量。当每个指针占用一字节的内存时,每个字符的字符串值占用的内存量与 Python 中单独存储时相同。让我们使用 sys.getsizeof() 来自证明这一点:先查看单个字符串,然后查看 Pandas 系列中的项目(items)。

你可以看到,存储在 Pandas 中的字符串的大小与作为 Python 中单独字符串的大小相同。

使用分类来优化对象类型

Pandas 在 0.15版引入了 Categoricals (分类)。category 类型在底层使用整数类型来表示该列的值,而不是原始值。Pandas 用一个单独的字典来映射整数值和相应的原始值之间的关系。当某一列包含的数值集有限时,这种设计是很有用的。当我们将列转换为 category dtype 时,Pandas 使用了最省空间的 int 子类型,来表示一列中所有的唯一值。

想要知道我们可以怎样使用这种类型来减少内存使用量。首先 ,让我们看看每一种对象类型的唯一值的数量。

可以看到,我们的数据集中一共有 17.2 万场比赛, 而唯一值的数量是非常少的。

在我们深入分析之前,我们首先选择一个对象列,当我们将其转换为 categorical type时,观察下会发生什么。我们选择了数据集中的第二列 day_of_week 来进行试验。

在上面的表格中,我们可以看到它只包含了七个唯一的值。我们将使用 .astype() 的方法将其转换为 categorical。

如你所见,除了列的类型已经改变,这些数据看起来完全一样。我们来看看发生了什么。

在下面的代码中,我们使用 Series.cat.codes 属性来返回 category 类型用来表示每个值的整数值。

你可以看到,每个唯一值都被分配了一个整数,并且该列的底层数据类型现在是 int8。该列没有任何缺失值,如果有的话,这个 category 子类型会将缺省值设置为 -1。最后,我们来看看这个列在转换到 category 类型之前和之后的内存使用情况。

可以看到,内存使用量从原来的 9.8MB 降到了 0.16MB,相当于减少了 98%!请注意,这一列可能代表我们最好的情况之一:一个具有 172,000 个项目的列,只有 7 个唯一的值。

将所有的列都进行同样的操作,这听起来很吸引人,但使我们要注意权衡。可能出现的最大问题是无法进行数值计算。我们不能在将其转换成真正的数字类型的前提下,对这些 category 列进行计算,或者使用类似 Series.min() 和 Series.max() 的方法。

当对象列中少于 50% 的值时唯一对象时,我们应该坚持使用 category 类型。但是如果这一列中所有的值都是唯一的,那么 category 类型最终将占用更多的内存。这是因为列不仅要存储整数 category 代码,还要存储所有的原始字符串的值。你可以阅读 Pandas 文档,了解 category 类型的更多限制。

我们将编写一个循环程序,遍历每个对象列,检查其唯一值的数量是否小于 50%。如果是,那么我们就将这一列转换为 category 类型。

和之前的相比

在这种情况下,我们将所有对象列都转换为 category 类型,但是这种情况并不符合所有的数据集,因此务必确保事先进行过检查。

此外,对象列的内存使用量已经从 752MB 将至 52MB,减少了 93%。现在,我们将其与数据框的其余部分结合起来,再与我们最开始的 861MB 的内存使用量进行对比。

可以看到,我们已经取得了一些进展,但是我们还有一个地方可以优化。回到我们的类型表,里面有一个日期(datetime)类型可以用来表示数据集的第一列。

你可能记得这一列之前是作为整数型读取的,而且已经被优化为 uint32。因此,将其转换为 datetime 时,内存的占用量会增加一倍,因为 datetime 的类型是 64 位。无论如何,将其转换成 datetime 是有价值的,因为它将让时间序列分析更加容易。

我们将使用 pandas.to_datetime() 函数进行转换,并使用 format 参数让日期数据按照 YYYY-MM-DD 的格式存储。

‍‍‍‍‍‍在读取数据时选择类型‍‍‍‍‍‍

到目前为止,我们已经‍探索了减少现有数‍据框内存占用的方法。首先,读入阅读数据框,然后再反复迭代节省内存的方法,这让我们可以更好地了解每次优化可以节省的内存空间。然而,正如我们前面提到那样,我们经常没有足够的内存来表示数据集中所有的值。如果一开始就不能创建数据框,那么我们该怎样使用内存节省技术呢?

幸运的是,当我们读取数据集时,我们可以制定列的最优类型。pandas.read_csv() 函数有几个不同的参数可以让我们做到这一点。dtype 参数可以是一个以(字符串)列名称作为 keys、以 NumPy 类型对象作为值的字典。

首先,我们将每列的最终类型、以及列的名字的 keys 存在一个字典中。因为日期列需要单独对待,因此我们先要删除这一列。

现在,我们可以使用字典、以及几个日期的参数,通过几行代码,以正确的类型读取日期数据。

通过优化这些列,我们设法将 pandas 中的内存使用量,从 861.6MB 降到了 104.28MB,减少了 88%。

分析棒球比赛

我们已经优化了数据,现在我们可以开始对数据进行分析了。我们来看看比赛的时间分布。

可以看到,在二十世纪二十年代之前,棒球比赛很少在周日举行,一直到下半世纪才逐渐流行起来。此外,我们也可以清楚地看到,在过去的五十年里,比赛时间的分是相对静态的。我们来看看比赛时长多年来的变化。

看起来,棒球比赛的时长自 1940 年以来就一直处于增长状态。

总结和后续步骤

我们已经了解到 Pandas 是如何存储不同类型的数据的,然后我们使用这些知识将 Pandas 里的数据框的内存使用量降低了近 90%,而这一切只需要几个简单的技巧:

  • 将数字列 downcast 到更节省空间的类型;
  • 将字符串转换为分类类型(categorical type)。

你,学会了吗?

原文地址 https://www.dataquest.io/blog/pandas-big-data/

原文发布于微信公众号 - AI科技大本营(rgznai100)

原文发表时间:2017-08-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏轮子工厂

程序员必读:教你摸清哈希表的脾气

在哈希表中,记录的存储位置 = f (关键字),通过查找关键字的存储位置即可,不用进行比较。散列技术是在记录的存储位置和它的关键字之间建立一个明确的对应关系f ...

9820
来自专栏小樱的经验随笔

BugkuCTF 矛盾

15920
来自专栏C/C++基础

C++ 模板元编程简介

模板元编程(Template Metaprogramming,TMP)是编写生成或操纵程序的程序,也是一种复杂且功能强大的编程范式(Programming Pa...

1.6K30
来自专栏Java技术栈

进阶 | Java生成随机数的几种高级用法!

34030
来自专栏令仔很忙

UML之包图

   当对一个比较复杂的软件系统进行建模时,会有大量的类、接口、组件、节点和图需要处理;如果放在同一个地方的话,信息量非常的大,显得很乱,不方便查询,所以就对这...

1.6K10
来自专栏大史住在大前端

野生前端的数据结构基础练习(5)——散列

散列函数相关的应用非常广,例如webpack打包时在文件名中添加的哈希值,将给定信息转换为固定位数字符串的加密信息等都是散列的实际应用,感兴趣的读者可以自行搜索...

9320
来自专栏西枫里博客

Python学习笔记四(条件和循环)

写在开头:今天催更小伙伴们,突然发现自己的python学习笔记竟然一个月没更了,按照每月总更8篇计算,每月应更2篇左右的python学习笔记,也不知是杂文更的太...

8010
来自专栏听雨堂

Python学习笔记(1):列表元组结构

Python的列表元组功能强大,令人印象深刻。一是非常灵活,二是便于集体操作。特别是以元组作为列表项的结构,和数据访问的结果能够对应起来,和习惯的二维表理解上也...

22460
来自专栏小樱的经验随笔

Java中import及package的用法

有些人写了一阵子 Java,可是对於 Java 的 package 跟 import 还是不  太了解很多人以為原始码 .java 档案中的 import 会让...

32050
来自专栏高性能服务器开发

写给新手们看的编程修养

什么是好的程序员?是不是懂得很多技术细节?还是懂底层编程?还是编程速度比较快?我觉得都不是。对于一些技术细节来说和底层的技术,只要看帮助,查资料就能找到,对于速...

15130

扫码关注云+社区

领取腾讯云代金券