# Pandas acceleration

我们来看有关pandas加速的小技巧：

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

现在就让我们开始吧

## 1. 使用datetime类型来处理和时间序列有关的数据 <a href="#shi-yong-datetime-lei-xing-lai-chu-li-he-shi-jian-xu-lie-you-guan-de-shu-ju" id="shi-yong-datetime-lei-xing-lai-chu-li-he-shi-jian-xu-lie-you-guan-de-shu-ju"></a>

首先这里我们使用的数据源是一个电力消耗情况的数据(energy\_cost.csv)，非常贴近生活而且也是和时间息息相关的，用来做测试在合适不过了，这个csv文件大家可以在第四部分找到下载的地方哈

```python
import numpy as np
import pandas as pd
f"Using {pd.__name__},{pd.__version__}"
```

```python
'Using pandas,0.23.0'
```

```python
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这两列，来表示时间和消耗的电力，比较好理解，下面让我们来看一下数据类型

```python
df.dtypes
>>> date_time      object
    energy_kwh    float64
    dtype: object
```

```python
type(df.iat[0,0])
>>> str
```

这里有个小问题，Pandas和NumPy有dtypes（数据类型）的概念。如果未指定参数，则date\_time这一列的数据类型默认object，所以为了之后运算方便，我们可以把str类型的这一列转化为timestamp类型:

```python
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类型

```python
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参数实现一样的效果，速度上也会快一些

```python
%%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)
```

有关具体的日期自定义相关方法，大家点击[这里](http://strftime.org/)查看

## 2. 批量计算的技巧 <a href="#pi-liang-ji-suan-de-ji-qiao" id="pi-liang-ji-suan-de-ji-qiao"></a>

首先，我们假设根据用电的时间段不同，电费价目表如下：

| 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“

```python
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()*** <a href="#iterrows" id="iterrows"></a>

首先我们能做的是循环遍历流程，让我们先用.iterrows()替代上面的方法来试试：

```python
%%timeit -n 10
def apply_tariff_iterrows(df):
    energy_cost_list = []
    for index, row in df.iterrows():
        
        energy_used = row['energy_kwh']
        hour = row['date_time'].hour
        
        energy_cost = apply_tariff(energy_used, hour)
        energy_cost_list.append(energy_cost)
    df['cost_cents'] = energy_cost_list

apply_tariff_iterrows(df)
```

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

我们为了测试方便，所有的方法都会循环10次来比较耗时，这里很明显我们有很大的改进空间，下面我们用apply方法来优化

### ***apply()*** <a href="#apply" id="apply"></a>

```python
%%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)
```

```python
247 ms ± 24.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

这回速度得到了很大的提升,但是显然我们还没有get到pandas加速的精髓：矢量化操作。下面让我们开始提速

### **isin()** <a href="#isin" id="isin"></a>

假设我们现在的电价是定值，不根据用电时间段来改变，那么pandas中最快的方法那就是采用(df\[‘cost\_cents’] = df\[‘energy\_kwh’] \* price)，这就是一个简单的矢量化操作示范。它基本是在Pandas中运行最快的方式。

目前的问题是我们的价格是动态的，那么如何将条件判断添加到Pandas中的矢量化运算中呢？答案就是我们根据条件选择和分组DataFrame，然后对每个选定的组应用矢量化操作:

```python

df.set_index('date_time', inplace=True)
```

```python
%%timeit -n 10
def apply_tariff_isin(df):
    
    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)
```

```
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方法了，因为不在存在遍历所有行的问题

### 我们可以做的更好吗？ <a href="#wo-men-ke-yi-zuo-de-geng-hao-ma" id="wo-men-ke-yi-zuo-de-geng-hao-ma"></a>

通过观察可以发现，在apply\_tariff\_isin（）中，我们仍然在通过调用df.loc和df.index.hour.isin（）来进行一些“手动工作”。如果想要进一步提速,我们可以使用cut方法

```python
%%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']
```

```python
140 ns ± 29.9 ns per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

效果依然锋利，速度上有了成倍的提升

### 用Numpy <a href="#bu-yao-wang-le-yong-numpy" id="bu-yao-wang-le-yong-numpy"></a>

众所周知，Pandas是在Numpy上建立起来的，所以在Numpy中当然有类似cut的方法可以实现分组,从速度上来讲差不太多

```python
%%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

```

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

正常情况下，以上的加速方法是能满足日常需要的，如果有特殊的需求，大家可以上网看看有没有相关的第三方加速包

## 3. HDFStore存储数据 <a href="#tong-guo-hdfstore-cun-chu-shu-ju-jie-sheng-shi-jian" id="tong-guo-hdfstore-cun-chu-shu-ju-jie-sheng-shi-jian"></a>

这里主要想强调的是节省预处理的时间，假设我们辛辛苦苦搭建了一些模型，但是每次运行之前都要进行一些预处理，比如类型转换，用时间序列做索引等，如果不用HDFStore的话每次都会花去不少时间，这里Python提供了一种解决方案，可以把经过预处理的数据存储为HDF5格式，方便我们下次运行时直接调用。

下面就让我们把本篇文章的df通过HDF5来存储一下：

```python

data_store = pd.HDFStore('processed_data.h5')


data_store['preprocessed_df'] = df
data_store.close()
```

现在我们可以关机下班了，当明天接着上班后，通过key（“preprocessed\_df”）就可以直接使用经过预处理的数据了

```python

data_store = pd.HDFStore('processed_data.h5')


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. 总结 <a href="#yuan-ma-xiang-guan-shu-ju-ji-github-di-zhi" id="yuan-ma-xiang-guan-shu-ju-ji-github-di-zhi"></a>

* Github仓库地址： [https://github.com/yaozeliang/pandas\_share](https://github.com/yaozeliang/pandas_share/tree/master/Pandas%E4%B9%8B%E6%97%85_04%20pandas%E8%B6%85%E5%AE%9E%E7%94%A8%E6%8A%80%E5%B7%A7)
