NumPy 博客总结:
《Python数据分析基础教程:NumPy学习指南(第2版)》所有章节阅读笔记+代码
pandas博客总结:
数据分析处理库Pandas(4)
4 大数据处理技巧
使用Pandas工具包可以处理千万级别的数据量,但读取过于庞大的数据特征时,经常会遇到内存溢出等问题。估计绝大多数读者使用的笔记本电脑都是8GB内存,没关系,这里教给大家一些大数据处理技巧,使其能够占用更少内存。
4.1 数值类型转换
下面读取一个稍大数据集,特征比较多,一共有161列,目标就是尽可能减少占用的内存。
1
2
df = pd.read_csv("./game_logs.csv")
display(df)
1
df.shape
1
df.info(memory_usage="deep")
注:当 memory_usage="deep"
时,info() 方法会更精确地计算内存使用情况。 它会递归地计算每个对象内部的内存使用情况,特别是对于 object 类型的列。 这对于包含大量字符串或其他复杂对象的 DataFrame 来说非常重要,因为浅层内存使用情况可能无法准确反映真实的内存消耗。
输出结果显示这份数据读取进来后占用859.4 MB内存,数据类型主要有3种,其中,float64类型有77 个特征,int64类型有6个特征,object类型有78个特征。 对于不同的数据类型来说,其占用的内存相同吗?应该是不同的,先来计算一下各种类型平均占用内存:
1
2
3
4
5
for dtype in ['float64', 'int64', 'object']:
select_dtype = df.select_dtypes(include=[dtype])
mean_usage_b = select_dtype.memory_usage(deep=True).mean()
mean_usage_mb = mean_usage_b/1024**2
print("平均内存占用:", dtype, mean_usage_mb)
注:
1.df.select_dtypes(include=[dtype])
是 Pandas DataFrame 的一个方法,用于选择 DataFrame 中特定数据类型的列。选择的结果(一个新的 DataFrame,只包含指定数据类型的列)会被赋值给变量 select_dtype。
2.select_dtype.memory_usage(deep=True)
计算 select_dtype DataFrame 中每列的内存使用量,单位是字节。eep=True 参数非常重要,它指示 Pandas 深入检查对象类型(尤其是字符串类型)的内存使用情况。如果没有 deep=True,字符串的内存使用量可能不会准确反映实际使用情况,因为它只计算了字符串对象的指针大小,而不是实际存储的字符串数据的内存大小。对于数字类型,deep参数没有影响。
循环中会遍历3种类型,通过select_dtypes()
函数选中属于当前类型的特征,接下来计算其平均占用内存,最后转换成MB看起来更直接一些。从结果可以发现,float64类型和int64类型平均占用内存差不多,而object类型占用的内存最多。
接下来就要分类型对数据进行处理,首先处理一下数值型,经常会看到有int64、int32等不同的类型,它们分别表示什么含义呢?
1
2
3
4
5
import numpy as np
int_types = ["int8", "int16", "int32", "int64"]
for it in int_types:
print(np.iinfo(it))
np.iinfo(it)
是 NumPy 库中的一个函数,它接收一个整数类型作为参数,并返回一个包含该整数类型信息的对象。- 返回的 iinfo 对象包含以下属性:
- min: 该整数类型可以表示的最小值。
- max: 该整数类型可以表示的最大值。
- 其他属性 (如 dtype) 也可能包含在 iinfo 对象中,但 min 和 max 通常是最重要的。
输出结果分别打印了int8~int64
可以表示的数值取值范围,int8和int16能表示的数值范围有点儿小,一般不用。int32看起来范围足够大了,基本任务都能满足,而int64能表示的就更多了。原始数据是int64 类型,但是观察数据集可以发现,并不需要这么大的数值范围,用int32类型就足够了。下面先将数据集中所有int64类型转换成int32类型,再来看看内存占用会不会减少一些。
1
2
3
4
5
6
7
8
9
10
11
12
def mem_usage(pandas_obj):
if isinstance(pandas_obj, pd.DataFrame):
usage_b = pandas_obj.memory_usage(deep=True).sum()
else:
usage_b = pandas_obj.memory_usage(deep=True)
usage_mb = usage_b/1024**2
return '{:03.2f} MB'.format(usage_mb)
gl_int = df.select_dtypes(include=["int64"])
covered_int = gl_int.apply(pd.to_numeric, downcast = "integer")
print(mem_usage(gl_int))
print(mem_usage(covered_int))
注:
1.if isinstance(pandas_obj, pd.DataFrame):
: 检查 pandas_obj
是否为 DataFrame 类型。
2.usage_b = pandas_obj.memory_usage(deep=True).sum()
: 如果 pandas_obj 是 DataFrame,则计算 DataFrame 的总内存使用量(以字节为单位)。memory_usage(deep=True)
返回一个 Series,其中包含每列的内存使用量,然后 .sum()
将这些值加总。deep=True
确保计算对象类型(如字符串)的真实内存占用量。
3.else: usage_b = pandas_obj.memory_usage(deep=True)
: 如果 pandas_obj
不是 DataFrame (例如,是一个 Series),则直接计算 Series 的内存使用量(以字节为单位)。同样,deep=True
确保对象类型(如字符串)的真实内存占用量。
4.return '{:03.2f} MB'.format(usage_mb)
: 将内存使用量格式化为字符串,精确到小数点后两位,并在前面填充零,使总宽度至少为 3 个字符。例如,1.5 MB 会格式化为 “01.50 MB”,而 12.345 MB 会格式化为 “12.35 MB”。 然后返回格式化后的字符串。
5.gl_int = df.select_dtypes(include=["int64"])
: 从 DataFrame df 中选择所有数据类型为 “int64” 的列,并将结果存储在 DataFrame gl_int 中。
6.covered_int = gl_int.apply(pd.to_numeric, downcast = "integer")
: 对 gl_int DataFrame 的每一列应用 pd.to_numeric
函数,并使用 downcast="integer"
参数尝试将每一列向下转换为最小的可用整数类型。pd.to_numeric
会尝试将列转换为数值类型,downcast="integer"
会尝试将结果转换为 int8, int16, int32,甚至 int64,具体取决于列中的实际数据。如果列中的数据不需要 int64 的全部范围,则可以减小该列的内存占用。转换结果存储在 DataFrame covered_int
中。
可以查看下向下转换之后都是什么类型
1
covered_int.dtypes
其中mem_usage()
函数的主要功能就是计算传入数据的内存占用量,为了让程序更通用,写了一个判断方法,分别表示计算DataFrame和Series类型数据,如果包含多列就求其总和,如果只有一列,那就是它自身。select_dtypes(include=['int64'])
表示此时要处理的是全部int64格式数据,先把它们都拿到手。接下来对这部分数据进行向下转换,可以通过打印coverted_int.info()
来观察转换结果。
可以看到在进行向下转换的时候,程序已经自动地选择了合适类型,再来看看内存占用情况,原始数据占用7.87MB,转换后仅占用1.80MB,大幅减少了。由于int型数据特征并不多,差异还不算太大,转换float类型的时候就能明显地看出差异了。
1
covered_int.info()
1
2
3
4
gl_float = df.select_dtypes(include=["float64"])
covered_float = gl_float.apply(pd.to_numeric, downcast = "float")
print(mem_usage(gl_float))
print(mem_usage(covered_float))
可以明显地发现内存节约了正好一半,通常在数据集中float类型多一些,如果对其进行合适的向下转换,基本上能节省一半内存。
4.2 属性类型转换
最开始就发现object类型占用内存最多,也就是字符串,可以先看看各列object类型的特征:
1
2
gl_obj = df.select_dtypes(include="object").copy()
gl_obj.describe()
其中count表示数据中每一列特征的样本个数(有些存在缺失值),unique表示不同属性值的个数,例如day_of_week
列表示当前数据是星期几,所以只有7个不同的值,但是默认object类型会把出现的每一条样本数值都开辟一块内存区域,其内存占用情况如下图所示。
由图可见,很明显,星期一和星期二出现多次,它们只是一个字符串代表一种结果而已,共用一块内存就足够了。但是在object类型中却为每一条数据开辟了单独的一块内存,一共有171907条数据,但只有7个不同值,这样做岂不是浪费了?所以还是要把object类型转换成category类型。先来看看这种新类型的特性:
1
2
3
dow = gl_obj.day_of_week
dow_cat = dow.astype("category")
dow_cat.head()
可以发现,其中只有7种编码方式,也可以实际打印一下具体编码:
1
dow_cat.head(10).cat.codes
无论打印多少条数据,其编码结果都不会超过7种,这就是category类型的特性,相同的字符占用一块内存就好了。转换完成之后,是时候看看结果了:
1
2
print(mem_usage(dow))
print(mem_usage(dow_cat))
对day_of_week
列特征进行转换后,内存占用大幅下降,效果十分明显,其他列也是同理,但是,如果不同属性值比较多,效果也会有所折扣。接下来对所有object类型都执行此操作:
1
2
3
4
5
6
7
8
9
10
11
12
covered_obj = pd.DataFrame()
for col in gl_obj.columns:
num_unique_values = len(gl_obj[col].unique())
num_total_values = len(gl_obj[col])
if num_unique_values / num_total_values < 0.5:
covered_obj.loc[:, col] = gl_obj[col].astype('category')
else:
covered_obj.loc[:, col] = gl_obj[col]
print(mem_usage(gl_obj))
print(mem_usage(covered_obj))
注:
1.如果一列中唯一值的数量占总值的比例小于 0.5,则意味着该列中存在大量的重复值。 将这种列转换为 category 类型可以显著减小内存占用。
2.covered_obj.loc[:, col] = ...
将转换后的 category 类型列赋值给 covered_obj
DataFrame 中同名的列。 loc[:, col]
确保正确地分配列,即使 covered_obj
最初是空的。
3.这段代码试图通过将具有大量重复值的 object 类型列转换为 category 类型来优化 DataFrame 的内存使用。
首先对object类型数据中唯一值个数进行判断,如果数量不足整体的一半(此时能共用的内存较多),就执行转换操作,如果唯一值过多,就没有必要执行此操作。最终的结果非常不错,内存只占用很小部分了。
本节演示了如何处理大数据占用内存过多的问题,最简单的解决方案就是将其类型全部向下转换,这个例子中,内存从860.5 MB下降到51.67 MB,效果还是十分明显的。
如果加载千万级别以上数据源,还是有必要对数据先进行上述处理,否则会经常遇到内存溢出错误。