Pandas之旅(七) 谁说pandas慢

57次阅读

共计 5903 个字符,预计需要花费 15 分钟才能阅读完成。

Pandas 加速
大家好,今天我们来看有关 pandas 加速的小技巧,不知道大家在刚刚接触 pandas 的时候有没有听过如下的说法

pandas 太慢了,运行要等半天 **
其实我想说的是,慢不是 pandas 的错,大家要知道 pandas 本身是在 Numpy 上建立起来的包,在很多情况下是支持向量化运算的,而且还有 C 的底层设计,所以我今天主要想从几个方面和大家分享一下 pandas 加速的小技巧,与往常一样,文章分成四部分,本文结构如下:

使用 datetime 类型来处理和时间序列有关的数据
批量计算的技巧
通过 HDFStore 存储数据节省时间
源码,相关数据及 GitHub 地址

现在就让我们开始吧
1. 使用 datetime 类型来处理和时间序列有关的数据
首先这里我们使用的数据源是一个电力消耗情况的数据 (energy_cost.csv),非常贴近生活而且也是和时间息息相关的,用来做测试在合适不过了,这个 csv 文件大家可以在第四部分找到下载的地方哈
import os
# 这两行仅仅是切换路径,方便我上传 Github,大家不用理会, 只要确认 csv 文件和 py 文件再一起就行啦
os.chdir(“F:\\Python 教程 \\segmentfault\\pandas_share\\Pandas 之旅_07 谁说 pandas 慢 ”)
现在让我们看看数据大概长什么样子
import numpy as np
import pandas as pd
f”Using {pd.__name__},{pd.__version__}”
‘Using pandas,0.23.0’

df = pd.read_csv(‘energy_cost.csv’,sep=’,’)
df.head()

date_time
energy_kwh

0
2001/1/13 0:00
0.586

1
2001/1/13 1:00
0.580

2
2001/1/13 2:00
0.572

3
2001/1/13 3:00
0.596

4
2001/1/13 4:00
0.592

现在我们看到初始数据的样子了,主要有 date_time 和 energy_kwh 这两列,来表示时间和消耗的电力,比较好理解,下面让我们来看一下数据类型
df.dtypes
>>> date_time object
energy_kwh float64
dtype: object
type(df.iat[0,0])
>>> str
这里有个小问题,Pandas 和 NumPy 有 dtypes(数据类型)的概念。如果未指定参数,则 date_time 这一列的数据类型默认 object,所以为了之后运算方便,我们可以把 str 类型的这一列转化为 timestamp 类型:
df[‘date_time’] = pd.to_datetime(df[‘date_time’])
df.dtypes

>>> date_time datetime64[ns]
energy_kwh float64
dtype: object
先在大家可以发现我们通过用 pd.to_datetime 这个方法已经成功的把 date_time 这一列转化为了 datetime64 类型
df.head()

date_time
energy_kwh

0
2001-01-13 00:00:00
0.586

1
2001-01-13 01:00:00
0.580

2
2001-01-13 02:00:00
0.572

3
2001-01-13 03:00:00
0.596

4
2001-01-13 04:00:00
0.592

现在再来看数据, 发现已经和刚才不同了, 我们还可以通过指定 format 参数实现一样的效果,速度上也会快一些
%%timeit -n 10
def convert_with_format(df, column_name):
return pd.to_datetime(df[column_name],format=’%Y/%m/%d %H:%M’)

df[‘date_time’]=convert_with_format(df, ‘date_time’)

>>>722 µs ± 334 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
有关具体的日期自定义相关方法,大家点击这里查看
2. 批量计算的技巧
首先,我们假设根据用电的时间段不同,电费价目表如下:

Type
cents/kwh
periode

Peak
28
17:00 to 24:00

Shoulder
20
7:00 to 17:00

Off-Peak
12
0:00 to 7:00

假设我们想要计算出电费,我们可以先写出一个根据时间动态计算电费的方法“apply_tariff“
def apply_tariff(kwh, hour):
“””Calculates cost of electricity for given 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
好啦,现在我们想要在数据中新增一列 ‘cost_cents’ 来表示总价钱,我们有很多选择,首先能想到的方法便是 iterrows(),它可以让我们循环遍历 Dataframe 的每一行,根据条件计算并赋值给新增的‘cost_cents’列
iterrows()
首先我们能做的是循环遍历流程,让我们先用.iterrows() 替代上面的方法来试试:
%%timeit -n 10
def apply_tariff_iterrows(df):
energy_cost_list = []
for index, row in df.iterrows():
# Get electricity used and hour of day
energy_used = row[‘energy_kwh’]
hour = row[‘date_time’].hour
# Append cost list
energy_cost = apply_tariff(energy_used, hour)
energy_cost_list.append(energy_cost)
df[‘cost_cents’] = energy_cost_list

apply_tariff_iterrows(df)
983 ms ± 65.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

我们为了测试方便,所有的方法都会循环 10 次来比较耗时,这里很明显我们有很大的改进空间,下面我们用 apply 方法来优化
apply()
%%timeit -n 10
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)
247 ms ± 24.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

这回速度得到了很大的提升, 但是显然我们还没有 get 到 pandas 加速的精髓:矢量化操作。下面让我们开始提速
isin()
假设我们现在的电价是定值,不根据用电时间段来改变,那么 pandas 中最快的方法那就是采用 (df[‘cost_cents’] = df[‘energy_kwh’] * price),这就是一个简单的矢量化操作示范。它基本是在 Pandas 中运行最快的方式。
目前的问题是我们的价格是动态的,那么如何将条件判断添加到 Pandas 中的矢量化运算中呢?答案就是我们根据条件选择和分组 DataFrame,然后对每个选定的组应用矢量化操作:
#先让我们把时间序列作为索引
df.set_index(‘date_time’, inplace=True)
%%timeit -n 10
def apply_tariff_isin(df):
# Define hour range Boolean arrays
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))

# Apply tariffs to hour ranges
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)
5.7 ms ± 871 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

这回我们发现速度是真正起飞了,首先我们根据用电的三个时段把 df 进行分三组,再依次进行三次矢量化操作,大家可以发现最后减少了很多时间,原理很简单:
在运行的时候,.isin()方法返回一个布尔值数组,如下所示:
[False, False, False, …, True, True, True]
接下来布尔数组传递给 DataFrame 的.loc 索引器时,我们获得一个仅包含与 3 个用电时段匹配 DataFrame 切片。然后简单的进行乘法操作就行了,这样做的好处是我们已经不需要刚才提过的 apply 方法了,因为不在存在遍历所有行的问题
我们可以做的更好吗?
通过观察可以发现,在 apply_tariff_isin()中,我们仍然在通过调用 df.loc 和 df.index.hour.isin()来进行一些“手动工作”。如果想要进一步提速, 我们可以使用 cut 方法
%%timeit -n 10
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’]
140 ns ± 29.9 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)

效果依然锋利,速度上有了成倍的提升
不要忘了用 Numpy
众所周知,Pandas 是在 Numpy 上建立起来的,所以在 Numpy 中当然有类似 cut 的方法可以实现分组, 从速度上来讲差不太多
%%timeit -n 10
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

54.9 ns ± 19.3 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)

正常情况下,以上的加速方法是能满足日常需要的,如果有特殊的需求,大家可以上网看看有没有相关的第三方加速包
3. 通过 HDFStore 存储数据节省时间
这里主要想强调的是节省预处理的时间,假设我们辛辛苦苦搭建了一些模型,但是每次运行之前都要进行一些预处理,比如类型转换,用时间序列做索引等,如果不用 HDFStore 的话每次都会花去不少时间,这里 Python 提供了一种解决方案,可以把经过预处理的数据存储为 HDF5 格式,方便我们下次运行时直接调用。
下面就让我们把本篇文章的 df 通过 HDF5 来存储一下:
# Create storage object with filename `processed_data`
data_store = pd.HDFStore(‘processed_data.h5’)

# Put DataFrame into the object setting the key as ‘preprocessed_df’
data_store[‘preprocessed_df’] = df
data_store.close()
现在我们可以关机下班了,当明天接着上班后,通过 key(”preprocessed_df”)就可以直接使用经过预处理的数据了
# Access data store
data_store = pd.HDFStore(‘processed_data.h5’)

# Retrieve data using key
preprocessed_df = data_store[‘preprocessed_df’]
data_store.close()
preprocessed_df.head()

energy_kwh
cost_cents

date_time

2001-01-13 00:00:00
0.586
7.032

2001-01-13 01:00:00
0.580
6.960

2001-01-13 02:00:00
0.572
6.864

2001-01-13 03:00:00
0.596
7.152

2001-01-13 04:00:00
0.592
7.104

如上图所示,现在我们可以发现 date_time 已经是处理为 index 了
4. 源码,相关数据及 GitHub 地址
这一期为大家分享了一些 pandas 加速的实用技巧,希望可以帮到各位小伙伴,当然,类似的技巧还有很多,但是核心思想应该一直围绕矢量化操作上,毕竟是基于 Numpy 上建立的包,如果大家有更好的办法,希望可以在我的文章底下留言哈
我把这一期的 ipynb 文件,py 文件以及我们用到的 energy_cost.csv 放到了 Github 上,大家可以点击下面的链接来下载:
Github 仓库地址:https://github.com/yaozeliang/pandas_share

希望大家能够继续支持我,这一篇文章已经是 Pandas 系列的最后一篇了,虽然一共只写了 7 篇文章,但是我认为从实用性上来讲并没有太逊色于收费课程(除了少了很多漂亮的 ppt),接下来我会再接再厉,分享一下我对 R(ggplot2)或者 matplotlib 的学习经验!!
Pandas 之旅到此结束。撒花

正文完
 0