Pandas 在資料分析領域應用廣泛,但效能問題不容忽視。提升 Pandas 效能的關鍵在於理解其運作機制並善用其內建功能。本文將探討如何透過向量化操作、預處理資料、有效迭代 GroupBy 物件等技巧,最大化 Pandas 的效能。此外,也將介紹如何利用測試驅動開發的策略,撰寫更可靠且易於維護的 Pandas 程式碼。透過整合這些技巧,可以有效提升資料處理效率,避免常見的效能陷阱。
使用與效能建議
在處理資料分析時,Pandas 是一個強大的工具,但在使用時需要注意效能問題。本文將探討如何在使用 Pandas 時,最大化效能並避免常見的錯誤。
向量化與迴圈
向量化是 Pandas 的一大特點,這種方式透過低層次語言(如 C)來處理資料,避免了 Python 執行時的互動,從而提高了效能。以求和為例,Pandas 的向量化操作比起 Python 的迴圈來說,效能提升非常明顯。
內容解密:
result = 0
for x in ser:
result += x
這段程式碼展示瞭如何使用 Python 的迴圈來對 Pandas Series 的元素進行求和。然而,這種方法在效能上並不理想。
ser.sum()
這行程式碼則利用了 Pandas 的向量化功能,可以更高效地進行求和操作。以下是兩者的效能比較:
import timeit
# 使用 Pandas 的向量化求和
timeit.timeit(ser.sum, number=1000)
def loop_sum():
result = 0
for x in ser:
result += x
# 使用 Python 迴圈求和
timeit.timeit(loop_sum, number=1000)
從結果可以看出,Pandas 的向量化求和操作遠比 Python 迴圈快速。因此,在進行資料分析時,應盡量使用 Pandas 的內建向量化函式。
資料變異
Pandas 鼓勵在載入資料前進行變異操作,這樣可以避免在載入後對資料進行變異帶來的效能損耗。
內容解密:
def mutate_after():
data = ["foo", "bar", "baz"]
ser = pd.Series(data, dtype=pd.StringDtype())
ser.iloc[1] = "BAR"
這段程式碼展示瞭如何在載入後對 Pandas Series 的資料進行變異操作。
def mutate_before():
data = ["foo", "bar", "baz"]
data[1] = "BAR"
ser = pd.Series(data, dtype=pd.StringDtype())
這段程式碼則是在載入前對資料進行變異操作。以下是兩者的效能比較:
timeit.timeit(mutate_after, number=1000)
timeit.timeit(mutate_before, number=1000)
從結果可以看出,在載入前進行變異操作的效能更高。因此,應盡量在載入資料前完成所有需要的變異操作。
Pipeline實作
當處理 pd.GroupBy 物件時,可以透過類別似字典的方式高效地進行迭代。
df = pd.DataFrame({
"column": ["a", "a", "b", "a", "b"],
"value": [0, 1, 2, 4, 8],
})
df = df.convert_dtypes(dtype_backend="numpy_nullable")
for label, group in df.groupby("column"):
print(f"The group for label {label} is:\n{group}\n")
此圖示展示瞭如何使用 pd.GroupBy 物件進行迭代:
此圖示展示了 pd.GroupBy 的群組情況。每一個群組都有一個標籤和對應的值列表。
測試驅動開發
測試驅動開發(TDD)是一種提升程式碼品質和維護性的開發方式。TDD 的核心思想是先編寫測試案例,然後再編寫實作程式碼。當測試透過後,開發者才能確信程式碼是正確的。
測試框架
Python 提供了 unittest 模組來編寫和自動執行測試案例。以下是一個簡單的測試範例:
import unittest
class MyTests(unittest.TestCase):
def test_42(self):
self.assertEqual(21 * 2, 42)
def suite():
suite = unittest.TestSuite()
suite.addTest(MyTests("test_42"))
return suite
runner = unittest.TextTestRunner()
runner.run(suite())
此圖示展示了 unittest 的執行流程:
此圖示展示了測試案例的執行流程。測試框架會執行 MyTests 中的所有測試方法,並在每個方法中進行斷言。
Pandas 測試模組
Pandas 提供了 pd.testing 模組來方便編寫對 Pandas Series 和 DataFrame 的測試案例。以下是一個範例:
def some_cool_numbers():
return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
此圖示展示瞭如何使用 pd.testing 模組進行測試:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Pandas 效能最佳化與測試策略
package "Pandas 資料處理" {
package "資料結構" {
component [Series
一維陣列] as series
component [DataFrame
二維表格] as df
component [Index
索引] as index
}
package "資料操作" {
component [選取 Selection] as select
component [篩選 Filtering] as filter
component [分組 GroupBy] as group
component [合併 Merge/Join] as merge
}
package "資料轉換" {
component [重塑 Reshape] as reshape
component [透視表 Pivot] as pivot
component [聚合 Aggregation] as agg
}
}
series --> df : 組成
index --> df : 索引
df --> select : loc/iloc
df --> filter : 布林索引
df --> group : 分組運算
group --> agg : 聚合函數
df --> merge : 合併資料
df --> reshape : melt/stack
reshape --> pivot : 重新組織
note right of df
核心資料結構
類似 Excel 表格
end note
@enduml
此圖示展示瞭如何使用 pd.testing 模組來編寫對 Pandas Series 的測試案例。這種方法可以確保資料分析過程中的各種操作都能透過測試驗證。
使用 Pandas 進行 DataFrame 和 Series 範例測試
Pandas 是一個強大的資料處理函式庫,常用於資料分析和機器學習領域。在開發過程中,單元測試是確保程式碼正確性的重要步驟。本文將介紹如何使用 Python 的 unittest 模組來測試 Pandas 的 DataFrame 和 Series,並探討在測試過程中可能遇到的問題及其解決方案。
使用 unittest 測試 Pandas Series
首先,我們來看一個簡單的範例,展示如何使用 unittest 測試 Pandas Series。以下是一個基本的測試框架:
import unittest
import pandas as pd
def some_cool_numbers():
return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
class MyTests(unittest.TestCase):
def test_cool_numbers(self):
result = some_cool_numbers()
expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
self.assertEqual(result, expected)
def suite():
suite = unittest.TestSuite()
suite.addTest(MyTests("test_cool_numbers"))
return suite
runner = unittest.TextTestRunner()
runner.run(suite())
在這段程式碼中,我們定義了一個函式 some_cool_numbers,它傳回一個包含整數和缺失值的 Pandas Series。接著,我們使用 unittest.TestCase 來撰寫測試使用案例,並使用 assertEqual 來比較結果和預期值。
然而,當我們執行這段程式碼時,會遇到以下錯誤:
ERROR: test_cool_numbers (__main__.MyTests)
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
這是因為 Pandas 過載了等號運算元,使得 Series 的比較結果不是簡單的 True 或 False,而是一個元素級別的比較結果。
內容解密:
result = some_cool_numbers()
expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
result == expected
some_cool_numbers()函式傳回一個包含整數和缺失值的 Pandas Series。expected是我們預期的結果。result == expected的結果是一個布林型的 Series,表示每個元素的比較結果。
為瞭解決這個問題,我們可以使用 Pandas 提供的 pd.testing.assert_series_equal 函式來進行比較:
import pandas.testing as tm
def some_cool_numbers():
return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
class MyTests(unittest.TestCase):
def test_cool_numbers(self):
result = some_cool_numbers()
expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
tm.assert_series_equal(result, expected)
def suite():
suite = unittest.TestSuite()
suite.addTest(MyTests("test_cool_numbers"))
return suite
runner = unittest.TextTestRunner()
runner.run(suite())
這樣就可以成功地比較兩個 Series。
內容解密:
tm.assert_series_equal(result, expected)
tm.assert_series_equal是 Pandas 提供的一個用來比較 Series 的函式。- 它會考慮行索引、列索引、值以及缺失值語意等因素,並且會提供詳細的錯誤訊息。
引發故意失敗以檢視輸出
為了更全面地理解測試結果,我們可以故意引發一個失敗並檢視錯誤訊息:
def some_cool_numbers():
return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())
class MyTests(unittest.TestCase):
def test_cool_numbers(self):
result = some_cool_numbers()
expected = pd.Series([42, 555, pd.NA], dtype=pd.Int32Dtype()) # 差異在這裡
tm.assert_series_equal(result, expected)
def suite():
suite = unittest.TestSuite()
suite.addTest(MyTests("test_cool_numbers"))
return suite
runner = unittest.TextTestRunner()
runner.run(suite())
執行這段程式碼會得到以下錯誤訊息:
FAIL: test_cool_numbers (__main__.MyTests)
AssertionError: Attributes of Series are different
Attribute "dtype" are different
[left]: Int64
[right]: Int32
這表明我們比較的兩個 Series 的資料型別不同。
內容解密:
expected = pd.Series([42, 555, pd.NA], dtype=pd.Int32Dtype())
- 在這裡,我們故意讓預期值的資料型別與結果不同,以引發測試失敗。
- 測試框架會顯示詳細的錯誤訊息,指出資料型別不同。
DataFrame 測試
類別似地,對於 Pandas DataFrame 的測試,我們可以使用 pd.testing.assert_frame_equal 函式。這個函式也會考慮行索引、列索引、值以及缺失值語意等因素。
import pandas as pd
def create_sample_dataframe():
data = {'A': [1, 2, 3], 'B': [4, 5, 6]}
return pd.DataFrame(data)
class DataFrameTests(unittest.TestCase):
def test_dataframe(self):
result = create_sample_dataframe()
expected = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
tm.assert_frame_equal(result, expected)
def suite():
suite = unittest.TestSuite()
suite.addTest(DataFrameTests("test_dataframe"))
return suite
runner = unittest.TextTestRunner()
runner.run(suite())
內容解密:
tm.assert_frame_equal(result, expected)
tm.assert_frame_equal是用來比較 DataFrame 的函式。- 和
assert_series_equal一樣,它也會考慮多種因素並提供詳細的錯誤訊息。
其他測試框架:pytest
雖然本文主要介紹了使用內建的 unittest 模組來進行測試,但很多大型 Python 專案特別是科學計算相關的專案更傾向於使用 pytest 函式庫。pytest 提供了一個更加靈活且強大的測試框架,並且擁有豐富的外掛生態系統。
# 安裝 pytest
pip install pytest
# 執行 pytest 測試
pytest -v test_file.py
內容解密:
pytest是一個強大且靈活的 Python 測試框架。- 支援許多外掛和擴充套件功能。
- 常用於大型 Python 專案中。
玄貓專屬的 Pandas 生態系統
Pandas 是 Python 中一個非常強大的資料處理函式庫,其功能之豐富和流行,很大程度上得益於一大群第三方函式庫的支援。在這一章中,我們無法涵蓋所有這些第三方函式庫,也無法探討每個函式庫的運作原理。然而,瞭解這些工具的存在及其提供的功能,將為未來的學習提供良好的靈感。
儘管 Pandas 是一個非常強大的工具,但它並非無懈可擊。在這本文中,我們一直強調其不足之處。Pandas 無法解決所有的分析問題。我強烈建議熟悉本章節中提到的工具,並參考 Pandas 生態系統檔案(https://pandas.pydata.org/about/)來尋找新的專業工具。
技術備註:本章節中的程式碼可能會因為這些函式庫的新版本發布而出現問題或改變行為。儘管我們在書中盡力撰寫未來可用的 Pandas 程式碼,但涉及到第三方依賴時,難以保證其穩定性。如果遇到執行本章節程式碼時出現問題,請參考隨附程式碼範例提供的 requirements.txt 檔案。該檔案包含已知能夠與本章節相容的依賴和版本。
本章節將涵蓋以下主題:
- 基礎函式庫
- 探索性資料分析
- 資料驗證
- 資料視覺化
- 資料科學
- 資料函式庫
- 其他 DataFrame 函式庫
基礎函式庫
Pandas 就像許多開源函式庫一樣,建立在其他基礎函式庫之上,讓它們處理低層次細節,而 Pandas 提供更友好的使用者功能。如果你想深入瞭解超越 Pandas 的技術細節,這些就是你需要關注的函式庫。
NumPy
NumPy 自稱為 Python 中進行科學計算的基本套件,也是 Pandas 原始建立基礎之上的函式庫。NumPy 本質上是一個多維陣列函式庫,你不僅僅限於二維資料(如 pd.DataFrame),而是可以處理更高維度資料(Pandas 曾經提供過三維和四維面板結構,但現在已經被移除)。
在這本文中,我們展示瞭如何從 NumPy 物件構建 Pandas 物件:
import numpy as np
import pandas as pd
arr = np.arange(1, 10).reshape(3, -1)
df = pd.DataFrame(arr)
print(df)
內容解密:
此段程式碼首先匯入 NumPy 和 Pandas 函式庫。接著,使用 np.arange 函式建立一個範圍從 1 到 9 的陣列並重塑為 3 行和多列的矩陣。最後使用 pd.DataFrame 建構函式將 NumPy 陣列轉換為 DataFrame 物件並列印。
此外,你也可以從 pd.DataFrame 物件建立 NumPy 陣列:
print(df.to_numpy())
內容解密:
此段程式碼使用 to_numpy 方法將 DataFrame 物件轉換為 NumPy 陣列並列印。
許多 NumPy 函式接受 pd.DataFrame 作為引數並傳回 pd.DataFrame:
print(np.log(df))
內容解密:
此段程式碼使用 NumPy 的 log 函式對 DataFrame 物件進行對數轉換並列印結果。
需要注意的是,當你使用非浮點數型資料型別或包含缺失值時,NumPy 與 Pandas 的互操作性會變差。因此在這類別情況下建議避免直接使用 pd.Series.to_numpy 或 pd.DataFrame.to_numpy。
PyArrow
另一個 Pandas 建立基礎之上的主要函式庫是 Apache Arrow,它自稱為跨語言開發平台用於記憶體分析。由 Wes McKinney(Pandas 的創造者)發起並在《Apache Arrow 和我對 Pandas 的十大不滿》文章中宣佈(https://wesmckinney.com/blog/apache-arrow-pandas-internals/),Apache Arrow 專案定義了一種一維資料結構的記憶體佈局方式,允許不同語言、程式和函式庫共同使用相同資料。
Python 中 Apache Arrow 的實作 PyArrow 在整本文中有特定情況被使用。雖然 Pandas 沒有直接提供將 pd.DataFrame 轉換成 PyArrow 的方法,但 PyArrow 函式庫提供了 pa.Table.from_pandas 方法來完成此任務:
import pyarrow as pa
tbl = pa.Table.from_pandas(df)
print(tbl)
內容解密:
此段程式碼匯入 PyArrow 函式庫後使用 pa.Table.from_pandas 方法將 DataFrame 轉換為 PyArrow Table 物件並列印。
同樣地,PyArrow 提供了 pa.Table.to_pandas 方法來將 Table 轉換回 DataFrame:
print(tbl.to_pandas())
內容解密:
此段程式碼使用 to_pandas 方法將 PyArrow Table 轉換回 DataFrame 物件並列印。
總地來說,PyArrow 是一個比 Pandas 更低階的函式庫。它主要針對其他函式庫開發者設計而非一般使用者查詢 DataFrame。除非你正在開發一個函式庫,否則可能不頻繁需要從 pd.DataFrame 轉換到 PyArrow。但是隨著 Apache Arrow 生態系統的成長,Pandas 和 PyArrow 能夠互操作為許多其他分析函式庫和資料函式庫帶來了整合機會。