在資料科學和金融領域,資料精確度至關重要。PyArrow 提供的十進位制型別有效解決了浮點數固有的精確度限制,確保金融計算等高精確度場景的可靠性。文章將示範如何使用 pa.decimal128() 和 pa.decimal256(),並以 Python 的 decimal 模組進行精確的十進位制運算。此外,管理 pandas 的資料型別也是資料處理的關鍵環節。使用 object 型別雖然靈活,但缺乏型別約束,容易造成效能問題和資料錯誤。文章將重點說明如何利用 pd.BooleanDtype 和 pd.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 檔案時會自動為包含逗號的欄位加上參照符號,以避免與分隔符號混淆。