擴充 pandas#
雖然 pandas 提供豐富的方法、容器和資料類型,但可能無法完全滿足您的需求。pandas 提供了一些擴充 pandas 的選項。
註冊自訂存取器#
函式庫可以使用裝飾器 pandas.api.extensions.register_dataframe_accessor()
、pandas.api.extensions.register_series_accessor()
和 pandas.api.extensions.register_index_accessor()
,將額外的「命名空間」新增到 pandas 物件。所有這些都遵循類似的慣例:您裝飾一個類別,提供要新增的屬性名稱。類別的 __init__
方法會取得正在裝飾的物件。例如
@pd.api.extensions.register_dataframe_accessor("geo")
class GeoAccessor:
def __init__(self, pandas_obj):
self._validate(pandas_obj)
self._obj = pandas_obj
@staticmethod
def _validate(obj):
# verify there is a column latitude and a column longitude
if "latitude" not in obj.columns or "longitude" not in obj.columns:
raise AttributeError("Must have 'latitude' and 'longitude'.")
@property
def center(self):
# return the geographic center point of this DataFrame
lat = self._obj.latitude
lon = self._obj.longitude
return (float(lon.mean()), float(lat.mean()))
def plot(self):
# plot this array's data on a map, e.g., using Cartopy
pass
現在使用者可以使用 geo
命名空間存取您的方法
>>> ds = pd.DataFrame(
... {"longitude": np.linspace(0, 10), "latitude": np.linspace(0, 20)}
... )
>>> ds.geo.center
(5.0, 10.0)
>>> ds.geo.plot()
# plots data on a map
這可能是擴充 pandas 物件而不使用子類別的便利方式。如果您撰寫自訂存取器,請提出拉取請求,將其新增到我們的 生態系統 頁面。
我們強烈建議驗證存取器的 __init__
中的資料。在我們的 GeoAccessor
中,我們驗證資料是否包含預期的欄位,當驗證失敗時引發 AttributeError
。對於 Series
存取器,如果存取器僅適用於特定資料類型,則應驗證 dtype
。
擴充類型#
注意
pandas.api.extensions.ExtensionDtype
和 pandas.api.extensions.ExtensionArray
API 在 pandas 1.5 之前是實驗性質的。從版本 1.5 開始,未來的變更將遵循 pandas 棄用政策。
pandas 定義了一個介面,用於實作資料類型和陣列,以擴充 NumPy 的類型系統。pandas 本身使用擴充系統處理一些未內建於 NumPy 的類型(類別、期間、區間、帶有時區的日期時間)。
程式庫可以定義自訂陣列和資料類型。當 pandas 遇到這些物件時,它們將被適當處理(即不會轉換為物件的 ndarray)。許多方法,例如 pandas.isna()
,將傳送至擴充類型的實作。
如果您正在建置實作介面的程式庫,請在 生態系統頁面 上公佈它。
介面包含兩個類別。
ExtensionDtype
#
一個 pandas.api.extensions.ExtensionDtype
類似於 numpy.dtype
物件。它描述資料類型。實作者負責幾個獨特的項目,例如名稱。
特別重要的項目之一是 type
屬性。這應該是您資料的標量類型的類別。例如,如果您正在為 IP 位址資料撰寫擴充陣列,這可能是 ipaddress.IPv4Address
。
請參閱 擴充資料類型來源 以取得介面定義。
pandas.api.extensions.ExtensionDtype
可以註冊到 pandas 以允許透過字串資料類型名稱建立。這允許使用已註冊的字串名稱建立 Series
和 .astype()
,例如 'category'
是 CategoricalDtype
的已註冊字串存取器。
請參閱 擴充資料類型資料類型 以進一步瞭解如何註冊資料類型。
ExtensionArray
#
這個類別提供所有類似陣列的功能。ExtensionArrays 僅限於 1 個維度。ExtensionArray 透過 dtype
屬性連結到 ExtensionDtype。
pandas 對於如何透過其 __new__
或 __init__
建立延伸陣列沒有任何限制,且對於如何儲存資料也沒有任何限制。我們要求陣列可以轉換為 NumPy 陣列,即使這相對昂貴(就像 Categorical
)。
它們可以由沒有、一個或多個 NumPy 陣列作為後盾。例如,pandas.Categorical
是由兩個陣列作為後盾的延伸陣列,一個用於代碼,一個用於類別。IPv6 位址陣列可以由具有兩個欄位的 NumPy 結構化陣列作為後盾,一個用於較低的 64 位元,一個用於較高的 64 位元。或者它們可以由其他儲存類型(例如 Python 清單)作為後盾。
請參閱 延伸陣列來源 以取得介面定義。文件字串和註解包含正確實作介面的指南。
ExtensionArray
運算子支援#
預設情況下,類別 ExtensionArray
沒有定義任何運算子。有兩種方法可為 ExtensionArray 提供運算子支援
在
ExtensionArray
子類別上定義每個運算子。使用 pandas 中的運算子實作,該實作依賴於已定義在 ExtensionArray 的基礎元素(純量)上的運算子。
注意
無論採用哪種方法,如果您希望在與 NumPy 陣列進行二元運算時呼叫您的實作,您可能想設定 __array_priority__
。
對於第一種方法,您定義選定的運算子,例如 __add__
、__le__
等,您希望 ExtensionArray
子類別支援這些運算子。
第二種方法假設 ExtensionArray
的基礎元素(即純量類型)已定義個別運算子。換句話說,如果您的 ExtensionArray
名稱為 MyExtensionArray
,且實作方式是每個元素都是類別 MyExtensionElement
的執行個體,則如果運算子已定義為 MyExtensionElement
,第二種方法會自動為 MyExtensionArray
定義運算子。
一個 mixin 類別,ExtensionScalarOpsMixin
支援第二種方法。如果開發一個 ExtensionArray
子類別,例如 MyExtensionArray
,可以簡單地將 ExtensionScalarOpsMixin
包含為 MyExtensionArray
的父類別,然後呼叫方法 _add_arithmetic_ops()
和/或 _add_comparison_ops()
,將運算子掛入您的 MyExtensionArray
類別,如下所示
from pandas.api.extensions import ExtensionArray, ExtensionScalarOpsMixin
class MyExtensionArray(ExtensionArray, ExtensionScalarOpsMixin):
pass
MyExtensionArray._add_arithmetic_ops()
MyExtensionArray._add_comparison_ops()
注意
由於 pandas
會自動逐一呼叫每個元素上的底層運算子,這可能不如直接在 ExtensionArray
上實作您自己的版本關聯運算子來得有效能。
對於算術運算,這個實作會嘗試重建一個新的 ExtensionArray
,其結果為逐元素運算。是否成功取決於運算是否傳回一個對 ExtensionArray
有效的結果。如果無法重建 ExtensionArray
,則會包含傳回的純量之 ndarray。
為方便實作並與 pandas 和 NumPy ndarrays 之間的運算保持一致,我們建議不要在二元運算中處理 Series 和 Indexes。相反地,您應該偵測這些情況並傳回 NotImplemented
程式碼。當 pandas 遇到像 op(Series, ExtensionArray)
這樣的運算時,pandas 將會
從
Series
(Series.array
) 中取出陣列呼叫
result = op(values, ExtensionArray)
將結果重新放入
Series
中
NumPy 通用函數#
Series
實作 __array_ufunc__
。作為實作的一部分,pandas 會從 Series
中取出 ExtensionArray
,套用 ufunc,並在必要時重新放入。
如果適用,我們強烈建議您在延伸陣列中實作 __array_ufunc__
以避免強制轉換為 ndarray。請參閱 NumPy 文件 以取得範例。
在您的實作中,我們要求您在 inputs
中偵測到 pandas 容器(Series
、DataFrame
、Index
)時,將其遞延至 pandas。如果存在任何一個,您應該傳回 NotImplemented
。pandas 將負責從容器中解除陣列封裝,並使用未封裝的輸入重新呼叫 ufunc。
測試延伸陣列#
我們提供一個測試套件,用於確保您的延伸陣列符合預期的行為。若要使用測試套件,您必須提供多個 pytest 固定裝置,並繼承自基本測試類別。必要的固定裝置可在 pandas-dev/pandas 中找到。
若要使用測試,請建立其子類別
from pandas.tests.extension import base
class TestConstructors(base.BaseConstructorsTests):
pass
請參閱 pandas-dev/pandas,以取得所有可用測試的清單。
與 Apache Arrow 的相容性#
透過實作兩個方法,ExtensionArray
可以支援轉換至 / 從 pyarrow
陣列(因此支援例如序列化至 Parquet 檔案格式):ExtensionArray.__arrow_array__
和 ExtensionDtype.__from_arrow__
。
確保 ExtensionArray.__arrow_array__
pyarrow
知道如何將特定延伸陣列轉換為
(也包括在 pandas DataFrame 中作為一欄時)pyarrow.Array
class MyExtensionArray(ExtensionArray):
...
def __arrow_array__(self, type=None):
# convert the underlying array values to a pyarrow Array
import pyarrow
return pyarrow.array(..., type=type)
然後,
方法控制從 pyarrow 轉換回 pandas ExtensionArray。此方法接收 pyarrow ExtensionDtype.__from_arrow__
或 Array
作為唯一引數,預期會傳回適當的 pandas ChunkedArray
,以取得此資料型態和傳遞的值ExtensionArray
class ExtensionDtype:
...
def __from_arrow__(self, array: pyarrow.Array/ChunkedArray) -> ExtensionArray:
...
在 Arrow 文件 中查看更多資訊。
這些方法已實作於 pandas 中包含的可為空整數和字串延伸資料型態,並確保往返 pyarrow 和 Parquet 檔案格式。
子類化 pandas 資料結構#
本節說明如何子類化
資料結構,以滿足更特定的需求。有兩點需要注意pandas
覆寫建構函數屬性。
定義原始屬性
注意
您可以在 geopandas 專案中找到一個不錯的範例。
覆寫建構函數屬性#
每個資料結構都有幾個建構函數屬性,可用於傳回新的資料結構作為運算的結果。透過覆寫這些屬性,您可以透過 pandas
資料處理保留子類別。
子類別上可以定義 3 個可能的建構函數屬性
DataFrame/Series._constructor
:用於處理結果與原始結果具有相同維度時。DataFrame._constructor_sliced
:用於DataFrame
(子)類別處理結果應為Series
(子)類別時。Series._constructor_expanddim
:用於Series
(子)類別處理結果應為DataFrame
(子)類別時,例如Series.to_frame()
。
以下範例顯示如何定義 SubclassedSeries
和 SubclassedDataFrame
,並覆寫建構函數屬性。
class SubclassedSeries(pd.Series):
@property
def _constructor(self):
return SubclassedSeries
@property
def _constructor_expanddim(self):
return SubclassedDataFrame
class SubclassedDataFrame(pd.DataFrame):
@property
def _constructor(self):
return SubclassedDataFrame
@property
def _constructor_sliced(self):
return SubclassedSeries
>>> s = SubclassedSeries([1, 2, 3])
>>> type(s)
<class '__main__.SubclassedSeries'>
>>> to_framed = s.to_frame()
>>> type(to_framed)
<class '__main__.SubclassedDataFrame'>
>>> df = SubclassedDataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
>>> df
A B C
0 1 4 7
1 2 5 8
2 3 6 9
>>> type(df)
<class '__main__.SubclassedDataFrame'>
>>> sliced1 = df[["A", "B"]]
>>> sliced1
A B
0 1 4
1 2 5
2 3 6
>>> type(sliced1)
<class '__main__.SubclassedDataFrame'>
>>> sliced2 = df["A"]
>>> sliced2
0 1
1 2
2 3
Name: A, dtype: int64
>>> type(sliced2)
<class '__main__.SubclassedSeries'>
定義原始屬性#
若要讓原始資料結構具有其他屬性,您應讓 pandas
知道新增了哪些屬性。 pandas
會將未知屬性對應至資料名稱,並覆寫 __getattribute__
。定義原始屬性有兩種方式
定義
_internal_names
和_internal_names_set
以用於不會傳遞給處理結果的暫時屬性。定義
_metadata
以用於會傳遞給處理結果的常規屬性。
以下是一個範例,用於定義兩個原始屬性,將「internal_cache」定義為暫時屬性,將「added_property」定義為常規屬性
class SubclassedDataFrame2(pd.DataFrame):
# temporary properties
_internal_names = pd.DataFrame._internal_names + ["internal_cache"]
_internal_names_set = set(_internal_names)
# normal properties
_metadata = ["added_property"]
@property
def _constructor(self):
return SubclassedDataFrame2
>>> df = SubclassedDataFrame2({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
>>> df
A B C
0 1 4 7
1 2 5 8
2 3 6 9
>>> df.internal_cache = "cached"
>>> df.added_property = "property"
>>> df.internal_cache
cached
>>> df.added_property
property
# properties defined in _internal_names is reset after manipulation
>>> df[["A", "B"]].internal_cache
AttributeError: 'SubclassedDataFrame2' object has no attribute 'internal_cache'
# properties defined in _metadata are retained
>>> df[["A", "B"]].added_property
property
繪製後端#
pandas 可以透過第三方繪製後端進行擴充。其主要概念是讓使用者選擇不同於基於 Matplotlib 所提供的繪製後端。例如
>>> pd.set_option("plotting.backend", "backend.module")
>>> pd.Series([1, 2, 3]).plot()
這或多或少等同於
>>> import backend.module
>>> backend.module.plot(pd.Series([1, 2, 3]))
後端模組接著可以使用其他視覺化工具(Bokeh、Altair 等)來產生繪製結果。
實作繪製後端的函式庫應使用 進入點,讓 pandas 可以偵測到其後端。其金鑰為 "pandas_plotting_backends"
。例如,pandas 會以下列方式註冊預設的「matplotlib」後端。
# in setup.py
setup( # noqa: F821
...,
entry_points={
"pandas_plotting_backends": [
"matplotlib = pandas:plotting._matplotlib",
],
},
)
有關如何實作第三方繪製後端的詳細資訊,請參閱 pandas-dev/pandas。
與第三方類型進行算術運算#
若要控制自訂類型與 pandas 類型之間的運算方式,請實作 __pandas_priority__
。類似於 numpy 的 __array_priority__
語意,DataFrame
、Series
和 Index
物件上的運算方法會委派給 other
,如果它具有 __pandas_priority__
屬性且值較高。
預設情況下,pandas 物件會嘗試與其他物件進行運算,即使這些物件不是 pandas 已知的類型。
>>> pd.Series([1, 2]) + [10, 20]
0 11
1 22
dtype: int64
在上面的範例中,如果 [10, 20]
是可以理解為清單的自訂類型,pandas 物件仍會以相同的方式與它進行運算。
在某些情況下,將運算委派給其他類型會很有用。例如,假設我實作一個自訂清單物件,而且我希望將我的自訂清單與 pandas Series
相加的結果為我的清單實例,而不是如前一個範例中所示的 Series
。現在,這可以透過定義我的自訂清單的 __pandas_priority__
屬性,並將其設定為高於我想要運算的 pandas 物件優先權的值來達成。
的 __pandas_priority__
DataFrame
、Series
和 Index
分別為 4000
、3000
和 2000
。基本 ExtensionArray.__pandas_priority__
為 1000
。
class CustomList(list):
__pandas_priority__ = 5000
def __radd__(self, other):
# return `self` and not the addition for simplicity
return self
custom = CustomList()
series = pd.Series([1, 2, 3])
# Series refuses to add custom, since it's an unknown type with higher priority
assert series.__add__(custom) is NotImplemented
# This will cause the custom class `__radd__` being used instead
assert series + custom is custom