熊猫大数据分析指南

当使用pandas库分析大小不超过100 MB的小型数据集时,性能很少会成为问题。 但是,当研究数据集的大小可能达到数GB时,性能问题可能导致数据分析的持续时间显着增加,甚至由于内存不足而导致无法进行分析。

尽管Spark之类的工具可以有效地处理大型数据集(从数百GB到几TB),但是为了充分利用它们的功能,您通常需要功能强大且昂贵的硬件。 而且,与大熊猫相比,它们在丰富的工具集(用于高质量清洁,研究和数据分析)方面没有差异。 对于中型数据集,最好尝试更有效地使用熊猫,而不是切换到其他工具。



在今天要翻译的材料中,我们将讨论使用熊猫时使用内存的功能,以及如何通过简单地选择存储在DataFrame的表数据结构的列中的适当数据类型来简单地将内存消耗减少近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进行游戏的字段的标识符。
  • attendance -观众人数。

为了找到有关DataFrame对象的常规信息,可以使用DataFrame.info()方法。 通过这种方法,您可以了解对象的大小,数据类型以及内存使用情况。

默认情况下,为了节省时间,pandas DataFrame了有关DataFrame内存使用情况的大概信息。 我们对准确的信息感兴趣,因此我们将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列。 熊猫库自动检测到数据类型。 有83列包含数值数据,78列包含对象。 对象列用于存储字符串数据,并且在该列包含不同类型的数据的情况下。

现在,为了更好地理解如何使用此DataFrame优化内存使用,让我们讨论一下熊猫如何将数据存储在内存中。

数据框的内部视图


在大熊猫内部,数据列被分组为具有相同类型值的块。 这是一个如何将DataFrame的前12列存储在熊猫中的DataFrame


熊猫中不同类型数据的内部表示

您可能会注意到,块不存储列名信息。 这是由于对数据块进行了优化,以将可用值存储在DataFrame对象的表单元格中。 BlockManager类负责存储有关数据集的行索引和列索引之间的对应关系以及存储在相同类型数据块中的内容的信息。 它充当提供对基本数据的访问的API的角色。 当我们读取,编辑或删除值时, DataFrame类与BlockManager类进行交互以将我们的请求转换为函数和方法调用。

每个数据类型在pandas.core.internals模块中都有一个专门的类。 例如,pandas使用ObjectBlock类表示包含字符串列的块,使用FloatBlock类表示包含包含浮点数的列的块。 对于代表看起来像整数或浮点数的数值的块,pandas ndarray列并将其存储为NumPy库的ndarray数据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列上。 稍后我们将详细讨论,但现在让我们考虑是否可以通过存储数字数据的列来提高内存使用率。

亚型


正如我们已经说过的,熊猫将数值表示为ndarray NumPy数据结构并将其存储在连续的内存块中。 该数据存储模型使您可以节省内存并快速访问值。 由于熊猫使用相同数量的字节表示相同类型的每个值,并且ndarray结构存储有关值数量的信息,因此熊猫可以快速,准确地显示有关存储数值的列所消耗的内存量的信息。

大熊猫中的许多数据类型都有许多子类型,这些子类型可以使用更少的字节来表示每个值。 例如, float类型具有子类型float16float32float64 。 类型名称中的数字表示该子类型用来表示值的位数。 例如,在刚列出的子类型中,分别将2、4、8和16个字节用于数据存储。 下表显示了熊猫中最常用的数据类型的子类型。
内存使用量(字节)
浮点数
整数
无符号整数
日期和时间
布尔值
对象
1个
int8
uint8
布尔
2
float16
int16
uint16
4
float32
int32
uint32
8
float64
int64
uint64
日期时间64
可变存储容量
对象

int8类型的值使用1个字节(8位)存储一个数字,可以表示256个二进制值(2到8的幂)。 这意味着该子类型可用于存储-128到127(包括0)范围内的值。

要检查适用于使用每个整数子类型存储的最小值和最大值,可以使用numpy.iinfo()方法。 考虑一个例子:

 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()方法,然后对其进行优化并比较优化前后的内存使用情况。

 #     ,   , #   ,      . def mem_usage(pandas_obj):   if isinstance(pandas_obj,pd.DataFrame):       usage_b = pandas_obj.memory_usage(deep=True).sum()   else: #     ,     DataFrame,   Series       usage_b = pandas_obj.memory_usage(deep=True)   usage_mb = usage_b / 1024 ** 2 #       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


之后
uint8
N
5.0
uint32
N
1.0
int64
6.0
N

结果,您可以看到内存使用量从7.9兆字节减少到1.5兆字节,即-我们将内存消耗降低了80%以上。 但是,此优化对原始DataFrame的总体影响不是特别强烈,因为它具有很少的整数列。

让我们对包含浮点数的列进行相同的操作。

 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


之后
float32
N
77.0
float64
77.0
N

结果,所有存储浮点数且数据类型为float64现在都存储了float32类型的数字,这使我们的内存使用量减少了50%。

创建原始DataFrame的副本,使用这些优化的数字列而不是其中最初存在的数字列,并查看优化后的整体内存使用情况。

 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


尽管我们通过存储数字数据的列显着减少了内存消耗,但是通常,在整个DataFrame ,内存消耗仅减少了7%。 对象类型存储的优化可以成为情况严重改善的源泉。

在进行优化之前,我们将仔细研究熊猫中字符串的存储方式,并将其与数字在此处的存储方式进行比较。

比较存储数字和字符串的机制


object类型使用Python字符串对象表示值。 部分原因是NumPy不支持缺少字符串值的表示。 由于Python是一种高级解释语言,因此它没有为程序员提供用于微调数据在内存中存储方式的工具。

此限制导致以下事实:字符串未存储在连续的内存碎片中;它们在内存中的表示是碎片化的。 这导致内存消耗增加,并且处理字符串值的速度变慢。 实际上,存储对象数据类型的列中的每个元素都是一个指针,该指针包含实际地址位于内存中的“地址”。

下图是基于材料的图, 图比较了使用NumPy数据类型存储数字数据和使用Python的内置数据类型存储字符串。


存储数字和字符串数据

在这里您可以回想起,在上面的表之一中,显示了使用可变数量的内存来存储对象类型的数据。 尽管每个指针占用1个字节的内存,但是每个特定的字符串值占用的内存量与在Python中用于存储单个字符串的内存量相同。 为了确认这一点,我们将使用sys.getsizeof()方法。 首先,查看各个行,然后查看存储字符串数据的Series 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


现在让我们看一下Series对象中字符串的用法:

 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 

在这里,您可以看到Series pandas对象中存储的行的大小与在Python中使用它们并将它们表示为单独的实体时的大小相似。

使用分类变量优化对象类型数据的存储


分类变量出现在熊猫0.15版中。 对应的类型category ,在其内部机制中使用整数值,而不是存储在表列中的原始值。 熊猫使用单独的字典来设置整数和初始值的对应关系。 当列包含有限集中的值时,此方法很有用。 当将存储在列中的数据转换为category类型时,pandas使用int子类型,该子类型可以最有效地使用内存,并且能够表示在该列中找到的所有唯一值。


使用int8子类型的源数据和分类数据

为了准确了解我们可以在何处使用分类数据来减少内存消耗,我们在存储对象类型值的列中找到唯一值的数量:

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

您可以在表格 上找到

例如,在day_of_week列(即玩游戏的星期几)中,有171907个值。 其中只有7个是唯一的。 总体而言,单看此报告就足以理解,许多列中使用了许多唯一值来表示大约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属性来找出category类型用来表示一周中每一天的整数值:

 dow_cat.head().cat.codes 

我们设法找出以下内容:

 0    4 1    0 2    2 3    1 4    5 dtype: int8 

在这里,您可以看到为每个唯一值分配了一个整数值,并且该列现在为int8类型。 没有丢失的值,但是如果是这种情况,则将使用-1表示这样的值。

现在,我们比较将day_of_week列转换为category类型之前和之后的内存消耗。

 print(mem_usage(dow)) print(mem_usage(dow_cat)) 

结果如下:

9.84 MB
0.16 MB


如您所见,最初消耗了9.84兆字节的内存,优化后仅消耗了0.16兆字节,这意味着该指标提高了98%。 请注意,当在包含大约172,000个元素的列中仅使用7个唯一值时,使用此列可能证明是最有利可图的优化方案之一。

尽管将所有列转换为该数据类型的想法看起来很吸引人,但是在执行此操作之前,请考虑这种转换的负面影响。 因此,此转换最严重的缺点是不可能对分类数据执行算术运算。 这也适用于普通的算术运算,以及使用Series.min()Series.max()而无需先将数据转换为实数类型的情况。

我们应该将category类型的使用限制为主要存储类型为object数据的列,其中少于50%的值是唯一的。 如果列中的所有值都是唯一的,则使用category类型将增加内存使用量。 这是由于以下事实:除了数字类别代码之外,您还必须在内存中存储原始字符串值。 有关category类型限制的详细信息,请参见pandas 文档

让我们创建一个循环,循环访问存储类型为object数据的所有列,找出列中唯一值的数量是否超过50%,如果是,则将其转换为category类型。

 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


之后
对象
78.0
N
类别
N
78.0

category , , , , , , , , .

, , , object , 752 52 , 93%. , . , , , , 891 .

 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 

, uint32 . - datetime , 64 . datetime , , , .

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] 


DataFrame . , , , , , , , . , . , , , . , , DataFrame , .

, . 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)) #    161 ,  #  10  /   #     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.6 104.28 , 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 , .

, , , 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, , DataFrame , 90%. :

  • , , , , .
  • .

, , , , , , pandas, , .

亲爱的读者们! eugene_bb . - , — .

Source: https://habr.com/ru/post/zh-CN442516/


All Articles