做者:xiaoyupython
微信公众号:Python数据科学git
知乎:python数据分析师编程
当你们谈到数据分析时,说起最多的语言就是Python
和SQL
。Python之因此适合数据分析,是由于它有不少第三方强大的库来协助,pandas
就是其中之一。pandas
的文档中是这样描述的:数组
“快速,灵活,富有表现力的数据结构,旨在使”关系“或”标记“数据的使用既简单又直观。”bash
咱们知道pandas
的两个主要数据结构:dataframe
和series
,咱们对数据的一些操做都是基于这两个数据结构的。但在实际的使用中,咱们可能不少时候会感受运行一些数据结构的操做会异常的慢。一个操做慢几秒可能看不出来什么,可是一整个项目中不少个操做加起来会让整个开发工做效率变得很低。有的朋友抱怨pandas简直太慢了,其实对于pandas的一些操做也是有必定技巧的。微信
pandas是基于numpy库
的数组结构上构建的,而且它的不少操做都是(经过numpy或者pandas自身由Cpython实现并编译成C的扩展模块)在C语言中实现的。所以,若是正确使用pandas的话,它的运行速度应该是很是快的。数据结构
本篇将要介绍几种pandas中经常使用到的方法,对于这些方法使用存在哪些须要注意的问题,以及如何对它们进行速度提高。架构
咱们来看一个例子。app
>>> import pandas as pd
>>> pd.__version__
'0.23.1'
# 导入数据集
>>> df = pd.read_csv('demand_profile.csv')
>>> df.head()
date_time energy_kwh
0 1/1/13 0:00 0.586
1 1/1/13 1:00 0.580
2 1/1/13 2:00 0.572
3 1/1/13 3:00 0.596
4 1/1/13 4:00 0.592
复制代码
从运行上面代码获得的结果来看,好像没有什么问题。但实际上pandas和numpy都有一个dtypes
的概念。若是没有特殊声明,那么date_time将会使用一个 object
的dtype类型,以下面代码所示:scrapy
>>> df.dtypes
date_time object
energy_kwh float64
dtype: object
>>> type(df.iat[0, 0])
str
复制代码
object
类型像一个大的容器,不只仅能够承载 str,也能够包含那些不能很好地融进一个数据类型的任何特征列。而若是咱们将日期做为 str 类型就会极大的影响效率。
所以,对于时间序列的数据而言,咱们须要让上面的date_time列格式化为datetime
对象数组(pandas称之为时间戳)。pandas在这里操做很是简单,操做以下:
>>> df['date_time'] = pd.to_datetime(df['date_time'])
>>> df['date_time'].dtype
datetime64[ns]
复制代码
咱们来运行一下这个df看看转化后的效果是什么样的。
>>> df.head()
date_time energy_kwh
0 2013-01-01 00:00:00 0.586
1 2013-01-01 01:00:00 0.580
2 2013-01-01 02:00:00 0.572
3 2013-01-01 03:00:00 0.596
4 2013-01-01 04:00:00 0.592
复制代码
date_time的格式已经自动转化了,但这还没完,在这个基础上,咱们仍是能够继续提升运行速度的。如何提速呢?为了更好的对比,咱们首先经过 timeit
装饰器来测试一下上面代码的转化时间。
>>> @timeit(repeat=3, number=10)
... def convert(df, column_name):
... return pd.to_datetime(df[column_name])
>>> df['date_time'] = convert(df, 'date_time')
Best of 3 trials with 10 function calls per trial:
Function `convert` ran in average of 1.610 seconds.
复制代码
1.61s,看上去挺快,但其实能够更快,咱们来看一下下面的方法。
>>> @timeit(repeat=3, number=100)
>>> def convert_with_format(df, column_name):
... return pd.to_datetime(df[column_name],
... format='%d/%m/%y %H:%M')
Best of 3 trials with 100 function calls per trial:
Function `convert_with_format` ran in average of 0.032 seconds.
复制代码
**结果只有0.032s,快了将近50倍。**缘由是:咱们设置了转化的格式format。因为在CSV中的datetimes并非 ISO 8601 格式的,若是不进行设置的话,那么pandas将使用 dateutil
包把每一个字符串str转化成date日期。
相反,若是原始数据datetime已是 ISO 8601
格式了,那么pandas就能够当即使用最快速的方法来解析日期。这也就是为何提早设置好格式format能够提高这么多。
仍然基于上面的数据,咱们想添加一个新的特征,但这个新的特征是基于一些时间条件的,根据时长(小时)而变化,以下:
所以,按照咱们正常的作法就是使用apply方法写一个函数,函数里面写好时间条件的逻辑代码。
def apply_tariff(kwh, hour):
"""计算每一个小时的电费"""
if 0 <= hour < 7:
rate = 12
elif 7 <= hour < 17:
rate = 20
elif 17 <= hour < 24:
rate = 28
else:
raise ValueError(f'Invalid hour: {hour}')
return rate * kwh
复制代码
而后使用for循环来遍历df,根据apply函数逻辑添加新的特征,以下:
>>> # 不赞同这种操做
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
... """Calculate costs in loop. Modifies `df` inplace."""
... energy_cost_list = []
... for i in range(len(df)):
... # 获取用电量和时间(小时)
... energy_used = df.iloc[i]['energy_kwh']
... hour = df.iloc[i]['date_time'].hour
... energy_cost = apply_tariff(energy_used, hour)
... energy_cost_list.append(energy_cost)
... df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_loop(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_loop` ran in average of 3.152 seconds.
复制代码
对于那些写Pythonic风格的人来讲,这个设计看起来很天然。然而,这个循环将会严重影响效率,也是不赞同这么作。缘由有几个:
(0,len(df))
循环,而后在应用apply_tariff()
以后,它必须将结果附加到用于建立新DataFrame列的列表中。它还使用df.iloc [i] ['date_time']
执行所谓的链式索引,这一般会致使意外的结果。那么推荐作法是什么样的呢?
实际上能够经过pandas引入itertuples和iterrows方法可使效率更快。这些都是一次产生一行的生成器方法,相似scrapy中使用的yield用法。
.itertuples
为每一行产生一个namedtuple
,而且行的索引值做为元组的第一个元素。nametuple是Python的collections模块中的一种数据结构,其行为相似于Python元组,但具备可经过属性查找访问的字段。
.iterrows
为DataFrame中的每一行产生(index,series)
这样的元组。
虽然.itertuples每每会更快一些,可是在这个例子中使用.iterrows,咱们看看这使用iterrows后效果如何。
>>> @timeit(repeat=3, number=100)
... def apply_tariff_iterrows(df):
... energy_cost_list = []
... for index, row in df.iterrows():
... # 获取用电量和时间(小时)
... energy_used = row['energy_kwh']
... hour = row['date_time'].hour
... # 添加cost列表
... energy_cost = apply_tariff(energy_used, hour)
... energy_cost_list.append(energy_cost)
... df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_iterrows(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_iterrows` ran in average of 0.713 seconds.
复制代码
语法方面:这样的语法更明确,而且行值引用中的混乱更少,所以它更具可读性。
在时间收益方面:快了近5倍! 可是,还有更多的改进空间。咱们仍然在使用某种形式的Python for循环,这意味着每一个函数调用都是在Python中完成的,理想状况是它能够用Pandas内部架构中内置的更快的语言完成。
咱们可使用.apply
方法而不是.iterrows进一步改进此操做。Pandas的.apply方法接受函数(callables)并沿DataFrame的轴(全部行或全部列)应用它们。在此示例中,lambda函数将帮助你将两列数据传递给apply_tariff()
:
>>> @timeit(repeat=3, number=100)
... def apply_tariff_withapply(df):
... df['cost_cents'] = df.apply(
... lambda row: apply_tariff(
... kwh=row['energy_kwh'],
... hour=row['date_time'].hour),
... axis=1)
...
>>> apply_tariff_withapply(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_withapply` ran in average of 0.272 seconds.
复制代码
.apply
的语法优势很明显,行数少,代码可读性高。在这种状况下,所花费的时间大约是.iterrows
方法的一半。
可是,这还不是“很是快”。一个缘由是.apply()将在内部尝试循环遍历Cython迭代器。可是在这种状况下,传递的lambda不是能够在Cython中处理的东西,所以它在Python中调用,所以并非那么快。
若是你使用.apply()获取10年的小时数据,那么你将须要大约15分钟的处理时间。若是这个计算只是大型模型的一小部分,那么你真的应该加快速度。这也就是矢量化操做派上用场的地方。
什么是矢量化操做?若是你不基于一些条件,而是能够在一行代码中将全部电力消耗数据应用于该价格(df ['energy_kwh'] * 28)
,相似这种。这个特定的操做就是矢量化操做的一个例子,它是在Pandas中执行的最快方法。
可是如何将条件计算应用为Pandas中的矢量化运算?一个技巧是根据你的条件选择和分组DataFrame,而后对每一个选定的组应用矢量化操做。 在下一个示例中,你将看到如何使用Pandas的.isin()方法选择行,而后在向量化操做中实现上面新特征的添加。在执行此操做以前,若是将date_time列设置为DataFrame的索引,则会使事情更方便:
df.set_index('date_time', inplace=True)
@timeit(repeat=3, number=100)
def apply_tariff_isin(df):
# 定义小时范围Boolean数组
peak_hours = df.index.hour.isin(range(17, 24))
shoulder_hours = df.index.hour.isin(range(7, 17))
off_peak_hours = df.index.hour.isin(range(0, 7))
# 使用上面的定义
df.loc[peak_hours, 'cost_cents'] = df.loc[peak_hours, 'energy_kwh'] * 28
df.loc[shoulder_hours,'cost_cents'] = df.loc[shoulder_hours, 'energy_kwh'] * 20
df.loc[off_peak_hours,'cost_cents'] = df.loc[off_peak_hours, 'energy_kwh'] * 12
复制代码
咱们来看一下结果如何。
>>> apply_tariff_isin(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_isin` ran in average of 0.010 seconds.
复制代码
为了了解刚才代码中发生的状况,咱们须要知道.isin()方法返回的是一个布尔值数组,以下所示:
[False, False, False, ..., True, True, True]
复制代码
这些值标识哪些DataFrame索引(datetimes)
落在指定的小时范围内。而后,当你将这些布尔数组传递给DataFrame的.loc索引器时,你将得到一个仅包含与这些小时匹配的行的DataFrame切片。在那以后,仅仅是将切片乘以适当的费率,这是一种快速的矢量化操做。
这与咱们上面的循环操做相好比何?首先,你可能会注意到再也不须要apply_tariff()
,由于全部条件逻辑都应用于行的选择。所以,你必须编写的代码行和调用的Python代码会大大减小。
处理时间怎么样?比不是Pythonic的循环快315倍,比.iterrows快71倍,比.apply快27倍。
在apply_tariff_isin中,咱们仍然能够经过调用df.loc
和df.index.hour.isin
三次来进行一些“手动工做”。若是咱们有更精细的时隙范围,你可能会争辩说这个解决方案是不可扩展的。幸运的是,在这种状况下,你可使用Pandas的pd.cut()
函数以编程方式执行更多操做:
@timeit(repeat=3, number=100)
def apply_tariff_cut(df):
cents_per_kwh = pd.cut(x=df.index.hour,
bins=[0, 7, 17, 24],
include_lowest=True,
labels=[12, 20, 28]).astype(int)
df['cost_cents'] = cents_per_kwh * df['energy_kwh']
复制代码
让咱们看看这里发生了什么。pd.cut()
根据每小时所属的bin应用一组标签(costs)。
注意include_lowest参数表示第一个间隔是否应该是包含左边的(您但愿在组中包含时间= 0)。 这是一种彻底矢量化的方式来得到咱们的预期结果,它在时间方面是最快的:
>>> apply_tariff_cut(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_cut` ran in average of 0.003 seconds.
复制代码
到目前为止,时间上基本快达到极限了,只须要花费不到一秒的时间来处理完整的10年的小时数据集。可是,最后一个选项是使用 NumPy
函数来操做每一个DataFrame的底层NumPy数组,而后将结果集成回Pandas数据结构中。
使用Pandas时不该忘记的一点是Pandas Series
和DataFrames
是在NumPy库之上设计的。这为你提供了更多的计算灵活性,由于Pandas能够与NumPy阵列和操做无缝衔接。
下面,咱们将使用NumPy的 digitize()
函数。它相似于Pandas的cut(),由于数据将被分箱,但此次它将由一个索引数组表示,这些索引表示每小时所属的bin。而后将这些索引应用于价格数组:
@timeit(repeat=3, number=100)
def apply_tariff_digitize(df):
prices = np.array([12, 20, 28])
bins = np.digitize(df.index.hour.values, bins=[7, 17, 24])
df['cost_cents'] = prices[bins] * df['energy_kwh'].values
复制代码
与cut函数同样,这种语法很是简洁易读。但它在速度方面有何比较?让咱们来看看:
>>> apply_tariff_digitize(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_digitize` ran in average of 0.002 seconds.
复制代码
在这一点上,仍然有性能提高,但它本质上变得更加边缘化。使用Pandas,它能够帮助维持“层次结构”,若是你愿意,能够像在此处同样进行批量计算,这些一般排名从最快到最慢(最灵活到最不灵活):
使用向量化操做:没有for循环的Pandas方法和函数。
将.apply方法:与可调用方法一块儿使用。
使用.itertuples:从Python的集合模块迭代DataFrame行做为namedTuples。
使用.iterrows:迭代DataFrame行做为(index,Series)对。虽然Pandas系列是一种灵活的数据结构,但将每一行构建到一个系列中而后访问它可能会很昂贵。
使用“element-by-element”循环:使用df.loc或df.iloc一次更新一个单元格或行。
如今你已经了解了Pandas中的加速数据流程,接着让咱们探讨如何避免与最近集成到Pandas中的HDFStore
一块儿从新处理时间。
一般,在构建复杂数据模型时,能够方便地对数据进行一些预处理。例如,若是您有10年的分钟频率耗电量数据,即便你指定格式参数,只需将日期和时间转换为日期时间可能须要20分钟。你真的只想作一次,而不是每次运行你的模型,进行测试或分析。
你能够在此处执行的一项很是有用的操做是预处理,而后将数据存储在已处理的表单中,以便在须要时使用。可是,如何以正确的格式存储数据而无需再次从新处理?若是你要另存为CSV,则只会丢失datetimes对象,而且在再次访问时必须从新处理它。
Pandas有一个内置的解决方案,它使用 HDF5
,这是一种专门用于存储表格数据阵列的高性能存储格式。 Pandas的 HDFStore
类容许你将DataFrame存储在HDF5文件中,以即可以有效地访问它,同时仍保留列类型和其余元数据。它是一个相似字典的类,所以您能够像读取Python dict对象同样进行读写。
如下是将预处理电力消耗DataFrame df存储在HDF5文件中的方法:
# 建立储存对象,并存为 processed_data
data_store = pd.HDFStore('processed_data.h5')
# 将 DataFrame 放进对象中,并设置 key 为 preprocessed_df
data_store['preprocessed_df'] = df
data_store.close()
复制代码
如今,你能够关闭计算机并休息一下。等你回来的时候,你处理的数据将在你须要时为你所用,而无需再次加工。如下是如何从HDF5文件访问数据,并保留数据类型:
# 获取数据储存对象
data_store = pd.HDFStore('processed_data.h5')
# 经过key获取数据
preprocessed_df = data_store['preprocessed_df']
data_store.close()
复制代码
数据存储能够容纳多个表,每一个表的名称做为键。
关于在Pandas中使用HDFStore的注意事项:您须要安装PyTables> = 3.0.0
,所以在安装Pandas以后,请确保更新PyTables
,以下所示:
pip install --upgrade tables
复制代码
若是你以为你的Pandas项目不够快速,灵活,简单和直观,请考虑从新考虑你使用该库的方式。
这里探讨的示例至关简单,但说明了Pandas功能的正确应用如何可以大大改进运行时和速度的代码可读性。如下是一些经验,能够在下次使用Pandas中的大型数据集时应用这些经验法则:
若是以为有帮助,还请给点个赞!
欢迎关注个人我的公众号:Python数据科学