【Pandas】Pandas处理大数据集的方法(内存优化,减少内存使用量90%)

目录

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

使用棒球比赛日志

数据帧的内部表示

了解子类型

使用子类型优化数值列

将Numeric与String存储进行比较

使用分类优化对象类型

读取数据时选择类型

分析棒球比赛

总结和后续步骤


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

当使用具有小数据(小于100兆字节)的pandas时,性能很少成为问题。当我们迁移到更大的数据(100兆字节到几千兆字节)时,性能问题会使运行时间更长,并导致代码因内存不足而完全失败。

虽然像Spark这样的工具可以处理大型数据集(100千兆字节到多兆兆字节),但充分利用它们的功能通常需要更昂贵的硬件。与熊猫不同,它们缺乏丰富的功能集,可用于高质量的数据清理,探索和分析。对于中型数据,我们最好尝试从熊猫中获取更多,而不是切换到不同的工具。

在这篇文章中,我们将了解大熊猫的内存使用情况,如何通过为列选择适当的数据类型,将数据帧的内存占用量减少近90%。

使用棒球比赛日志

我们将处理130年大联盟棒球比赛的数据,最初来自Retrosheet

最初数据是在127个单独的CSV文件中,但我们使用csvkit合并文件,并在第一行添加了列名。如果您想下载我们的数据版本以及此帖子,我们已在此处提供

让我们首先导入我们的数据并查看前五行。

import pandas as pd

gl = pd.read_csv('game_logs.csv')
gl.head()

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

  • date - 比赛日期。
  • v_name - 访问团队名称。
  • v_league - 参观团队联赛。
  • h_name - 主队名称。
  • h_league - 主队联赛。
  • v_score - 访问团队得分。
  • h_score - 主队得分。
  • v_line_score- 访问团队线路分数,例如010000(10)00
  • h_line_score- 主队线得分,例如010000(10)0X
  • park_id - 举行比赛的公园的ID。
  • attendance- 游戏参与。

我们可以使用该DataFrame.info()方法为我们提供有关数据帧的高级信息,包括其大小,有关数据类型和内存使用情况的信息。

默认情况下,pandas近似于数据帧的内存使用量以节省时间。因为我们对准确性感兴趣,所以我们将memory_usage参数设置'deep'为获得准确的数字。

gl.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 171907 entries, 0 to 171906
Columns: 161 entries, date to acquisition_info
dtypes: float64(77), int64(6), object(78)
memory usage: 861.6 MB

我们可以看到我们有171,907行和161列。Pandas为我们自动检测了类型,包含83个数字列和78个对象列。对象列用于字符串或列包含混合数据类型。

因此,我们可以更好地了解我们可以减少内存使用的位置,让我们来看看pandas如何在内存中存储数据。

数据帧的内部表示

在引擎盖下,pandas将列分组为相同类型的值块。以下是pandas如何存储数据帧的前12列的预览。

您会注意到块不维护对列名的引用。这是因为块被优化用于在数据帧中存储实际值。该图块管理员类是负责维护的行和列索引和实际块之间的映射。它充当API,提供对底层数据的访问。每当我们选择,编辑或删除值时,dataframe类都与BlockManager类接口,以将我们的请求转换为函数和方法调用。

每种类型在pandas.core.internals模块中都有一个专门的类。Pandas使用ObjectBlock类来表示包含字符串列的块,使用FloatBlock类来表示包含float列的块。对于表示整数和浮点数等值的块,pandas组合列并将它们存储为NumPy ndarray。NumPy ndarray围绕C数组构建,值存储在连续的内存块中。由于这种存储方案,访问一片值非常快。

因为每种数据类型都是单独存储的,所以我们将按数据类型检查内存使用情况。让我们从查看数据类型的平均内存使用情况开始。

for dtype in ['float','int','object']:
    selected_dtype = gl.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: 1.29 MB
Average memory usage for int columns: 1.12 MB
Average memory usage for object columns: 9.53 MB

我们立即可以看到78 object列中使用了大部分内存。我们稍后会看一下,但首先让我们看看我们是否可以改进数字列的内存使用情况。

 

了解子类型

正如我们之前简要提到的,在引擎盖下,pandas将数值表示为NumPy ndarrays,并将它们存储在连续的内存块中。此存储模型占用的空间更少,并允许我们快速访问值本身。因为pandas使用相同的字节数表示相同类型的每个值,并且NumPy ndarray存储值的数量,所以pandas可以返回数字列快速准确地消耗的字节数。

pandas中的许多类型都有多个子类型,可以使用更少的字节来表示每个值。例如,该float类型具有float16float32float64亚型。类型名称的数字部分表示类型用于表示值的位数。例如,我们亚型刚刚上市使用248和 16字节,分别。下表显示了最常见的pandas类型的子类型:

一个int8值使用1的字节(或8比特)来存储的值,并且可以表示256值(2^8)的二进制。这意味着我们可以使用该亚型代表值范围从-128127(包括0)。

我们可以使用numpy.info该类来验证每个整数子类型的最小值和最大值。我们来看一个例子:

import numpy as np
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(无符号整数)和int(有符号整数)之间的区别。两种类型都具有相同的存储容量,但只存储正值,无符号整数使我们能够更有效地存储仅包含正值的列。

 

使用子类型优化数值列

我们可以使用该函数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)

gl_int = gl.select_dtypes(include=['int'])
converted_int = gl_int.apply(pd.to_numeric,downcast='unsigned')

print(mem_usage(gl_int))
print(mem_usage(converted_int))

compare_ints = pd.concat([gl_int.dtypes,converted_int.dtypes],axis=1)
compare_ints.columns = ['before','after']
compare_ints.apply(pd.Series.value_counts)
7.87 MB
1.48 MB

我们可以看到内存使用量下降了7.9到1.5兆字节,减少了80%以上。然而,对我们原始数据帧的总体影响并不大,因为整数列很少。

让我们的浮动列做同样的事情。

gl_float = gl.select_dtypes(include=['float'])
converted_float = gl_float.apply(pd.to_numeric,downcast='float')

print(mem_usage(gl_float))
print(mem_usage(converted_float))

compare_floats = pd.concat([gl_float.dtypes,converted_float.dtypes],axis=1)
compare_floats.columns = ['before','after']
compare_floats.apply(pd.Series.value_counts)
100.99 MB
50.49 MB

我们可以看到我们所有的浮动列都已转换float64float32,使我们的内存使用量减少了50%。

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

optimized_gl = gl.copy()

optimized_gl[converted_int.columns] = converted_int
optimized_gl[converted_float.columns] = converted_float

print(mem_usage(gl))
print(mem_usage(optimized_gl))

861.57 MB

804.69 MB

虽然我们已经大大减少了数字列的内存使用量,但总体而言我们只将数据帧的内存使用量减少了7%。我们的大部分收益来自优化对象类型。

在我们开始之前,让我们仔细看看与数字类型相比如何在pandas中存储字符串

 

将Numeric与String存储进行比较

object类型使用Python字符串对象表示值,部分原因是缺少对NumPy中缺少字符串值的支持。因为Python是一种高级解释语言,所以它没有对内存中的值的存储方式进行细粒度控制。

此限制导致字符串以碎片方式存储,消耗更多内存并且访问速度较慢。对象列中的每个元素实际上都是一个指针,其中包含实际值在内存中的位置的“地址”。

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

图表改编自优秀帖子为什么Python很慢

您可能已经注意到我们之前的图表描述的object类型是使用可变数量的内存。虽然每个指针占用1个字节的内存,但每个实际的字符串值使用与在Python中单独存储时字符串将使用的相同数量的内存。让我们sys.getsizeof()用来证明这一点,先看看单个字符串,然后再查看熊猫系列中的项目。

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版本中引入了Categoricals。该category类型使用引擎盖下的整数值来表示列中的值,而不是原始值。Pandas使用单独的映射字典将整数值映射到原始值。只要列包含一组有限的值,此排列就很有用。当我们将列转换为categorydtype时,pandas使用最节省空间的int子类型,该子类型可以表示列中的所有唯一值。

为了概述我们可以使用此类型减少内存的位置,让我们看一下每个对象类型的唯一值的数量。

gl_obj = gl.select_dtypes(include=['object']).copy()
gl_obj.describe()

快速浏览一下就会发现很多列,相对于我们数据集中的总体约172,000个游戏,几乎没有独特的值。

在我们深入研究之前,我们首先选择一个对象列,然后查看将其转换为分类类型时幕后发生的情况。我们将使用数据集的第二列day_of_week

看着上面的表。我们可以看到它只包含七个唯一值。我们将使用该.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属性返回category类型用于表示每个值的整数值。

dow_cat.head().cat.codes
0    4
1    0
2    2
3    1
4    5
dtype: int8

您可以看到每个唯一值都已分配一个整数,并且该列的基础数据类型现在已经分配int8。此列没有任何缺失值,但如果有,则category子类型通过将其设置为缺失值来处理-1

最后,让我们看一下转换为category类型之前和之后此列的内存使用情况 。

print(mem_usage(dow))
print(mem_usage(dow_cat))
9.84 MB
0.16 MB

我们已经从9.8MB的内存使用量减少到0.16MB的内存使用量,或者减少了98%!请注意,此特定列可能代表我们最好的情况之一,一个包含约172,000个项目的列,其中只有7个唯一值。

虽然将所有列转换为此类型听起来很吸引人,但重要的是要注意权衡。最大的一个是无法进行数值计算。我们不能对category列进行算术运算,也不能先使用Series.min()Series.max()不转换为真正的数字dtype的方法。

我们应该坚持category主要使用类型的object列,其中少于50%的值是唯一的。如果列中的所有值都是唯一的,则category类型最终将使用更多内存。这是因为除了整数类别代码之外,该列还存储了所有原始字符串值。您可以categorypandas文档中阅读有关该类型限制的更多信息。

我们将编写一个循环来迭代每object列,检查唯一值的数量是否小于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

在这种情况下,我们所有的对象列都被转换为category类型,但是对于所有数据集都不是这种情况,因此您应该确保使用上面的过程进行检查。

更重要的是,我们的object列的内存使用量从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()函数转换,使用format参数告诉它我们的日期数据已存储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

通过优化列,我们设法将大熊猫的内存使用量从861.6 MB减少到104.28 MB - 令人印象深刻的减少了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()

我们可以看到,在20世纪20年代之前,周日棒球比赛在星期日很少见,直到上世纪下半叶逐渐流行。

我们还可以清楚地看到,过去50年来游戏日的分布一直相对稳定。

让我们看一下这些年来游戏长度的变化情况。

game_lengths = optimized_gl.pivot_table(index='year', values='length_minutes')
game_lengths.reset_index().plot.scatter('year','length_minutes')
plt.show()

看起来棒球比赛从20世纪40年代开始持续变长。

 

总结和后续步骤

我们已经了解了pandas如何使用不同的类型存储数据,然后我们使用这些知识将我们的pandas数据帧的内存使用量减少了近90%,只需使用一些简单的技术:

  • 将数字列向下转换为更有效的类型。
  • 将字符串列转换为分类类型。

 

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

 

 

猜你喜欢

转载自blog.csdn.net/ChenVast/article/details/86570586
今日推荐