返回文章列表

Pandas高效能資料型別與CSV檔案處理

本文探討 Pandas 中資料型別管理的最佳實務,特別針對布林值和字串型別容易遇到的 object dtype 問題,提供使用 `pd.BooleanDtype` 和 `pd.StringDtype` 的解決方案,並示範如何利用 `astype` 和 `convert_dtypes` 方法最佳化 DataFrame

資料科學 Python

Pandas 是資料科學領域不可或缺的工具,但資料型別管理不當會導致效能瓶頸。尤其在處理布林值和字串時,預設的 object dtype 容易造成資料混亂和效能下降。為此,建議使用 pd.BooleanDtypepd.StringDtype 確保資料型別一致性,並搭配 astypeconvert_dtypes 方法最佳化 DataFrame 的資料型別,有效提升資料處理效能。此外,CSV 檔案的讀寫效率也至關重要。Pandas 提供了 to_csvread_csv 方法,可透過指定分隔符號、處理參照以及搭配 gzip 壓縮等技巧,有效管理大型 CSV 檔案,降低記憶體使用並提升 I/O 效能。實務上,建議先預覽資料並分析其結構與型別,再根據需求調整資料型別,以達到最佳效能。

資料型別的挑戰與解決方案

在 pandas 中,資料型別的管理是資料處理的基礎。然而,歷史上的布林資料型別卻帶來了一些令人困惑的問題。讓我們從一個簡單的例子開始:

pd.Series([True, False])

輸出結果為:

0     True
1    False
dtype: bool

看起來一切正常,但當我們引入缺失值時,情況就變得複雜了:

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

輸出結果為:

0     True
1    False
2     None
dtype: object

這裡出現了 object 資料型別,這是 pandas 中最不理想的資料型別之一。因為它允許任何有效的數值被儲存,從而喪失了資料型別的強制性。

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

這種混亂的情況可以透過使用 pd.BooleanDtype 來避免:

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

輸出結果為:

0     True
1    False
2      <NA>
dtype: boolean

內容解密:

  1. 使用 pd.BooleanDtype 的好處:明確指定布林資料型別,避免資料型別被降級為 object
  2. 缺失值的處理:在 pd.BooleanDtype 中,缺失值被表示為 <NA>,保持了資料的一致性。
  3. 資料型別的重要性:正確的資料型別能夠提高資料處理的效率和準確性。

另一個值得注意的問題是,預設情況下,pandas 將字串資料型別視為 object

pd.Series(["foo", "bar", "baz"])

輸出結果為:

0    foo
1    bar
2    baz
dtype: object

這同樣會導致資料型別的不嚴謹性。使用 pd.StringDtype() 可以解決這個問題:

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

輸出結果會報錯:

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

內容解密:

  1. pd.StringDtype() 的作用:強制執行字串資料型別,確保資料的一致性和準確性。
  2. 錯誤處理:嘗試將非字串值賦給字串型別的 Series 會引發 TypeError,從而避免了資料汙染。

資料型別管理的最佳實踐

在實際應用中,我們應該盡量避免使用 object 資料型別,因為它會導致效能下降和程式碼維護困難。對於 DataFrame,我們可以使用 astype 方法或 convert_dtypes 方法來轉換資料型別:

df = pd.DataFrame([
    ["foo", 1, 123.45],
    ["bar", 2, 333.33],
    ["baz", 3, 999.99],
], columns=list("abc"))

df.astype({
    "a": pd.StringDtype(),
    "b": pd.Int64Dtype(),
    "c": pd.Float64Dtype(),
})

或者:

df.convert_dtypes(dtype_backend="numpy_nullable")

內容解密:

  1. astype 方法的使用:明確指定每列的資料型別,提高資料處理的效率和準確性。
  2. convert_dtypes 方法的使用:自動轉換 DataFrame 的資料型別,簡化程式碼並提高可讀性。

pandas I/O 系統詳解:CSV 檔案讀寫實務

CSV(逗號分隔值)是一種常見的資料交換格式,儘管缺乏官方標準定義,但大多數開發者和使用者仍將其視為純文字檔案,每行代表一筆資料記錄,各欄位間以特定分隔符號區隔。最常用的分隔符號是逗號,但也常見使用豎線(|)、波浪符號(~)或反引號(`)等其他字元。

CSV 格式的挑戰

CSV 格式簡單易寫,但也因此導致讀取時的困難。CSV 檔案不包含任何後設資料(如分隔符號、參照規則等),也未提供資料型別的相關資訊。這使得 CSV 讀取器必須自行推斷資料結構,不僅增加效能開銷,也容易導致資料誤判。此外,作為純文字格式,CSV 在儲存效率上遠不如 Apache Parquet 等二進位格式。

使用 pandas 讀寫 CSV 檔案

首先,建立一個簡單的 DataFrame 物件,並使用 convert_dtypes 方法最佳化資料型別:

import pandas as pd

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 方法將 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 檔案讀取資料

使用 read_csv 方法讀取 CSV 資料,並指定 dtype_backend="numpy_nullable" 以最佳化資料型別:

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

輸出結果:

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

注意到預設情況下,read_csv 會將原始 DataFrame 的索引視為普通欄位讀入。若要還原始索引,可使用 index_col=0 引數指定第一欄為索引。

處理參照和分隔符號

當資料欄位中包含分隔符號時,pandas 能夠正確處理參照規則。以下範例展示包含逗號的姓名資料:

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

df_quoted = df_quoted.convert_dtypes(dtype_backend="numpy_nullable")
print(df_quoted)

輸出結果:

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

使用 to_csv 方法匯出時,pandas 自動處理參照規則,確保資料正確性。

內容解密:

  1. to_csv 方法用於將 DataFrame 資料匯出為 CSV 格式,可透過 index=False 引數避免匯出索引欄位。
  2. read_csv 方法用於讀取 CSV 資料,並可透過 dtype_backend="numpy_nullable" 引數最佳化資料型別。
  3. index_col=0 引數用於指定 CSV 第一欄為索引欄位,以還原始 DataFrame 結構。
  4. 參照規則確保資料中包含分隔符號時仍能正確解析。

使用pandas高效處理CSV檔案

在資料分析的過程中,CSV檔案是一種常見的資料儲存格式。然而,當面對大型CSV檔案時,直接讀取可能會導致記憶體耗盡或效能瓶頸。本章節將介紹如何使用pandas來高效地處理大型CSV檔案。

CSV檔案的寫入與壓縮

首先,讓我們看看如何使用pandas將DataFrame寫入CSV檔案。預設情況下,pandas會使用逗號作為分隔符,並且會對包含特殊字元的欄位進行參照處理。

import pandas as pd
import io

# 建立範例DataFrame
data = {
    "name": ["McCartney, Paul", "Lennon, John", "Starkey, Richard", "Harrison, George"],
    "birth": [1942, 1940, 1940, 1943]
}
df = pd.DataFrame(data)

# 將DataFrame寫入CSV
buf = io.StringIO()
df.to_csv(buf, index=False)
print(buf.getvalue())

輸出結果:

name,birth
"McCartney, Paul",1942
"Lennon, John",1940
"Starkey, Richard",1940
"Harrison, George",1943

內容解密:

  1. 使用io.StringIO()建立一個記憶體中的字串緩衝區。
  2. df.to_csv(buf, index=False)將DataFrame寫入緩衝區,不包含索引欄位。
  3. print(buf.getvalue())輸出緩衝區中的內容。

除了使用逗號作為分隔符外,我們還可以指定其他字元作為分隔符。

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

輸出結果:

name|birth
McCartney, Paul|1942
Lennon, John|1940
Starkey, Richard|1940
Harrison, George|1943

內容解密:

  1. sep="|"指定使用豎線作為分隔符。

對於大型CSV檔案,壓縮可以有效地減少儲存空間。pandas支援多種壓縮格式,包括gzip。

# 建立較大的DataFrame
df_large = pd.DataFrame({
    "col1": ["a"] * 1000,
    "col2": ["b"] * 1000,
    "col3": ["c"] * 1000,
})

# 比較未壓縮與壓縮的檔案大小
buf_uncompressed = io.StringIO()
df_large.to_csv(buf_uncompressed, index=False)
print(f"未壓縮大小:{len(buf_uncompressed.getvalue())} 位元組")

buf_compressed = io.BytesIO()
df_large.to_csv(buf_compressed, index=False, compression="gzip")
print(f"壓縮大小:{len(buf_compressed.getvalue())} 位元組")

輸出結果:

未壓縮大小:6015 位元組
壓縮大小:69 位元組

內容解密:

  1. 使用compression="gzip"啟用gzip壓縮。
  2. 比較未壓縮與壓縮後的檔案大小,可以看出壓縮顯著減少了檔案大小。

高效讀取大型CSV檔案

當處理大型CSV檔案時,直接讀取可能會導致記憶體不足。以下是一些提高讀取效率的方法。

預覽資料

首先,我們可以預覽檔案的前幾行,以瞭解資料的結構和型別。

df_preview = pd.read_csv("data/diamonds.csv", nrows=1000)
print(df_preview.head())

輸出結果:

   carat      cut color clarity  depth  table  price     x     y     z
0   0.23    Ideal     E     SI2   61.5   55.0    326  3.95  3.98  2.43
1   0.21  Premium     E     SI1   59.8   61.0    326  3.89  3.84  2.31
2   0.23     Good     E     VS1   56.9   65.0    327  4.05  4.07  2.31
3   0.29  Premium     I     VS2   62.4   58.0    334  4.20  4.23  2.63
4   0.31     Good     J     SI2   63.3   58.0    335  4.34  4.35  2.75

分析資料型別

接下來,我們可以分析預覽資料的資料型別,以確定是否可以使用更節省記憶體的型別。

print(df_preview.info())

輸出結果:

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 10 columns):
 #   Column   Non-Null Count  Dtype 
---
 
---
---
   
---
-
---
-
---
---
  
---
-- 
 0   carat    1000 non-null   Float64
 1   cut      1000 non-null   string 
 2   color    1000 non-null   string 
 3   clarity  1000 non-null   string 
 4   depth    1000 non-null   Float64
 5   table    1000 non-null   Float64
 6   price    1000 non-null   Int64  
 7   x        1000 non-null   Float64
 8   y        1000 non-null   Float64
 9   z        1000 non-null   Float64
dtypes: Float64(6), Int64(1), string(3)
memory usage: 85.1 KB

調整資料型別

透過分析price欄位的統計資訊,我們發現其範圍在326至2898之間,可以使用Int16型別來儲存,從而節省記憶體。

print(df_preview["price"].describe())

輸出結果:

count    1000.00000
mean     2476.54000
std       839.57562
min       326.00000
25%      2777.00000
50%      2818.00000
75%      2856.00000
max      2898.00000
Name: price, dtype: Float64

使用合適的資料型別讀取大型CSV檔案

根據前面的分析,我們可以使用更合適的資料型別來讀取大型CSV檔案,以減少記憶體使用。

df_large = pd.read_csv("data/diamonds.csv", dtype={"price": "Int16"})

透過上述方法,我們可以更高效地處理大型CSV檔案,避免記憶體不足的問題。