# 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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://zeliang-yao.gitbook.io/my-note-zeliang-yao/useful/pandas/accelerate.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
