将内存使用量减少高达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 类型具有 float16 float32 float64 亚型。类型名称的数字部分表示类型用于表示值的位数。例如,我们亚型刚刚上市使用 2 4 8 16 字节,分别。下表显示了最常见的pandas类型的子类型:

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

我们可以看到我们所有的浮动列都已转换 float64 float32 ,使我们的内存使用量减少了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))
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使用单独的映射字典将整数值映射到原始值。只要列包含一组有限的值,此排列就很有用。当我们将列转换为 category dtype时,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 类型最终将使用 更多 内存。这是因为除了整数类别代码之外,该列还存储了所有原始字符串值。您可以 category pandas文档中 阅读有关该类型限制的更多信息。

我们将编写一个循环来迭代每 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/

pd.read_csv()中有个参数chunksize用来块的方式读取数据,例如:将chunksize指定为每次100万行,将大 数据集 分成许多小块 通过迭代每个块,在将每个块添加到列表之前,我使用函数chunk_preprocessing执行数据过滤/预 处理 。最后,我将列表连接到一个最终的dataframe中,以适应本地 内存 # read the large csv file wi... pandas 是一个 Python 软件库,可用于数据操作和分析。数据科学博客 Dataquest.io 发布了一篇关于如何 优化 pandas 内存 占用的教程:仅需进行简单的数据类型转换,就能够将一个棒球比赛 数据集 内存 占用 减少 了近 90 %,机器之心对本教程进行了编译介绍。当使用 pandas 操作小规模数据(低于 100 MB)时,性能一般不是问题。而当面对更大规模的数据(100 MB 到数 G... 将数值型列降级到更高效的类型 我们可以用函数pd.to_numeric()来对数值型进行向下类型转换。我们用DataFrame. select _d types 来只选择整型列,然后我们 优化 这种类型,并比较 内存 使用量 。 先找到要转化的类型,可用以下语句: DataFrame... pandas 读取文件占用 内存 多主要是没有准确识别每一列的数据类型,采用了object进行存储,所有的 优化 办法都是围绕数据类型转换进行的:一是在读取时指定最佳的数据类型,二是在读取后进行数据转换;更进一步的的 优化 操作有:(1)将数值向下转换为更高效的类型;(2)将字符串列转换为categorical类型。 确认 内存 占用: 我这边有一个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 --- ------ ------------ 第12章 pandas 高级应用1、分类数据1.1、 pandas 处理 重复值常见函数1.2、分类编码 series_str.take(values)1.3、 pandas 的分类类型1.4、用分类进行计算 (pd.Series(draws).groupby(bins).agg(['count', 'min', 'max']).reset_index())1.5、用分类提高性能1.6、分类 方法 1.7、为建模... 吴恩达老师有言:“机器学习在本质上还是特征工程,数据和特征决定了机器学习的上限,模型和算法只是逼近这个上限而已。” 特征工程主要分为:数据预 处理 、特征变换、特征提取、特征选择四部分 #mermaid-svg-Ybe7a9ZvmxjBI7JK {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Ybe7a9ZvmxjBI7JK .error-icon{fill:#552222;} 编译 | AI科技大本营(rgznai100)参与 | 周翔 注: Pandas (Python Data Analysis Library) 是基于 NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。此外, Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型 数据集 所需的工具。相比较于 Numpy, Pandas 使用一个二维的数据结构 DataFrame 来表示表格式的数据... 一般来说,用 pandas 处理 小于100兆的数据,性能不是问题。当用 pandas 处理 100兆至几个G的数据时,将会比较耗时,同时会导致程序因 内存 不足而运行失败。当然,像Spark这类的工具能够胜任 处理 100G至几个T的大 数据集 ,但要想充分发挥这些工具的优势,通常需要比较贵的硬件设备。而且,这些工具不像 pandas 那样具有丰富的进行高质量数据清洗、探索和分析的特性。对于中等规模的数据,我们的愿望是尽量 # 由于数据量较大,一次性读入可能造成 内存 错误(Memmory Error),因而使用 pandas 的分块读取 def read_from_local(file_name, chunk_size=500000): def reduce_mem_usage(df): """ iterate through all the columns of a dataframe and modify the data type to reduce memory usage. start_mem = df.memory... 最近因为各种事情用Python 处理 文件,经常遇到文件太大, 内存 不够 的问题,在此整理了以下几种办法。 pandas 读文件失败–》分块 处理 有些时候使用pd.read_csv函数读文件会非常尴尬,读到一半 内存 不够 了,这时候可以使用其提供的分块读取的功能 不想看我废话可以直接-》pd.read_csv函数官方文档 chunksize 参数(int, optional) 使用chunksize后pd.read_csv将会返回一个可以迭代的TextFileReader对象。 chunksize的值代表了每次迭代