Pandas之旅(七) 誰說pandas慢

Pandas 加速

你們好,今天咱們來看有關pandas加速的小技巧,不知道你們在剛剛接觸pandas的時候有沒有聽過以下的說法python

pandas太慢了,運行要等半天

其實我想說的是,慢不是pandas的錯,你們要知道pandas自己是在Numpy上創建起來的包,在不少狀況下是支持向量化運算的,並且還有C的底層設計,因此我今天
主要想從幾個方面和你們分享一下pandas加速的小技巧,與往常同樣,文章分紅四部分,本文結構以下:git

  1. 使用datetime類型來處理和時間序列有關的數據
  2. 批量計算的技巧
  3. 經過HDFStore存儲數據節省時間
  4. 源碼,相關數據及GitHub地址

如今就讓咱們開始吧github

1. 使用datetime類型來處理和時間序列有關的數據

首先這裏咱們使用的數據源是一個電力消耗狀況的數據(energy_cost.csv),很是貼近生活並且也是和時間息息相關的,用來作測試在合適不過了,這個csv文件你們能夠在第四部分找到下載的地方哈segmentfault

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這兩列,來表示時間和消耗的電力,比較好理解,下面讓咱們來看一下數據類型app

df.dtypes
>>> date_time      object
    energy_kwh    float64
    dtype: object
type(df.iat[0,0])
>>> str

這裏有個小問題,Pandas和NumPy有dtypes(數據類型)的概念。若是未指定參數,則date_time這一列的數據類型默認object,因此爲了以後運算方便,咱們能夠把str類型的這一列轉化爲timestamp類型:oop

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上,你們能夠點擊下面的連接來下載:

但願你們可以繼續支持我,這一篇文章已是Pandas系列的最後一篇了,雖然一共只寫了7篇文章,可是我認爲從實用性上來說並無太遜色於收費課程(除了少了不少漂亮的ppt),接下來我會再接再礪,分享一下我對R (ggplot2)或者matplotlib的學習經驗!!

Pandas之旅到此結束。撒花

相關文章
相關標籤/搜索