pandasの拡張配列

拡張性は、ここ数回のpandas開発における主要なテーマでした。この記事では、pandas拡張配列インターフェース、その背後にある動機、そしてそれがpandasユーザーとしてあなたにどのように影響するかを紹介します。最後に、拡張配列がpandasの将来をどのように形成する可能性があるかを見ていきます。

拡張配列は、pandas 0.24.0 の変更点の1つにすぎません。完全な変更履歴については、新機能を参照してください。

動機

PandasはNumPyの上に構築されています。SeriesをNumPy配列をラップするもの、DataFrameを共有インデックスを持つSeriesのコレクションと大まかに定義できます。いくつかの理由でこれは完全に正しくありませんが、私は「NumPy配列をラップするもの」という部分に焦点を当てたいと思います。より正確には「配列のようなオブジェクトをラップするもの」と言うべきでしょう。

PandasはほとんどNumPyの組み込みデータ表現を使用しています。私たちは一部を制限し、他の部分を拡張しました。たとえば、pandasの初期のユーザーは、NumPyがサポートしていないタイムゾーン対応のdatetimeを非常に気にしていました。そこでpandasは内部的にDatetimeTZ dtype(NumPy dtypeを模倣したもの)を定義し、そのdtypeをIndexSeries、およびDataFrameの列として使用できるようにしました。このdtypeはtzinfoを保持していましたが、それ自体は有効なNumPy dtypeではありませんでした。

別の例として、Categoricalを考えてみましょう。これは実際には、categories用とcodes用の2つの配列で構成されています。しかし、他の列と同じようにDataFrameに格納できます。

pandasが追加したこれらの拡張タイプはそれぞれ便利ですが、高いメンテナンスコストがかかります。コードベースの大部分は、NumPy配列またはこれらの他の種類の特殊配列をどのように処理するかを認識する必要があります。これにより、pandasに新しい拡張タイプを追加することが非常に困難になりました。

Anaconda, Inc.には、IPアドレスを持つデータセットを日常的に扱うクライアントがいました。彼らはIPArrayをpandasに追加することは理にかなっているかと疑問に思っていました。結局、私たちはそれがpandas*自体*に含めるコストパフォーマンステストには合格しないと考えましたが、サードパーティのpandas拡張のためのインターフェースを定義することには興味がありました。このインターフェースを実装するオブジェクトはすべてpandasで許可されます。私はpandasの外部でcyberpandasを作成することができましたが、pandasに組み込まれている他のdtypeを使用しているように感じます。

現状

pandas 0.24.0現在、pandasのすべての内部拡張配列 (Categorical, Datetime with Timezone, Period, Interval, Sparse) はExtensionArrayインターフェースの上に構築されています。ユーザーは多くの変更に気づかないはずです。気づく主なことは、object dtypeにキャストされる場所が減り、コードがより速く実行され、型がより安定することです。これには、PeriodおよびIntervalデータをSeriesに格納することも含まれます(これらは以前はobject dtypeにキャストされていました)。

さらに、比較的簡単に**新しい**拡張配列を追加できるようになります。たとえば、0.24.0では、pandasの長年の問題点の1つである、欠損値が整数型を浮動小数点型にキャストする問題が(オプションで)解決されました。

>>> int_ser = pd.Series([1, 2], index=[0, 2])
>>> int_ser
0    1
2    2
dtype: int64

>>> int_ser.reindex([0, 1, 2])
0    1.0
1    NaN
2    2.0
dtype: float64

新しいIntegerArrayとNullable integer dtypesにより、欠損値を持つ整数データをネイティブに表現できるようになりました。

>>> int_ser = pd.Series([1, 2], index=[0, 2], dtype=pd.Int64Dtype())
>>> int_ser
0    1
2    2
dtype: Int64

>>> int_ser.reindex([0, 1, 2])
0      1
1    NaN
2      2
dtype: Int64

わずかに変更される点としては、SeriesまたはIndex内部に格納されている生の(ラベルなしの)配列へのアクセス方法があります。これは時折便利です。たとえば、呼び出すメソッドがNumPy配列でのみ機能する場合や、自動アライメントを無効にしたい場合などです。

以前は、「SeriesまたはDataFrameからNumPy配列を抽出するには.valuesを使用する」といった話を聞いたでしょう。もしそれが良い情報源であれば、いくつかの例外があるのでそれは*完全に*真実ではないと教えてくれたでしょう。私はこれらの例外について掘り下げたいと思います。

.valuesの根本的な問題は、2つの目的があることです。

  1. Series、Index、またはDataFrameを裏付ける配列を抽出する
  2. Series、Index、またはDataFrameをNumPy配列に変換する

上記で見たように、SeriesまたはIndexを支える「配列」はNumPy配列ではなく、代わりに拡張配列(pandasまたはサードパーティライブラリのもの)である可能性があります。たとえば、Categoricalを考えてみましょう。

>>> cat = pd.Categorical(['a', 'b', 'a'], categories=['a', 'b', 'c'])
>>> ser = pd.Series(cat)
>>> ser
0    a
1    b
2    a
dtype: category
Categories (3, object): ['a', 'b', 'c']

>>> ser.values
[a, b, a]
Categories (3, object): ['a', 'b', 'c']

この場合、.valuesはNumPy配列ではなくCategoricalです。period-dtypeデータの場合、.valuesPeriodオブジェクトのNumPy配列を返しますが、これは作成にコストがかかります。タイムゾーン対応データの場合、.valuesはUTCに変換し、タイムゾーン情報を**破棄**します。これらの種類の驚き(異なる型、または高価な、あるいは情報が失われる変換)は、これらの拡張配列をNumPy配列に押し込もうとすることに起因します。しかし、拡張配列の目的は、NumPyがネイティブに表現**できない**データを表現することです。

.valuesの問題を解決するために、その役割を2つの専用メソッドに分割しました。

  1. .array を使用して、基となるデータへのゼロコピー参照を取得します。
  2. .to_numpy() を使用して、データの (場合によってはコストがかかる、情報が失われる) NumPy 配列を取得します。

カテゴリカルの例では、

>>> ser.array
[a, b, a]
Categories (3, object): ['a', 'b', 'c']

>>> ser.to_numpy()
array(['a', 'b', 'a'], dtype=object)

まとめると

もう.valuesを使用する必要はありません。

将来の可能性のある道筋

拡張配列は、いくつかの刺激的な機会を開きます。現在、pandasはPythonオブジェクトを使用して文字列データをNumPy配列に格納しており、これは遅いです。Apache Arrowのようなライブラリは可変長文字列のネイティブサポートを提供し、FletcherライブラリはArrow配列のpandas拡張配列を提供します。これにより、GeoPandasはジオメトリデータをより効率的に保存できるようになります。pandas(またはサードパーティライブラリ)は、ネストされたデータ、単位付きデータ、地理データ、GPU配列をサポートできるようになるでしょう。サードパーティの拡張配列を追跡するpandasエコシステムページに注目してください。pandas開発にとってエキサイティングな時期です。

その他の考察

これは*インターフェース*であり、具体的な配列実装ではないことを強調したいと思います。私たちはpandasでNumPyを**再実装しているわけではありません**。むしろ、これは、配列のようなデータ構造(1つまたは複数のNumPy配列、Apache Arrow配列、CuPy配列)をDataFrame内に配置する方法です。pandasが配列ビジネスから離れて、より高レベルの表形式データについて考えるようになることは、プロジェクトにとって健全な発展だと思います。

これはNumPyの__array_ufunc__プロトコルとNEP-18と完全に連携します。NumPyメモリに裏打ちされていないオブジェクトでも、馴染みのあるNumPy APIを使用できるようになります。

アップグレード

これらの新しい機能はすべて、最近リリースされたpandas 0.24 で利用可能です。

conda

conda install -c conda-forge pandas

pip

pip install --upgrade pandas

いつものように、メーリングリスト@pandas-dev、またはイシュートラッカーでのフィードバックをお待ちしております。

pandasコミュニティに関わる多くの貢献者、メンテナー、そして機関パートナーに感謝いたします。