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_colInt64 型別(可為空的整數),而 string_colstring[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():這是一個向量化操作,可以快速計算序列中的總和。
  • 與迴圈相比,向量化操作在處理大量資料時顯著提升效能。