PDEP-14: pandas 3.0における専用の文字列データ型

概要

このPDEPは、pandas 3.0でデフォルトで使用される専用の文字列dtypeの導入を提案します。

これにより、ユーザーは3.0で待望の適切な文字列dtypeを利用できるようになります。同時に、1) PyArrowを(まだ)*必須*の依存関係にするのではなく、デフォルトで使用される依存関係としてのみ扱い、2) 将来の改善(異なる欠損値セマンティクス、NumPy 2.0文字列の使用など)の余地を残します。

背景

現在、pandasはデフォルトでテキストデータをobject-dtype NumPy配列に格納しています。現在の実装には2つの主要な欠点があります。第一に、object dtypeは文字列に特化したものではなく、文字列だけでなく任意のPythonオブジェクトをobject-dtype配列に格納できるため、文字列を含む列のdtypeがobjectであることはユーザーにとって混乱を招きます。第二に、これは効率的ではありません(Series上のすべての文字列メソッドは、最終的に個々の文字列オブジェクトに対するPythonメソッドを呼び出します)。

最初の問題を解決するため、文字列データ用の専用の拡張dtypeがpandas 1.0で既に追加されています。これはこれまで常にオプトインであり、ユーザーが明示的にdtypeを要求する必要がありました(dtype="string"またはdtype=pd.StringDtype()を使用)。この文字列dtypeをバックアップする配列は、当初はデフォルトの実装とほぼ同じ、つまりPython文字列のobject-dtype NumPy配列でした。

2番目の問題(パフォーマンス)を解決するため、pandasはPyArrowパッケージにおける文字列カーネルの開発に貢献し、PyArrowによってバックアップされる文字列dtypeのバリアントがpandas 1.3で追加されました。これはオプトインの文字列dtypeでstorageキーワードを使って指定できます(pd.StringDtype(storage="pyarrow"))。

導入以来、StringDtypeは常にオプトインであり、欠損値には実験的なpd.NAセンチネルを使用してきました(これもpandas 1.0で導入されました)。しかし、現在まで、pandasはどのデフォルトdtypeに対してもpd.NAを使用する段階には至っておらず、そのためStringDtypeはデフォルトのデータ型と比較して欠損値の動作が異なります。

2023年、PDEP-10は、pandas 3.0でデフォルトでPyArrowバックアップの文字列dtypeを使用すること(つまり、object dtypeの代わりに文字列データに対してこの型を推論すること)を提案しました。より良いパフォーマンスのためにPythonオブジェクトの代わりにPyArrowにバックアップされるStringDtypeのバリアントを使用できるようにするため、pyarrowをpandasの新しい必須ランタイム依存関係にすることを提案しました。

その間、NumPyもネイティブな可変幅文字列データ型の開発に取り組んでおり、これはNumPy 2.0から利用可能になりました。これは、Pythonオブジェクトにバックアップされないpandasにおける文字列データ型を実装するためのPyArrowに対する潜在的な代替手段を提供できます。

PDEP-10の採択後、提案の2つの側面が再検討されています。

2番目の側面については、StringDtypeの別のバリアントがpandas 2.1で導入されました。これはPyArrowにバックアップされていますが、pandasが他のすべてのデフォルトデータ型で使用するデフォルトの欠損値セマンティクスに従います(欠損値センチネルとしてNaNを使用)(GH-54792)。当時、この新しいバリアントのstorageオプションは、pd.NAを使用する既存の"pyarrow"オプションと区別するために"pyarrow_numpy"と名付けられました(しかし、このPDEPではより良い命名スキームを提案しています。以下の「命名」セクションを参照)。

この最後のdtypeバリアントは、ユーザーが現在(pandas 2.2)future.infer_stringオプションを有効にしたときに文字列データに対して得られるものです(pandas 3.0でデフォルトになる予定の動作を有効にするため)。

提案

pandas 3.0で文字列データ型を前進させるために、このPDEPは以下を提案します。

  1. pandas 3.0では、"str"文字列dtypeがデフォルトで有効になります。つまり、この文字列dtypeは、pandasオブジェクトを作成する際に(コンストラクタ、I/O関数での推論など)テキストデータに対するデフォルトのdtypeとして使用されます。
  2. このデフォルトの文字列dtypeは、欠損値に対して他のデフォルトデータ型と同じ動作に従い、欠損値センチネルとしてNaNを使用します。
  3. 文字列dtypeは、PyArrowがインストールされていればそれを使用し、そうでなければ内部の機能的に同等な(ただし遅い)バージョンにフォールバックします。このフォールバックは、既存のnumpy object-dtypeバックアップのStringArrayを(わずかなコード追加で)実装に再利用できます。
  4. インストールガイドラインが更新され、デフォルトのユーザーエクスペリエンスのためにユーザーにpyarrowのインストールを明確に推奨します。

デフォルトで有効になるこれらの文字列dtypeは、もはや実験的とは見なされません。

文字列dtypeのデフォルト推論

デフォルトでは、pandasは文字列データに対して(コンストラクタやIO関数などのpandasオブジェクトを作成する際に)object dtypeの代わりにこの新しい文字列dtypeを推論します。

pandas 2.2では、既存のfuture.infer_stringオプションを使用して、将来のデフォルト動作をオプトインできます。

>>> pd.options.future.infer_string = True
>>> pd.Series(["a", "b", None])
0      a
1      b
2    NaN
dtype: string

現在(pandas 2.2)、既存のオプションはPyArrowベースの将来のdtypeのみを有効にします。残りの2.xリリースでは、PyArrowがインストールされていない場合でも機能するようにこのオプションが拡張され、その場合はobject-dtypeフォールバックが有効になります。

欠損値セマンティクス

背景セクションで述べたように、オリジナルのStringDtypeは常に欠損値に実験的なpd.NAセンチネルを使用してきました。欠損値のスカラーとしてpd.NAを使用することに加えて、これは本質的に以下のことを意味します。

しかし、現在まで、他のすべてのデフォルトデータ型は欠損値にNaNセマンティクスを使用しています。したがって、この提案では、新しいデフォルトの文字列dtypeも同じデフォルトの欠損値セマンティクスを使用し、文字列列に対する操作時にデフォルトのデータ型を返すことで、現時点での他のデフォルトdtypeとの一貫性を保つべきであると述べています。

実際には、これはデフォルトの文字列dtypeが欠損値センチネルとしてNaNを使用し、以下のことを意味します。

オリジナルのStringDtype実装はすでにpd.NAを使用し、操作でマスクされた整数およびブール配列を返すため、NaNとデフォルトデータ型を使用する既存のdtypeの新しいバリアントが必要でした。pd.NAを使用するStringDtypeの元のバリアントは、すでにそれを使用しているユーザーのために引き続き利用可能になります。

Object-dtype「フォールバック」実装

pandas 3.0でPyArrowへの厳密な依存関係を避けるため、このPDEPはPyArrowがインストールされていない場合に「フォールバック」オプションを保持することを提案しています。Python文字列のnumpy object-dtype配列にバックアップされたオリジナルのStringDtypeは、この目的のためにほとんど再利用できます(dtypeの新しいバリアントを追加する)。また、新しいStringArrayサブクラスは、上記の欠損値セマンティクスに従うためにわずかな変更しか必要ありません(GH-58451)。

pandas 3.0では、この実装が長い間利用可能であったことを考えると、これが最も現実的なオプションです。3.0以降、NumPy 2.0(GH-58503)やnanoarrow(GH-58552)の使用など、さらなる改善は引き続き検討できますが、その時点では実装の詳細であり、ユーザーに直接的な影響を与えるべきではありません(パフォーマンスを除いて)。

pd.NAを使用するStringDtypeの元のバリアントについては、現在デフォルトのストレージは"python"(object-dtypeベースの実装)です。このバリアントについても、デフォルトのストレージを決定する同じロジックに従うことが提案されています。つまり、利用可能であれば"pyarrow"をデフォルトとし、そうでなければ"python"にフォールバックします。

命名

このトピックの長い歴史を考えると、dtypeの命名は難しいトピックです。

まず、ほとんどのユーザーはストレージ固有のオプションを使用する必要がないことを認識すべきです。ユーザーは汎用名("str""string"など)を指定することが期待されており、それによってデフォルトの文字列dtypeが提供されます(これはPyArrowがインストールされているかどうかに依存します)。

dtypeを指定するための汎用的な文字列エイリアスとして、"string"pd.NAを使用するStringDtypeにすでに使用されています。このPDEPは、NaNを使用する新しいデフォルトのStringDtype"str"を使用することを提案しています。これにより、dtype="string"を使用するコードとの下位互換性が確保され、またdtype="str"またはdtype=strが現在すでにデータを文字列に変換する(結果としてobject dtypeのみを使用する)ために機能するため、この選択がなされました。

しかし、テスト目的やStringDtypeの正確なバリアントを制御したい高度なユースケースのために、これを指定し、他の文字列dtypeと区別する方法が必要です。

現在(pandas 2.2)、NaNを使用する新しいバリアントにはStringDtype(storage="pyarrow_numpy")が使用されており、"pyarrow_numpy"ストレージはpd.NAを使用する既存の"pyarrow"オプションと区別するために使用されていました。しかし、"pyarrow_numpy"はかなり混乱を招くオプションであり、汎用性が高くありません。したがって、このPDEPは以下に示す新しい命名スキームを提案し、"pyarrow_numpy"はpandas 2.3でエイリアスとして非推奨となり、pandas 3.0で削除されます。

StringDtypestorageキーワードは、文字列データ(pyarrowまたはpythonオブジェクトを使用)の基礎となるストレージを区別するために保持されますが、NAセマンティクスとNaNセマンティクスを使用するバリアントを区別するために、追加のna_valueが導入されます。

dtypeを指定するさまざまな方法と、データの具体的なdtypeの概要

ユーザー指定 具体的なdtype 文字列エイリアス
未指定(推論) StringDtype(storage="pyarrow"\|"python", na_value=np.nan) "str" (1)
"str" または StringDtype(na_value=np.nan) StringDtype(storage="pyarrow"\|"python", na_value=np.nan) "str" (1)
StringDtype("pyarrow", na_value=np.nan) StringDtype(storage="pyarrow", na_value=np.nan) "str"
StringDtype("python", na_value=np.nan) StringDtype(storage="python", na_value=np.nan) "str"
StringDtype("pyarrow") StringDtype(storage="pyarrow", na_value=pd.NA) "string[pyarrow]"
StringDtype("python") StringDtype(storage="python", na_value=pd.NA) "string[python]"
"string" または StringDtype() StringDtype(storage="pyarrow"\|"python", na_value=pd.NA) "string[pyarrow]" または "string[python]" (1)
StringDtype("pyarrow_numpy") StringDtype(storage="pyarrow", na_value=np.nan) "string[pyarrow_numpy]" (2)

注記

新しいデフォルトの文字列dtypeの場合、文字列としてdtypeを指定できるのは"str"エイリアスのみです。つまり、pandasは基礎となるストレージ(pyarrowまたはpython)を文字列エイリアスを通じて明示的に指定する方法を提供しません。この文字列エイリアスは単なる便利なショートカットであり、ほとんどのユーザーにとって"str"で十分です(ストレージを指定する必要はありません)。よりきめ細かい制御が必要な場合は、明示的なpd.StringDtype(storage=..., na_value=np.nan)が引き続き利用可能です。

また、pd.NAを使用する既存のバリアントについても、文字列エイリアスを通じてストレージを指定することは非推奨にできますが、それは別途決定されます。

代替案

なぜデフォルトの文字列dtypeの導入を遅らせないのか?

他の議論や変更が流動的である間に(最終的にpyarrowを必須の依存関係にするのか? pd.NAをデフォルトの欠損値センチネルとして採用するのか? 新しいNumPy 2.0の機能を使用するのか? すべてのdtypeを論理データ型システムを使用するように全面的に見直すのか?)、新しい文字列dtypeを導入することを避けるために、これらの他の議論でより明確になるまでデフォルトの文字列dtypeの導入を遅らせることもできます。具体的には、一時的に文字列dtypeにNaNを使用するように切り替え、将来のバージョンでデフォルトでpd.NAに戻る可能性を避けることができます。

しかし

  1. 遅延にはコストがかかります。それは、ユーザーにとって使いやすさの面でも、(PyArrowがインストールされているユーザーベースの)パフォーマンスの面でも大きなメリットがある専用の文字列dtypeの導入をさらに遅らせることになります。
  2. pandasが最終的にpd.NAをデフォルトの欠損値センチネルとして使用するように移行する場合、*すべての* pandasデータ型に移行パスが必要となり、この問題は文字列dtypeに固有のものではないため、遅延の理由にはなりません。

3.0でこの変更を行うことで、大多数のユーザーにメリットがあります。PDEPの著者は、これが「もう一つのdtype」に関する複雑さのコストに見合うと考えています(他のデータ型についてもすでに複数のバリアントがあります)。

既存のStringDtypeとpd.NAを使用しないのはなぜか?

文字列dtypeのバリアントをさらに追加すると、物事がさらに混乱するだけではないでしょうか?確かに、この提案は残念ながら文字列dtypeのバリアントを増やします。しかし、その理由は、実際のデフォルトのユーザーエクスペリエンスを*混乱させず*、新しい文字列dtypeが他のデフォルトデータ型とよりよく適合するようにするためです。

新しいデフォルトの文字列データ型がpd.NAを使用すると、いくつかの操作の後、ユーザーはNaNセマンティクスを使用する列とNAセマンティクスを使用する列が混在するDataFrameになってしまう可能性があります(そして、2種類のint64、2種類のfloat64、2種類のboolなど、異なるdtypeを持つ列を持つDataFrameになる可能性があります)。これは、非常に混乱を招くデフォルトのエクスペリエンスにつながります。

提案されたStringDtypeの新しいバリアントを使用することで、*デフォルト*のエクスペリエンスでは、ユーザーは1種類の整数dtype、1種類のbool dtypeなどしか見ないようにします。今のところ、ユーザーは明示的にオプトインした場合にのみpd.NAを使用する列を得るべきです。

命名の代替案

このPDEPの最初のバージョンでは、新しいデフォルトdtypeに"string"エイリアスとデフォルトのpd.StringDtype()クラスコンストラクタを使用することを提案していました。しかし、これは欠損値を表すためにpd.NAを使用するdtype=pd.StringDtype()およびdtype="string"の既存のユーザーとの下位互換性に関して多くの議論を引き起こしました。

議論の中で、いくつかの代替案が持ち上がりました。代替キーワード名と異なるコンストラクタの使用の両方です。最終的に、このPDEPは異なる文字列エイリアス("str")を使用することを提案しますが、既存のpd.StringDtype(既存のstorageキーワードと追加のna_valueキーワードを使用)は、変更を可能な限り最小限に抑えるために当面維持し、dtypeシステムのより大規模な見直し(異なるコンストラクタ関数や名前空間を含む可能性)は将来の議論に委ねることを提案しています。詳細な議論についてはGH-58613を参照してください。

結果として、デフォルトのdtypeにクラスコンストラクタを使用する場合、非デフォルトの引数を使用する必要があります。つまり、ユーザーはNaNを使用するデフォルトのdtypeを得るためにpd.StringDtype(na_value=np.nan)を指定する必要があります。したがって、pandasのドキュメントではdtype="str"の使用に焦点を当てます。

後方互換性

最も目に見える後方非互換の変更は、文字列データを含む列がもはやobject dtypeを持たなくなることです。したがって、object dtypeを前提とするコード(ser.dtype == objectなど)は更新する必要があります。この変更は、メジャーリリースで強制的な変更として行われます。変更された推論について事前に警告することは、あまりにもノイズが多いと判断されました。

コードを事前にテストできるように、pd.options.future.infer_string = Trueオプションがユーザーに提供されます。

そうでなければ、実際の文字列固有の機能(.strアクセサメソッドなど)は、一般的にすべてこれまで通り機能し続けるはずです。

現在の欠損値セマンティクスを維持することで、この提案はこの側面においてもほとんど後方互換性があります。ただし、pandasはobject dtypeに文字列を格納する際、欠損値インジケータとしてNoneも使用することを許可していました(そしてshiftメソッドなど、特定の場合にはpandas自身がこれを導入することさえありました)。現在Noneが欠損値センチネルとして使用されていたすべてのケースで、これは一貫してNaNを使用するように変更されます。

既存のStringDtypeユーザー向け

pd.NAを使用するStringDtypeをすでに使用している既存のコードは、一般的にこれまで通り機能し続けるはずです。このPDEPの最新バージョンは、dtype="string"またはdtype=pd.StringDtype()がdtypeのpd.NAバリアントを意味するという動作を維持します。

また、オプトインのpd.NAバリアントについても、デフォルトのストレージを"pyarrow"(利用可能な場合)に変更することを提案していますが、これはユーザーに目に見える影響を与えることはほとんどないはずです。

タイムライン

将来のPyArrowバックアップの文字列dtypeは、pandas 2.1で機能フラグの背後ですでに利用可能になっていました(pd.options.future.infer_string = Trueで有効)。

numpy object-dtypeを使用するバリアントも2.2.xブランチにバックポートして、テストを容易にすることができます。これは、命名スキームの変更とともに2.3.0としてリリースされることが提案されています(メインブランチにはすでに3.0をターゲットとした他の多くの変更が含まれているため、2.2.xブランチから作成)。

2.3.0リリースには、将来のすべての文字列機能が利用可能になります(デフォルトの文字列dtypeのpyarrowとobject-dtypeの両方のバリアント)。

pandas 3.0では、このfuture.infer_stringフラグがデフォルトで有効になります。

PDEP-14 履歴