PDEP-10: デフォルトの文字列推論実装に必要な依存関係としてのPyArrow

概要

このPDEPは、

これにより、**ユーザーにとって即座にメリットがもたらされる**と同時に、将来的な大きなメリットへの道が開かれます。

背景

PyArrowは、pandasに幅広い追加機能を提供する、pandasのオプション依存関係です。

pandas 2.0以降、次のような利点を持つPyArrowをNumPyの代替データ表現として利用できます。

  1. すべてのデータ型で一貫した`NA`サポート;
  2. `decimal`、`date`、ネスト型などのより幅広いデータ型のサポート;
  3. Arrowをベースとした他のデータフレームライブラリとの優れた相互運用性。

モチベーション

前段落で説明したすべての機能は現在オプションですが、PyArrowはpandasの多くの領域に大きく統合されています。pandasのロードマップでは、Apache Arrowとの相互運用性の向上[^1]と、Pythonエコシステム内外の多くのプロジェクト[^2]がArrow形式を採用または操作していることを示しており、PyArrowを必須の依存関係とすることで、Arrowエコシステムへの信頼度を高め(そしてそれとの相互運用性を向上させる)追加のシグナルを提供します。

ユーザーにとっての即時的なメリット1:pyarrow文字列

現在、ユーザーがデータ型を指定せずにpandasコンストラクターに文字列データを渡すと、結果のデータ型は`object`になります。これは、pyarrow文字列と比較して、メモリ使用量とパフォーマンスが大幅に劣ります。1.2.0以降で利用可能なpyarrow文字列サポートにより、3.0でpyarrowを必須とすることで、pandasは推論された型をより効率的なpyarrow文字列型にデフォルト設定できます。

In [1]: import pandas as pd

In [2]: pd.Series(["a"]).dtype
# Current behavior
Out[2]: dtype('O')

# Future behavior in 3.0
Out[2]: string[pyarrow]

Dask開発者は、pyarrow文字列のパフォーマンスとメモリこちらを調査し、現在の`object`dtypeよりも大幅な改善が見られることを発見しました。

簡単なデモ

import string
import random

import pandas as pd


def random_string() -> str:
    return "".join(random.choices(string.printable, k=random.randint(10, 100)))


ser_object = pd.Series([random_string() for _ in range(1_000_000)])
ser_string = ser_object.astype("string[pyarrow]")\

PyArrowベースの文字列は、NumPyオブジェクト文字列よりも大幅に高速です。

str.len

In[1]: %timeit ser_object.str.len()
118 ms ± 260 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In[2]: %timeit ser_string.str.len()
24.2 ms ± 187 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

str.startswith

In[3]: %timeit ser_object.str.startswith("a")
136 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In[4]: %timeit ser_string.str.startswith("a")
11 ms ± 19.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

ユーザーにとっての即時的なメリット2:ネストされたデータ型

現在、pandas `Series`に`dict`を格納しようとすると、再びひどい`object`dtypeになります。

In [6]: pd.Series([{'a': 1, 'b': 2}, {'a': 2, 'b': 99}])
Out[6]:
0     {'a': 1, 'b': 2}
1    {'a': 2, 'b': 99}
dtype: object

`pyarrow`が必須であれば、これは`pyarrow.struct`として自動的に推論でき、これもメモリとパフォーマンスの向上につながります。

ユーザーにとっての即時的なメリット3:相互運用性

他のArrowベースのデータフレームライブラリの人気が高まっています。メモリ表現が同じであれば、それらとの相互運用性が向上し、

import pandas as pd
import polars as pl

df = pd.DataFrame(
  {
    'a': ['one', 'two'],
    'b': [{'name': 'Billy', 'age': 3}, {'name': 'Bob', 'age': 4}],
  }
)
pl.from_pandas(df)

ゼロコピー操作が可能になります。複数のデータフレームライブラリを使用しているユーザーは、それらの間をより簡単に切り替えることができます。

将来的なユーザーメリット

PyArrowを必須にすることで、pandas内の関連開発が簡素化され、PyArrowの方が適しているNumPy機能が向上する可能性があります。

開発者のメリット

まず、これにより、オプションの依存関係チェックが不要になるため、pyarrowベースのデータ型の開発が簡素化されます。

次に、冗長な機能を削除できる可能性があります。 - `read_parquet`内のfastparquetエンジン; - `read_csv`ロジックの簡素化の可能性(さらなる調査が必要); - 因子化; - 日付/タイムゾーン操作。

欠点

PyArrowを含めると、pandasのインストールサイズが当然大きくなります。たとえば、ホイールからpipを使用してpandasとPyArrowをインストールする場合、numpyとpandasは約70MB必要ですが、PyArrowを含めるとさらに120MB必要になります。インストールサイズの増加は、AWS Lambdaなどのスペースが限られた開発環境や展開環境でpandasを使用する場合に悪影響を及ぼします。

さらに、`pip install`または`conda install`を介してホイールを利用できない環境にpandasをインストールする場合、ソースからインストールするときに、Arrow C++および関連する依存関係もビルドする必要があります。これらの環境には以下が含まれます。

最後に、pandasの開発とリリースは、PyArrowの開発とリリースのペースを考慮する必要があります。たとえば、新しくリリースされたPythonバージョンをサポートする場合、pandasは新しいpandasバージョンをリリースする前に、そのPythonバージョンのPyArrowのホイールサポートを考慮する必要があります。

よくある質問

Q:pandasは、pyarrow文字列とpyarrow structの代わりに、numpy文字列とnumpy voidデータ型を使用できないのはなぜですか?

A:NumPy文字列はまだ利用できませんが、pyarrow文字列は利用できます。NumPy voidデータ型はpyarrow structとは異なり、他のArrowベースのデータフレームライブラリとの同じ相互運用性のメリットが得られません。

Q:すべてのpyarrow dtypeの準備はできていますか?それらをデフォルトにするには早すぎませんか?

A:3.0までに準備できる可能性が高いです。ただし、それらをデフォルトに(まだ)していません。たとえば、`pd.Series([1, 2, 3])`は引き続き`np.int64`として自動的に推論されます。現在NumPyを基盤とした相当するものがない、そして`object`dtypeとして保存されているdtype(文字列やネストされたデータ型など)のデフォルトのみを変更します。

PDEP-10履歴

[^1] https://pandas.dokyumento.jp/docs/development/roadmap.html#apache-arrow-interoperability [^2] https://arrow.apache.org/powered_by/