擴充套件 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 物件而不對其進行子類化的方法。如果您編寫了一個自定義訪問器,請提交一個 pull request 將其新增到我們的 生態系統 頁面。

我們強烈建議驗證您訪問器 __init__ 中的資料。在我們的 GeoAccessor 中,我們驗證資料是否包含預期的列,並在驗證失敗時引發 AttributeError。對於 Series 訪問器,如果訪問器僅適用於某些 dtype,則應驗證 dtype

擴充套件型別#

注意

在 pandas 1.5 之前,pandas.api.extensions.ExtensionDtypepandas.api.extensions.ExtensionArray API 是實驗性的。從 1.5 版本開始,未來的更改將遵循 pandas 棄用策略

pandas 定義了一個介面,用於實現擴充套件 NumPy 型別的型別和陣列。pandas 本身使用擴充套件系統來處理一些 NumPy 未內建的型別(分類、週期、區間、帶時區的日期時間)。

庫可以定義自定義陣列和資料型別。當 pandas 遇到這些物件時,它們將被正確處理(即不會轉換為物件 ndarray)。許多函式,如 pandas.isna(),將分派到擴充套件型別的實現。

如果您正在構建一個實現該介面的庫,請在 生態系統頁面 上進行宣傳。

該介面包含兩個類。

ExtensionDtype#

一個 pandas.api.extensions.ExtensionDtype 類似於 numpy.dtype 物件。它描述了資料型別。實現者負責一些獨特的項,如名稱。

一個特別重要的項是 type 屬性。這應該是您資料的標量型別的類。例如,如果您正在為 IP 地址資料編寫擴充套件陣列,這可能是 ipaddress.IPv4Address

有關介面定義的更多資訊,請參閱 擴充套件 dtype 原始碼

可以將 pandas.api.extensions.ExtensionDtype 註冊到 pandas,以便透過字串 dtype 名稱進行建立。這允許人們例項化 Series 和使用註冊的字串名稱進行 .astype(),例如 'category'CategoricalDtype 的註冊字串訪問器。

有關如何註冊 dtype 的更多資訊,請參閱 擴充套件 dtype 原始碼

ExtensionArray#

此類提供了所有類陣列的功能。ExtensionArrays 僅限於 1 維。ExtensionArray 透過 dtype 屬性連結到 ExtensionDtype。

pandas 對如何透過 __new____init__ 建立擴充套件陣列沒有任何限制,並且對如何儲存資料也沒有任何限制。我們要求您的陣列可轉換為 NumPy 陣列,即使這相對昂貴(如 Categorical)。

它們可以由零個、一個或多個 NumPy 陣列支援。例如,pandas.Categorical 是由兩個陣列支援的擴充套件陣列,一個用於程式碼,一個用於類別。IPv6 地址陣列可以由一個包含兩個欄位(一個用於低 64 位,一個用於高 64 位)的 NumPy 結構化陣列支援。或者它們可以由其他儲存型別支援,例如 Python 列表。

有關介面定義的更多資訊,請參閱 擴充套件陣列原始碼。文件字串和註釋包含有關正確實現介面的指導。

ExtensionArray 運算子支援#

預設情況下,ExtensionArray 類沒有定義運算子。有兩種方法可以為您的 ExtensionArray 提供運算子支援

  1. 在您的 ExtensionArray 子類上定義每個運算子。

  2. 使用 pandas 中一個依賴於 ExtensionArray 底層元素(標量)上已定義的運算子的運算子實現。

注意

無論採用哪種方法,如果您希望在與 NumPy 陣列進行二元運算時呼叫您的實現,您可能需要設定 __array_priority__

對於第一種方法,您定義選定的運算子,例如 __add____le__ 等,您希望您的 ExtensionArray 子類支援這些運算子。

第二種方法假定 ExtensionArray 的底層元素(即標量型別)已經定義了單獨的運算子。換句話說,如果您的名為 MyExtensionArray 的 ExtensionArray 實現方式是每個元素都是 MyExtensionElement 類的例項,那麼如果為 MyExtensionElement 定義了運算子,第二種方法將自動為 MyExtensionArray 定義運算子。

混合類 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.__arrow_array__ExtensionDtype.__from_arrow__ExtensionArray 可以支援與 pyarrow 陣列的雙向轉換(從而支援例如序列化為 Parquet 檔案格式)。

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 作為引數,並應返回適合該 dtype 和傳入值的 pandas ExtensionArray

class ExtensionDtype:
    ...

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

有關更多資訊,請參閱 Arrow 文件

這些方法已為 pandas 中包含的可空整數和字串擴充套件 dtype 實現,並確保了與 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 物件更高的值,現在就可以實現這一點。

DataFrameSeriesIndex__pandas_priority__ 分別是 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