扩展熊猫# 虽然 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和APIpandas.api.extensions.ExtensionArray在 pandas 1.5 之前是实验性的。从 1.5 版本开始,未来的更改将遵循pandas 弃用政策。 pandas 定义了一个接口,用于实现扩展 NumPy 类型系统的数据类型和数组。 pandas 本身使用 NumPy 中未内置的某些类型的扩展系统(分类、周期、间隔、带时区的日期时间)。 库可以定义自定义数组和数据类型。当pandas遇到这些对象时,它们将被正确处理(即不转换为对象的ndarray)。许多方法(例如)pandas.isna()将分派到扩展类型的实现。 如果您正在构建一个实现该接口的库,请在生态系统页面上公开它。 该接口由两个类组成。 ExtensionDtype# Apandas.api.extensions.ExtensionDtype类似于一个numpy.dtype对象。它描述了数据类型。实施者负责一些独特的项目,例如名称。 一项特别重要的项目是type财产。这应该是数据标量类型的类。例如,如果您正在为 IP 地址数据编写扩展数组,则这可能是ipaddress.IPv4Address. 有关接口定义,请参阅扩展 dtype 源。 pandas.api.extensions.ExtensionDtype可以注册到 pandas 以允许通过字符串数据类型名称进行创建。这允许人们实例化Series并使用注册的字符串.astype()名称,例如.'category'CategoricalDtype 有关如何注册 dtype 的更多信息,请参阅扩展 dtype dtypes 。 ExtensionArray# 此类提供所有类似数组的功能。 ExtensionArray 仅限于一维。 ExtensionArray 通过 dtype属性链接到 ExtensionDtype。 __new__pandas 对如何通过或创建扩展数组没有任何限制 __init__,并且对如何存储数据没有限制。我们确实要求您的数组可转换为 NumPy 数组,即使这相对昂贵(因为它是Categorical)。 它们可能不受、一个或多个 NumPy 数组的支持。例如, pandas.Categorical是一个由两个数组支持的扩展数组,一个用于代码,一个用于类别。 IPv6 地址数组可以由具有两个字段的 NumPy 结构数组支持,一个用于低 64 位,一个用于高 64 位。或者它们可能由其他一些存储类型支持,例如 Python 列表。 有关接口定义,请参阅扩展数组源。文档字符串和注释包含正确实现接口的指南。 ExtensionArray运营商支持# 默认情况下,没有为该类定义任何运算符ExtensionArray。有两种方法可以为 ExtensionArray 提供操作员支持: 定义子类中的每个运算符ExtensionArray。 使用 pandas 的运算符实现,该实现依赖于已在 ExtensionArray 的基础元素(标量)上定义的运算符。 笔记 无论采用哪种方法,您可能需要设置__array_priority__ 是否希望在涉及 NumPy 数组的二进制运算时调用您的实现。 对于第一种方法,您定义您希望子类支持的选定运算符,例如__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 ndarray 之间的操作保持一致,我们建议不要在二进制操作中处理系列和索引。相反,您应该检测这些情况并返回NotImplemented。当pandas遇到像这样的操作时,pandas会op(Series, ExtensionArray) Series从( Series.array)中取消数组装箱 称呼result = op(values, ExtensionArray) 将结果重新装箱Series NumPy 通用函数# Series实现__array_ufunc__.作为实现的一部分,pandas 从ExtensionArray中拆箱Series,应用 ufunc,并在必要时重新装箱。 如果适用,我们强烈建议您__array_ufunc__在扩展数组中实现以避免强制 ndarray。 有关示例,请参阅 NumPy 文档。 作为实施的一部分,我们要求您在 中检测到 pandas 容器(Series、DataFrame、Index)时遵循 pandas inputs。如果存在其中任何一个,您应该返回NotImplemented。 pandas 将负责从容器中拆箱数组,并使用拆开的输入重新调用 ufunc。 测试扩展数组# 我们提供了一个测试套件,以确保您的扩展阵列满足预期的行为。要使用测试套件,您必须提供几个 pytest 固定装置并从基本测试类继承。所需的装置可在 pandas-dev/pandas中找到。 要使用测试,请将其子类化: from pandas.tests.extension import base class TestConstructors(base.BaseConstructorsTests): pass 有关所有可用测试的列表,请参阅pandas-dev/pandas 。 与 Apache Arrow 的兼容性# AnExtensionArray可以通过实现两种方法来支持与数组之间的转换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 Array或ChunkedArray作为唯一参数,并预计返回ExtensionArray此 dtype 和传递的值的适当 pandas: class ExtensionDtype: ... def __from_arrow__(self, array: pyarrow.Array/ChunkedArray) -> ExtensionArray: ... 请参阅Arrow 文档了解更多信息。 这些方法已针对 pandas 中包含的可空整数和字符串扩展数据类型实现,并确保与 pyarrow 和 Parquet 文件格式的往返。 子类化 pandas 数据结构# 警告 pandas在考虑子类化数据结构之前,有一些更简单的选择。 带管道的可扩展方法链 使用组合。看这里。 通过注册访问器进行扩展 按扩展类型扩展 本节介绍如何对pandas数据结构进行子类化以满足更具体的需求。有两点需要注意: 覆盖构造函数属性。 定义原始属性 笔记 你可以在geopandas项目中找到一个很好的例子。 覆盖构造函数属性# 每个数据结构都有多个构造函数属性,用于返回新的数据结构作为操作的结果。通过重写这些属性,您可以通过pandas数据操作保留子类。 子类上可以定义 3 种构造函数属性: DataFrame/Series._constructor:当操作结果与原始尺寸相同时使用。 DataFrame._constructor_slicedDataFrame:当(子)类操作结果应该是(子)类时使用Series。 Series._constructor_expanddimSeries:当(子)类操作结果应该是(子)类时使用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中找到。 与第 3 方类型的算术# 为了控制自定义类型和 pandas 类型之间算术的工作方式,请实现__pandas_priority__.与 numpy 的语义类似,如果、和对象具有更高值的属性,则 、 和 对象__array_priority__ 上的算术方法 将委托给, 。DataFrameSeriesIndexother__pandas_priority__ 默认情况下,pandas 对象会尝试与其他对象进行操作,即使它们不是 pandas 已知的类型: >>> pd.Series([1, 2]) + [10, 20] 0 11 1 22 dtype: int64 在上面的例子中,如果是一个可以理解为列表的自定义类型,pandas 对象仍然会以相同的方式对其进行操作。[10, 20] 在某些情况下,将操作委托给其他类型很有用。例如,假设我实现了一个自定义列表对象,并且我希望使用 pandas 添加自定义列表的结果Series是我的列表的实例,而不是Series前面示例中所示的 a。现在可以通过定义__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