Pandas 作為 Python 資料科學領域的核心函式庫,高效運用對於處理大量資料至關重要。然而,一些常見的錯誤用法會導致效能瓶頸。本文將探討如何避免這些陷阱,並介紹一些最佳實務,例如使用適當的資料型別、向量化運算以及 PyArrow 整合,來提升 Pandas 程式碼的執行效率和可維護性。特別是在處理字串和日期資料時,選擇正確的型別對於避免效能問題和確保資料完整性至關重要。此外,理解資料大小和記憶體管理也是最佳化 Pandas 效能的關鍵。
最佳化Pandas使用:提升效能與避免常見錯誤
在這裡,玄貓已經探討了Pandas函式庫的大部分功能,並透過例項應用來強化其正確使用。現在,你已經準備好進入實際世界,將所學應用到資料分析問題中。這一章節將提供一些提示和技巧,幫助你在獨立工作時記住它們。本章的食譜展示了Pandas使用者在各個經驗水平上常見的錯誤。儘管出於良好的意圖,但不當使用Pandas結構可能會留下大量未充分利用的效能。
當資料集較小時,這可能不會是個大問題,但資料傾向於增長而非減小。使用正確的語法並避免低效程式碼的維護負擔,可以為組織節省大量時間和金錢。
本章將涵蓋以下食譜:
- 避免使用
dtype=object - 注意資料大小
- 使用向量化函式取代迴圈
- 避免修改資料
- 使用字典編碼低基數資料
- 測試驅動開發功能
避免使用 dtype=object
在Pandas中,使用 dtype=object 來儲存字串是最容易出錯且最不高效的做法之一。很長一段時間內,dtype=object 是處理字串資料的唯一方法,直到1.0版本才得到解決。
需要注意的是,「解決」這一說法需要打引號,因為儘管Pandas 1.0引入了 pd.StringDtype() ,但很多構建和I/O方法直到3.0版本才預設使用它。因此,除非你明確告訴Pandas不同的做法,否則在2.x系列中所有字串資料都會被預設為 dtype=object 。值得一提的是,1.0版本引入的 pd.StringDtype() 幫助確保只儲存字串,但在Pandas 3.0版本之前並未針對效能進行最佳化。
如果你正在使用Pandas 3.0及更高版本,你可能仍然會遇到遺留程式碼,例如 ser = ser.astype(object) 。這類別呼叫通常應該被替換為 ser = ser.astype(pd.StringDtype()) ,除非你確實需要在 pd.Series 中儲存Python物件。不幸的是,沒有真正的方法知道這些操作的原始意圖,因此作為開發者應該瞭解使用 dtype=object 的潛在陷阱以及如何識別它是否可以適當地被替換為 pd.StringDtype() 。
如何做到
我們之前在第三章「資料型別」中已經覆寫了一些使用 dtype=object 的問題,但在這裡重申並擴充套件一些問題是有價值的。
為了進行簡單比較,讓我們建立兩個具有相同資料的 pd.Series 物件:一個使用物件資料型別(object),另一個使用 pd.StringDtype 。
import pandas as pd
ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object)
ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())
嘗試將非字串值指定給 ser_str 會失敗:
ser_str.iloc[0] = False
這會引發一個錯誤:
TypeError: Cannot set non-string value 'False' into a StringArray.
相比之下,物件型別(object)的 pd.Series 會欣然接受我們的布林值:
ser_obj.iloc[0] = False
這樣做只會使資料中的問題變得更加模糊。當我們嘗試指定非字串資料時,pd.StringDtype 的失敗點非常明顯。然而,對於物件型別(object),你可能直到後來在程式碼中嘗試某些字串操作(如大寫轉換)時才會發現問題:
ser_obj.str.capitalize().head()
這將輸出:
0 NaN
1 Bar
2 Baz
3 Foo
4 Bar
dtype: object
與其引發錯誤,「NaN」取代了第一行中的布林值「False」。很可能這不是你想要的行為,但當你使用物件型別(object)時,你會失去對資料品質的一些控制。
如果你正在使用Pandas 3.0及更高版本並且已安裝PyArrow ,那麼 pd.StringDtype 將變得顯著更快。讓我們重新建立我們的 pd.Series 物件來測量這一點:
ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object)
ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())
為了一個快速時間比較測試,我們可以使用內建於標準函式庫中的 timeit 模組:
import timeit
timeit.timeit(ser_obj.str.upper, number=1000)
這將輸出:
2.2286621460007154
然後與相同值但具有正確的 pd.StringDtype() 的執行時間進行比較:
timeit.timeit(ser_str.str.upper, number=1000)
這將輸出:
2.7227514309997787
不幸的是,3.0版本之前的使用者不會看到任何效能差異,但僅僅是資料驗證就足以值得避免使用 dtype=object 。
那麼避免使用 dtype=object 的最簡單方法是什麼?如果你幸運地正在使用Pandas 3.0及更高版本,你自然不會經常遇到這種資料型別問題,因為函式庫自然演變了。即使如此,「numpy_nullable」也會幫助某些方法更快地初始化必要引數:
例如:
import io
data = io.StringIO("int_col,string_col\n0,foo\n1,bar\n2,baz")
data.seek(0)
print(pd.read_csv(data, dtype_backend="numpy_nullable").dtypes)
內容解密:
import pandas as pd
# 建立兩個具有相同資料但不同資料型別(dtype)的 pd.Series 物件。
# 一個物件 ser_obj 的 dtype 是 object,
# 另一個物件 ser_str 的 dtype 是 pd.StringDtype()
ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object)
ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())
不同之處:
- Datatype:
- ser_obj:其 type 是 object 。主要用於存放不同型別資料。
- ser_str:其 type 是 StringDtype ,專門用於處理字串。
嘗試將非字串值賦予 ser_str:
# 嘗試將布林值 False 賦予 ser_str 的第一個位置。
# 應該報 TypeError 錯誤。
ser_str.iloc[0] = False
# 應該報 TypeError 錯誤:Cannot set non-string value 'False' into a StringArray。
原因:
- Type Error: StringDtype() 是專門為處理字串設計。
- 無法接受任何非字串型態資料指定。
嘗試將非字串值賦予 ser_obj:
# 則可以順利賦予布林值。
# 不會報錯。
ser_obj.iloc[0] = False
# 原因是 object type 的特性:
- 可同時接受不同型別資料。
- 若後續進一步處理時,
- 需要特別注意可能存在的資料問題。
##### string 轉換時可能遇到情況:
```python
# 效果展示:
- ser_obj.str.capitalize().head()
# 結果:
- 原本第1筆資料是 False (布林) 的,
- 底層自動轉換成 NaN (空)。
- 其他資料順利轉換成大寫。
原因: NaN (空) 是轉換不到大寫,
- 若後續需要進一步處理資料,
- 必需特別注意原始資料品質問題。
- 消耗更多時間與精力去檢查與修復。
時間檢測:
# 時間檢測比較語法:
timeit.timeit(函式名稱、number=次數)
# 作用:測試每次執行函式所需花費時間總和。
# 測試結果比較:
- object type 比 StringDType 快了約5成時間,
- 原因主要是底層設計特性影響。
- 不同之處主要歸結於底層設計,
- 應該選擇合適之方式來達到最佳效果。
初始化引數:
# numpy_nullable 作用:
- 用於初始化 numpy 或 pandas 函式庫時,
- 在設定 type 資料時自動指定或忽略特別指定。
-
# 特殊考量:
- 優先選擇 StringDType ,若無法則退而求其次選擇 Object 。
管理 Pandas 資料型別:最佳化與最佳實踐
在處理大型資料集時,選擇適當的資料型別對於提升 Pandas 資料框的效能至關重要。本篇文章將探討如何管理 Pandas 資料型別,並提供實際範例來說明如何最佳化資料儲存和計算效能。
資料型別概述
首先,我們來看看如何轉換 Pandas 資料框的資料型別。以下是一個簡單的範例:
import pandas as pd
import numpy as np
# 建立一個範例資料框
data = {
"int_col": [1, 2, np.nan, 4],
"string_col": ["a", "b", np.nan, "d"]
}
df = pd.DataFrame(data)
df = df.convert_dtypes(dtype_backend="numpy_nullable")
print(df.dtypes)
內容解密:
dtype_backend="numpy_nullable":這個引數告訴 Pandas 使用 NumPy 的可為空(nullable)資料型別,這樣可以支援缺失值(NaN)。df.dtypes:這個屬性會傳回每個欄位的資料型別,我們可以看到int_col是Int64型別(可為空的整數),而string_col是string[python]型別。
整數與字串資料型別的挑戰
dtype=object 經常被誤用來儲存字串,但這會帶來一些問題。例如,當處理日期時,以下範例展示瞭如何避免使用 object 資料型別:
import datetime
import pyarrow as pa
# 建立一個包含日期的 Series
ser = pd.Series([
datetime.date(2024, 1, 1),
datetime.date(2024, 1, 2),
datetime.date(2024, 1, 3)
])
print(ser)
內容解密:
datetime.date:這是 Python 標準函式庫中的日期型別,但它會被儲存為object資料型別。- 想要使用 Pandas 的
.dt屬性時,會遇到錯誤,因為 Pandas 不認識這種日期格式。
PyArrow 的優勢
PyArrow 提供了一個更原生的日期資料型別,可以避免上述問題:
ser = pd.Series([
datetime.date(2024, 1, 1),
datetime.date(2024, 1, 2),
datetime.date(2024, 1, 3)
], dtype=pd.ArrowDtype(pa.date32()))
print(ser)
內容解密:
pd.ArrowDtype(pa.date32()):這是 PyArrow 中的日期資料型別,可以支援更高效的日期操作。- 啟用後,我們可以使用
ser.dt.year獲得每個日期的年份。
最佳化資料儲存
隨著資料集的增長,選擇適當的資料型別變得越來越重要。以下是一個範例,展示如何最佳化資料儲存:
# 建立一個包含大量整數的 DataFrame
df = pd.DataFrame({
"a": [0] * 100_000,
"b": [2 ** 8] * 100_000,
"c": [2 ** 16] * 100_000,
"d": [2 ** 32] * 100_000,
})
df = df.convert_dtypes(dtype_backend="numpy_nullable")
print(df.memory_usage())
內容解密:
convert_dtypes(dtype_backend="numpy_nullable"):這樣做是為了確保所有欄位都使用可為空的資料型別。memory_usage():這個方法會傳回每個欄位所佔用的記憶體大小。
最佳化整數資料型別
如果我們知道某些欄位應該使用特定的整數資料型別,可以手動進行轉換來節省記憶體:
df = df.assign(
a=lambda x: x["a"].astype(pd.Int8Dtype()),
b=lambda x: x["b"].astype(pd.Int16Dtype()),
c=lambda x: x["c"].astype(pd.Int32Dtype())
)
print(df.memory_usage())
內容解密:
pd.Int8Dtype()、pd.Int16Dtype()和pd.Int32Dtype():這些是 Pandas 中的特定整數資料型別,分別佔用 1、2 和 4 個位元組。- 轉換後,記憶體使用顯著減少。
自動推斷最佳資料型別
Pandas 提供了一些方法來自動推斷最佳資料型別:
df = df.select_dtypes("number").assign(
**{x: pd.to_numeric(y, downcast="signed", dtype_backend="numpy_nullable") for x, y in df.items()}
)
print(df.memory_usage())
內容解密:
pd.to_numeric:這個方法會將欄位轉換為最小的整數或浮點數資料型別。downcast="signed":確保轉換後仍然是有符號整數。dtype_backend="numpy_nullable":確保支援缺失值。
輸入大量資料時,避免使用迴圈
Python 被認為是一種靈活且易於編寫迴圈的語言。然而在處理大量資料時,迴圈可能會導致效能瓶頸。以下範例展示瞭如何使用向量化操作來提升效能:
ser = pd.Series(range(100_000), dtype=pd.Int64Dtype())
print(ser.sum())
內容解密:
pd.Series.sum():這是一個向量化操作,可以快速計算序列中的總和。- 與迴圈相比,向量化操作在處理大量資料時顯著提升效能。