你们好,今天咱们来看有关pandas加速的小技巧,不知道你们在刚刚接触pandas的时候有没有听过以下的说法python
pandas太慢了,运行要等半天**git
其实我想说的是,慢不是pandas的错,你们要知道pandas自己是在Numpy上创建起来的包,在不少状况下是支持向量化运算的,并且还有C的底层设计,因此我今天 主要想从几个方面和你们分享一下pandas加速的小技巧,与往常同样,文章分红四部分,本文结构以下:github
如今就让咱们开始吧segmentfault
首先这里咱们使用的数据源是一个电力消耗状况的数据(energy_cost.csv),很是贴近生活并且也是和时间息息相关的,用来作测试在合适不过了,这个csv文件你们能够在第四部分找到下载的地方哈数组
import os
# 这两行仅仅是切换路径,方便我上传Github,你们不用理会,只要确认csv文件和py文件再一块儿就行啦
os.chdir("F:\\Python教程\\segmentfault\\pandas_share\\Pandas之旅_07 谁说pandas慢")
复制代码
如今让咱们看看数据大概长什么样子app
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这两列,来表示时间和消耗的电力,比较好理解,下面让咱们来看一下数据类型oop
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参数实现同样的效果,速度上也会快一些spa
%%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)
复制代码
有关具体的日期自定义相关方法,你们点击这里查看
首先,咱们假设根据用电的时间段不一样,电费价目表以下:
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()替代上面的方法来试试:
%%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方法来优化
%%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加速的精髓:矢量化操做。下面让咱们开始提速
假设咱们如今的电价是定值,不根据用电时间段来改变,那么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()方法返回一个布尔值数组,以下所示:
接下来布尔数组传递给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)
复制代码
效果依然锋利,速度上有了成倍的提高
众所周知,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)
复制代码
正常状况下,以上的加速方法是能知足平常须要的,若是有特殊的需求,你们能够上网看看有没有相关的第三方加速包
这里主要想强调的是节省预处理的时间,假设咱们辛辛苦苦搭建了一些模型,可是每次运行以前都要进行一些预处理,好比类型转换,用时间序列作索引等,若是不用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了
这一期为你们分享了一些pandas加速的实用技巧,但愿能够帮到各位小伙伴,固然,相似的技巧还有不少,可是核心思想应该一直围绕矢量化操做上,毕竟是基于Numpy上创建的包,若是你们有更好的办法,但愿能够在个人文章底下留言哈
我把这一期的ipynb文件,py文件以及咱们用到的energy_cost.csv放到了Github上,你们能够点击下面的连接来下载: