返回文章列表

PyArrow 十進位制與Pandas資料型別最佳化

本文探討 PyArrow 的十進位制資料型別如何解決浮點數精確度問題,並深入剖析 pandas 的資料型別管理,特別是布林值和字串型別的處理,以及如何使用 `pd.BooleanDtype` 和 `pd.StringDtype` 維持資料一致性。同時,文章也詳細介紹了 pandas 的 I/O 系統,涵蓋 CSV

資料科學 Python

在資料科學和金融領域,資料精確度至關重要。PyArrow 提供的十進位制型別有效解決了浮點數固有的精確度限制,確保金融計算等高精確度場景的可靠性。文章將示範如何使用 pa.decimal128()pa.decimal256(),並以 Python 的 decimal 模組進行精確的十進位制運算。此外,管理 pandas 的資料型別也是資料處理的關鍵環節。使用 object 型別雖然靈活,但缺乏型別約束,容易造成效能問題和資料錯誤。文章將重點說明如何利用 pd.BooleanDtypepd.StringDtype 確保布林值和字串型別的正確性,並避免資料汙染。最後,文章探討 pandas 的 I/O 系統,特別是 CSV 檔案的讀寫操作。從基礎的讀寫到索引處理、參照和分隔符號的最佳實踐,文章提供了全面的,並簡要介紹了其他常見資料格式的處理方法,例如 Excel、SQL、Parquet 和 JSON。

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

在進行財務分析或需要高精確度的計算時,浮點數的誤差可能會造成嚴重的後果。PyArrow 提供的十進位制資料型別(decimal data types)能夠有效解決這個問題,確保計算結果的精確性。

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

浮點數在電腦中是以二進製表示的,這導致了精確度的問題。例如,簡單的十進位制小數,如 0.1,在二進位中無法精確表示,因此會產生誤差。在大多數情況下,這些誤差是可以接受的,但對於金融計算或需要高精確度的應用來說,這些誤差是無法容忍的。

十進位制資料型別的優勢

PyArrow 的 pa.decimal128()pa.decimal256() 資料型別提供了高精確度的十進位制計算。這些資料型別允許開發者指定數字的精確度和小數位數,從而確保計算結果的準確性。

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

使用 pa.decimal128()pa.decimal256() 建立 pandas Series 時,需要指定精確度和小數位數。例如:

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 位。
  • 資料必須以字串形式提供,以避免浮點數轉換帶來的誤差。
  • 輸出結果保持了原始資料的精確度。

使用十進位制物件進行計算

Python 的 decimal 模組提供了 Decimal 物件,可以用於建立十進位制數字。這些物件可以直接用於建立 PyArrow 的十進位制 Series:

import decimal

# 使用 decimal.Decimal 物件建立 Series
data = pd.Series([
    decimal.Decimal("123456789.123456789"),
    decimal.Decimal("-987654321.987654321"),
    decimal.Decimal("99999999.9999999999"),
], dtype=pd.ArrowDtype(pa.decimal128(19, 10)))

print(data)

輸出結果與前例相同。

內容解密:

  • 使用 decimal.Decimal 物件可以避免浮點數轉換的誤差。
  • Decimal 物件直接傳遞給 pandas Series 建構函式,可以正確建立十進位制 Series。

更高的精確度:pa.decimal256()

當需要更高的精確度時,可以使用 pa.decimal256() 資料型別。例如:

# 使用 pa.decimal256() 建立 Series
data = pd.Series([
    "123456789123456789123456789123456789.123456789"
], dtype=pd.ArrowDtype(pa.decimal256(76, 10)))

print(data)

輸出結果:

0    123456789123456789123456789123456789.1234567890
dtype: decimal256(76, 10)[pyarrow]

內容解密:

  • pa.decimal256() 提供了比 pa.decimal128() 更高的精確度,最高可達 76 位有效數字。
  • 使用更高的精確度會消耗更多的記憶體,並可能導致計算速度變慢。

資料型別的困擾與解決方案

在 pandas 中,資料型別的管理一直是個令人頭痛的問題。讓我們從最基本的布林值(Boolean)資料型別開始討論。

布林值資料型別的問題

當我們建立一個包含布林值的 Series 時,看起來一切正常:

pd.Series([True, False])

輸出結果為:

0     True
1    False
dtype: bool

然而,當我們加入一個缺失值(None)時,事情就變得複雜了:

pd.Series([True, False, None])

輸出結果變為:

0     True
1    False
2     None
dtype: object

這裡出現了 object 資料型別,這是 pandas 中最不受歡迎的資料型別之一。因為 object 型別幾乎可以容納任何值,這使得資料的型別系統無法對資料進行有效的約束。

程式碼範例與解密

pd.Series([True, False, None, "one of these things", ["is not like"], ["the other"]])

輸出結果:

0              True
1             False
2              None
3    one of these things
4        [is not like]
5         [the other]
dtype: object

#### 內容解密: 此範例展示了 object 資料型別的靈活性與危險性。程式碼嘗試將不同型別的值存入 Series 中,結果全部被轉換為 object 型別。這種做法雖然方便,但卻犧牲了資料的嚴謹性和效能。

使用 pd.BooleanDtype 解決問題

幸好,pandas 提供了 pd.BooleanDtype 來解決這個問題:

pd.Series([True, False, None], dtype=pd.BooleanDtype())

輸出結果:

0     True
1    False
2     <NA>
dtype: boolean

這樣,我們就能正確地處理布林值,並將缺失值表示為 <NA>

程式碼範例與解密

ser = pd.Series(["foo", "bar", "baz"], dtype=pd.StringDtype())
ser.iloc[2] = 42

輸出結果會引發錯誤:

TypeError: Cannot set non-string value '42' into a StringArray.

#### 內容解密: 此範例展示了使用 pd.StringDtype 的好處。當我們嘗試將非字串值賦給一個字串 Series 時,pandas 會丟擲錯誤,確保資料的型別安全性。這種機制有效地避免了資料汙染和型別混亂。

資料輸入輸出的重要性

在實際應用中,我們很少直接在程式碼中建立 DataFrame 或 Series。相反,我們通常會使用 pandas 的 I/O 函式來讀取和寫入各種資料格式,如 CSV、Excel、JSON 等。

常見的資料格式與處理方法

  • CSV:基本的讀取和寫入
  • Microsoft Excel:讀取和寫入 Excel 檔案
  • SQL:使用 SQLAlchemy 或 ADBC 與 SQL 資料函式庫互動
  • Apache Parquet:高效的列式儲存格式
  • JSON:處理半結構化資料

pandas I/O 系統詳解:CSV 檔案的讀寫操作

CSV(逗號分隔值)是一種常見的資料交換格式。雖然沒有官方標準定義 CSV 檔案的具體內容,但大多數開發者和使用者會將其視為純文字檔案,每行代表一筆資料,而每筆資料內部則使用特定的分隔符號來區分不同的欄位。最常用的分隔符號是逗號,但這並不是硬性規定;常見的替代分隔符號包括豎線(|)、波浪符號(~)或反引號(`)。

CSV 檔案的讀寫挑戰

CSV 格式簡單易寫,但這也使得讀取 CSV 檔案變得更加困難。CSV 格式不提供任何中繼資料(如分隔符號、參照規則等),也不提供有關資料型別的任何資訊(例如第 X 欄應該是什麼型別的資料)。這使得 CSV 讀取器必須自行推斷這些資訊,不僅增加了效能開銷,也容易導致資料誤解。作為一種根據文字的格式,CSV 在儲存資料方面也比二進位格式(如 Apache Parquet)更為低效。

使用 pandas 讀寫 CSV 檔案

首先,我們建立一個簡單的 pd.DataFrame,並使用 convert_dtypes 方法來最佳化資料型別:

df = pd.DataFrame([
    ["Paul", "McCartney", 1942],
    ["John", "Lennon", 1940],
    ["Richard", "Starkey", 1940],
    ["George", "Harrison", 1943],
], columns=["first", "last", "birth"])
df = df.convert_dtypes(dtype_backend="numpy_nullable")
print(df)

輸出結果:

     first        last  birth
0      Paul  McCartney   1942
1      John     Lennon   1940
2   Richard    Starkey   1940
3    George   Harrison   1943

將 DataFrame 輸出為 CSV 檔案

使用 to_csv 方法將 pd.DataFrame 輸出為 CSV 檔案。在這個範例中,我們使用 io.StringIO 物件來模擬檔案操作,而不實際寫入磁碟:

import io
buf = io.StringIO()
df.to_csv(buf)
print(buf.getvalue())

輸出結果:

,first,last,birth
0,Paul,McCartney,1942
1,John,Lennon,1940
2,Richard,Starkey,1940
3,George,Harrison,1943

從 CSV 檔案讀取資料

使用 pd.read_csv 函式從 CSV 檔案中讀取資料。為了避免預設資料型別的問題,我們同樣使用 dtype_backend="numpy_nullable" 引數:

buf.seek(0)
pd.read_csv(buf, dtype_backend="numpy_nullable")

輸出結果:

   Unnamed: 0   first        last  birth
0           0    Paul  McCartney   1942
1           1    John     Lennon   1940
2           2  Richard    Starkey   1940
3           3   George   Harrison   1943

處理索引欄位

預設情況下,pd.read_csv 會將所有欄位視為普通欄位,而不會自動將第一欄識別為索引欄位。我們可以透過 index_col=0 引數來指定第一欄為索引欄位:

buf.seek(0)
pd.read_csv(buf, dtype_backend="numpy_nullable", index_col=0)

輸出結果:

         first        last  birth
0        Paul  McCartney   1942
1        John     Lennon   1940
2     Richard    Starkey   1940
3      George   Harrison   1943

或者,我們可以在寫入 CSV 檔案時使用 index=False 引數來避免寫入索引欄位:

buf = io.StringIO()
df.to_csv(buf, index=False)
print(buf.getvalue())

輸出結果:

first,last,birth
Paul,McCartney,1942
John,Lennon,1940
Richard,Starkey,1940
George,Harrison,1943

處理參照和分隔符號

當 CSV 檔案中的欄位內包含分隔符號時,需要使用參照符號來避免混淆。pandas 在預設情況下能夠合理處理這種情況:

df = pd.DataFrame([
    ["McCartney, Paul", 1942],
    ["Lennon, John", 1940],
    ["Starkey, Richard", 1940],
    ["Harrison, George", 1943],
], columns=["name", "birth"])
df = df.convert_dtypes(dtype_backend="numpy_nullable")
print(df)

輸出結果:

              name  birth
0   McCartney, Paul   1942
1      Lennon, John   1940
2    Starkey, Richard   1940
3   Harrison, George   1943

詳細解說

  • 在這個範例中,我們建立了一個包含逗號的姓名欄位。
  • pandas 在寫入 CSV 檔案時會自動為包含逗號的欄位加上參照符號,以避免與分隔符號混淆。