0%

优化pandas内存入门

降低数据存储消耗并提高处理效率,本文教你如何通过优化数值类型、object类型和 datetime类型三个方面来减少内存使用量。其中,使用 Categoricals优化 object类型数据是效果最显著的一种优化方式。另外,还介绍了如何在读取数据的时候就完成优化步骤,避免加载完数据再做优化的麻烦。

简单的概念

1
2
3
4
5
6
7
8
9
10
# coding: utf-8
import pandas as pd

# 读取数据
df = pd.read_csv("test.csv")

# 得到精确的内存信息
df.info(memory_usage='deep')

# 说明:之后都用df来表示读取到的dataframe

pandas中每一个数据类型都有一个专门的类来处理。

  • ObjectBlock: 字符串列的块
  • FloatBlock: 浮点数列的块
  • Numpy ndarray:整型和浮点数值的块(非常快,用C数组构建的)
1
2
3
4
5
6
7
8
for dtype in ['float', 'int', 'object']:
# 选中对应的dtype列
selected_dtype = df.select_dtypes(include=[dtype])
# 查看内存使用量的平均值
mean_usage_b = selected_dtype.memory_usage(deep=True).mean()
# 获取到的数据单位为K, 这里转换一下
mean_usage_mb = mean_usage_b / 1024 ** 2
print("Average memory usage for {} columns:{:03.2f} MB".format(dtype, mean_usage_mb))

这里我们可以发现 object类占用内存最大

子类型(subtype)

memory usage float int uint datetime bool object
1 bytes int8 uint8 bool
2 bytes float16 int16 uint16
4 bytes float32 int32 uint32
8 bytes float64 int64 uint64 datetime64
variable object

一个 int8类型值使用1个字节的存储空间,可以表示256(2^8)个二进制数,即-128到127的所有整数值。根据表可以得出,对于每个列,应该尽可能"抠"一点, 物尽其用。这里的 uint表示无符号整型,可以更有效地处理正整数的列。

优化

数值型数据

这里可以使用 to_numeric()对数值类型进行 downcast(向下转型)的操作。

1
2
3
4
5
6
7
8
9
# 选中整数型的列
df_int_columns = df.select_dtypes(include=['int'])
# 向下转型
df_int_columns.apply(pd.to_numeric, downcast='unsigned')

# 选中浮点型数据
df_float_columns = df.select_dtypes(include=['float'])
# 向下转型
df_float_columns.apply(pd.to_numeric, downcast='float')

这里能够有效的利用内存,但是缺点很明显的就是只能用于数值型数据,而且优化的空间有限。

object型数据

object类型实际上使用的是Python字符串对应的值,由于Python解释性语言的特性,字符串存储方式很碎片化(消耗更多内存,访问速度更慢)。object列中的每个元素都是一个指针。object类型的数据内存使用情况是可变的。如果每个字符串是单独存储的,那么实际上字符串占用内存是很大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [1]: from sys import getsizeof
# 字符串本身内存使用情况
In [2]: s1 = "hello world!"
In [3]: s2 = "hello pandas!"
In [4]: getsizeof(s1), getsizeof(s2)
Out[4]: (61, 62)

# 字符串(object)在pandas中内存使用情况
In [9]: import pandas as pd
In [10]: ser = pd.Series([s1, s2])
In [11]: ser.apply(getsizeof)
Out[11]:
0 61
1 62
dtype: int64

这里通过观察看到,在这里pandas使用了int64来存储,实际占用大小与字符串本身是一样的。这里可以使用 Categoricals来优化 object类型。Categoricals的工作原理我理解为,某个 object类有有限的分类情况(比如只有 redyellowblueblack等颜色相关),那么 Categoricals将这些分类 object对象转换为 int子类型(对应上面的0, 1, 2, 3)

1
2
3
4
# 选中object类型
df_obj_columns = df.select_dtypes(include=['object'])
# 查看object类型列的相关信息
df_obj_columns.describe()

如果 unique(不同值)的数量很少,那么就可以使用这种优化方案。

1
2
3
4
5
6
# 假设某一列符合优化条件,为df_obj_less,使类型转换为category
df_category_column = df_obj_less.astype('category')
# 转换之后,主观观察数据没有什么区别,只是类型改变了
# 观察转换之后实际上的数据
df_category_column.cat.codes
# 变成了由0,1,2等数字构成的int8类型的数据

这里提升的空间远远超过第一步优化的空间,具体要结合数据来检验(最好貌似可以减少98%的使用量)。这里有个很大的缺点就是无法进行数值计算,即没有办法使用 pd.Series.min()pd.Series.max()等与数值相关操作。

还有一个问题就是在有多少个 unique(不同值)的情况下才使用这种方法。首先,毫无疑问的是如果需要计算操作的列是不能使用的。第二是如果 unique的比例小于50%(个人觉得比例应该更小)就可以使用这种情况,如果过多的话,转换之后消耗的内存会更多(不仅需要存储string还有int)。

附上筛选 unique比例小于50%的代码

1
2
3
4
5
6
7
8
9
# 提取unique少于50%的object列
df_obj_columns = df.select_dtypes(include=['object'])
for obj_col in df_obj_columns:
unique_values = len(df_obj_columns[obj_col].unique())
total_values = len(df_obj_columns[obj_col])
if unique_values / total_values < 0.5:
obj_col.astype('category')

# 建议提前copy一份数据,再做上面的操作

datetime类型

这里其实不能叫做优化,因为这里是将数值型数据转换为 datetime类型,虽然提高了内存的使用,但是转换为 datetime类型的数据更容易进行分析(时间序列分析)。当然如果有 datetime类型不在分析的范围内,自然可以无视。

1
2
# 假设df_num_col为需要转换的列, 这里format格式看情况更改
df_num_col = pd.to_datetime(df_num_col, format="%Y%m%d")

总结

写了这么多,如果每次等加载完数据再做优化操作,感觉有些鸡肋。但是其实可以使用 read_csv等读取函数的几个参数来帮助我们在读取的时候就完成优化步骤。

1
2
3
4
5
6
# 首先,需要整理出每一列最终的数据类型,组成一个dict
dtypes_lst = [col.name for name in df.dtypes]
column_types = dict(zip(dtypes.index, dtypes_lst))

# 重新读取数据, dtype指定数据格式,parse_dates指定列转换格式为datetime,infer_datetime_format尝试解析字符串形式的datatime格式(在一些情况下可以加快解析速度5-10倍)
new_df = pd.read_csv('test.csv', dtpye=column_types, parse_dates=['date'], infer_datetime_format=True)