做者:xiaoyupython
微信公衆號:Python數據科學git
知乎:python數據分析師編程
當你們談到數據分析時,說起最多的語言就是Python
和SQL
。Python之因此適合數據分析,是由於它有不少第三方強大的庫來協助,pandas
就是其中之一。pandas
的文檔中是這樣描述的:數組
「快速,靈活,富有表現力的數據結構,旨在使」關係「或」標記「數據的使用既簡單又直觀。」bash
咱們知道pandas
的兩個主要數據結構:dataframe
和series
,咱們對數據的一些操做都是基於這兩個數據結構的。但在實際的使用中,咱們可能不少時候會感受運行一些數據結構的操做會異常的慢。一個操做慢幾秒可能看不出來什麼,可是一整個項目中不少個操做加起來會讓整個開發工做效率變得很低。有的朋友抱怨pandas簡直太慢了,其實對於pandas的一些操做也是有必定技巧的。微信
pandas是基於numpy庫
的數組結構上構建的,而且它的不少操做都是(經過numpy或者pandas自身由Cpython實現並編譯成C的擴展模塊)在C語言中實現的。所以,若是正確使用pandas的話,它的運行速度應該是很是快的。數據結構
本篇將要介紹幾種pandas中經常使用到的方法,對於這些方法使用存在哪些須要注意的問題,以及如何對它們進行速度提高。架構
咱們來看一個例子。app
>>> 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類型,以下面代碼所示:scrapy
>>> 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能夠提高這麼多。
仍然基於上面的數據,咱們想添加一個新的特徵,但這個新的特徵是基於一些時間條件的,根據時長(小時)而變化,以下:
所以,按照咱們正常的作法就是使用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']
執行所謂的鏈式索引,這一般會致使意外的結果。那麼推薦作法是什麼樣的呢?
實際上能夠經過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內部架構中內置的更快的語言完成。
咱們可使用.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分鐘的處理時間。若是這個計算只是大型模型的一小部分,那麼你真的應該加快速度。這也就是矢量化操做派上用場的地方。
什麼是矢量化操做?若是你不基於一些條件,而是能夠在一行代碼中將全部電力消耗數據應用於該價格(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數據結構中。
使用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,它能夠幫助維持「層次結構」,若是你願意,能夠像在此處同樣進行批量計算,這些一般排名從最快到最慢(最靈活到最不靈活):
使用向量化操做:沒有for循環的Pandas方法和函數。
將.apply方法:與可調用方法一塊兒使用。
使用.itertuples:從Python的集合模塊迭代DataFrame行做爲namedTuples。
使用.iterrows:迭代DataFrame行做爲(index,Series)對。雖然Pandas系列是一種靈活的數據結構,但將每一行構建到一個系列中而後訪問它可能會很昂貴。
使用「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中的大型數據集時應用這些經驗法則:
若是以爲有幫助,還請給點個贊!
歡迎關注個人我的公衆號:Python數據科學