扩展 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 将其添加到我们的 ecosystem 页面。

我们强烈建议在 accessor 的 __init__ 中验证数据。在我们的 GeoAccessor 中,我们会验证数据是否包含预期的列,并在验证失败时引发 AttributeError。对于 Series accessor,如果 accessor 只适用于特定 dtype,您应该验证 dtype

扩展类型#

备注

pandas.api.extensions.ExtensionDtypepandas.api.extensions.ExtensionArray API 在 pandas 1.5 之前是实验性的。从 1.5 版本开始,未来的更改将遵循 pandas deprecation policy

pandas 定义了一个接口,用于实现*扩展* NumPy 类型系统的_数据_类型和数组。pandas 本身将扩展系统用于一些 NumPy 中没有内置的类型(分类、周期、区间、带时区的日期时间)。

库可以定义自定义数组和数据类型。当 pandas 遇到这些对象时,它们将被正确处理(即不会转换为对象的 ndarray)。许多方法(如 pandas.isna() )将分发到扩展类型的实现。

如果您正在构建一个实现该接口的库,请在 the ecosystem page 上宣传它。

该接口包含两个类。

ExtensionDtype#

pandas.api.extensions.ExtensionDtype 类似于 numpy.dtype 对象。它描述了数据类型。实现者负责一些独特的项目,如名称。

一个特别重要的项目是 type 属性。这应该是您数据标量类型的类。例如,如果您正在为 IP 地址数据编写一个扩展数组,这可能是 ipaddress.IPv4Address

请参阅 extension dtype source 以获取接口定义。

pandas.api.extensions.ExtensionDtype 可以注册到 pandas,以便通过字符串 dtype 名称进行创建。这允许实例化 Series 和使用注册的字符串名称进行 .astype(),例如 'category'CategoricalDtype 的注册字符串访问器。

有关如何注册 dtype 的更多信息,请参阅 extension dtype dtypes

ExtensionArray#

此类提供所有类数组的功能。ExtensionArrays 仅限于 1 维。ExtensionArray 通过 dtype 属性链接到 ExtensionDtype。

pandas 对通过 __new____init__ 创建扩展数组没有限制,并且对您存储数据的方式也没有限制。我们确实要求您的数组能够转换为 NumPy 数组,即使这可能相对昂贵(如 Categorical 的情况)。

它们可以由零个、一个或多个 NumPy 数组支持。例如,pandas.Categorical 是一个由两个数组支持的扩展数组,一个用于代码,一个用于类别。IPv6 地址数组可以由一个具有两个字段的 NumPy 结构化数组支持,一个用于低 64 位,一个用于高 64 位。或者它们可以由其他存储类型(如 Python 列表)支持。

请参阅 extension array source 以获取接口定义。文档字符串和注释中包含有关正确实现接口的指南。

ExtensionArray 运算符支持#

默认情况下,ExtensionArray 类没有定义运算符。有两种方法可以为您的 ExtensionArray 提供运算符支持:

  1. 在您的 ExtensionArray 子类中定义每个运算符。

  2. 使用 pandas 的运算符实现,该实现依赖于 ExtensionArray 的底层元素(标量)上已定义的运算符。

备注

无论采用哪种方法,如果您希望在与 NumPy 数组进行二元运算时调用您的实现,您可能需要设置 __array_priority__

对于第一种方法,您定义所选的运算符,例如 __add____le__ 等,您希望您的 ExtensionArray 子类支持。

第二种方法假定 ExtensionArray 的底层元素(即标量类型)已经定义了单个运算符。换句话说,如果您的名为 MyExtensionArray 的 ExtensionArray 实现使得每个元素都是 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。 有关示例,请参见 the NumPy documentation

作为实现的一部分,我们要求你在 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 作为唯一参数,并应返回此 dtype 和传入值的相应 pandas ExtensionArray

class ExtensionDtype:
    ...

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

有关更多信息,请参阅 Arrow documentation

pandas 中包含的可空整数和字符串扩展数据类型的这些方法已实现,并确保了与 pyarrow 和 Parquet 文件格式的往返转换。

子类化 pandas 数据结构#

警告

在考虑子类化 pandas 数据结构之前,有一些更简单的替代方案。

  1. 使用 pipe 进行可扩展的方法链

  2. 使用*组合*。 请参阅 here

  3. 通过 registering an accessor 进行扩展

  4. 通过 extension type 进行扩展

本节介绍如何通过继承``pandas``数据结构来满足更具体的需求。有两点需要注意:

  1. 重写构造器属性。

  2. 定义原生属性

备注

您可以在 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__``将未知属性映射到数据名称。定义原生属性可以通过以下两种方法之一实现:

  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 等)来生成图表。

实现绘图后端的库应该使用 entry points 来让 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 对象上的算术方法如果具有更高值的``__pandas_priority__``属性,则会委托给``other``。

默认情况下,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__``分别为``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