在上表中,您可能已经注意到对象类型的内存使用量是可变的。虽然每个指针只占用 1 个字节的内存,但如果每个字符串在 Python 中单独存储内存卡无法访问函数不正确,它会占用与实际字符串一样多的空间。我们可以使用 sys.getsizeof 函数来演示这一点,首先查看单个字符串,然后查看 pandas 系列中的项目。
from sys import getsizeof
s1 = 'working out'
s2 = 'memory usage for'
s3 = 'strings in python is fun!'
s4 = 'strings in python is fun!'
for s in [s1, s2, s3, s4]:
print(getsizeof(s))
60
65
74
74
obj_series = pd.Series(['working out',
'memory usage for',
'strings in python is fun!',
'strings in python is fun!'])
obj_series.apply(getsizeof)
0 60
1 65
2 74
3 74
dtype: int64
可以看到,在pandas系列中存储时,字符串的大小和Python中单独存储的字符串大小是一样的。
使用分类优化对象类型
pandas 在 0.15 版本中引入了类别。类别类型在底层使用整数值来表示列中的值,而不是原始值。pandas 使用单独的映射字典将这些整数值映射到原始值。只要一列包含一组有限的值,这种方法就很有用。当我们将一列转换为类别 dtype 时,pandas 使用最节省空间的 int 子类型来表示该列中所有不同的值。
要了解为什么我们可以使用这种类型来减少内存使用量,我们来看看我们的对象类型中每种类型的不同值的数量。
gl_obj = gl.select_dtypes(include=['object']).copy
gl_obj.describe
上图完整图片见原文
乍一看,对于我们整个 172,000 个游戏的数据集,唯一值的数量可以说是非常少的。
要了解当我们将其转换为分类类型时究竟会发生什么,让我们看一下对象列。我们将使用数据集 day_of_week 的第二列。
查看上表,您可以看到它仅包含 7 个不同的值。我们将使用 .astype 方法将其转换为分类类型。
dow = gl_obj.day_of_week
print(dow.head)
dow_cat = dow.astype('category')
print(dow_cat.head)
0 Thu
1 Fri
2 Sat
3 Mon
4 Tue
Name: day_of_week, dtype: object
0 Thu
1 Fri
2 Sat
3 Mon
4 Tue
Name: day_of_week, dtype: category
Categories (7, object): [Fri, Mon, Sat, Sun, Thu, Tue, Wed]
如您所见内存卡无法访问函数不正确,数据看起来仍然完全相同,只是该列的类型发生了变化。让我们看看这背后发生了什么。
在下面的代码中,我们使用 Series.cat.codes 属性返回类别类型用来表示每个值的整数值。
dow_cat.head.cat.codes
0 4
1 0
2 2
3 1
4 5
dtype: int8
您可以看到每个不同的值都被分配了一个整数值,并且列的基本数据类型现在是 int8。此列没有任何缺失值,但即使有,类别子类型也可以处理,只需将其设置为 -1。
最后,我们来看看将这一列转换为类别类型前后的内存使用情况对比。
print(mem_usage(dow))
print(mem_usage(dow_cat))
9.84 MB
0.16 MB
内存使用量从 9.8 MB 减少到 0.16 MB,减少了 98%!请注意,这个特定的列可能代表了我们最好的情况之一——大约 172,000 个项目,只有 7 个不同的值。
虽然将所有列都转换为这种类型听起来很诱人,但了解权衡也很重要。最大的缺点是无法进行数值计算。如果不首先将其转换为数字 dtype,我们就不能对类别列进行算术运算,这意味着我们不能使用像 Series.min 和 Series.max 这样的方法。
对于不同值的数量少于值总数的 50% 的对象列,我们应该坚持主要使用类别类型。如果某一列中的所有值都是不同的,那么类别类型会占用更多的内存。因为该列不仅存储了所有原始字符串值,还存储了它们的整数值代码。您可以在 pandas 文档中了解类别类型的限制:.
我们会编写一个循环函数来迭代检查每个对象列中不同值的数量是否小于50%;如果是这样,将其转换为类别类型。
converted_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:
converted_obj.loc[:,col] = gl_obj[col].astype('category')
else:
converted_obj.loc[:,col] = gl_obj[col]
和以前比较:
print(mem_usage(gl_obj))
print(mem_usage(converted_obj))
compare_obj = pd.concat([gl_obj.dtypes,converted_obj.dtypes],axis=1)
compare_obj.columns = ['before','after']
compare_obj.apply(pd.Series.value_counts)
752.72 MB
51.67 MB
在这种情况下,所有对象列都转换为类别类型,但不是所有数据集,因此您应该使用上述过程进行检查。
对象列的内存使用量从 752MB 减少到 52MB,减少了 93%。让我们将它与我们数据帧的其余部分结合起来,看看从原来的 861MB 中取得了多少改进。
optimized_gl[converted_obj.columns] = converted_obj
mem_usage(optimized_gl)
'103.64 MB'
哇,进步真好!我们可以执行另一个优化 – 如果您还记得前面给出的数据类型表,您就会知道还有一个 datetime 类型。这个数据集的第一列可以使用这种类型。
date = optimized_gl.date
print(mem_usage(date))
date.head
0.66 MB
0 18710504
1 18710505
2 18710506
3 18710508
4 18710509
Name: date, dtype: uint32
您可能还记得,该列最初是一个整数,现在已经优化为 unint32 类型。因此,将其转换为 datetime 类型实际上会使内存使用量翻倍,因为 datetime 类型是 64 位的。将其转换为 datetime 类型很有价值,因为它可以让我们进行更好的时间序列分析。
pandas.to_datetime 函数可以为我们进行这种转换,使用它的格式参数将我们的日期数据存储为 YYYY-MM-DD。
optimized_gl['date'] = pd.to_datetime(date,format='%Y%m%d')
print(mem_usage(optimized_gl))
optimized_gl.date.head
104.29 MB
0 1871-05-04
1 1871-05-05
2 1871-05-06
3 1871-05-08
4 1871-05-09
Name: date, dtype: datetime64[ns]
读入数据时选择类型
我们现在已经探索了减少现有数据帧内存占用的方法。通过首先读取数据帧,然后迭代整个过程以减少内存占用,我们了解了每种优化方法可以带来多少内存节省。但正如我们前面提到的,我们经常没有足够的内存来表示数据集中的所有值。如果我们甚至不能一开始就创建数据帧,我们如何应用内存节省技术?
幸运的是,我们可以在读入数据时指定最佳的列类型。pandas.read_csv 函数有几个不同的参数允许我们这样做。dtype 参数接受以(字符串)列名作为键和 NumPy 类型对象作为值的字典。
首先,我们可以将每一列的最终类型存储在字典中,其中键值表示列名,然后先删除日期列,因为日期列需要不同的处理方式。
dtypes = optimized_gl.drop('date',axis=1).dtypes
dtypes_col = dtypes.index
dtypes_type = [i.name for i in dtypes.values]
column_types = dict(zip(dtypes_col, dtypes_type))
# rather than print all 161 items, we'll
# sample 10 key/value pairs from the dict
# and print it nicely using prettyprint
preview = first2pairs = {key:value for key,value in list(column_types.items)[:10]}
import pprint
pp = pp = pprint.PrettyPrinter(indent=4)
pp.pprint(preview)
{ 'acquisition_info': 'category',
'h_caught_stealing': 'float32',
'h_player_1_name': 'category',
'h_player_9_name': 'category',
'v_assists': 'float32',
'v_first_catcher_interference': 'float32',
'v_grounded_into_double': 'float32',
'v_player_1_id': 'category',
'v_player_3_id': 'category',
'v_player_5_id': 'category'}
现在我们可以使用这个字典,以及更多的参数来读取日期作为正确的类型,并且只需要几行代码:
read_and_optimized = pd.read_csv('game_logs.csv',dtype=column_types,parse_dates=['date'],infer_datetime_format=True)
print(mem_usage(read_and_optimized))
read_and_optimized.head
104.28 MB
上图完整图片见原文
通过优化这些列,我们设法将 pandas 的内存占用从 861.6MB 减少到 104.28MB——减少了惊人的 88%!
分析棒球比赛
现在我们已经优化了我们的数据,我们可以进行一些分析。让我们从了解这些匹配的日期分布开始。
optimized_gl['year'] = optimized_gl.date.dt.year
games_per_day = optimized_gl.pivot_table(index='year',columns='day_of_week',values='date',aggfunc=len)
games_per_day = games_per_day.divide(games_per_day.sum(axis=1),axis=0)
ax = games_per_day.plot(kind='area',stacked='true')
ax.legend(loc='upper right')
ax.set_ylim(0,1)
plt.show
我们可以看到,周日棒球比赛在 1920 年代之前很少,但在上世纪下半叶变得越来越普遍。
我们还可以清楚地看到,比赛的日期分布在过去 50 年中基本保持不变。
让我们再看看游戏时长是如何变化的:
game_lengths = optimized_gl.pivot_table(index='year', values='length_minutes')
game_lengths.reset_index.plot.scatter('year','length_minutes')
plt.show
自 1940 年代以来,棒球比赛的时间越来越长。
总结和后续步骤
我们已经了解了 pandas 如何使用不同的数据类型,并且我们已经使用这些知识将 pandas 数据帧的内存使用量减少了近 90%,只使用了一些简单的技术:
福利
Python入门最强三件套《ThinkPython》、《简明Python教程》、《Python进阶》PDF电子版已打包提供给你,可在《P3》中获取.
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 欧资源网