Расширение 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(object):
def __init__(self, pandas_obj):
self._validate(pandas_obj)
self._obj = pandas_obj
@staticmethod
def _validate(obj):
if 'lat' not in obj.columns or 'lon' not in obj.columns:
raise AttributeError("Must have 'lat' and 'lon'.")
@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 без их подклассирования. Если вы пишете собственный аксессор, создайте запрос на добавление его на страницу нашего Экосистемы Pandas.
Мы настоятельно рекомендуем проверять данные в __init__ вашего аксессора. В нашем GeoAccessor, мы проверяем, что данные содержат ожидаемые столбцы, генерируя AttributeError при ошибке проверки. Для Series аксессора, вы должны проверить dtype , если аксессор применяется только к определённым типам данных.
Типы расширений
Новое в версии 0.23.0.
Предупреждение
API pandas.api.extensions.ExtensionDtype и pandas.api.extensions.ExtensionArray являются новыми и экспериментальными. Они могут изменяться между версиями без предупреждения.
Pandas определяет интерфейс для реализации типов данных и массивов, которые расширяют систему типов NumPy. Сам Pandas использует систему расширений для некоторых типов, которые не встроены в NumPy (категориальные, период, интервал, datetime с часовым поясом).
Библиотеки могут определять пользовательские массивы и типы данных. Когда Pandas встречает эти объекты, они будут обработаны должным образом (т. е. не будут преобразованы в ndarray объектов). Многие методы, такие как pandas.isna(), будут направлены на реализацию расширенного типа.
Если вы разрабатываете библиотеку, которая реализует интерфейс, опубликуйте её на Типы данных расширений.
Интерфейс состоит из двух классов.
ExtensionDtype
A pandas.api.extensions.ExtensionDtype аналогичен объекту numpy.dtype. Он описывает тип данных. Реализаторы несут ответственность за несколько уникальных элементов, таких как имя.
Особо важным элементом является свойство type. Это должен быть класс, который является скалярным типом для ваших данных. Например, если вы пишете расширенный массив для данных IP-адресов, это может быть ipaddress.IPv4Address.
См. исходный код типа расширения для определения интерфейса.
Новое в версии 0.24.0.
pandas.api.extension.ExtensionDtype может быть зарегистрирован в Pandas для создания с помощью имени типа строки. Это позволяет создать Series и .astype() с зарегистрированным именем строки, например, 'category' — это зарегистрированный строковый аксессор для CategoricalDtype.
См. типы данных расширений для получения дополнительной информации о регистрации типов данных.
ExtensionArray
Этот класс предоставляет все функциональные возможности массива. Расширенные массивы ограничены 1 размерностью. Расширенный массив связан с расширенным типом данных через атрибут dtype.
Pandas не накладывает ограничений на то, как создается расширенный массив с помощью __new__ или __init__, и не накладывает ограничений на то, как вы храните свои данные. Мы требуем, чтобы ваш массив можно было преобразовать в массив NumPy, даже если это относительно дорого (как это происходит для Categorical).
Они могут быть основаны на нуле, одном или нескольких массивах NumPy. Например, pandas.Categorical — это расширенный массив, основанный на двух массивах, один для кодов, а другой для категорий. Массив IPv6-адресов может быть основан на структурированном массиве NumPy с двумя полями, одно для нижних 64 бит и одно для верхних 64 бит. Или они могут быть основаны на другом типе хранения, например, на списках Python.
См. исходный код расширенного массива для определения интерфейса. Стр. описание и комментарии содержат рекомендации по правильной реализации интерфейса.
ExtensionArray Поддержка операторов
Новое в версии 0.24.0.
По умолчанию для класса ExtensionArray не определены операторы. Существует два подхода для предоставления поддержки операторов для вашего расширенного массива:
- Определите каждый из операторов в подклассе
ExtensionArray. - Используйте реализацию оператора из Pandas, которая зависит от операторов, уже определённых для базовых элементов (скаляров) расширенного массива.
Примечание
Независимо от подхода, вы можете установить __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, мы рекомендуем не обрабатывать Series и Indexes в бинарных операциях. Вместо этого вы должны распознать эти случаи и вернуть NotImplemented. Когда Pandas сталкивается с операцией, такой как op(Series, ExtensionArray), pandas
- развернёт массив из
Series(Series.array) - вызовет
result = op(values, ExtensionArray) - снова завернёт результат в
Series
Тестирование расширенных массивов
Мы предоставляем тестовый набор для проверки того, что ваши расширенные массивы удовлетворяют ожидаемому поведению. Чтобы использовать тестовый набор, вам необходимо предоставить несколько фикстур pytest и унаследовать от базового тестового класса. Необходимые фикстуры находятся в https://github.com/pandas-dev/pandas/blob/master/pandas/tests/extension/conftest.py.
Чтобы использовать тест, подклассифицируйте его:
from pandas.tests.extension import base
class TestConstructors(base.BaseConstructorsTests):
pass
См. https://github.com/pandas-dev/pandas/blob/master/pandas/tests/extension/base/__init__.py для списка всех доступных тестов.
Наследование от структур данных Pandas
Предупреждение
Есть несколько более простых альтернатив, прежде чем рассматривать наследование от структур данных pandas.
- Цепочки расширяемых методов с pipe
- Используйте композицию. См. здесь.
- Расширение с помощью регистрации аксессора
- Расширение с помощью типа расширения
Этот раздел описывает, как наследовать структуры данных pandas, чтобы удовлетворить более специфическим потребностям. Следует обратить внимание на два момента:
- Переопределить свойства конструктора.
- Определить исходные свойства
Примечание
Вы можете найти хороший пример в проекте geopandas.
Переопределение свойств конструктора
Каждая структура данных имеет несколько свойств конструктора для возвращения новой структуры данных в результате операции. Переопределяя эти свойства, вы можете сохранить подклассы через pandas манипуляции данными.
Следует определить 3 свойства конструктора:
-
_constructor: Используется, когда результат манипуляции имеет такие же размеры, как и исходные. -
_constructor_sliced: Используется, когда результат манипуляции имеет одну или несколько более низких размерностей, например, при нарезкеDataFrameотдельных столбцов. -
_constructor_expanddim: Используется, когда результат манипуляции имеет одну или несколько более высоких размерностей, например, приSeries.to_frame()иDataFrame.to_panel().
В следующей таблице показано, как структуры данных pandas определяют свойства конструктора по умолчанию.
| Атрибуты свойства | Series | DataFrame |
|---|---|---|
_constructor | Series | DataFrame |
_constructor_sliced | NotImplementedError | Series |
_constructor_expanddim | DataFrame | Panel |
Ниже приведен пример того, как определить 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
© 2008–2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/0.24.2/development/extending.html