优化pandas运行速度

参考 还在抱怨pandas运行速度慢?这几个方法会颠覆你的看法

1. 将datetime数据与时间序列一起使用的优点

数据样本

   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

备注: 这里日期(时间序列的数据)是 str 类型,会极大的影响效率,因此需要把date_time列格式化为datetime对象数组(pandas称之为时间戳)。

df['date_time'] = pd.to_datetime(df['date_time'])
df['date_time'].dtype
# datetime64[ns]

转化后的样本数据

     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

为了更好的对比,我们通过 timeit 装饰器来测试一下代码的转化时间。

@timeit(repeat=3, number=10)
def convert(df, col_name):
    return pd.to_datetime(df[col_name])

df['date_time'] = convert(df, 'date_time')
# 1.610 seconds.

1.1 优化一(设置转化的格式format)

@timeit(repeat=3, number=100)
def convert_with_format(df, col_name):
    return pd.to_datetime(df[col_name], format='%d/%m/%y %H:%M')
# 0.032 seconds.

由于在CSV中的datetimes并不是 ISO 8601 格式的,如果不进行设置的话,那么pandas将使用 dateutil 包把每个字符串str转化成date日期。
相反,如果原始数据datetime已经是 ISO 8601 格式了,那么pandas就可以立即使用最快速的方法来解析日期。这样快了将近50倍。

2. 进行批量计算的最有效途径

一个新的需求: 我们想要按照下表的格式,针对不同时间range,对energy_kw乘上相应的权重。
在这里插入图片描述2.2 优化二(使用itertuples() 和iterrows() 循环)

通过pandas引入itertuples和iterrows方法(这些都是一次产生一行的生成器方法)。

.itertuples为每一行产生一个namedtuple,并且行的索引值作为元组的第一个元素。nametuple是Python的collections模块中的一种数据结构,其行为类似于Python元组,但具有可通过属性查找访问的字段。
.iterrows为DataFrame中的每一行产生(index,series)这样的元组。
虽然.itertuples往往会更快一些,但在这个例子中我们使用.iterrows。
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
    
@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)
# 0.713 seconds.

这里还有更多的改进空间。我们仍然在使用某种形式的Python for循环,这意味着每个函数调用都是在Python中完成的,理想情况是它可以用Pandas内部架构中内置的更快的语言完成。

2.3 优化三(Pandas的 .apply()方法)

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)
# 0.272 seconds.

所花费时间大约是.iterrows方法的一半,但这还不是“非常快”。一个原因是.apply()将在内部尝试循环遍历Cython迭代器。但是在这种情况下,传递的lambda不是可以在Cython中处理的东西,因此它在Python中调用,因此并不是那么快。

2.4 优化四(矢量化操作,使用.isin()选择数据)

矢量化操作就是不基于一些条件,而是可以在一行代码中将所有电力消耗数据应用于该价格(df ['energy_kwh'] * 28),类似这种,它是在Pandas中执行的最快方法。

这里根据你的条件选择和分组DataFrame,对每个选定的组应用矢量化操作。 下面用.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)
# 0.010 seconds.
其中: .isin()方法返回的是一个布尔值数组:[False, False, False, ..., True, True, True]
这些值标识哪些DataFrame索引(datetimes)落在指定的小时范围内。然后,当你将这些布尔数组传递给DataFrame的.loc索引器时,你将获得一个仅包含与这些小时匹配的行的DataFrame切片。在这之后,仅仅是将切片乘以适当的费率,这是一种快速的矢量化操作。 

这样比不是Pythonic的循环快315倍,比.iterrows快71倍,比.apply快27倍。

2.5 优化五(矢量化操作,使用Pandas的pd.cut() )

pd.cut() 根据每小时所属的bin应用一组标签(costs)。
注意 :include_lowest参数表示第一个间隔是否应该是包含左边的。

@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']
   
apply_tariff_cut(df)
# 0.003 seconds.

这是一种完全矢量化的方式,它在时间方面是最快的,(到目前为止,时间上基本快达到极限了,只需要花费不到一秒的时间来处理完整的10年的小时数据集)。

2.6 优化六(使用Numpy的 digitize() 继续加速 )

Pandas Series和DataFrames是在NumPy库之上设计的,因此Pandas可以与NumPy阵列和操作无缝衔接。 
使用 NumPy 函数来操作每个DataFrame的底层NumPy数组,然后将结果集成回Pandas数据结构中。

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
    
apply_tariff_digitize(df)
# 0.002 seconds.

在这一点上,仍然有性能提升,但它本质上变得更加边缘化。使用Pandas,它可以帮助维持“层次结构”,如果你愿意,可以像在此处一样进行批量计算,这些通常排名从最快到最慢(最灵活到最不灵活):

1. 使用向量化操作:没有for循环的Pandas方法和函数。
2. 将.apply方法:与可调用方法一起使用。
3. 使用.itertuples:从Python的集合模块迭代DataFrame行作为namedTuples。
4. 使用.iterrows:迭代DataFrame行作为(index,Series)对。虽然Pandas系列是一种灵活的数据结构,但将每一行构建到一个系列中然后访问它可能会很昂贵。
5. 使用“element-by-element”循环:使用df.loc或df.iloc一次更新一个单元格或行。

在这里插入图片描述

3. 通过HDFStore存储数据节省时间

通常,在构建复杂数据模型时,会对数据进行一些预处理,然后将数据存储在已处理的表单中,以便在需要时使用。
但如何以正确的格式存储数据而无需再次重新处理?
如果你要另存为CSV,则只会丢失datetimes对象,并且在再次访问时必须重新处理它。

其实Pandas有一个内置的解决方案,它使用 HDF5(一种专门用于存储表格数据阵列的高性能存储格式)。
 Pandas的 HDFStore 类允许你将DataFrame存储在HDF5文件中,以便可以有效地访问它,同时仍保留列类型和其他元数据。
 它是一个类似字典的类,因此您可以像读取Python dict对象一样进行读写。

存数据:

# 创建储存对象,并存为 processed_data
data_store = pd.HDFStore('processed_data.h5')
# 将 DataFrame 放进对象中,并设置 key 为 preprocessed_df
data_store['preprocessed_df'] = df
data_store.close()

取数据:

# 获取数据储存对象
data_store = pd.HDFStore('processed_data.h5')
# 通过key获取数据
preprocessed_df = data_store['preprocessed_df']
data_store.close()

数据存储可以容纳多个表,每个表的名称作为键。
备注:使用HDFStore时需要安装PyTables> = 3.0.0,因此在安装Pandas之后,请确保更新PyTables:

pip install --upgrade tables

参考:HDF5文件写入和读取Demo

4. 总结

4.1 一些经验

(1)尝试尽可能使用矢量化操作,而不是在df 中解决for x的问题。
如果你的代码是许多for循环,那么它可能更适合使用本机Python数据结构,因为Pandas会带来很多开销。

(2)如果你有更复杂的操作,其中矢量化根本不可能或太难以有效地解决,请使用.apply方法。

(3)如果必须循环遍历数组,请使用.iterrows()或.itertuples()来提高速度和语法。

(4) Pandas有很多可选性,几乎总有几种方法可以从A到B。请注意这一点,比较不同方法的执行方式,并选择在项目环境中效果最佳的路线。

(5)一旦建立了数据清理脚本,就可以通过使用HDFStore存储中间结果来避免重新处理。

(6)将NumPy集成到Pandas操作中通常可以提高速度并简化语法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值