PDEP-10: デフォルトの文字列推論実装におけるPyArrowの必須依存関係化

概要

このPDEPでは、以下のことを提案します。

これにより、**ユーザーに即座のメリット**がもたらされ、将来的にはさらに大きなメリットをもたらす道が開かれます。

背景

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

pandas 2.0現在、PyArrowはNumPyの代替データ表現として、以下のような利点とともに利用できます。

  1. すべてのデータ型で一貫したNAサポート;
  2. decimaldate、ネストされた型など、より幅広いデータ型のサポート;
  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のSeriesdictを保存しようとすると、またしても恐ろしい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ロジックの簡素化の可能性(さらなる調査が必要); - 因数分解; - datetime/timezone操作。

欠点

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のホイールサポートも考慮する必要があります。

F.A.Q.

Q: pandasはpyarrow文字列とpyarrow構造体ではなく、numpy文字列とnumpy voidデータ型を使用できないのですか?

A: NumPy文字列はまだ利用可能ではありませんが、pyarrow文字列は利用可能です。NumPy voidデータ型はpyarrow構造体とは異なり、他の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/