pandas100個騷操作:再見 for 循環!速度提升315倍!

2021-03-02 Python數據科學

關註上方「Python數據科學」,選擇星標,

關鍵時間,第一時間送達!


來源:Python數據科學

作者:東哥起飛

大家好,我是東哥。

本篇是pandas100個騷操作系列的第 11 篇:再見 for 循環!速度提升315倍!

系列內容,請看👉「pandas100個騷操作」話題,訂閱後文章更新可第一時間推送至訂閱號。

for是所有程式語言的基礎語法,初學者為了快速實現功能,依懶性較強。但如果從運算時間性能上考慮可能不是特別好的選擇。本次東哥介紹幾個常見的提速方法,一個比一個快,了解pandas本質,才能知道如何提速。

>>> import pandas as pd
# 導入數據集
>>> df = pd.read_csv('demand_profile.csv')
>>> df.head()
     date_time  energy_kwh
0  1/1/13 0:00       0.586
1  1/1/13 1:00       0.580
2  1/1/13 2:00       0.572
3  1/1/13 3:00       0.596
4  1/1/13 4:00       0.592

基於上面的數據,我們現在要增加一個新的特徵,但這個新的特徵是基於一些時間條件生成的,根據時長(小時)而變化,如下:

因此,如果你不知道如何提速,那正常第一想法可能就是用apply方法寫一個函數,函數裡面寫好時間條件的邏輯代碼。

def apply_tariff(kwh, 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

然後使用for循環來遍歷df,根據apply函數邏輯添加新的特徵,如下:

>>> # 不贊同這種操作
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
...     """用for循環計算enery cost,並添加到列表"""
...     energy_cost_list = []
...     for i in range(len(df)):
...         # 獲取用電量和時間(小時)
...         energy_used = df.iloc[i]['energy_kwh']
...         hour = df.iloc[i]['date_time'].hour
...         energy_cost = apply_tariff(energy_used, hour)
...         energy_cost_list.append(energy_cost)
...     df['cost_cents'] = energy_cost_list
... 
>>> apply_tariff_loop(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_loop` ran in average of 3.152 seconds.

對於那些寫Pythonic風格的人來說,這個設計看起來很自然。然而,這個循環將會嚴重影響效率。原因有幾個:其次,它使用不透明對象範圍(0,len(df))循環,然後再應用apply_tariff()之後,它必須將結果附加到用於創建新DataFrame列的列表中。另外,還使用df.iloc [i]['date_time']執行所謂的鏈式索引,這通常會導致意外的結果。這種方法的最大問題是計算的時間成本。對於8760行數據,此循環花費了3秒鐘。一、使用 iterrows循環第一種可以通過pandas引入iterrows方法讓效率更高。這些都是一次產生一行的生成器方法,類似scrapy中使用的yield用法。.itertuples為每一行產生一個namedtuple,並且行的索引值作為元組的第一個元素。nametuple是Python的collections模塊中的一種數據結構,其行為類似於Python元組,但具有可通過屬性查找訪問的欄位。.iterrows為DataFrame中的每一行產生(index,series)這樣的元組。在這個例子中使用.iterrows,我們看看這使用iterrows後效果如何。

>>> @timeit(repeat=3, number=100)
... def apply_tariff_iterrows(df):
...     energy_cost_list = []
...     for index, row in df.iterrows():
...         # 獲取用電量和時間(小時)
...         energy_used = row['energy_kwh']
...         hour = row['date_time'].hour
...         # 添加cost列表
...         energy_cost = apply_tariff(energy_used, hour)
...         energy_cost_list.append(energy_cost)
...     df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_iterrows(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_iterrows` ran in average of 0.713 seconds.

這樣的語法更明確,並且行值引用中的混亂更少,因此它更具可讀性。但是,還有更多的改進空間,理想情況是可以用pandas內置更快的方法完成。二、pandas的apply方法我們可以使用.apply方法而不是.iterrows進一步改進此操作。pandas的.apply方法接受函數callables並沿DataFrame的軸(所有行或所有列)應用。下面代碼中,lambda函數將兩列數據傳遞給apply_tariff():

>>> @timeit(repeat=3, number=100)
... 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)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_withapply` ran in average of 0.272 seconds.

apply的語法優點很明顯,行數少,代碼可讀性高。在這種情況下,所花費的時間大約是iterrows方法的一半。但是,這還不是「非常快」。一個原因是apply()將在內部嘗試循環遍歷Cython迭代器。但是在這種情況下,傳遞的lambda不是可以在Cython中處理的東西,因此它在Python中調用並不是那麼快。如果我們使用apply()方法獲取10年的小時數據,那麼將需要大約15分鐘的處理時間。如果這個計算只是大規模計算的一小部分,那麼真的應該提速了。這也就是矢量化操作派上用場的地方。三、矢量化操作:使用.isin選擇數據如果你不基於一些條件,而是可以在一行代碼中將所有電力消耗數據應用於該價格:df ['energy_kwh'] * 28,類似這種。那麼這個特定的操作就是矢量化操作的一個例子,它是在pandas中執行的最快方法。但是如何將條件計算應用為pandas中的矢量化運算?一個技巧是:根據你的條件,選擇和分組DataFrame,然後對每個選定的組應用矢量化操作。在下面代碼中,我們將看到如何使用pandas的.isin()方法選擇行,然後在矢量化操作中實現新特徵的添加。在執行此操作之前,如果將date_time列設置為DataFrame的索引,會更方便:

# 將date_time列設置為DataFrame的索引
df.set_index('date_time', inplace=True)

@timeit(repeat=3, number=100)
def apply_tariff_isin(df):
    # 定義小時範圍Boolean數組
    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_traffic函數中的定義
    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)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_isin` ran in average of 0.010 seconds.

提示,上面.isin()方法返回的是一個布爾值數組,如下:

[False, False, False, ..., True, True, True]

布爾值標識了DataFrame索引datetimes是否落在了指定的小時範圍內。然後把這些布爾數組傳遞給DataFrame的.loc,將獲得一個與這些小時匹配的DataFrame切片。然後再將切片乘以適當的費率,這就是一種快速的矢量化操作了。上面的方法完全取代了我們最開始自定義的函數apply_tariff(),代碼大大減少,同時速度起飛。運行時間比Pythonic的for循環快315倍,比iterrows快71倍,比apply快27倍!四、還能更快?在上面apply_tariff_isin中,我們通過調用df.loc和df.index.hour.isin三次來進行一些手動調整。如果我們有更精細的時間範圍,你可能會說這個解決方案是不可擴展的。但在這種情況下,我們可以使用pandas的pd.cut()函數來自動完成切割:

@timeit(repeat=3, number=100)
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']

上面代碼pd.cut()會根據bin列表應用分組。其中include_lowest參數表示第一個間隔是否應該是包含左邊的。

>>> apply_tariff_cut(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_cut` ran in average of 0.003 seconds.

到目前為止,使用pandas處理的時間上基本快達到極限了!只需要花費不到一秒的時間即可處理完整的10年的小時數據集。但是,最後一個其它選擇,就是使用 NumPy,還可以更快!五、使用Numpy繼續加速使用pandas時不應忘記的一點是Pandas的Series和DataFrames是在NumPy庫之上設計的。並且,pandas可以與NumPy陣列和操作無縫銜接。下面我們使用NumPy的 digitize()函數更進一步。它類似於上面pandas的cut(),因為數據將被分箱,但這次它將由一個索引數組表示,這些索引表示每小時所屬的bin。然後將這些索引應用於價格數組:

@timeit(repeat=3, number=100)
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

>>> apply_tariff_digitize(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_digitize` ran in average of 0.002 seconds.

0.002秒! 雖然仍有性能提升,但已經很邊緣化了。以上就是本次加速的技巧分享。樣本數據可在公眾號回覆:加速 獲取。如果喜歡東哥的騷操作,請給我點個讚和在看

我是東哥,最後給大家分享《100本Python電子書》,包括Python編程技巧、數據分析、爬蟲、Web開發、機器學習、深度學習。

現在免費分享出來,有需要的讀者可以下載學習,在下面的公眾號GitHuboy」裡回復關鍵字:Python,就行。

愛點讚的人,運氣都不會太差

相關焦點

  • pandas100個騷操作:使用 Datetime 提速 50 倍運行速度!
    本篇是pandas100個騷操作系列的第 10 篇:使用 Datetime
  • 再見 for 循環!pandas 提速 315 倍~
    然而,這個循環將會嚴重影響效率。原因有幾個:其次,它使用不透明對象範圍(0,len(df))循環,然後再應用apply_tariff()之後,它必須將結果附加到用於創建新DataFrame列的列表中。另外,還使用df.iloc [i]['date_time']執行所謂的鏈式索引,這通常會導致意外的結果。這種方法的最大問題是計算的時間成本。對於8760行數據,此循環花費了3秒鐘。
  • pandas100個騷操作:變量類型自動轉換
    本篇是pandas100個騷操作的第一篇:變量類型自動轉換在用pandas進行數據清洗的過程中,變量的類型轉換是一個必然會遇到的步驟。清洗初期查看dtypes經常出現object類型,但其實變量本身可能就是個字符串,或者是數字(但因存在空值,導致出現了object類型)。通常大家所熟知的方法是使用astype進行類型轉換,或者自己利用astype造個輪子,寫個函數方法實現自動轉換類型。
  • 用這幾個方法提高pandas運行速度
    我們知道pandas的兩個主要數據結構:dataframe和series,我們對數據的一些操作都是基於這兩個數據結構的。但在實際的使用中,我們可能很多時候會感覺運行一些數據結構的操作會異常的慢。一個操作慢幾秒可能看不出來什麼,但是一整個項目中很多個操作加起來會讓整個開發工作效率變得很低。
  • pandas100個騷操作:強大的 accessor 方法
    沒錯,在pandas中你一樣可以這樣簡單的操作,而不同的是你操作的是一整列的字符串數據。仍然基於以上數據集,再看它的另一個操作:>>> regex = (r'(?P<city>[A-Za-z ]+), '      # 一個或更多字母...          r'(?
  • 還在抱怨pandas運行速度慢?這幾個方法會顛覆你的看法
    我們知道pandas的兩個主要數據結構:dataframe和series,我們對數據的一些操作都是基於這兩個數據結構的。但在實際的使用中,我們可能很多時候會感覺運行一些數據結構的操作會異常的慢。一個操作慢幾秒可能看不出來什麼,但是一整個項目中很多個操作加起來會讓整個開發工作效率變得很低。
  • 還在抱怨Pandas運行速度慢?這幾個方法會顛覆你的看法
    我們知道pandas的兩個主要數據結構:dataframe和series,我們對數據的一些操作都是基於這兩個數據結構的。但在實際的使用中,我們可能很多時候會感覺運行一些數據結構的操作會異常的慢。一個操作慢幾秒可能看不出來什麼,但是一整個項目中很多個操作加起來會讓整個開發工作效率變得很低。
  • 那些功能逆天,卻鮮為人知的pandas騷操作
    str對象的2個方法說明:其實不難發現,該用法的使用與Python中字符串的操作很相似。dt對象的使用Series數據類型:datetime因為數據需要datetime類型,所以下面使用pandas的date_range()生成了一組日期datetime演示如何進行dt對象操作。
  • 如何正確使用Pandas庫提升項目的運行速度?
    format='%d/%m/%y %H:%M')Best of 3 trials with 100 function calls per trial:Function `convert_with_format` ran in average of 0.032 seconds.新的結果如何?0.032秒,速度提升了50倍!
  • 如何將數據處理速度提升1000+倍
    但是如果不能有效利用pandas和numpy中的各種函數和方法,反而會降低數據處理的效率。以下就以PyGotham 2019的一個演講介紹如何大幅提升數據處理的速度。notebook和數據見文末連結,代碼較多,建議下載notebook和數據測試。
  • 數據處理必看:如何讓你的 pandas 循環加快 71803 倍
    雷鋒網 AI 開發者按,如果你使用 python 和 pandas 進行數據分析,那麼不久你就會第一次使用循環了。然而,即使是對小型數據集,使用標準循環也很費時,你很快就會意識到大型數據幀可能需要很長的時間。當我第一次等了半個多小時來執行代碼時,我找到了接下來想與你共享的替代方案。標準循環數據幀是具有行和列的 pandas 對象。
  • pandas使用的100個trick
    pandas是處理數據的必備工具,可以方便地篩選,統計,分組集計,轉換等操作,而且pandas(熊貓)這麼可愛,誰能不愛呢kaggle上一位pandas大神總結了100個tricks,覺得有些很有意思,整理了一些,點擊文末閱讀原文查看完整的100個trick•Trick 1: 一行查看數據總體情況•Trick 2: pd.read_csv
  • pandas100個騷操作:transform 數據轉換的 4 個常用技巧!
    transform有4個比較常用的功能,總結如下:一.字符串函數也可以傳遞任何有效的pandas內置的字符串函數,例如sqrt:df.transform('sqrt')3. 函數列表func還可以是一個函數的列表。
  • Nervana技術使AI運行速度提升100倍
    (原標題:Nervana技術使AI運行速度提升100倍)
  • Pandas on Ray:僅需改動一行代碼,即可讓Pandas加速四倍
    這個小例子旨在演示一些 Pandas 操作,這些操作作為並行實現可在 Pandas on Ray 上找到。下面,我們會展示一些性能對比,以及我們可以利用機器上更多的資源來實現更快的運行速度,甚至是在很小的數據集上。轉置分布式轉置是 DataFrame 操作所需的更複雜的功能之一。
  • Pandas常見的性能優化方法
    但Pandas在使用上有一些技巧和需要注意的地方,如果你沒有合適的使用,那麼Pandas可能運行速度非常慢。本文將整理一些Pandas使用技巧,主要是用來節約內存和提高代碼速度。1 數據讀取與存取在Pandas中內置了眾多的數據讀取函數,可以讀取眾多的數據格式,最常見的就是read_csv函數從csv文件讀取數據了。
  • 懶人秘籍:教你如何避免編寫pandas代碼
    開始pandas遊戲之旅,請閱讀如下資源:5個鮮為人知的pandas技巧使用pandas進行探索性數據分析數據集有三列:id表示唯一的標識city表示預定的城市信息booked perc表示特定時間預定的百分比數據集有一萬條,這使速度改進更加明顯。注意,如果代碼以正確的pandas方式編寫,pandas可以利用DataFrames計算數百萬(甚至數十億)行的統計數據。
  • 十分鐘學習pandas! pandas常用操作總結!
    學習Python, 當然少不了pandas,pandas是python數據科學中的必備工具
  • 十分鐘學習pandas!pandas常用操作總結!
    學習Python, 當然少不了pandas,pandas是python數據科學中的必備工具,熟練使用pandas是從sql boy/girl 跨越到一名優秀的數據分析師傅的必備技能。這篇pandas常用操作總結幫大家回顧下pandas的常用語法,尤其是我們分析數據時常用的方法。
  • 教你如何將Pandas迭代速度加快150倍?
    目前,筆者嘗試在Go語言中進行數據科學研究——這是有可能的——但操作起來根本不像在Python中那樣令人愉快,多半是由於語言的靜態特性和數據科學大多是探索性領域。並不是說用Go語言重寫完成的解決方案不能提高性能,但這是另一篇文章的主題。迄今為止,筆者至少忽略了Python可以更快地處理任務這一能力。