将内存使用量减少高达90%的方法

当使用 pandas 处理小规模数据,如 100M 左右,性能问题不用担心。但是当处理稍大规模数据,如 G 级别的数据,性能问题就会使运行时间变长,甚至会出现内存不足而导致失败,就是所谓的 OOM 问题。

一些大数据工具如 Spark 可以处理大规模数据集,但是要发挥这些工具的价值往往需要昂贵的硬件条件支持。并且这些工具缺少 pandas 中丰富的数据清洗、数据探索、数据分析等功能。对于数据工作者而言,在中等规模的数据上更愿意使用 pandas,而不是切换到其它工具。这时候如何优化 pandas 内存性能就显得十分重要。

这篇博客将介绍 pandas 的内存使用情况,如何通过为列选择合适的数据类型,而将内存使用量减少近 90% 左右。

Mock big DataFrame

这里构造一个大数据 DataFrame,后面讲以此为例子进行介绍。

import pandas as pd
import numpy as np
rows = 5000000
a1 = np.random.randint(-100, 100, size=(rows))
a2 = np.random.randint(1, 100, size=(rows))
a3 = np.random.randn(rows)
a4 = [1.234] * rows
a5 = ["hello_" + str(i) for i in range(50)] * int((rows / 50))
a6 = ["world_" + str(i) for i in range(rows)]
a7 = ["2019-10-01"] * rows
df = pd.DataFrame({'A':a1, 'B':a2, 'C':a3, 'D':a4, 'E':a5, 'F':a6, 'G':a7})
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5000000 entries, 0 to 4999999
Data columns (total 7 columns):
A    int64
B    int64
C    float64
D    float64
E    object
F    object
G    object
dtypes: float64(2), int64(2), object(3)
memory usage: 267.0+ MB

df.info 可以简单的看出,数据一共有 5000000 行 7 列,其中 2列是 int64,2 列是 float64 ,3 列是 object,这里显示的内存占用只是一个近似占用,如果要获得更精确的内存使用情况,需要设置参数 df.info(memory_usage='deep'),结果如下,精确的内存占用为 1.1 G

RangeIndex: 5000000 entries, 0 to 4999999
Data columns (total 7 columns):
A    int64
B    int64
C    float64
D    float64
E    object
F    object
G    object
dtypes: float64(2), int64(2), object(3)
memory usage: 1.1 GB

DataFrame 内部表示

pandas 内部在存储的时候把相同类型的列作为一个 Block 进行存储,下面是对这个 DataFrame 的前 3 行的预览。
pandas block表示
从上图可以看出 Block 并不维护对列名的引用,这是由于为了存储dataframe中的真实数据,这些数据块都经过了优化。有个
BlockManager类负责维护行索引和列索引到具体的 Block 块之间的映射。它扮演一个 API,提供对底层数据的访问。每当我们查询、编辑或删除数据时,dataframe 类会利用 BlockManager 类的接口将我们的请求转换为函数和方法的调用。

由于不同类型的数据是分开存放的,下面来看下不同数据类型的内存使用情况,先来看看各数据类型的平均内存使用量:

for dtype in ['float','int','object']:
    selected_dtype = df.select_dtypes(include=[dtype])
    mean_usage_b = selected_dtype.memory_usage(deep=True).mean()
    mean_usage_mb = mean_usage_b / 1024 ** 2
    print("Average memory usage for {} columns: {:03.2f} MB".format(dtype,mean_usage_mb))
Average memory usage for float columns: 25.43 MB
Average memory usage for int columns: 25.43 MB
Average memory usage for object columns: 234.48 MB

可以看到大部分的内存占用来源于 3 列 object 类型的数据。我们待会再来看 object,先来看看能否降低数值型列的内存占用率。

Subtypes 子类

pandas 在底层将数值型数据表示成 Numpy 数组(ndarray),Numpy 数组是在 C 数组上创建的,其值存储在连续的内存块中,基于这种机制,对数值进行切片访问,速度很快。

df.info() 之所以可以很快的返回 DataFrame 的内存占用,正是因为 pandas 使用同样的字节表示同一个类型中的每个值,然后数值的数量又可以在 ndarray 中很快的查到,因此就可以计算出整个 DataFrame 占用的字节数。

pandas中的许多数据类型具有多个子类型,它们可以使用较少的字节去表示不同数据,比如,float 型就有 float16、float32 和 float64这些子类型。下面这张表列出了pandas中常用类型的子类型及所占用的字节数:
int子类型字节数

可以使用 numpy.iinfo 类来确认每个子类型的最大值和最小值,如下:

int_types = ["uint8", "int8", "int16"]
for it in int_types:
    print(np.iinfo(it))
Machine parameters for uint8
---------------------------------------------------------------
min = 0
max = 255
---------------------------------------------------------------
Machine parameters for int8
---------------------------------------------------------------
min = -128
max = 127
---------------------------------------------------------------
Machine parameters for int16
---------------------------------------------------------------
min = -32768
max = 32767

这里可以看到 uint (unsigned integers) and int (signed integers)的不同,两者都占用相同的内存存储量,但无符号整型由于只存正数,所以可以更高效的存储只含正数的列。

用子类型优化数值型列

我们可以用函数 pd.to_numeric() 来对数值型进行向下类型转换。现在用 DataFrame.select_dtypes 来只选择整型列,然后在这些列上进行优化,并比较内存使用量。

# We're going to be calculating memory usage a lot,
# so we'll create a function to save us some time!
def mem_usage(pandas_obj):
    if isinstance(pandas_obj,pd.DataFrame):
        usage_b = pandas_obj.memory_usage(deep=True).sum()
    else: # we assume if not a df it's a series
        usage_b = pandas_obj.memory_usage(deep=True)
    usage_mb = usage_b / 1024 ** 2 # convert bytes to megabytes
    return "{:03.2f} MB".format(usage_mb)
df_int = df.select_dtypes(include=['int'])
converted_int = df_int.apply(pd.to_numeric,downcast='unsigned')
print(mem_usage(df_int))
print(mem_usage(converted_int))
compare_ints = pd.concat([df_int.dtypes,converted_int.dtypes],axis=1)
compare_ints.columns = ['before','after']
compare_ints.apply(pd.Series.value_counts)
76.29 MB
42.92 MB
beforeafter
uint8NaN1
int642.01

可以看到这列 int 型的列内存使用从 76.29M 降成了 42.92M,减少了 43%。原来的两列都是 int64 类型,转化之后有一列变成了 uint8 类型,由此可见 pd.to_numeric() 向下类型转化只是在这列的值都可以向下转的时候才转。

现在在所有的 float 列上做同样的转化

df_float = df.select_dtypes(include=['float'])
converted_float = df_float.apply(pd.to_numeric,downcast='float')
print(mem_usage(df_float))
print(mem_usage(converted_float))
compare_floats = pd.concat([df_float.dtypes,converted_float.dtypes],axis=1)
compare_floats.columns = ['before','after']
compare_floats.apply(pd.Series.value_counts)
76.29 MB
38.15 MB
beforeafter
float32NaN2.0
float642.0NaN

float 列上的内存由原来的 76.29M 降为 38.15M,减少 50% 左右。原来的两列都是 float64 类型,转化后的两列都变为 float32 类型。

对于 int 类型,向下类型转化只是改变的存储类型,并没有改变值,我们可以 check 下和原来的数值是一样的。

(df_int.values - converted_int.values).sum()

但是对于 float 类型,因为精度不同,小数点后的位置也不同,会损失一定精度,对于这个 DataFrame ,可以看下转化前和转化后的精度损失:

(df_float.values - converted_float.values).mean()
1.6209753501559987e-08

平均每个值会有 1.6209753501559987e-08 的误差,总体上可以忽略不计。

现在来看一下,对 int 和 float 都做向下类型转化的话,内存使用量可以降低多少 ?

optimized_df = df.copy()
optimized_df[converted_int.columns] = converted_int
optimized_df[converted_float.columns] = converted_float
print(mem_usage(df))
print(mem_usage(optimized_df))
1090.53 MB
1019.00 MB

整体上减少约 7% 左右,剩余的大部分优化将针对 object 类型进行。

使用类别(Category)类型优化 object 类型

object 类型用来表示使用了 Python 字符串对象的值,有一部分原因是 Numpy 缺少对缺失字符串值的支持。因为 Python 是一种高级解析型语言,它并没有提供很好的对内存中数据如何存储的细粒度控制。

这一限制导致了字符串以一种碎片化方式进行存储,消耗更多的内存,并且访问速度低下。在 object 列中的每一个元素实际上都是存放内存中真实数据位置的指针。

下图对比了数值型数据如何以 Numpy 数据类型存储,和字符串如何以Python 内置类型进行存储的。
numpy_vs_python
图片来源于
Why Python Is Slow.

pandas 从 0.15 版本开始引入了 Category类型,Category 类型在底层使用整数值来表示该列的值,而不是使用原始值。Pandas 用一个字典来维护这些整型数值到原数据的映射关系。当一列中只包含有限种值时,这种方式是非常有效的。当把一列转换成category 类型时,pandas 会用一种最省空间的 int 子类型来表示这一列中所有的唯一值。
category存储
当前 DataFrame 共有 3 个 object 列,其中 E 这一列有 50 个 unique 值,F 这列无重复值,G 这列值全都相同。

df_obj = df.select_dtypes(include=['object']).copy()
df_obj.describe()

df_obj
先来把 E 这列转成 Category 类型,看看是什么样

dow = df_obj['E']
print(dow.head())
dow_cat = dow.astype('category')
print(dow_cat.head())
0    hello_0
1    hello_1
2    hello_2
3    hello_3
4    hello_4
Name: E, dtype: object
0    hello_0
1    hello_1
2    hello_2
3    hello_3
4    hello_4
Name: E, dtype: category
Categories (50, object): [hello_0, hello_1, hello_10, hello_11, ..., hello_6, hello_7, hello_8, hello_9]

可以看到列的类型发生了转化,但数据看上去好像没什么变化。下面是用 Series.cat.codes 来返回 category 类型用以表示每个值的 int 数值。

print(dow_cat.head().cat.codes)
0     0
1     1
2    12
3    23
4    34
dtype: int8

可以看到,每一个值都被赋值为一个整数,而且这一列在底层是 int8 类型。这一列没有任何缺失数据,但是如果有,category 子类型会将缺失数据设为 -1。

最后,我们来看看这一列在转换为 category 类型前后的内存使用量。

print(mem_usage(dow))
print(mem_usage(dow_cat))
308.99 MB
4.77 MB

unbelievable,近这一列内存占用从 308.99M 降到了 4.77M,超过 98% 的降幅!这一列中有 50 个 unique 值,实际中的数据会有很多情况,unique 后小于 50。

转换后的 value 并没有变,可用 dow_cat.values == dow.values 进行检验。

那么这样的话, 所有的 object 列都用 category 转一下不就好了,内存就会刷刷的降?too young too naive, 古话说得好 “有得必有失”。首要问题就是转变为类别类型后会丧失数值计算能力,我们不能对category 列做任何算术运算,像 Series.min()Series.max() 等方法,不信你可以试一下;第二个问题就是,刚才提到 pandas 自己用一个字典来存储整形编码和原始数据的映射,这也算是额外的存储开销,所以当列中重复值较少时,此种方法并不适用,没准还会增大内存开销。下面是将 F 这列转化为 Category 类型后的内存对比,F 列无重复值出现

df_f = df_obj['F']
df_f_cat = df_f.astype('category')
print(mem_usage(df_f))
print(mem_usage(df_f_cat))
332.73 MB
511.80 MB

可见内存占用不仅没有降下来,反而还增大了,就是因为额外的映射关系表。一般情况下,对于唯一值数量少于 50% 的 object 列,我们应该坚持首先使用category 类型。更多说明可以参考 pandas 文档

下面用一个循环,对每一个 object 列进行迭代,检查其唯一值是否少于 50%,如果是,则转换成类别类型。

converted_obj = pd.DataFrame()
for col in df_obj.columns:
    num_unique_values = len(df_obj[col].unique())
    num_total_values = len(df_obj[col])
    if num_unique_values / num_total_values < 0.5:
        converted_obj.loc[:,col] = df_obj[col].astype('category')
    else:
        converted_obj.loc[:,col] = df_obj[col]
print(mem_usage(df_obj))
print(mem_usage(converted_obj))
compare_obj = pd.concat([df_obj.dtypes,converted_obj.dtypes],axis=1)
compare_obj.columns = ['before','after']
compare_obj.apply(pd.Series.value_counts)
937.36 MB
342.27 MB

object 类型的内存使用量从 937.36M 降为 342.27M ,减少了 63% ,因为这里的 F 列没有做转换,刚才也看到 F 列自己就占用 332.73M,也就是说另外两列转换后只占用了 10M 不到的空间,这个降幅还是很大的。

现在把 object 类型的优化和之前的数值型列的优化合并起来,看看整体 DataFrame 的优化情况:

optimized_df[converted_obj.columns] = converted_obj
print(mem_usage(df))
print(mem_usage(optimized_df))
1089.94 MB
423.33 MB

整体 DataFrame 的内存量从 1089.94M 降到 423.33M,减少 62% 左右。

在数据读入的时候设定数据类型

上面的例子中,我们是先读入 dataframe 然后再一步步的进行内存优化,以便清晰的看到节省了多少内存。那么能不能在开始读入数据的时候就指定每列的最优 类型呢?答案是可以的,而且这也是 pandas 比较推崇的做法。下面我们先保存原始的 dataframe,然后记下优化过的 optimized_df 每列的类型,然后在读入数据的时候指定每列类型,来对比下这种情况下的 dataframe 和不指定列类型时的内存对比。

dtypes = optimized_df.dtypes
dtypes_col = dtypes.index
dtypes_type = [i.name for i in dtypes.values]
column_types = dict(zip(dtypes_col, dtypes_type))
path = "./tmp.csv"
df.to_csv(path, index=False)
df2 = pd.read_csv(path,dtype=column_types)
print(mem_usage(df))
print(mem_usage(df2))
print(mem_usage(optimized_df))
1089.94 MB
423.33 MB
423.33 MB

可以看到,指定列名后,新读进来的 dataframe 和优化过的 dataframe 内存使用量是一样的。

在这个例子中,通过对 DataFrame 中不同列的优化,将内存使用量从 1089.94M 降到了 423M ,有效降低 62% 左右,用到的两个技巧:

  • 将数值型列向下降级到更高效的类型
  • 将字符串列转换为类别类型
腾讯校招官网显示,2022届腾讯校招开放技术、产品、设计等岗位共计 78 个,且以IT岗为主。 另据各大招聘平台统计数据,腾讯2022届研发岗应届生基础月薪在1.7万-2.3万之间,签字费(针对优秀人才的额外奖励)3万,股票依据不同等级从6万到20万不等,就算是最低等级的“白菜包”,年薪总包也远超40万元。 作为一个代码打工仔,对于绝大部分程序员来说,想要成为牛 点击上方“数据不吹牛”,选择“置顶星标”公众号干货福利,第一时间送达选自DATAQUEST作者:Josh Devlin机器之心编译参与:Pandapandas一个 Python 软件... data = pd.read_csv('total_data.csv', index_col=0) data.info(memory_usage='deep')         首先,我们读取total_data.csv这个数据,并制定第一列是index,然后,我们获取一下这个dataframe这个对象在内... Table of Contents Pandas 那些年踩过的坑——by 江凯1. Pandas IO中的坑1.1 解决读的坑,让pandas读文件内存占用减小 80%1.2 解决写的坑,让磁盘空间节约60%1.3 解决写的坑,避免挖个坑1.4 python2:加上encoding, 读写好习惯2. DataFrame 链式索引的坑2.1 解决:SettingWithCopyWarning:2.2... 在处理大型数据集的时候,经常碰见python把内存吃满的情况,影响性能,此时就可以对数据集在内存中的存储方式进行优化。所用函数如下: pandas.DataFrame.memory_usage(index=True, deep=False) 返回该数据框所用的字节数(bytes) index参数控制索引占用字节是否出现在结果中。 deep参数返回object对象在系统层面(不是很懂)的存储消耗。 pandas.to_numeric(arg, errors=‘raise’, downcast=None) pandas读取文件占用内存多主要是没有准确识别每一列的数据类型,采用了object进行存储,所有的优化办法都是围绕数据类型转换进行的:一是在读取时指定最佳的数据类型,二是在读取后进行数据转换;更进一步的的优化操作有:(1)将数值向下转换为更高效的类型;(2)将字符串列转换为categorical类型。 把多个Pandas对象(DataFrame/Series)合并成一个。 concat语法:pandas.concat(objs, axis=0, join=‘outer’, ignore_i... pandas.concat()通常用来连接DataFrame对象。默认情况下是对两个DataFrame对象进行纵向连接, 当然通过设置参数,也可以通过它实现DataFrame对象的横向连接。 1. 纵向连接DataFrame对象 (1)两个DataFrame对象的列完全相同 # 初始化两个DataFrame对象 df1 = pd.DataFrame([['a', 1], ['b', 2]], columns=['letter', 'number']) df2 = p 以下是我工作中使用pandas时的一些常用操作python pandas常用操作1. df.isna().sum() # 统计所有列的缺失值 df.iana().sum()/df.shape[0] # 计算所有列缺失率 df['feature'].isna().sum() # 统计单列的缺失值 df['feature'].isna().sum()/df.shape[0] # 计... 确认内存占用: 我这边有一个20M的文件,然后使用: df = pandas.read_csv(r"F:\mail_log\idc\mail_file\1624-信息.csv", encoding="utf-8") df.info(memory_usage="deep") 然后返回值: # Column Non-Null Count Dtype --- ------ ------------ 详情可点击datatable_library[1]。 除此之外,还有一些其他技巧可以在一定程度上帮助我们解决pandas内存问题。它们可能不是最佳的解决方案,但这些技巧有时很方便。而且,了解一下它们对你也没有坏处,对吧?在我之前的一篇文章中,我谈到了两种在pandas中加载大型数据集[2]的方法。 这两个技巧分别是: 分块:将数据集细 1.python pandas最多处理100M数据,再大就会OOM 2.spark中collect(),也要注意数据量太大时会OOM(我今天用2.3G数据collect就出现了OOM) 如果数据量比较大的时候,尽量不要使用collect函数,因为这可能导致Driver端内存溢出问题。 pandas一个 Python 软件库,可用于数据操作和分析。数据科学博客 Dataquest.io 发布了一篇关于如何优化 pandas 内存占用的教程:仅需进行简单的数据类型转换,就能够将一个棒球比赛数据集的内存占用减少了近 90%,机器之心对本教程进行了编译介绍。当使用 pandas 操作小规模数据(低于 100 MB)时,性能一般不是问题。而当面对更大规模的数据(100 MB 到数 G...