首頁 > 軟體

Pandas資料處理加速技巧彙總

2022-04-18 19:00:46

Pandas 處理資料的效率還是很優秀的,相對於大規模的資料集只要掌握好正確的方法,就能讓在資料處理時間上節省很多很多的時間。

Pandas 是建立在 NumPy 陣列結構之上的,許多操作都是在 C 中執行的,要麼通過 NumPy,要麼通過 Pandas 自己的 Python 擴充套件模組庫,這些模組用 Cython 編寫並編譯為 C。理論上來說處理速度應該是很快的。

那麼為什麼同樣一份資料由2個人處理,在裝置相同的情況下處理時間會出現天差地別呢?

需要明確的是,這不是關於如何過度優化 Pandas 程式碼的指南。如果使用得當 Pandas 已經構建為可以快速執行。此外優化和編寫乾淨的程式碼之間存在很大差異。

這是以 Python 方式使用 Pandas 以充分利用其強大且易於使用的內建功能的指南。

資料準備

此範例的目標是應用分時能源關稅來計算一年的能源消耗總成本。 也就是說,在一天中的不同時間,電價會有所不同,因此任務是將每小時消耗的電量乘以消耗該小時的正確價格。

從一個包含兩列的 CSV 檔案中讀取資料,一列用於日期加時間,另一列用於以千瓦時 (kWh) 為單位消耗的電能。

日期時間資料優化

import pandas as pd
df = pd.read_csv('資料科學必備Pandas實操資料處理加速技巧彙總/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 的容器,而且是任何不能完全適合一種資料型別的列的容器。將日期作為字串處理會既費力又低效(這也會導致記憶體效率低下)。為了處理時間序列資料,需要將 date_time 列格式化為日期時間物件陣列( Timestamp)。

df['date_time'] = pd.to_datetime(df['date_time'])
df['date_time'].dtype
datetime64[ns]

現在有一個名為 df 的 DataFrame,有兩列和一個用於參照行的數位索引。

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

使用 Jupyter 自帶的 %%time 計時裝飾器進行測試。

def convert(df, column_name):
	return pd.to_datetime(df[column_name])

%%time
df['date_time'] = convert(df, 'date_time')

Wall time: 663 ms

def convert_with_format(df, column_name):
	return pd.to_datetime(df[column_name],format='%d/%m/%y %H:%M')

%%time
df['date_time'] = convert(df, 'date_time')

Wall time: 1.99 ms

處理效率提高將近350倍。如果在處理大規模資料的情況下,處理資料的時間會無限的放大。

資料的簡單迴圈

既然日期和時間格式處理完畢,就可以著手計算電費了。成本因小時而異,因此需要有條件地將成本因素應用於一天中的每個小時。

在此範例中,使用時間成本將定義成三個部分。

data_type = {
    # 高峰
    "Peak":{"Cents per kWh":28,"Time Range":"17:00 to 24:00"},
    # 正常時段
    "Shoulder":{"Cents per kWh":20,"Time Range":"7:00 to 17:00"},
    # 非高峰
    "Off-Peak":{"Cents per kWh":12,"Time Range":"0:00 to 7:00"}, 
}

如果價格是一天中每小時每千瓦時 28 美分。

df['cost_cents'] = df['energy_kwh'] * 28

               date_time    energy_kwh       cost_cents
0    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 的地方,通過編寫一個迴圈來進行條件計算。

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'無效時間: {hour}')
    return rate * kwh

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'無效時間: {hour}')
    return rate * kwh

def apply_tariff_loop(df):
    energy_cost_list = []
    for i in range(len(df)):
    	# 迴圈資料直接修改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

Wall time: 2.59 s

迴圈 .itertuples() 和 .iterrows() 方法

Pandas 實際上 for i in range(len(df)) 通過引入 DataFrame.itertuples() 和 DataFrame.iterrows() 方法使語法就可能顯得多餘,這些都是yield一次一行的生成器方法。

.itertuples() 為每一行生成一個命名元組,行的索引值作為元組的第一個元素。 名稱元組是來自 Python 集合模組的資料結構,其行為類似於 Python 元組,但具有可通過屬性查詢存取的欄位。

.iterrows() 為 DataFrame 中的每一行生成 (index, Series) 對(元組)。

def apply_tariff_iterrows(df):
    energy_cost_list = []
    for index, row in df.iterrows():
        energy_used = row['energy_kwh']
        hour = row['date_time'].hour
        energy_cost = apply_tariff(energy_used, hour)
        energy_cost_list.append(energy_cost)
    df['cost_cents'] = energy_cost_list

%%time
apply_tariff_iterrows(df)

Wall time: 808 ms

速度提高又3倍之多。

.apply() 方法

可以使用 .apply() 方法進一步改進此操作。 Pandas 的 .apply() 方法採用函數(可呼叫物件)並將它們沿 DataFrame 的軸(所有行或所有列)應用。

lambda 函數將兩列資料傳遞給 apply_tariff()。

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)

%%time
apply_tariff_withapply(df)

Wall time: 181 ms

.apply() 的語法優勢很明顯,程式碼簡潔、易讀、明確。在這種情況下所用時間大約是該 .iterrows() 方法的4分之一。

.isin() 資料選擇

但是如何在 Pandas 中將條件計算應用為向量化操作呢?一個技巧是根據的條件選擇和分組 DataFrame 的部分,然後對每個選定的組應用向量化操作。

使用 Pandas 的.isin()方法選擇行,然後在向量化操作中應用。在執行此操作之前,如果將 date_time 列設定為 DataFrame 的索引會更方便。

df.set_index('date_time', inplace=True)

def apply_tariff_isin(df):
    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

%%time
apply_tariff_isin(df)

Wall time: 53.5 ms

其中整個過程方法返回一個布林列表。

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

.cut() 資料分箱

設定時間切分的列表和對那個計算的函數公式,讓操作起來更簡單,但是這個對於新手來說程式碼閱讀起來有些困難。

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']
    
%%time
apply_tariff_cut(df)

Wall time: 2.99 ms

Numpy 方法處理

Pandas Series 和 DataFrames 是在 NumPy 庫之上設計的。這為提供了更大的計算靈活性,因為 Pandas 可以與 NumPy 陣列和操作無縫共同作業。

使用 NumPy 的digitize()函數。它與 Pandas 的相似之處cut()在於資料將被分箱,但這次它將由一個索引陣列表示,該陣列表示每個小時屬於哪個箱。然後將這些索引應用於價格陣列。

import numpy as np

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

%%time
apply_tariff_digitize(df)

Wall time: 1.99 ms

處理效率比較

對比一下上面幾種不同的處理方式的效率吧。

功能執行時間(秒)
apply_tariff_loop()2.59 s
apply_tariff_iterrows()808 ms
apply_tariff_withapply()181 ms
apply_tariff_isin()53.5 ms
apply_tariff_cut()2.99 ms
apply_tariff_digitize()1.99 ms

HDFStore 防止重新處理

通常構建複雜的資料模型時,對資料進行一些預處理會很方便。如果有 10 年的分鐘頻率用電量資料,即指定了格式引數簡單地將日期和時間轉換為日期時間也可能需要 20 分鐘。只需要這樣做一次而不是每次執行模型時都需要進行測試或分析。

可以在這裡做的一件非常有用的事情是預處理,然後以處理後的形式儲存資料,以便在需要時使用。但是如何才能以正確的格式儲存資料而無需再次重新處理呢?如果要儲存為 CSV 只會丟失您的日期時間物件,並且在再次存取時必須重新處理它。

Pandas 有一個內建的解決方案使用 HDF5,一種專為儲存表格資料陣列而設計的高效能儲存格式。Pandas 的 HDFStore 類允許將 DataFrame 儲存在 HDF5 檔案中,以便可以有效地存取它,同時仍保留列型別和其他後設資料。dict 是一個類似字典的類,因此可以像對 Python物件一樣進行讀寫。

將預處理的耗電量 DataFrame 儲存df在 HDF5 檔案中。

data_store = pd.HDFStore('processed_data.h5')

# 將 DataFrame 放入物件中,將鍵設定為 preprocessed_df 
data_store['preprocessed_df'] = df
data_store.close()

從 HDF5 檔案存取資料的方法,並保留資料型別。

data_store = pd.HDFStore('processed_data.h5')

preprocessed_df = data_store['preprocessed_df']
data_store.close()

到此這篇關於Pandas資料處理加速技巧彙總的文章就介紹到這了,更多相關Pandas資料處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com