擴充 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.ExtensionDtypepandas.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 提供運算子支援

  1. ExtensionArray 子類別上定義每個運算子。

  2. 使用 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 將會

  1. Series (Series.array) 中取出陣列

  2. 呼叫 result = op(values, ExtensionArray)

  3. 將結果重新放入 Series

NumPy 通用函數#

Series 實作 __array_ufunc__。作為實作的一部分,pandas 會從 Series 中取出 ExtensionArray,套用 ufunc,並在必要時重新放入。

如果適用,我們強烈建議您在延伸陣列中實作 __array_ufunc__ 以避免強制轉換為 ndarray。請參閱 NumPy 文件 以取得範例。

在您的實作中,我們要求您在 inputs 中偵測到 pandas 容器(SeriesDataFrameIndex)時,將其遞延至 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 知道如何將特定延伸陣列轉換為 pyarrow.Array (也包括在 pandas DataFrame 中作為一欄時)

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)

然後,ExtensionDtype.__from_arrow__ 方法控制從 pyarrow 轉換回 pandas ExtensionArray。此方法接收 pyarrow ArrayChunkedArray 作為唯一引數,預期會傳回適當的 pandas ExtensionArray,以取得此資料型態和傳遞的值

class ExtensionDtype:
    ...

    def __from_arrow__(self, array: pyarrow.Array/ChunkedArray) -> ExtensionArray:
        ...

Arrow 文件 中查看更多資訊。

這些方法已實作於 pandas 中包含的可為空整數和字串延伸資料型態,並確保往返 pyarrow 和 Parquet 檔案格式。

子類化 pandas 資料結構#

警告

在考慮子類化 pandas 資料結構之前,有一些較為簡單的替代方案。

  1. 使用 pipe 進行可延伸的方法鏈

  2. 使用組合。請參閱 此處

  3. 透過 註冊存取器 進行延伸

  4. 透過 延伸類型 進行延伸

本節說明如何子類化 pandas 資料結構,以滿足更特定的需求。有兩點需要注意

  1. 覆寫建構函數屬性。

  2. 定義原始屬性

注意

您可以在 geopandas 專案中找到一個不錯的範例。

覆寫建構函數屬性#

每個資料結構都有幾個建構函數屬性,可用於傳回新的資料結構作為運算的結果。透過覆寫這些屬性,您可以透過 pandas 資料處理保留子類別。

子類別上可以定義 3 個可能的建構函數屬性

  • DataFrame/Series._constructor:用於處理結果與原始結果具有相同維度時。

  • DataFrame._constructor_sliced:用於 DataFrame (子)類別處理結果應為 Series (子)類別時。

  • Series._constructor_expanddim:用於 Series (子)類別處理結果應為 DataFrame (子)類別時,例如 Series.to_frame()

以下範例顯示如何定義 SubclassedSeriesSubclassedDataFrame,並覆寫建構函數屬性。

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__。定義原始屬性有兩種方式

  1. 定義 _internal_names_internal_names_set 以用於不會傳遞給處理結果的暫時屬性。

  2. 定義 _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__ 語意,DataFrameSeriesIndex 物件上的運算方法會委派給 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__ DataFrameSeriesIndex 分別為 400030002000。基本 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