延伸大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请求,将其添加到我们的 大Pandas生态系统 佩奇。

我们强烈建议您验证访问者的 __init__ 。在我们的 GeoAccessor ,我们验证数据是否包含预期的列,引发 AttributeError 当验证失败时。为. Series 访问者,则应验证 dtype 如果访问器仅应用于某些数据类型。

扩展类型#

警告

这个 pandas.api.extensions.ExtensionDtypepandas.api.extensions.ExtensionArray API是新的和试验性的。它们可能会在没有警告的情况下在不同版本之间发生变化。

Pandas定义了用于实现数据类型和数组的接口,该接口 延伸 NumPy的类型系统。Pandas本身对NumPy中没有内置的一些类型使用扩展系统(分类、期间、间隔、带有时区的日期时间)。

库可以定义自定义数组和数据类型。当Pandas遇到这些物体时,它们将得到适当的处理(即不会转换为ndarray物体)。很多方法,比如 pandas.isna() 将调度到扩展类型的实现。

如果您正在构建实现该接口的库,请在 扩展模块数据类型

该接口由两个类组成。

ExtensionDtype#

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

其中一个特别重要的项目是 type 财产。这应该是数据的标量类型的类。例如,如果要为IP地址数据编写扩展数组,则可能是 ipaddress.IPv4Address

请参阅 extension dtype source 用于接口定义。

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

请参阅 extension dtype dtypes 有关如何注册数据类型的更多信息。

ExtensionArray#

这个类提供了所有类似数组的功能。ExtensionArray限制为1维。属性链接到ExtensionDtype。 dtype 属性。

Pandas对如何通过其 __new____init__ ,并且对您存储数据的方式没有限制。我们确实要求您的阵列可以转换为NumPy阵列,即使这相对昂贵(因为 Categorical )。

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

请参阅 extension array source 用于接口定义。文档字符串和注释包含正确实现接口的指导。

ExtensionArray 操作员支持#

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

  1. 定义每个运算符 ExtensionArray 子类。

  2. 使用Pandas中的运算符实现,该实现依赖于已在Extension数组的基础元素(标量)上定义的运算符。

备注

无论采用哪种方法,您都可能希望设置 __array_priority__ 如果希望在涉及NumPy数组的二进制操作时调用您的实现。

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

第二种方法假设的底层元素(即标量类型) ExtensionArray 已经定义了各个运算符。换句话说,如果你的 ExtensionArray 已命名 MyExtensionArray 实现,以便每个元素都是类的一个实例 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 ndarray之间的操作保持一致,我们建议 not 处理二进制操作中的序列和索引。相反,您应该检测到这些情况并返回 NotImplemented 。当Pandas遇到像这样的操作 op(Series, ExtensionArray) ,大Pandas将

  1. unbox the array from the Series (Series.array)

  2. 打电话 result = op(values, ExtensionArray)

  3. 将结果重新装箱到 Series

NumPy泛函#

Series 机具 __array_ufunc__ 。作为实施的一部分,Pandas打开了 ExtensionArraySeries ,应用ufunc,并在必要时重新装箱。

如果适用,我们强烈建议您实现 __array_ufunc__ in your extension array to avoid coercion to an ndarray. See the NumPy documentation 举个例子。

作为您实施的一部分,我们要求您在Pandas容器 (SeriesDataFrameIndex )被检测到 inputs 。如果其中任何一个都存在,您应该返回 NotImplemented 。Pandas将负责从容器中取消数组的装箱,并使用取消包装的输入重新调用ufunc。

测试扩展阵列#

我们提供了一套测试套件来确保您的扩展阵列满足预期行为。要使用测试套件,您必须提供几个pytest装置并从基本测试类继承。所需的夹具可在https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/conftest.py.中找到

要使用测试,请将其派生为子类:

from pandas.tests.extension import base


class TestConstructors(base.BaseConstructorsTests):
    pass

看见 https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/base/__init__. Py,查看所有可用测试的列表。

与阿帕奇箭头的兼容性#

一个 ExtensionArray 可以支持转换为/从 pyarrow 数组(因此支持串行化为Parquet文件格式),通过实现两种方法: ExtensionArray.__arrow_array__ExtensionDtype.__from_arrow__

这个 ExtensionArray.__arrow_array__ 确保 pyarrow 知道如何将特定扩展数组转换为 pyarrow.Array (在PandasDataFrame中作为列包含时也是如此):

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 Extension数组的转换。此方法接收一个yarrow ArrayChunkedArray 作为唯一的论据,预计将返回适当的Pandas ExtensionArray 对于此数据类型和传递的值:

class ExtensionDtype:
    ...

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

有关详细信息,请参阅 Arrow documentation

这些方法已经为Pandas中包含的可为空的整数和字符串扩展数据类型实现,并确保往返到yarrow和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()

下面的示例显示如何定义 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”作为临时属性,而“ADDITED_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

打印后端#

从0.25开始,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",
        ],
    },
)

有关如何实施第三方绘图后端的更多信息,请访问 https://github.com/pandas-dev/pandas/blob/main/pandas/plotting/__init__. PY#L1.