目录html
将内存使用量减小高达90%的方法python
使用棒球比赛日志git
数据帧的内部表示github
了解子类型数组
使用子类型优化数值列app
分析棒球比赛性能
当使用具备小数据(小于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中存储字符串
该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使用单独的映射字典将整数值映射到原始值。只要列包含一组有限的值,此排列就颇有用。当咱们将列转换为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%,只需使用一些简单的技术: