Pandas 提供了多種資料型別,選擇正確的型別對於資料處理效能至關重要。類別型資料適用於有限分類別變數,透過內部整數對映有效降低記憶體佔用。時間型資料則用於處理日期與時間,納秒精確度足以應付大多數應用場景,並可透過 dt 存取器輕鬆提取時間資訊。然而,預設的時間戳是時區無知的,需使用 pd.DatetimeTZDtypetz_localizetz_convert 確保跨時區資料處理的正確性。時間差計算則可透過兩個時間序列相減獲得,單位可依需求調整。更進一步地,PyArrow 擴充套件了 Pandas 的功能,提供更精確的時間和十進位資料型別。pd.Timedelta 可用於日期時間加減運算,而 PyArrow 的 list_ 型別則能處理更複雜的資料結構,例如儲存員工及其直屬下屬的列表。最後,PyArrow 的十進位制資料型別,例如 pa.decimal128(),能有效避免浮點數誤差,確保金融資料或高精確度計算的準確性,彌補了 Pandas 原生型別系統的不足。

資料型別最佳化:類別型資料與時間型資料處理

在資料分析過程中,適當的資料型別選擇對於提升效能和降低記憶體使用至關重要。本篇將探討 pandas 中兩種重要的資料型別:類別型(Categorical)與時間型(Temporal),並介紹如何有效運用這兩種型別來最佳化資料處理。

類別型資料(Categorical Data)

類別型資料是一種特殊的資料型別,用於表示有限且固定的分類別變數。在 pandas 中,類別型資料透過 pd.CategoricalDtype 來定義。

建立類別型資料

accepted_values = ["foo", "bar"]
cat = pd.CategoricalDtype(accepted_values)
ser = pd.Series(["foo", "bar", "foo"], dtype=cat)
ser

輸出結果:

0    foo
1    bar
2    foo
dtype: category
Categories (2, object): ['foo', 'bar']

內容解密:

  1. 類別對映:pandas 內部將類別值對映為整數索引,例如 foo 對應 0bar 對應 1
  2. 記憶體最佳化:類別型資料顯著降低記憶體使用,特別是在重複值較多的情況下。
  3. 使用限制:需注意缺失值的處理,因為直接使用 pd.CategoricalDtype() 可能引入 np.nan 而非預期的 pd.NA

實際應用與記憶體比較

# 一般字串型別記憶體使用
pd.Series(["foo", "bar", "baz"] * 100, dtype=pd.StringDtype()).memory_usage()
# 2528 位元組

# 類別型別記憶體使用
cat = pd.CategoricalDtype(["foo", "bar", "baz"])
pd.Series(["foo", "bar", "baz"] * 100, dtype=cat).memory_usage()
# 552 位元組

內容解密:

  1. 類別型別的記憶體佔用遠低於字串型別。
  2. 這種最佳化在處理大量重複分類別資料時尤為重要。

時間型資料(Temporal Data)

時間型資料用於處理日期與時間相關的資料,是時間序列分析的基礎。pandas 使用 datetime64[ns] 來表示納秒精確度的時間資料。

建立時間型資料

ser = pd.Series([
    "2024-01-01 00:00:00",
    "2024-01-02 00:00:01",
    "2024-01-03 00:00:02"
], dtype="datetime64[ns]")
ser

輸出結果:

0   2024-01-01 00:00:00
1   2024-01-02 00:00:01
2   2024-01-03 00:00:02
dtype: datetime64[ns]

內容解密:

  1. 時間精確度:預設納秒精確度,能滿足大多數高精確度需求。
  2. 時間存取器:使用 dt 存取器可方便提取年、月、日等資訊。
  3. 內部儲存:即使只顯示日期,pandas 內部仍以完整時間戳儲存。

時間資訊提取範例

# 提取年份資訊
ser.dt.year
# 提取月份資訊
ser.dt.month
# 提取日期資訊
ser.dt.day

輸出結果範例(年份):

0    2024
1    2024
2    2024
dtype: int32

圖表翻譯:

以下 Mermaid 圖表展示了時間型資料的內部處理流程:

  graph LR
    A[原始字串] --> B[轉換為 datetime64]
    B --> C[內部以納秒儲存]
    C --> D[使用 dt 存取器提取資訊]
    D --> E[取得年/月/日等屬性]

圖表翻譯:

  1. 原始字串首先被轉換為 datetime64 格式。
  2. 資料內部以納秒精確度儲存。
  3. 使用 dt 存取器可以進一步提取所需時間資訊。
  4. 可獲得年、月、日等具體屬性值。

時間序列資料處理:深入理解時區與時間差

在處理時間序列資料時,時區和時間差的概念至關重要。pandas 函式庫提供了豐富的功能來處理這些挑戰,從建立時區感知(timezone-aware)的時間戳到計算時間差(timedeltas)。

時區感知時間戳

預設情況下,pandas 中的時間戳是時區無知(timezone-naive)的,這意味著它們不包含任何時區資訊。這在處理跨時區的資料時會造成混淆。為瞭解決這個問題,pandas 提供了 pd.DatetimeTZDtype 來建立時區感知的時間戳。

建立時區感知時間戳

import pandas as pd

# 建立 UTC 時區的時間戳
ser_utc = pd.Series([
    "2024-01-01 00:00:01",
    "2024-01-02 00:00:01",
    "2024-01-03 00:00:01"
], dtype=pd.DatetimeTZDtype(tz="UTC"))

print(ser_utc)

內容解密:

  1. 使用 pd.DatetimeTZDtype(tz="UTC") 指定時區為 UTC。
  2. 輸出結果中的 +00:00 表示 UTC 時區的偏移量為零。
  3. 這種方式確保了時間戳的時區資訊被正確儲存。

時區轉換

當資料的時區資訊缺失,但已知其所屬時區時,可以使用 dt.tz_localize 方法為其新增時區資訊。進一步地,可以使用 dt.tz_convert 將時間戳轉換到其他時區。

時區本地化和轉換

# 原始無時區資訊的時間序列
ser_no_tz = pd.Series([
    "2024-01-01 00:00:00",
    "2024-01-01 00:01:10",
    "2024-01-01 00:02:42"
], dtype="datetime64[ns]")

# 新增 America/New_York 時區資訊
ser_et = ser_no_tz.dt.tz_localize("America/New_York")
print(ser_et)

# 轉換到 America/Los_Angeles 時區
ser_pt = ser_et.dt.tz_convert("America/Los_Angeles")
print(ser_pt)

內容解密:

  1. dt.tz_localize("America/New_York") 為無時區資訊的時間序列增加了 America/New_York 的時區資訊。
  2. dt.tz_convert("America/Los_Angeles") 將時間序列從 America/New_York 時區轉換到了 America/Los_Angeles 時區。
  3. 輸出結果中的 -05:00-08:00 分別表示兩個時區相對於 UTC 的偏移量。

時間差(Timedeltas)

時間差用於測量兩個時間點之間的持續時間。在 pandas 中,時間差可以透過對兩個 datetime 型別的 Series 進行減法運算得到。

計算時間差

# 建立 datetime 型別的 Series
ser = pd.Series([
    "2024-01-01",
    "2024-01-02",
    "2024-01-03"
], dtype="datetime64[ns]")

# 計算與特定日期的時間差
time_diff = ser - pd.Timestamp("2023-12-31 12:00:00")
print(time_diff)

內容解密:

  1. 透過對 datetime 型別的 Series 和一個特定的 Timestamp 進行減法運算,得到一個 timedelta64[ns] 型別的 Series。
  2. 輸出結果表示每個日期與指定日期之間的時間差。
  3. 時間差的單位可以是納秒(ns)、微秒(us)、毫秒(ms)或秒(s),取決於指定的精確度。

時間相關資料型別與 PyArrow 擴充套件應用

在 pandas 中,除了常見的日期時間(datetime)處理外,還有 pd.Timedelta 這個標量可用於對日期時間進行加減運算。例如,若要對一個 pd.Series 中的每個日期時間加上 3 天,可以使用以下程式碼:

ser + pd.Timedelta("3 days")

內容解密:

  • pd.Timedelta 用於表示時間間隔,可以用在運算式中對日期時間進行加減。
  • 在範例中,ser 是一個包含日期時間的 pd.Series,透過加上 pd.Timedelta("3 days"),每個日期時間都被往後推 3 天。
  • 這種操作對於處理需要時間偏移的資料非常有用,例如計算未來日期或過去日期。

手動建立 Timedelta 物件的 Series

如果需要手動建立一個包含 timedelta 物件的 pd.Series,可以使用 dtype="timedelta64[ns]" 引數:

pd.Series([
    "-1 days",
    "6 hours",
    "42 minutes",
    "12 seconds",
    "8 milliseconds",
    "4 microseconds",
    "300 nanoseconds",
], dtype="timedelta64[ns]")

內容解密:

  • 這段程式碼展示瞭如何手動建立一個包含不同時間單位的 timedelta 物件的 Series
  • 使用 dtype="timedelta64[ns]" 保證了所有時間間隔都被轉換為奈秒(nanoseconds)單位,這使得不同單位的時間可以直接比較和運算。
  • 輸出的結果顯示了每個時間間隔在轉換後的具體數值。

無法建立月份為單位的 Timedelta

若嘗試建立一個以月份為單位的 timedelta,會遇到錯誤:

pd.Series([
    "1 months",
], dtype="timedelta64[ns]")

執行結果會出現 ValueError: invalid unit abbreviation: months,原因是月份作為時間單位並不固定(可能是 28、29、30 或 31 天),不符合 timedelta 對固定時間間隔的要求。

內容解密:

  • timedelta 需要表示一個固定的時間長度,而月份的天數不固定,因此無法直接用於 timedelta
  • 若需要進行涉及月份的時間運算,可以考慮使用 pd.DateOffset 物件,這將在後續章節中介紹。

PyArrow 的 Temporal 資料型別

Apache Arrow 專案定義了真正的 DATE 型別,從 pandas 2.0 版本開始,使用者可以透過 PyArrow 函式庫利用這些型別。

如何使用 PyArrow 建立日期型別

可以使用 pd.ArrowDtype(pa.date32()) 建立一個日期型別的 Series

ser = pd.Series([
    "2024-01-01",
    "2024-01-02",
    "2024-01-03",
], dtype=pd.ArrowDtype(pa.date32()))

內容解密:

  • 這裡使用 pa.date32() 定義了一個日期型別,並透過 pd.ArrowDtype 將其套用到 Series 中。
  • 這種型別可以表示更廣泛的日期範圍,且不需要擔心精確度切換的問題。

PyArrow 的 List 型別

在某些情況下,資料並不能簡單地放在單一的 DataFrame 中,例如公司員工及其直屬下屬的關係。這時可以使用 PyArrow 的 pa.list_() 資料型別來表達更複雜的資料結構。

如何使用 PyArrow 的 List 型別

首先,建立一個包含員工名稱及其直屬下屬的 Series

ser = pd.Series([
    ["Bob", "Michael"],
    None,
    None,
    ["Janice"],
    None,
], dtype=pd.ArrowDtype(pa.list_(pa.string())))

然後,將這個 Series 新增到原有的 DataFrame 中:

df["direct_reports"] = ser

內容解密:

  • 這段程式碼展示瞭如何使用 PyArrow 的 list_ 型別來表示複雜的資料關係,例如員工與其直屬下屬。
  • 使用 .list 存取器可以進一步操作這些列表資料,例如取得列表長度或存取特定位置的元素。

使用 .list 存取器操作 List 資料

對於具有 PyArrow list 型別的 Series,可以使用 .list.len() 方法來取得列表長度,或使用 .list[0] 語法來存取特定位置的元素:

ser.list.len()
ser.list[0]

內容解密:

  • .list.len() 用於計算每個列表中的元素數量。
  • .list[0] 用於存取每個列表中的第一個元素。
  • 這些操作對於分析和處理複雜的列表資料非常有用。

綜上所述,pandas 結合 PyArrow 可以有效地處理多種時間相關和複雜結構的資料,為資料分析和處理提供了更大的彈性和功能。

精確計算的關鍵:PyArrow 的十進位制資料型別

在處理金融資料或需要高度精確的計算時,浮點數的誤差可能會導致嚴重的後果。PyArrow 的十進位制資料型別(Decimal Data Types)提供了一種解決方案,能夠確保計算的精確性。

為什麼需要十進位制資料型別?

浮點數在電腦中是以二進製表示的,這可能導致精確度的損失。例如,簡單的加法運算可能會產生微小的誤差。在大多數情況下,這些誤差是可以接受的,但對於金融或科學計算來說,這些誤差可能是無法容忍的。

如何使用 PyArrow 的十進位制資料型別?

PyArrow 提供了 pa.decimal128()pa.decimal256() 兩種十進位制資料型別。其中,pa.decimal128() 可以支援最多 38 位有效數字,而 pa.decimal256() 則可以支援更多的有效數字。

使用範例

import pandas as pd
import pyarrow as pa

# 建立一個包含十進位制數字的 Series
data = pd.Series([
    "123456789.123456789",
    "-987654321.987654321",
    "99999999.9999999999",
], dtype=pd.ArrowDtype(pa.decimal128(19, 10)))

print(data)

輸出結果:

0    123456789.1234567890
1   -987654321.9876543210
2     99999999.9999999999
dtype: decimal128(19, 10)[pyarrow]

#### 內容解密:

  • pa.decimal128(19, 10) 表示該十進位制數字的總位數為 19 位,其中小數點後有 10 位。
  • 使用字串來表示十進位制數字可以避免浮點數誤差的問題。
  • 如果直接使用浮點數,則會產生誤差,如下所示:
data = pd.Series([
    123456789.123456789,
    -987654321.987654321,
    99999999.9999999999,
], dtype=pd.ArrowDtype(pa.decimal128(19, 10)))

print(data)

輸出結果:

0    123456789.1234567910
1   -987654321.9876543283
2    100000000.0000000000
dtype: decimal128(19, 10)[pyarrow]

#### 內容解密:

  • 直接使用浮點數會導致精確度的損失。
  • 使用 decimal 模組可以避免這個問題。

使用 decimal 模組

Python 的 decimal 模組提供了另一種方式來處理十進位制數字。

import decimal

# 建立十進位制數字
num1 = decimal.Decimal("99999999.9999999999")
num2 = decimal.Decimal("100000000.0")

# 比較兩個數字
print(num1 == num2)  # False

# 進行算術運算
result = num1 + num2
print(result)  # Decimal('199999999.9999999999')

#### 內容解密:

  • decimal.Decimal 可以精確地表示十進位制數字。
  • 使用 decimal.Decimal 可以避免浮點數誤差的問題。

NumPy 型別系統和物件型別的陷阱

在 pandas 中,預設的資料型別可能不是最優選擇。瞭解 NumPy 型別系統和物件型別的陷阱對於避免潛在問題非常重要。

使用範例

import pandas as pd

# 建立一個包含整數的 Series
data = pd.Series([0, 1, 2])
print(data.dtype)  # int64

# 建立一個包含缺失值的 Series
data = pd.Series([0, None, 2])
print(data.dtype)  # float64

#### 內容解密:

  • 當 Series 中包含缺失值時,pandas 會將資料型別轉換為浮點數。
  • 使用 dtype 引數無法直接指定整數型別,因為缺失值無法表示為整數。