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() のような多くのメソッドは、拡張タイプの implementation にディスパッチされます。

インターフェースを実装するライブラリを構築している場合は、 エコシステムページ で公開してください。

インターフェースは 2 つのクラスで構成されています。

ExtensionDtype#

pandas.api.extensions.ExtensionDtypenumpy.dtype オブジェクトに似ています。データ型を表します。実装者は、名前など、いくつかの固有の項目を担当します。

type プロパティは特に重要です。これは、データのスカラー型であるクラスである必要があります。たとえば、IP アドレスデータの拡張配列を作成する場合、これは ipaddress.IPv4Address になる可能性があります。

インターフェースの定義については、拡張 dtype ソース を参照してください。

pandas.api.extensions.ExtensionDtype は、文字列 dtype 名を使用して作成できるように pandas に登録できます。これにより、登録された文字列名を使用して Series.astype() をインスタンス化できます。たとえば、'category'CategoricalDtype の登録された文字列アクセサーです。

dtype の登録方法の詳細については、拡張 dtype dtypes を参照してください。

ExtensionArray#

このクラスは、すべての配列のような機能を提供します。ExtensionArray は 1 次元に制限されています。ExtensionArray は、dtype 属性を介して ExtensionDtype にリンクされています。

pandas は、__new__ または __init__ を介して拡張配列がどのように作成されるかについて、またデータの格納方法について、一切制限を設けていません。ただし、たとえ比較的コストがかかる場合でも(Categorical の場合など)、配列を NumPy 配列に変換できる必要があることを要求します。

それらは、NumPy 配列を 0 個、1 個、または複数使用してバックアップされる場合があります。たとえば、pandas.Categorical は、コード用とカテゴリ用の 2 つの配列をバックアップとする拡張配列です。IPv6 アドレスの配列は、下位 64 ビットと上位 64 ビットの 2 つのフィールドを持つ NumPy 構造化配列によってバックアップされる場合があります。または、Python リストなどの他のストレージタイプによってバックアップされる場合もあります。

インターフェースの定義については、拡張配列ソース を参照してください。docstring とコメントには、インターフェースを正しく実装するためのガイダンスが含まれています。

ExtensionArray 演算子サポート#

デフォルトでは、ExtensionArray クラスには演算子が定義されていません。ExtensionArray の演算子サポートを提供するための 2 つの方法があります。

  1. ExtensionArray サブクラスで各演算子を定義します。

  2. ExtensionArray の基礎となる要素(スカラー)ですでに定義されている演算子に依存する pandas の演算子実装を使用します。

注記

どちらの方法の場合でも、NumPy 配列との 2 項演算に関与したときに実装が呼び出されるようにするには、__array_priority__ を設定することをお勧めします。

最初の方法では、__add____le__ など、ExtensionArray サブクラスでサポートする演算子を選択します。

2 番目の方法は、ExtensionArray の基礎となる要素(つまり、スカラー型)に個々の演算子がすでに定義されていることを前提としています。つまり、MyExtensionArray という名前の ExtensionArray が、各要素が MyExtensionElement クラスのインスタンスであるように実装されている場合、MyExtensionElement で演算子が定義されていれば、2 番目の方法によって MyExtensionArray の演算子が自動的に定義されます。

mixin クラス ExtensionScalarOpsMixin は、この 2 番目の方法をサポートしています。ExtensionArray サブクラス(たとえば MyExtensionArray)を開発する場合は、ExtensionScalarOpsMixinMyExtensionArray の親クラスとして含め、次にメソッド _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)`のような演算を検出すると、

  1. `Series`から配列をアンボックスします(`Series.array`)

  2. `result = op(values, ExtensionArray)` を呼び出します

  3. 結果を`Series`に再ボックスします

NumPyユニバーサル関数#

Seriesは`__array_ufunc__`を実装しています。実装の一部として、pandasは`Series`から`ExtensionArray`をアンボックスし、ufuncを適用し、必要に応じて再ボックスします。

該当する場合は、ndarrayへの強制変換を避けるため、拡張配列に`__array_ufunc__`を実装することを強くお勧めします。例についてはNumPyのドキュメントを参照してください。

実装の一環として、`inputs`でpandasコンテナ(SeriesDataFrameIndex)が検出された場合は、pandasに処理を委譲する必要があります。それらのいずれかが存在する場合は、`NotImplemented`を返す必要があります。pandasはコンテナから配列をアンボックスし、展開された入力でufuncを再呼び出しします。

拡張配列のテスト#

拡張配列が期待される動作を満たしていることを確認するためのテストスイートを提供しています。テストスイートを使用するには、いくつかのpytest fixtureを提供し、基本テストクラスを継承する必要があります。必要なfixtureは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データ構造のサブクラス化を検討する前に、より簡単な代替手段がいくつかあります。

  1. pipeによる拡張可能なメソッドチェーン

  2. 合成を使用します。こちらを参照してください。

  3. アクセサの登録による拡張

  4. 拡張型による拡張

このセクションでは、より具体的なニーズを満たすためにpandasデータ構造をサブクラス化する方法について説明します。注意が必要な点が2点あります。

  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)
<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つの方法で行うことができます。

  1. 操作の結果に渡されない一時的なプロパティとして`_internal_names`と`_internal_names_set`を定義します。

  2. 操作の結果に渡される通常のプロパティとして`_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__`のセマンティクスと同様に、DataFrameSeriesIndexオブジェクトの算術メソッドは、`__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オブジェクトの優先度よりも高い値に設定することで実現できます。

DataFrameSeriesIndex__pandas_priority__はそれぞれ400030002000です。基本の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