摘要:它還使用執行所謂的鏈式索引,這通常會導致意外的結果。但這種方法的最大問題是計算的時間成本。這些都是一次產生一行的生成器方法,類似中使用的用法。在這種情況下,所花費的時間大約是方法的一半。根據每小時所屬的應用一組標簽。
作者:xiaoyu
微信公眾號:Python數據科學
知乎:python數據分析師
當大家談到數據分析時,提及最多的語言就是Python和SQL。Python之所以適合數據分析,是因為它有很多第三方強大的庫來協助,pandas就是其中之一。pandas的文檔中是這樣描述的:
“快速,靈活,富有表現力的數據結構,旨在使”關系“或”標記“數據的使用既簡單又直觀。”
我們知道pandas的兩個主要數據結構:dataframe和series,我們對數據的一些操作都是基于這兩個數據結構的。但在實際的使用中,我們可能很多時候會感覺運行一些數據結構的操作會異常的慢。一個操作慢幾秒可能看不出來什么,但是一整個項目中很多個操作加起來會讓整個開發工作效率變得很低。有的朋友抱怨pandas簡直太慢了,其實對于pandas的一些操作也是有一定技巧的。
pandas是基于numpy庫的數組結構上構建的,并且它的很多操作都是(通過numpy或者pandas自身由Cpython實現并編譯成C的擴展模塊)在C語言中實現的。因此,如果正確使用pandas的話,它的運行速度應該是非常快的。
本篇將要介紹幾種pandas中常用到的方法,對于這些方法使用存在哪些需要注意的問題,以及如何對它們進行速度提升。
將datetime數據與時間序列一起使用的優點
進行批量計算的最有效途徑
通過HDFStore存儲數據節省時間
使用Datetime數據節省時間我們來看一個例子。
>>> import pandas as pd >>> pd.__version__ "0.23.1" # 導入數據集 >>> 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
從運行上面代碼得到的結果來看,好像沒有什么問題。但實際上pandas和numpy都有一個dtypes 的概念。如果沒有特殊聲明,那么date_time將會使用一個 object 的dtype類型,如下面代碼所示:
>>> df.dtypes date_time object energy_kwh float64 dtype: object >>> type(df.iat[0, 0]) str
object 類型像一個大的容器,不僅僅可以承載 str,也可以包含那些不能很好地融進一個數據類型的任何特征列。而如果我們將日期作為 str 類型就會極大的影響效率。
因此,對于時間序列的數據而言,我們需要讓上面的date_time列格式化為datetime對象數組(pandas稱之為時間戳)。pandas在這里操作非常簡單,操作如下:
>>> df["date_time"] = pd.to_datetime(df["date_time"]) >>> df["date_time"].dtype datetime64[ns]
我們來運行一下這個df看看轉化后的效果是什么樣的。
>>> df.head() date_time energy_kwh 0 2013-01-01 00:00:00 0.586 1 2013-01-01 01:00:00 0.580 2 2013-01-01 02:00:00 0.572 3 2013-01-01 03:00:00 0.596 4 2013-01-01 04:00:00 0.592
date_time的格式已經自動轉化了,但這還沒完,在這個基礎上,我們還是可以繼續提高運行速度的。如何提速呢?為了更好的對比,我們首先通過 timeit 裝飾器來測試一下上面代碼的轉化時間。
>>> @timeit(repeat=3, number=10) ... def convert(df, column_name): ... return pd.to_datetime(df[column_name]) >>> df["date_time"] = convert(df, "date_time") Best of 3 trials with 10 function calls per trial: Function `convert` ran in average of 1.610 seconds.
1.61s,看上去挺快,但其實可以更快,我們來看一下下面的方法。
>>> @timeit(repeat=3, number=100) >>> def convert_with_format(df, column_name): ... return pd.to_datetime(df[column_name], ... 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.032s,快了將近50倍。原因是:我們設置了轉化的格式format。由于在CSV中的datetimes并不是 ISO 8601 格式的,如果不進行設置的話,那么pandas將使用 dateutil 包把每個字符串str轉化成date日期。
相反,如果原始數據datetime已經是 ISO 8601 格式了,那么pandas就可以立即使用最快速的方法來解析日期。這也就是為什么提前設置好格式format可以提升這么多。
pandas數據的循環操作仍然基于上面的數據,我們想添加一個新的特征,但這個新的特征是基于一些時間條件的,根據時長(小時)而變化,如下:
因此,按照我們正常的做法就是使用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): ... """Calculate costs in loop. Modifies `df` inplace.""" ... 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秒鐘。接下來,你將看到一些改進的Pandas結構迭代解決方案。
使用itertuples() 和iterrows() 循環那么推薦做法是什么樣的呢?
實際上可以通過pandas引入itertuples和iterrows方法可以使效率更快。這些都是一次產生一行的生成器方法,類似scrapy中使用的yield用法。
.itertuples為每一行產生一個namedtuple,并且行的索引值作為元組的第一個元素。nametuple是Python的collections模塊中的一種數據結構,其行為類似于Python元組,但具有可通過屬性查找訪問的字段。
.iterrows為DataFrame中的每一行產生(index,series)這樣的元組。
雖然.itertuples往往會更快一些,但是在這個例子中使用.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.
語法方面:這樣的語法更明確,并且行值引用中的混亂更少,因此它更具可讀性。
在時間收益方面:快了近5倍! 但是,還有更多的改進空間。我們仍然在使用某種形式的Python for循環,這意味著每個函數調用都是在Python中完成的,理想情況是它可以用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的索引,則會使事情更方便:
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)) # 使用上面的定義 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(),因為所有條件邏輯都應用于行的選擇。因此,你必須編寫的代碼行和調用的Python代碼會大大減少。
處理時間怎么樣?比不是Pythonic的循環快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應用一組標簽(costs)。
注意include_lowest參數表示第一個間隔是否應該是包含左邊的(您希望在組中包含時間= 0)。
這是一種完全矢量化的方式來獲得我們的預期結果,它在時間方面是最快的:
>>> 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.
到目前為止,時間上基本快達到極限了,只需要花費不到一秒的時間來處理完整的10年的小時數據集。但是,最后一個選項是使用 NumPy 函數來操作每個DataFrame的底層NumPy數組,然后將結果集成回Pandas數據結構中。
使用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
與cut函數一樣,這種語法非常簡潔易讀。但它在速度方面有何比較?讓我們來看看:
>>> 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.
在這一點上,仍然有性能提升,但它本質上變得更加邊緣化。使用Pandas,它可以幫助維持“層次結構”,如果你愿意,可以像在此處一樣進行批量計算,這些通常排名從最快到最慢(最靈活到最不靈活):
1. 使用向量化操作:沒有for循環的Pandas方法和函數。
2. 將.apply方法:與可調用方法一起使用。
3. 使用.itertuples:從Python的集合模塊迭代DataFrame行作為namedTuples。
4. 使用.iterrows:迭代DataFrame行作為(index,Series)對。雖然Pandas系列是一種靈活的數據結構,但將每一行構建到一個系列中然后訪問它可能會很昂貴。
5. 使用“element-by-element”循環:使用df.loc或df.iloc一次更新一個單元格或行。
現在你已經了解了Pandas中的加速數據流程,接著讓我們探討如何避免與最近集成到Pandas中的HDFStore一起重新處理時間。
通常,在構建復雜數據模型時,可以方便地對數據進行一些預處理。例如,如果您有10年的分鐘頻率耗電量數據,即使你指定格式參數,只需將日期和時間轉換為日期時間可能需要20分鐘。你真的只想做一次,而不是每次運行你的模型,進行測試或分析。
你可以在此處執行的一項非常有用的操作是預處理,然后將數據存儲在已處理的表單中,以便在需要時使用。但是,如何以正確的格式存儲數據而無需再次重新處理?如果你要另存為CSV,則只會丟失datetimes對象,并且在再次訪問時必須重新處理它。
Pandas有一個內置的解決方案,它使用 HDF5,這是一種專門用于存儲表格數據陣列的高性能存儲格式。 Pandas的 HDFStore 類允許你將DataFrame存儲在HDF5文件中,以便可以有效地訪問它,同時仍保留列類型和其他元數據。它是一個類似字典的類,因此您可以像讀取Python dict對象一樣進行讀寫。
以下是將預處理電力消耗DataFrame df存儲在HDF5文件中的方法:
# 創建儲存對象,并存為 processed_data data_store = pd.HDFStore("processed_data.h5") # 將 DataFrame 放進對象中,并設置 key 為 preprocessed_df data_store["preprocessed_df"] = df data_store.close()
現在,你可以關閉計算機并休息一下。等你回來的時候,你處理的數據將在你需要時為你所用,而無需再次加工。以下是如何從HDF5文件訪問數據,并保留數據類型:
# 獲取數據儲存對象 data_store = pd.HDFStore("processed_data.h5") # 通過key獲取數據 preprocessed_df = data_store["preprocessed_df"] data_store.close()
數據存儲可以容納多個表,每個表的名稱作為鍵。
關于在Pandas中使用HDFStore的注意事項:您需要安裝PyTables> = 3.0.0,因此在安裝Pandas之后,請確保更新PyTables,如下所示:
pip install --upgrade tables結論
如果你覺得你的Pandas項目不夠快速,靈活,簡單和直觀,請考慮重新考慮你使用該庫的方式。
這里探討的示例相當簡單,但說明了Pandas功能的正確應用如何能夠大大改進運行時和速度的代碼可讀性。以下是一些經驗,可以在下次使用Pandas中的大型數據集時應用這些經驗法則:
嘗試盡可能使用矢量化操作,而不是在df 中解決for x的問題。如果你的代碼是許多for循環,那么它可能更適合使用本機Python數據結構,因為Pandas會帶來很多開銷。
如果你有更復雜的操作,其中矢量化根本不可能或太難以有效地解決,請使用.apply方法。
如果必須循環遍歷數組(確實發生了這種情況),請使用.iterrows()或.itertuples()來提高速度和語法。
Pandas有很多可選性,幾乎總有幾種方法可以從A到B。請注意這一點,比較不同方法的執行方式,并選擇在項目環境中效果最佳的路線。
一旦建立了數據清理腳本,就可以通過使用HDFStore存儲中間結果來避免重新處理。
將NumPy集成到Pandas操作中通常可以提高速度并簡化語法。
參考:https://realpython.com/fast-f...
如果覺得有幫助,還請給點個贊!
歡迎關注我的個人公眾號:Python數據科學
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/43165.html
摘要:入門,第一個這是一門很新的語言,年前后正式公布,算起來是比較年輕的編程語言了,更重要的是它是面向程序員的函數式編程語言,它的代碼運行在之上。它通過編輯類工具,帶來了先進的編輯體驗,增強了語言服務。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不覺已經到來了,總結過去的 2017,相信小伙們一定有很多收獲...
摘要:入門,第一個這是一門很新的語言,年前后正式公布,算起來是比較年輕的編程語言了,更重要的是它是面向程序員的函數式編程語言,它的代碼運行在之上。它通過編輯類工具,帶來了先進的編輯體驗,增強了語言服務。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不覺已經到來了,總結過去的 2017,相信小伙們一定有很多收獲...
摘要:入門,第一個這是一門很新的語言,年前后正式公布,算起來是比較年輕的編程語言了,更重要的是它是面向程序員的函數式編程語言,它的代碼運行在之上。它通過編輯類工具,帶來了先進的編輯體驗,增強了語言服務。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不覺已經到來了,總結過去的 2017,相信小伙們一定有很多收獲...
摘要:下面讓我們開始提速假設我們現在的電價是定值,不根據用電時間段來改變,那么中最快的方法那就是采用,這就是一個簡單的矢量化操作示范。它基本是在中運行最快的方式。 Pandas 加速 大家好,今天我們來看有關pandas加速的小技巧,不知道大家在剛剛接觸pandas的時候有沒有聽過如下的說法 pandas太慢了,運行要等半天 其實我想說的是,慢不是pandas的錯,大家要知道pandas本身...
摘要:概述我非常認同前百度數據工程師現神策分析創始人桑老師最近談到的數據分析三重境界統計計數多維分析機器學習數據分析的統計計數和多維分析,我們通常稱之為數據探索式分析,這個步驟旨在了解數據的特性,有助于我們進一步挖掘數據的價值。 showImg(https://camo.githubusercontent.com/f98421e503a81176b003ddd310d97e1e1214625...
閱讀 2688·2023-04-25 17:21
閱讀 2550·2021-11-23 09:51
閱讀 2836·2021-09-24 10:32
閱讀 3768·2021-09-23 11:33
閱讀 1973·2019-08-30 15:44
閱讀 3452·2019-08-30 11:18
閱讀 3518·2019-08-30 10:53
閱讀 622·2019-08-26 13:25