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_pickle 和 read_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.add、pd.Series.sub、pd.Series.mul 和 pd.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 - 42將ser中的每個元素都減去 42。- 結果保持了與原始
ser相同的索引和資料型別。
乘法可以使用 * 運算子來表示:
ser * 2
輸出結果:
0 0
1 2
2 4
dtype: Int64
內容解密:
ser * 2將ser中的每個元素都乘以 2。- 結果仍然保持著與原始
ser一致的資料型別。
除法可以使用 / 運算子來表示:
ser / 2
輸出結果:
0 0.0
1 0.5
2 1.0
dtype: Float64
內容解密:
ser / 2將ser中的每個元素都除以 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.add、pd.Series.sub、pd.Series.mul 和 pd.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)將ser1和ser2對應的元素相加。- 當遇到缺失值 (
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
輸出結果取決於具體的 ser1 和 ser3 的定義,但基本原理是根據索引標籤對齊後進行相應的運算。
基本的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>。
內容解密:
ser1和ser3進行加法運算時,pandas會根據索引標籤進行對齊。- 索引
0和1在兩個pd.Series中都存在,因此對應的值被加在一起。 - 索引
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中所有索引標籤的並集。
內容解密:
ser1的索引為[0, 1, 2],而ser4的索引為[1, 2, 3]。- 索引
1和2在兩個pd.Series中都存在,因此對應的值被加在一起。 - 索引
0和3只在一個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會將具有相同索引標籤的值全部匹配起來。
內容解密:
ser5中有兩個索引標籤為1的值,分別是4和8。- 因此,當與
ser1相加時,索引標籤為1的值會被加到這兩個值上,分別得到5和9。
這種行為類別似於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.Series 和 pd.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
此時,同樣需要注意索引對齊的規則。