Pandas 作為 Python 資料科學領域的核心函式庫,其 I/O 系統和資料結構操作是資料處理的根本。理解如何有效地讀取和寫入各種格式的資料,以及如何靈活運用 Series 和 DataFrame 進行運算,對於提升資料分析效率至關重要。本文將從 HTML 資料讀取、Pickle 物件序列化,以及 Series 和 DataFrame 的算術運算等方面,深入剖析 Pandas 的進階應用技巧。透過 pd.read_html 可以輕鬆擷取網頁表格資料,並利用引數設定處理多層級欄位和缺失值。使用 Pickle 則能有效地序列化和反序列化包含 Python 物件的 Pandas 資料結構。此外,瞭解 Series 和 DataFrame 的向量化運算、廣播機制以及索引對齊規則,能更好地掌握資料操作技巧,避免常見錯誤。

Pandas I/O 系統進階應用

在處理資料時,我們經常需要從不同的來源讀取或寫入資料。Pandas 提供了多種方法來支援不同的檔案格式和資料來源。本章節將介紹 Pandas I/O 系統的進階應用,包括如何使用 pd.read_html 從 HTML 表格中讀取資料、使用 Pickle 格式儲存和讀取 Python 物件,以及介紹一些第三方 I/O 函式庫。

從 HTML 表格中讀取資料

Pandas 提供了 pd.read_html 函式,可以從 HTML 檔案或 URL 中讀取表格資料。我們可以透過 match 引數來指定要讀取的表格。

使用 match 引數

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match=r"List of studio albums",
    dtype_backend="numpy_nullable",
)
print(f"Number of tables returned was: {len(dfs)}")
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

處理多層級欄位名稱

當表格中有多層級欄位名稱時,Pandas 會自動將其轉換為 pd.MultiIndex。我們可以透過 header 引數來指定要使用的欄位名稱層級。

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match="List of studio albums",
    header=1,
    dtype_backend="numpy_nullable",
)
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

處理缺失值

我們可以透過 na_values 引數來指定要視為缺失值的字串。

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match="List of studio albums",
    header=1,
    na_values=["—"],
    dtype_backend="numpy_nullable",
)
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

內容解密:

  • match 引數用於指定要讀取的表格標題。
  • header 引數用於指定要使用的欄位名稱層級。
  • na_values 引數用於指定要視為缺失值的字串。

使用 Pickle 格式儲存和讀取 Python 物件

Pickle 是 Python 的內建序列化格式,可以用於儲存和讀取 Python 物件。Pandas 提供了 to_pickleread_pickle 函式來支援 Pickle 格式。

使用 to_pickle 和 read_pickle

from collections import namedtuple
Member = namedtuple("Member", ["first", "last", "birth"])
ser = pd.Series([
    Member("Paul", "McCartney", 1942),
    Member("John", "Lennon", 1940),
    Member("Richard", "Starkey", 1940),
    Member("George", "Harrison", 1943),
])

buf = io.BytesIO()
ser.to_pickle(buf)
buf.seek(0)
ser = pd.read_pickle(buf)
ser

內容解密:

  • to_pickle 函式用於將 Pandas 物件儲存為 Pickle 格式。
  • read_pickle 函式用於從 Pickle 格式讀取 Pandas 物件。
  • Pickle 格式可以用於儲存和讀取包含 Python 物件的 Pandas 物件。

第三方 I/O 函式庫

Pandas 提供了多種內建的 I/O 方法,但仍有一些格式和資料來源未被涵蓋。第三方函式庫可以用於填補這一空白。

常見的第三方 I/O 函式庫

  • pandas-gbq:與 Google BigQuery 交換資料
  • AWS SDK for pandas:與 Redshift 和 AWS 生態系統交換資料
  • Snowflake Connector for Python:與 Snowflake 資料函式庫交換資料
  • pantab:將 Pandas DataFrame 物件移入或移出 Tableau 的 Hyper 資料函式庫格式

這些第三方函式庫通常遵循相同的模式,提供讀取函式傳回 pd.DataFrame 物件和寫入方法接受 pd.DataFrame 引數。

基本 pd.Series 運算

在探索 pandas 演算法時,最簡單的起點是使用 pd.Series,因為它也是 pandas 函式庫提供的最基本結構。基本算術運算涵蓋了加法、減法、乘法和除法,而正如您將在本文中看到的,pandas 提供了兩種執行這些運算的方法。第一種方法是使用 Python 語言中內建的 +-*/ 運算子,這對於新使用者來說是一種直觀的學習方式。然而,為了涵蓋 Python 語言未涵蓋的資料分析特定功能,並支援我們稍後在本章中將介紹的使用 .pipe 方法進行鏈式呼叫,pandas 也提供了 pd.Series.addpd.Series.subpd.Series.mulpd.Series.div 方法。

如何實作

讓我們從一個簡單的 Python range 表示式建立一個 pd.Series

ser = pd.Series(range(3), dtype=pd.Int64Dtype())
ser

輸出結果:

0    0
1    1
2    2
dtype: Int64

為了建立術語,讓我們簡要考慮一個像 a + b 這樣的表示式。在這樣的表示式中,我們使用了一個二元運算子 (+)。二元是指您需要將兩個事物相加才能使這個表示式有意義,也就是說,只有 a + 這樣的表示式是沒有意義的。這兩個「事物」在技術上被視為運算元;因此,在 a + b 中,我們有一個左運算元 a 和一個右運算元 b

當其中一個運算元是 pd.Series 時,pandas 中最基本的演算法表示式將涵蓋另一個運算元是純量(即單一值)的情況。當這種情況發生時,純量值會被廣播到 pd.Series 的每個元素以應用演算法。

例如,如果我們想將數字 42 加到我們的 pd.Series 中的每個元素上,我們可以簡單地表達為:

ser + 42

輸出結果:

0    42
1    43
2    44
dtype: Int64

內容解密:

  • ser + 42 使用了 pandas 的向量化運算,能夠直接將 42 加到 ser 的每個元素上。
  • 這種向量化運算是 pandas 的一大特色,避免了使用 Python 的 for 迴圈,從而提高了效率。

同樣地,減法可以使用 - 運算子來表示:

ser - 42

輸出結果:

0   -42
1   -41
2   -40
dtype: Int64

內容解密:

  • ser - 42ser 中的每個元素都減去 42。
  • 結果保持了與原始 ser 相同的索引和資料型別。

乘法可以使用 * 運算子來表示:

ser * 2

輸出結果:

0    0
1    2
2    4
dtype: Int64

內容解密:

  • ser * 2ser 中的每個元素都乘以 2。
  • 結果仍然保持著與原始 ser 一致的資料型別。

除法可以使用 / 運算子來表示:

ser / 2

輸出結果:

0    0.0
1    0.5
2    1.0
dtype: Float64

內容解密:

  • ser / 2ser 中的每個元素都除以 2。
  • 由於除法可能會產生浮點數,結果的資料型別變成了 Float64

兩個運算元都是 pd.Series 的情況也是有效的:

ser2 = pd.Series(range(10, 13), dtype=pd.Int64Dtype())
ser + ser2

輸出結果:

0    10
1    12
2    14
dtype: Int64

內容解密:

  • 當兩個 pd.Series 相加時,pandas 會根據索引對齊兩個序列的元素。
  • 如果索引匹配,則對應的元素會被相加;如果索引不匹配,則結果會是缺失值。

除了使用內建運算子之外,pandas 也提供了專門的方法,如 pd.Series.addpd.Series.subpd.Series.mulpd.Series.div

ser1 = pd.Series([1., 2., 3.], dtype=pd.Float64Dtype())
ser2 = pd.Series([4., pd.NA, 6.], dtype=pd.Float64Dtype())
ser1.add(ser2)

輸出結果:

0     5.0
1    <NA>
2     9.0
dtype: Float64

內容解密:

  • ser1.add(ser2)ser1ser2 對應的元素相加。
  • 當遇到缺失值 (pd.NA) 時,結果也是缺失值。

使用這些方法的一個好處是它們接受一個可選的 fill_value= 引數來處理缺失資料:

ser1.add(ser2, fill_value=0.)

輸出結果:

0     5.0
1     2.0
2     9.0
dtype: Float64

內容解密:

  • 當使用 fill_value=0. 時,缺失值會被視為 0。
  • 這樣可以避免因缺失值導致的結果缺失。

更多內容…

當您的表示式中的兩個運算元都是 pd.Series 物件時,必須注意 pandas 會根據行標籤對齊。這種對齊行為被視為一個特性,但也可能令新手感到意外。

讓我們首先考慮兩個具有相同行索引的 pd.Series 物件。當我們將它們相加時,會得到一個相當預期的結果:

ser1 = pd.Series(range(3), dtype=pd.Int64Dtype())
ser2 = pd.Series(range(3), dtype=pd.Int64Dtype())
ser1 + ser2

輸出結果:

0    0
1    2
2    4
dtype: Int64

但當行索引值不相同時會發生什麼?一種簡單的情況可能涉及將兩個 pd.Series 物件相加,其中一個 pd.Series 使用的行索引是另一個的子集。您可以透過以下程式碼中的 ser3 看到這一點,它只有 2 個值,並使用預設的 pd.RangeIndex,其值為 [0, 1]。當與 ser1 相加時,我們仍然得到一個包含 3 個元素的 pd.Series,但只有當行索引標籤可以從兩個 pd.Series 物件對齊時,才會相加對應的值:

ser3 = pd.Series(range(2), dtype=pd.Int64Dtype())
ser1 + ser3

輸出結果取決於具體的 ser1ser3 的定義,但基本原理是根據索引標籤對齊後進行相應的運算。

基本的pd.Series算術運算

在pandas中,pd.Series物件之間的算術運算非常直觀且強大。讓我們來看看當兩個pd.Series物件進行加法運算時會發生什麼。

不同的長度

首先,考慮兩個長度不同的pd.Series

ser1 = pd.Series([0, 1, 2], dtype=pd.Int64Dtype())
ser3 = pd.Series([2, 4], dtype=pd.Int64Dtype())
ser1 + ser3

輸出結果為:

0       2
1       5
2    <NA>
dtype: Int64

可以看到,pandas會根據索引標籤對齊兩個pd.Series。如果某個索引標籤在其中一個pd.Series中不存在,則結果中對應的值將為<NA>

內容解密:

  1. ser1ser3進行加法運算時,pandas會根據索引標籤進行對齊。
  2. 索引01在兩個pd.Series中都存在,因此對應的值被加在一起。
  3. 索引2只在ser1中存在,因此結果中對應的值為<NA>

相同的長度但不同的索引標籤

接下來,考慮兩個長度相同但索引標籤不同的pd.Series

ser1 = pd.Series([0, 1, 2], dtype=pd.Int64Dtype())
ser4 = pd.Series([2, 4, 8], index=[1, 2, 3], dtype=pd.Int64Dtype())
ser1 + ser4

輸出結果為:

0    <NA>
1       3
2       6
3    <NA>
dtype: Int64

這裡,pandas同樣根據索引標籤進行對齊。結果中包含了兩個pd.Series中所有索引標籤的並集。

內容解密:

  1. ser1的索引為[0, 1, 2],而ser4的索引為[1, 2, 3]
  2. 索引12在兩個pd.Series中都存在,因此對應的值被加在一起。
  3. 索引03只在一個pd.Series中存在,因此結果中對應的值為<NA>

非唯一的索引標籤

最後,考慮一個具有非唯一索引標籤的pd.Series

ser1 = pd.Series([0, 1, 2], dtype=pd.Int64Dtype())
ser5 = pd.Series([2, 4, 8], index=[0, 1, 1], dtype=pd.Int64Dtype())
ser1 + ser5

輸出結果為:

0       2
1       5
1       9
2    <NA>
dtype: Int64

這種情況下,pandas會將具有相同索引標籤的值全部匹配起來。

內容解密:

  1. ser5中有兩個索引標籤為1的值,分別是48
  2. 因此,當與ser1相加時,索引標籤為1的值會被加到這兩個值上,分別得到59

這種行為類別似於SQL中的FULL OUTER JOIN操作。所有的索引標籤都會被包含在結果中,並且pandas會盡可能地匹配具有相同標籤的值。

基本的pd.DataFrame算術運算

pd.Series類別似,pd.DataFrame也支援各種算術運算,並且運算規則基本相同,只不過現在是在兩個維度上進行。

與純量的運算

首先,我們可以建立一個小的3x3 pd.DataFrame

np.random.seed(42)
df = pd.DataFrame(
    np.random.randn(3, 3),
    columns=["col1", "col2", "col3"],
    index=["row1", "row2", "row3"],
).convert_dtypes(dtype_backend="numpy_nullable")
df

輸出結果為:

          col1      col2      col3
row1  0.496714 -0.138264  0.647689
row2  1.523030 -0.234153 -0.234137
row3  1.579213  0.767435 -0.469474

然後,我們可以對這個pd.DataFrame進行簡單的加法或乘法運算:

df + 1
df * 2

輸出結果分別為:

          col1      col2      col3
row1  1.496714  0.861736  1.647689
row2  2.523030  0.765847  0.765863
row3  2.579213  1.767435  0.530526

          col1      col2      col3
row1  0.993428 -0.276529  1.295377
row2  3.046060 -0.468307 -0.468274
row3  3.158426  1.534869 -0.938949

與pd.Series的運算

我們也可以將一個 pd.Series 加到 pd.DataFrame 上。預設情況下,pandas會根據 pd.Series 的索引標籤和 pd.DataFrame 的列標籤進行對齊。

ser = pd.Series(
    [20, 10, 0],
    index=["col1", "col2", "col3"],
    dtype=pd.Int64Dtype(),
)
df + ser

輸出結果為:

          col1      col2      col3
row1 20.496714   9.861736   0.647689
row2 21.523030   9.765847  -0.234137
row3 21.579213  10.767435  -0.469474

如果 pd.Series 的索引標籤與 pd.DataFrame 的列標籤不完全匹配,可能會導致缺失值:

ser = pd.Series(
    [20, 10, 0, 42],
    index=["col1", "col2", "col3", "new_column"],
    dtype=pd.Int64Dtype(),
)
df + ser

輸出結果為:

          col1      col2      col3 new_column
row1   NaN       NaN       NaN        NaN
row2   NaN       NaN       NaN        NaN
row3   NaN       NaN       NaN        NaN

控制對齊方式

如果需要控制 pd.Seriespd.DataFrame 之間的對齊方式,可以使用 axis= 引數:

ser = pd.Series(
    [20, 10, 0,42],
    index=["row1", "row2", "row3","ROW4"],
    dtype=pd.Int64Dtype(),
)
df.add(ser, axis=0)

輸出結果為:

          col1      col2      col3
row1   NaN       NaN       NaN     
row2   NaN       NaN       NaN     
row3   NaN       NaN       NaN     
ROW4   NaN       NaN       NaN 

DataFrame之間的運算

同樣地,我們也可以對兩個 `pd.DataFrame進行乘法或除法等運算:

df * df 

此時,同樣需要注意索引對齊的規則。