pandas 对某一行标准化_Python中的神器Pandas,但是有人说Pandas慢...
如果你從事大數據工作,用Python的Pandas庫時會發現很多驚喜。Pandas在數據科學和分析領域扮演越來越重要的角色,尤其是對于從Excel和VBA轉向Python的用戶。
所以,對于數據科學家,數據分析師,數據工程師,Pandas是什么呢?Pandas文檔里的對它的介紹是:“快速、靈活、和易于理解的數據結構,以此讓處理關系型數據和帶有標簽的數據時更簡單直觀。”
快速、靈活、簡單和直觀,這些都是很好的特性。當你構建復雜的數據模型時,不需要再花大量的開發時間在等待數據處理的任務上了。這樣可以將更多的精力集中去理解數據。
但是,有人說Pandas慢…
第一次使用Pandas時,有人評論說:Pandas是很棒的解析數據的工具,但是Pandas太慢了,無法用于統計建模。第一次使用的時候,確實如此,真的慢。
但是,Pandas是建立在NumPy數組結構之上的。所以它的很多操作通過NumPy或者Pandas自帶的擴展模塊編寫,這些模塊用Cython編寫并編譯到C,并且在C上執行。因此,Pandas不也應該很快的嗎?
事實上,使用姿勢正確的話,Pandas確實很快。
在使用Pandas時,使用純“python”式代碼并不是最效率的選擇。和NumPy一樣,Pandas專為向量化操作而設計,它可在一次掃描中完成對整列或者數據集的操作。而單獨處理每個單元格或某一行這種遍歷的行為,應該作為備用選擇。
本教程
先說明下,本教程不是引導如何過度優化Pandas代碼。因為Pandas在正確的使用下已經很快了。此外,優化代碼和編寫清晰的代碼之間的差異是巨大的。
這是一篇關于“如何充分利用Pandas內置的強大且易于上手的特性”的指引。此外,你將學習到一些實用的節省時間的技巧。在這篇教程中,你將學習到:
· 使用datetime時間序列數據的優勢
· 處理批量計算更效率的方法
· 利用HDFStore節省時間
在本文中,耗電量時間序列數據將被用于演示本主題。加載數據后,我們將逐步了解更有效率的方法取得最終結果。對于Pandas用戶而言,會有多種方法預處理數據。但是這不意味著所有方法都適用于更大、更復雜的數據集。
任務
本例使用能源消耗的時間序列數據計算一年能源的總成本。由于不同時間段的電價不同,因此需要將各時段的耗電量乘上對應時段的電價。
從CSV文件中可以讀取到兩列數據:日期時間和電力消耗(千瓦)
每行數據中都包含每小時耗電量數據,因此整年會產生8760(356×24)行數據。每行的小時數據表示計算的開始時間,因此1/1/13 0:00的數據指1月1號第1個小時的耗電量數據。?
用Datetime類節省時間?
首先用Pandas的一個I/O函數讀取CSV文件:
>>>?import?pandas?as?pd>>>?pd.__version__
'0.23.1'
>>>?df?=?pd.read_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:
>>>?df.dtypesdate_time??????object
energy_kwh????float64
dtype:?object
>>>?type(df.iat[0,?0])
str
默認類object不僅是str類的容器,而且不能齊整的適用于某一種數據類型。字符串str類型的日期在數據處理中是非常低效的,同時內存效率也是低下的。
為了處理時間序列數據,需要將date_time列格式化為datetime類的數組,Pandas 稱這種數據類型為時間戳Timestamp。用Pandas進行格式化相當簡單:
>>>?df['date_time']?=?pd.to_datetime(df['date_time'])>>>?df['date_time'].dtype
datetime64[ns]
至此,新的df和CSV file內容基本一樣。它有兩列和一個索引。
>>>?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
上述代碼簡單且易懂,但是有執行速度如何呢?這里我們使用了timing裝飾器,這里將裝飾器稱為@timeit。這個裝飾器模仿了Python標準庫中的timeit.repeat() 方法,但是它可以返回函數的結果,并且打印多次重復調試的平均運行時間。Python的timeit.repeat() 只返回調試時間結果,但不返回函數結果。
將裝飾器@timeit放在函數上方,每次運行函數時可以同時打印該函數的運行時間。
>>>?@timeit(repeat=3,?number=10)...?def?convert(df,?column_name):
...?????return?pd.to_datetime(df[column_name])
>>>?#?Read?in?again?so?that?we?have?`object`?dtype?to?start?
>>>?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.
看結果如何?處理8760行數據耗時1.6秒。這似乎沒啥毛病。但是當處理更大的數據集時,比如計算更高頻的電費數據,給出每分鐘的電費數據去計算一整年的總成本。數據量會比現在多60倍,這意味著你需要大約90秒去等待輸出的結果。這就有點忍不了了。
實際上,作者工作中需要分析330個站點過去10年的每小時電力數據。按上邊的方法,需要88分鐘完成時間列的格式化轉換。
有更快的方法嗎?一般來說,Pandas可以更快的轉換你的數據。在本例中,使用格式參數將csv文件中特定的時間格式傳入Pandas的to_datetime中,可以大幅的提升處理效率。
>>>?@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.032秒,速度提升了50倍!所以之前330站點的數據處理時間節省了86分鐘。
一個需要注意的細節是CSV中的時間格式不是ISO 8601格式:YYYY-mm-dd HH:MM。如果沒有指定格式,Pandas將使用dateuil包將每個字符串格式的日期格式化。相反,如果原始的時間格式已經是ISO 8601格式了,Pandas可以快速的解析日期。
遍歷
日期時間已經完成格式化,現在準備開始計算電費了。由于每個時段的電價不同,因此需要將對應的電價映射到各個時段。此例中,電價收費標準如下:
如果電價全天統一價28美分每千瓦每小時,大多數人都知道可以一行代碼實現電費的計算:
>>>?df['cost_cents']?=?df['energy_kwh']?*?28這行代碼將創建一行新列,該列包含當前時段的電費:
??????????????date_time????energy_kwh???????cost_cents0????2013-01-01?00:00:00?????????0.586???????????16.408
1????2013-01-01?01:00:00?????????0.580???????????16.240
2????2013-01-01?02:00:00?????????0.572???????????16.016
3????2013-01-01?03:00:00?????????0.596???????????16.688
4????2013-01-01?04:00:00?????????0.592???????????16.576
#?...
但是電費的計算取決于不用的時段對應的電價。這里許多人會用非Pandas式的方式:用遍歷去完成這類計算。
在本文中,將從最基礎的解決方案開始介紹,并逐步提供充分利用Pandas性能優勢的Python式解決方案。
但是對于Pandas庫來說,什么是Python式方案?這里是指相比其他友好性較差的語言如C++或者Java,它們已經習慣了“運用遍歷”去編程。
如果不熟悉Pandas,大多數人會像以前一樣使用繼續遍歷方法。這里繼續使用@timeit裝飾器來看看這種方法的效率。
首先,創建一個不同時段電價的函數:
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
如下代碼就是一種常見的遍歷模式:
>>>?#?注意:不要嘗試該函數!>>>?@timeit(repeat=3,?number=100)
...?def?apply_tariff_loop(df):
...?????"""用遍歷計算成本,將結果并入到df中"""
...?????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.
對于沒有用過Pandas的Python用戶來說,這種遍歷很正常:對于每個x,再給定條件y下,輸出z。
但是這種遍歷很笨重。可以將上述例子視為Pandas用法的“反面案例”,原因如下幾個。
首先,它需要初始化一個列表用于存儲輸出結果。
其次,它用了隱晦難懂的類range(0, len(df))去做循環,接著在應用apply_tariff()函數后,還必須將結果增加到列表中用于生成新的DataFrame列。
最后,它還使用鏈式索引df.iloc[i]['date_time'],這可能會生產出很多bugs。
這種遍歷方式最大的問題在于計算的時間成本。對于8760行數據,花了3秒鐘完成遍歷。下面,來看看一些基于Pandas數據結構的迭代方案。
用.itertuples()和.iterrow()遍歷
還有其他辦法嗎?
Pandas實際上通過引入DataFrame.itertuples()和DataFrame.iterrows()方法使得for i in range(len(df))語法冗余。這兩種都是產生一次一行的生成器方法。
.itertuples()為每行生成一個nametuple類,行的索引值作為nametuple類的第一個元素。nametuple是來自Python的collections模塊的數據結構,該結構和Python中的元組類似,但是可以通過屬性查找可訪問字段。
.iterrows()為DataFrame的每行生成一組由索引和序列組成的元組。
與.iterrows()相比,.itertuples()運行速度會更快一些。本例中使用了.iterrows()方法,因為很多讀者很可能沒有用過nametuple。
>>>?@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
...?????????#?增加成本數據到list列表
...?????????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.
取得一些不錯的進步。語法更清晰,少了行值i的引用,整體更具有可讀性了。在時間收益方面,幾乎快了5倍!
但是,仍然有很大的改進空間。由于仍然在使用for遍歷,意味著每循環一次都需要調用一次函數,而這些本可以在速度更快的Pandas內置架構中完成。
Pandas的.apply()
可以用.apply()方法替代.iterrows()方法提升效率。Pandas的.apply()方法可以傳入可調用的函數并且應用于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中處理的輸入,因此調用.apply()時仍然不夠快。
如果使用.apply()在330個站點的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日期時間索引所在的時段。然后,將這些布爾值數組傳給DataFrame的.loc索引器時,會返回一個僅包含該時段的DataFrame切片。最后,將該切片數組乘以對應的時段的費率即可。
這與之前的遍歷方法相比如何?
首先,不需要apply_tariff()函數了,因為所有的條件邏輯都被應用在了被選中的行。這大大減少了代碼的行數。在速度方面,比普通的遍歷快了315倍,比.iterrows()方法快了71倍,且比.apply()方法快了27倍。現在可以快速的處理大數據集了。
還有提升空間嗎?
在apply_tariff_isin()中,需要手動調用三次df.loc和df.index.hour.isin()。比如24小時每個小時的費率不同,這意味著需要手動調用24次.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()根據分組bins產生的區間生成對應的標簽“費率”。
【注】include_lowest參數設定第一個間隔是否包含在組bins中,例如想要在該組中包含時間在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.
至此,現在可以將330個站點的數據處理時間從88分鐘縮小到只需不到1秒。但是,還有最后一個選擇,就是使用NumPy庫來操作DataFrame下的每個NumPy數組,然后將處理結果集成回DataFrame數據結構中。
還有NumPy!
別忘了Pandas的Series和DataFrame是在NumPy庫的基礎上設計的。這提供了更多的靈活性,因為Pandas和NumPy數組可以無縫操作。
在下一例中,將演示NumPy的digitize()功能。它和Pandas的cut()功能類似,將數據分組。本例中將DataFrame中的索引(日期時間)進行分組,將三個時段分入三組。然后將分組后的電力消耗數組應用在電價數組上:
@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():將DataFrame行作為nametuple類從Python的collections模塊中進行迭代。
4. 使用.iterrows():將DataFrame行作為(index,pd.Series)元組數組進行迭代。雖然Pandas的Series是一種靈活的數據結構,但將每一行生成一個Series并且訪問它,仍然是一個比較大的開銷。
5. 對逐個元素進行循環,使用df.loc或者df.iloc對每個單元格或者行進行處理。
以下是本文中所有函數的調試時間匯總:
用HDFstore存儲預處理數據
已經了解了用Pandas快速處理數據,現在我們需要探討如何避免重復的數據處理過程。這里使用了Pandas內置的HDFStore方法。
通常在建立一些復雜的數據模型時,對數據做一些預處理是很常見的。例如,假如有10年時間跨度的分鐘級的高頻數據,但是模型只需要20分鐘頻次的數據或者其他低頻次數據。你不希望每次測試分析模型時都需要預處理數據。
一種方案是,將已經完成預處理的數據存儲在已處理數據表中,方便需要時隨時調用。但是如何以正確的格式存儲數據?如果將預處理數據另存為CSV,那么會丟失datetime類,再次讀入時必須重新轉換格式。
Pandas有個內置的解決方案,它使用HDF5,這是一種專門用于存儲數組的高性能存儲格式。Pandas的HDFstore方法可以將DataFrame存儲在HDF5文件中,可以有效讀寫,同時仍然保留DataFrame各列的數據類型和其他元數據。它是一個類似字典的類,因此可以像Python中的dict類一樣讀寫。
以下是將已經預處理的耗電量DataFrame寫入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'的DataFrame
preprocessed_df?=?data_store['preprocessed_df']
data_store.close()
一個數據倉庫可以存儲多張表,每張表配有一個鍵。?
pip?install?--upgrade?tables總結
如果覺得你的Pandas項目不具備速度快、靈活、簡單且直觀的特征,那么該重新思考使用該庫的方式了。?
① 嘗試更多的向量化操作,盡量避免類似for x in df的操作。如果代碼中本身就有許多for循環,那么盡量使用Python自帶的數據結構,因為Pandas會帶來很多開銷。
② 如果因為算法復雜無法使用向量化操作,可以嘗試.apply()方法。
③?如果必須循環遍歷數組,可用.iterrows()或者.itertuples()改進語法和提升速度。
④?Pandas有很多可選項操作,總有幾種方法可以完成從A到B的過程,比較不同方法的執行方式,選擇最適合項目的一種。
⑤?做好數據處理腳本后,可以將中間輸出的預處理數據保存在HDFStore中,避免重新處理數據。
⑥?在Pandas項目中,利用NumPy可以提高速度同時簡化語法。
總結
以上是生活随笔為你收集整理的pandas 对某一行标准化_Python中的神器Pandas,但是有人说Pandas慢...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python重复元素判定_30段极简Py
- 下一篇: java md2_GitHub - ed