Расширение 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 если аксессор применяется только к определенным типам данных.

Расширенные типы#

Примечание

The pandas.api.extensions.ExtensionDtype и pandas.api.extensions.ExtensionArray API были experimental до pandas 1.5. Начиная с версии 1.5, будущие изменения будут следовать политика устаревания pandas.

pandas определяет интерфейс для реализации типов данных и массивов, которые расширить Система типов NumPy. pandas сам использует систему расширений для некоторых типов, которые не встроены в NumPy (категориальный, период, интервал, datetime с часовым поясом).

Библиотеки могут определить пользовательский массив и тип данных. Когда pandas встречает эти объекты, они будут обработаны правильно (т.е. не преобразованы в ndarray объектов). Многие методы, такие как pandas.isna() будет перенаправлен на реализацию расширенного типа.

Если вы создаете библиотеку, реализующую интерфейс, пожалуйста, опубликуйте ее на страница экосистемы.

Интерфейс состоит из двух классов.

ExtensionDtype#

A pandas.api.extensions.ExtensionDtype похож на numpy.dtype объект. Он описывает тип данных. Разработчики отвечают за несколько уникальных элементов, таких как имя.

Один особенно важный элемент - это type свойство. Это должен быть класс, который является скалярным типом для ваших данных. Например, если вы пишете расширенный массив для данных IP-адресов, это может быть ipaddress.IPv4Address.

См. источник типа данных расширения для определения интерфейса.

pandas.api.extensions.ExtensionDtype может быть зарегистрирован в pandas для создания через строковое имя типа данных. Это позволяет создавать Series и .astype() с зарегистрированным строковым именем, например 'category' является зарегистрированным строковым аксессором для CategoricalDtype.

См. типы данных расширения dtypes подробнее о том, как регистрировать dtypes.

ExtensionArray#

Этот класс предоставляет всю функциональность, подобную массиву. ExtensionArrays ограничены 1 измерением. ExtensionArray связан с ExtensionDtype через dtype атрибут.

pandas не накладывает ограничений на то, как массив расширения создаётся через его __new__ или __init__, и не накладывает ограничений на то, как вы храните свои данные. Мы требуем, чтобы ваш массив был преобразуем в массив NumPy, даже если это относительно дорого (как для Categorical).

Они могут поддерживаться ни одним, одним или несколькими массивами NumPy. Например, pandas.Categorical является массивом расширения, поддерживаемым двумя массивами: один для кодов и один для категорий. Массив IPv6-адресов может быть поддержан структурированным массивом NumPy с двумя полями: одно для младших 64 битов и одно для старших 64 битов. Или они могут быть поддержаны другим типом хранения, например, списками Python.

См. источник массива расширения для определения интерфейса. Документационные строки и комментарии содержат руководство по правильной реализации интерфейса.

ExtensionArray метка или список меток, опционально#

По умолчанию для класса не определены операторы ExtensionArray. Существует два подхода для предоставления поддержки операторов для вашего ExtensionArray:

  1. Определите каждый из операторов на вашем ExtensionArray подкласс.

  2. Используйте реализацию оператора из pandas, которая зависит от операторов, уже определенных на базовых элементах (скалярах) ExtensionArray.

Примечание

Независимо от подхода, вы можете установить __array_priority__ если вы хотите, чтобы ваша реализация вызывалась при участии в бинарных операциях с массивами NumPy.

Для первого подхода вы определяете выбранные операторы, например, __add__, __le__, и т.д., которые вы хотите, чтобы ваши ExtensionArray подкласс для поддержки.

Второй подход предполагает, что базовые элементы (т.е. скалярный тип) из ExtensionArray уже имеют определённые отдельные операторы. Другими словами, если ваш ExtensionArray named 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, мы рекомендуем не обработка Series и Indexes в ваших бинарных операциях. Вместо этого вы должны обнаруживать эти случаи и возвращать NotImplemented. Когда pandas встречает операцию типа op(Series, ExtensionArray), pandas будет

  1. распаковать массив из Series (Series.array)

  2. и всё остальное либо result = op(values, ExtensionArray)

  3. переупаковать результат в Series

Универсальные функции NumPy#

Series реализует __array_ufunc__. В рамках реализации, pandas распаковывает ExtensionArray из Series, применяет ufunc, и переупаковывает его при необходимости.

Если применимо, мы настоятельно рекомендуем реализовать __array_ufunc__ в вашем массиве расширения, чтобы избежать приведения к ndarray. См. документация NumPy для примера.

В рамках вашей реализации мы требуем, чтобы вы делегировали pandas, когда контейнер pandas (Series, DataFrame, Index) обнаруживается в inputs. Если присутствует любой из них, вы должны вернуть NotImplemented. pandas позаботится о распаковке массива из контейнера и повторном вызове ufunc с развернутым входом.

Тестирование расширенных массивов#

Мы предоставляем набор тестов для проверки, что ваши массивы расширений соответствуют ожидаемому поведению. Чтобы использовать набор тестов, необходимо предоставить несколько фикстур pytest и унаследовать от базового тестового класса. Необходимые фикстуры находятся в pandas-dev/pandas.

Чтобы использовать тест, создайте его подкласс:

from pandas.tests.extension import base


class TestConstructors(base.BaseConstructorsTests):
    pass

См. pandas-dev/pandas для списка всех доступных тестов.

Совместимость с Apache Arrow#

An ExtensionArray может поддерживать преобразование в / из pyarrow массивы (и, следовательно, поддержку, например, сериализации в формат файла Parquet) путем реализации двух методов: ExtensionArray.__arrow_array__ и ExtensionDtype.__from_arrow__.

The 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)

The ExtensionDtype.__from_arrow__ метод затем управляет преобразованием обратно из pyarrow в pandas ExtensionArray. Этот метод получает pyarrow Array или ChunkedArray в качестве единственного аргумента и ожидается, что вернет соответствующий pandas ExtensionArray для этого dtype и переданных значений:

class ExtensionDtype:
    ...

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

Подробнее в Документация Arrow.

Эти методы были реализованы для расширенных типов данных nullable integer и string, включённых в 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().

Пример ниже показывает, как определить 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)


>>> to_framed = s.to_frame()
>>> type(to_framed)


>>> 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)


>>> sliced1 = df[["A", "B"]]
>>> sliced1
   A  B
0  1  4
1  2  5
2  3  6

>>> type(sliced1)


>>> sliced2 = df["A"]
>>> sliced2
0    1
1    2
2    3
Name: A, dtype: int64

>>> type(sliced2)

Определить исходные свойства#

Чтобы позволить исходным структурам данных иметь дополнительные свойства, вы должны позволить pandas знать, какие свойства добавлены. pandas сопоставляет неизвестные свойства с именами данных, переопределяя __getattribute__. Определение исходных свойств может быть выполнено одним из 2 способов:

  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's __array_priority__ семантика, арифметические методы для DataFrame, Series, и Index объекты будут делегировать 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, с которыми я хочу работать.

The __pandas_priority__ of 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