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 オブジェクトをサブクラス化せずに拡張する便利な方法です。カスタムアクセサを作成する場合は、プルリクエストを作成して、エコシステムページに追加してください。
アクセサの __init__ でデータを検証することを強くお勧めします。私たちの GeoAccessor では、データに期待される列が含まれていることを検証し、検証に失敗した場合は AttributeError を発生させます。Series アクセサの場合、アクセサが特定の dtype にのみ適用される場合は dtype を検証する必要があります。
拡張型#
注
pandas.api.extensions.ExtensionDtype および pandas.api.extensions.ExtensionArray API は、pandas 1.5 より前は実験的でした。バージョン 1.5 以降、将来の変更は pandas 非推奨ポリシー に従います。
pandas は、NumPy の型システムを*拡張*するデータ型と配列を実装するためのインターフェースを定義します。pandas 自体は、NumPy に組み込まれていないいくつかの型 (カテゴリカル、期間、区間、タイムゾーン付き datetime) に拡張システムを使用しています。
ライブラリはカスタム配列とデータ型を定義できます。pandas がこれらのオブジェクトに遭遇すると、それらは適切に処理されます (つまり、オブジェクトの ndarray に変換されません)。pandas.isna() のような多くのメソッドは、拡張型の実装にディスパッチされます。
インターフェースを実装するライブラリを構築する場合は、エコシステムページで公開してください。
インターフェースは2つのクラスで構成されます。
ExtensionDtype#
pandas.api.extensions.ExtensionDtype は numpy.dtype オブジェクトに似ています。データ型を記述します。実装者は、名前のようないくつかの固有の項目を担当します。
特に重要な項目は type プロパティです。これは、データのスカラ型であるクラスである必要があります。たとえば、IPアドレスデータ用の拡張配列を作成している場合、これは ipaddress.IPv4Address になる可能性があります。
インターフェースの定義については、拡張 dtype のソースを参照してください。
pandas.api.extensions.ExtensionDtype は pandas に登録でき、文字列の dtype 名を介した作成を許可します。これにより、登録された文字列名で Series と .astype() をインスタンス化できます。たとえば、'category' は CategoricalDtype の登録された文字列アクセサです。
dtype の登録方法については、拡張 dtype dtypes を参照してください。
ExtensionArray#
このクラスは、すべての配列のような機能を提供します。ExtensionArray は 1 次元に制限されます。ExtensionArray は、dtype 属性を介して ExtensionDtype にリンクされます。
pandas は、ExtensionArray が __new__ または __init__ を介してどのように作成されるかについて制限を設けず、データの保存方法についても制限を設けません。ただし、配列が NumPy 配列に変換可能である必要があります。これは、比較的コストがかかる場合でも同様です (Categorical の場合と同様)。
それらは、ゼロ、1つ、または多数の NumPy 配列によってバックアップされる場合があります。たとえば、pandas.Categorical は、コード用とカテゴリ用の2つの配列によってバックアップされる拡張配列です。IPv6 アドレスの配列は、下位64ビット用と上位64ビット用の2つのフィールドを持つ NumPy 構造化配列によってバックアップされる場合があります。または、Python リストのような他のストレージ型によってバックアップされる場合があります。
インターフェース定義については、拡張配列のソースを参照してください。ドキュメント文字列とコメントには、インターフェースを適切に実装するためのガイダンスが含まれています。
ExtensionArray 演算子サポート#
デフォルトでは、クラス ExtensionArray には演算子は定義されていません。ExtensionArray の演算子サポートを提供するには、2つのアプローチがあります。
ExtensionArrayサブクラスに各演算子を定義します。ExtensionArray の基となる要素 (スカラ) に既に定義されている演算子に依存する pandas の演算子実装を使用します。
注
どちらのアプローチを使用するかにかかわらず、NumPy 配列との二項演算に関与する場合に実装を呼び出したい場合は、__array_priority__ を設定することをお勧めします。
最初のアプローチでは、ExtensionArray サブクラスでサポートしたい選択された演算子、たとえば __add__、__le__ など を定義します。
2番目のアプローチは、ExtensionArray の基となる要素 (つまり、スカラ型) に個々の演算子がすでに定義されていることを前提としています。言い換えれば、MyExtensionArray という名前の ExtensionArray が各要素がクラス MyExtensionElement のインスタンスであるように実装されている場合、MyExtensionElement に演算子が定義されていれば、2番目のアプローチは自動的に MyExtensionArray の演算子を定義します。
ミックスインクラス ExtensionScalarOpsMixin はこの2番目のアプローチをサポートします。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 と Index を処理*しない*ことを推奨します。代わりに、これらのケースを検出し、NotImplemented を返す必要があります。pandas が op(Series, ExtensionArray) のような演算に遭遇すると、pandas は
Seriesから配列をアンボックスします (Series.array)。result = op(values, ExtensionArray)を呼び出します。結果を
Seriesに再ボックスします。
NumPy ユニバーサル関数#
Series は __array_ufunc__ を実装しています。実装の一部として、pandas は Series から ExtensionArray をアンボックスし、ufunc を適用し、必要に応じて再ボックスします。
該当する場合、ndarray への強制を避けるために、拡張配列に __array_ufunc__ を実装することを強くお勧めします。例については、NumPy ドキュメントを参照してください。
実装の一部として、inputs で pandas コンテナ (Series、 DataFrame、 Index) が検出された場合は、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 は、2つのメソッド、ExtensionArray.__arrow_array__ と ExtensionDtype.__from_arrow__ を実装することで、pyarrow 配列との間で変換をサポートできます (したがって、たとえば Parquet ファイル形式へのシリアル化をサポートします)。
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 を唯一の引数として受け取り、この dtype と渡された値に対して適切な pandas ExtensionArray を返すことが期待されます。
class ExtensionDtype:
...
def __from_arrow__(self, array: pyarrow.Array/ChunkedArray) -> ExtensionArray:
...
詳細については、Arrow ドキュメントを参照してください。
これらのメソッドは、pandas に含まれる null 許容整数および文字列拡張 dtype に対して実装されており、pyarrow および Parquet ファイル形式への往復変換を保証します。
pandas データ構造のサブクラス化#
警告
pandas データ構造のサブクラス化を検討する前に、いくつかの簡単な代替案があります。
このセクションでは、より具体的なニーズを満たすために pandas データ構造をサブクラス化する方法について説明します。注意すべき点が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__ をオーバーライドしてデータ名にマッピングします。元のプロパティの定義は、次の2つの方法のいずれかで行うことができます。
操作結果に渡されない一時的なプロパティのために、
_internal_namesと_internal_names_setを定義します。操作結果に渡される通常のプロパティのために、
_metadataを定義します。
以下は、「internal_cache」を一時的なプロパティとして、「added_property」を通常のプロパティとして2つの元のプロパティを定義する例です。
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 の __array_priority__ セマンティクスと同様に、DataFrame、 Series、および Index オブジェクトの算術メソッドは、other がより高い値の __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 オブジェクトの優先度よりも高い値に設定することで可能になります。
DataFrame、 Series、および Index の __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