「静的解析と実行時の検証のためのDataFrameの型ヒント」
「美容とファッションのエキスパートによる、生き生きとした記事を執筆します」
StaticFrameが包括的なDataFrameの型ヒントを可能にする方法
Python 3.5で型ヒントが導入されて以来、DataFrameの静的な型指定は通常、単に型の指定に限られてきました:
def process(f: DataFrame) -> Series: ...
これは不十分であり、コンテナ内に含まれる型を無視しています。DataFrameには文字列の列ラベルと整数、文字列、浮動小数点の値からなる3つの列があるかもしれません。これらの特性は型を定義します。そのような型ヒントを持つ関数引数は、インターフェースの期待値を理解するために、開発者、静的解析ツール、および実行時チェッカーに必要な情報をすべて提供します。私がリードディベロッパーであるオープンソースプロジェクトであるStaticFrame 2では、これが可能になりました:
from typing import Anyfrom static_frame import Frame, Index, TSeriesAnydef process(f: Frame[ # コンテナの型 Any, # インデックスラベルの型 Index[np.str_], # 列ラベルの型 np.int_, # 最初の列の型 np.str_, # 2番目の列の型 np.float64, # 3番目の列の型 ]) -> TSeriesAny: ...
すべての中核となるStaticFrameコンテナは、ジェネリックな仕様をサポートしています。静的にチェック可能な新しいデコレータ@CallGuard.check
は、関数インターフェースの型ヒントを実行時に検証することも可能です。さらに、Annotated
ジェネリックを使用することで、新しいRequire
クラスは、列ごとまたは行ごとのデータの検証を行う強力なランタイムバリデーターのファミリーを定義します。最後に、各コンテナは新しいvia_type_clinic
インターフェースを公開して、型ヒントを派生し検証することができます。これらのツールを組み合わせることで、DataFrameの型ヒントと検証に統一的なアプローチが提供されます。
ジェネリックなDataFrameの要件
Pythonの組み込みジェネリック型(例:tuple
またはdict
)は、コンポーネントの型(例:tuple[int, str, bool]
またはdict[str, int]
)の指定を必要とします。コンポーネントの型を定義することで、より正確な静的解析が可能となります。DataFrameにも同様のことが言えますが、DataFrameの包括的な型ヒントを定義する試みはほとんどありませんでした。
Pandasは、pandas-stubs
パッケージを使用しても、DataFrameのコンポーネントの型の指定を許可していません。Pandas DataFrameは、広範なインプレース変更を許可しているため、静的な型指定は適切ではないかもしれません。幸いにも、StaticFrameでは不変なDataFrameが利用可能です。
さらに、Pythonのジェネリックを定義するためのツールは、最近までDataFrameには適していませんでした。DataFrameには可変長の異種の列型があるため、ジェネリックの指定には課題があります。このような構造に対する型指定は、Python 3.11で導入された新しいTypeVarTuple
を使用することで簡単になりました(また、typing_extensions
パッケージでバックポートが行われています)。
TypeVarTuple
を使用すると、可変数の型を受け入れるジェネリックを定義することができます(詳細については、PEP 646を参照)。この新しい型変数を使用することで、StaticFrameはインデックスのためのTypeVar
、列のためのTypeVar
、ゼロ個以上の列のためのTypeVarTuple
を持つジェネリックなFrame
を定義することができます。
ジェネリックなSeries
は、インデックスのためのTypeVar
と値のためのTypeVar
で定義されます。StaticFrameのIndex
とIndexHierarchy
もジェネリックであり、後者は再度TypeVarTuple
を活用して、各階層レベルのコンポーネントIndex
を定義しています。
StaticFrameは、Frame
の列の型やSeries
やIndex
の値の型を定義するためにNumPyの型を使用しています。これにより、np.uint8
やnp.complex128
などのサイズ指定された数値型、またはnp.integer
やnp.inexact
などの型のカテゴリを広く指定することができます。StaticFrameはNumPyのすべての型をサポートしているため、対応関係は直接的です。
ジェネリックデータフレームで定義されたインターフェース
上記の例を拡張して、以下の関数インターフェースは、3つの列を持つFrame
をSeries
の辞書に変換したものを示しています。コンポーネントの型ヒントによって提供される情報量が多いため、この関数の目的はほぼ明らかです。
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonthdef process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> dict[ int, Series[ # コンテナの型 IndexYearMonth, # インデックスラベルの型 np.float64, # 値の型 ], ]: ...
この関数は、Open Source Asset Pricing(OSAP)データセット(企業レベルの特性/個人/予測子)からのシグナルテーブルを処理します。各テーブルには3つの列があります:セキュリティ識別子(”permno” とラベル付けされた列)、年と月(”yyyymm” とラベル付けされた列)、およびシグナル(シグナルに特有の名前を持つ)。
この関数は、提供されたFrame
のインデックス(Any
として型付けされている)を無視し、「permno」列のnp.int_
値によって定義されるグループを作成します。各値が「permno」に対してnp.float64
値のSeries
である辞書が返されます。インデックスは、np.str_
「yyyymm」列から作成されたIndexYearMonth
です。(StaticFrameは、単位型のインデックスを定義するためにNumPyのdatetime64
値を使用します:IndexYearMonth
はdatetime64[M]
のラベルを格納します。)
dict
ではなく、以下の関数は階層的なインデックスを持つSeries
を返します。 IndexHierarchy
ジェネリックは、各深さレベルごとにコンポーネントIndex
を指定します。ここでは、最も外側の深さはIndex[np.int_]
(「permno」列から派生)であり、内側の深さはIndexYearMonth
(「yyyymm」列から派生)です。
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchydef process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> Series[ # コンテナの型 IndexHierarchy[ # インデックスラベルの型 Index[np.int_], # インデックスの深さ 0の型 IndexYearMonth], # インデックスの深さ 1の型 np.float64, # 値の型 ]: ...
豊富な型のヒントにより、機能が明示的になる自己文書化インターフェースが提供されます。さらに、これらの型のヒントは、Pyright(現在)およびMypy(TypeVarTuple
のサポートが完全に行われるまで)との静的解析に使用することができます。たとえば、np.float64
の2つの列を持つFrame
でこの関数を呼び出すと、エディターで静的解析の型チェックが失敗するか、警告が表示されます。
ランタイムタイプの検証
静的な型チェックだけでは十分ではない場合があります。ランタイム評価によるさらなる制約が提供されます。特に、動的または不完全(または不正確に)型ヒントが付いた値に対して有効です。
TypeClinic
という新しいランタイムタイプチェッカーをベースに、StaticFrame 2ではランタイムの型ヒントのバリデーション用のデコレータである@CallGuard.check
が導入されました。すべてのStaticFrameとNumPyのジェネリックをサポートし、ほとんどの組み込みのPythonの型もサポートされています(深くネストされている場合でも)。以下の関数は、@CallGuard.check
デコレータを追加しています。
from typing import Anyfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, [email protected] process(f: Frame[ Any, Index[np.str_], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
いま、@CallGuard.check
で装飾されています。上記の関数が、ラベルのついていない2列のnp.float64
のFrame
で呼び出された場合、ClinicError
例外が発生し、3つの列が期待されたのに2つしか提供されず、文字列の列ラベルが期待されたのに整数のラベルが提供されたことが示されます(例外を発生させずに警告を発行するには、@CallGuard.warn
のデコレータを使用してください)。
ClinicError:In args of (f: Frame[Any, Index[str_], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Index[str_], int64, str_, float64] └── Expected Frame has 3 dtype, provided Frame has 2 dtypeIn args of (f: Frame[Any, Index[str_], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Index[str_], int64, str_, float64] └── Index[str_] └── Expected str_, provided int64 invalid
ランタイムデータの検証
その他の特性はランタイムで検証することもできます。例えば、shape
属性やname
属性、またはインデックスや列のラベルの順序などを検証できます。StaticFrameのRequire
クラスは、設定可能なバリデータを提供します。
Require.Name
:コンテナのname
属性を検証します。Require.Len
:コンテナの長さを検証します。Require.Shape
:コンテナのshape
属性を検証します。Require.LabelsOrder
:ラベルの順序を検証します。Require.LabelsMatch
:順序に関係なくラベルの一部であることを検証します。Require.Apply
:コンテナに対してブール値を返す関数を適用します。
成長するトレンドに合わせて、これらのオブジェクトは、一つ以上の追加引数としてAnnotated
ジェネリックにタイプヒントとして提供されます(詳細については、PEP 593を参照してください)。
最初のAnnotated
引数で参照されるタイプは、後続の引数のバリデータの対象です。たとえば、Index[np.str_]
のタイプヒントをAnnotated[Index[np.str_], Require.Len(20)]
のタイプヒントで置き換えると、実行時の長さの検証が最初の引数に関連するインデックスに適用されます。
OSAPシグナルテーブルの処理の例を拡張して、列ラベルの期待値を検証することもできます。Require.LabelsOrder
バリデータは、ラベルのシーケンスを定義することができます。ゼロ個以上の未指定のラベルの範囲には...
を使用することもできます。テーブルの最初の2列が「permno」と「yyyymm」とラベル付けされていることを指定し、3番目のラベルが可変(シグナルに依存する)であることを指定するために、次のようにRequire.LabelsOrder
をAnnotated
ジェネリック内に定義できます:
from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, [email protected] process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder('permno', 'yyyymm', ...), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
インターフェースが小さなOSAPシグナルテーブルのコレクションを期待している場合、Require.LabelsMatch
バリデータを使用して3番目の列を検証することができます。このバリデータでは、要求されるラベル、ラベルのセット(少なくとも1つは一致する必要がある)、正規表現パターンを指定することができます。3つのファイルのテーブルが期待されている場合(つまり、「Mom12m.csv」、「Mom6m.csv」、「LRreversal.csv」)、3番目の列のラベルをRequire.LabelsMatch
で次のように定義できます:
@CallGuard.checkdef process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder('permno', 'yyyymm', ...), Require.LabelsMatch({'Mom12m', 'Mom6m', 'LRreversal'}), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
「Require.LabelsOrder」と「Require.LabelsMatch」の両方は、ラベル指定子と関数を関連付けてデータ値をバリデートすることができます。バリデータが列ラベルに適用される場合、関数には列値のシリーズが提供されます。バリデータがインデックスラベルに適用される場合、関数には行値のシリーズが提供されます。
「Annotated」と同様に、ラベル指定子はリストで置き換えられ、最初のアイテムはラベル指定子であり、残りのアイテムは行または列の処理関数であり、ブール値を返します。
上記の例を拡張すると、すべての「permno」の値がゼロより大きく、すべてのシグナル値(「Mom12m」、「Mom6m」、「LRreversal」)が-1以上であることをバリデートすることができます。
from typing import Any, Annotatedfrom static_frame import Frame, Series, Index, IndexYearMonth, IndexHierarchy, CallGuard, [email protected] process(f: Frame[ Any, Annotated[ Index[np.str_], Require.LabelsOrder( ['permno', lambda s: (s > 0).all()], 'yyyymm', ..., ), Require.LabelsMatch( [{'Mom12m', 'Mom6m', 'LRreversal'}, lambda s: (s >= -1).all()], ), ], np.int_, np.str_, np.float64, ]) -> Series[ IndexHierarchy[Index[np.int_], IndexYearMonth], np.float64, ]: ...
バリデーションに失敗する場合、@CallGuard.check
は例外を発生させます。たとえば、上記の関数が予期しない3番目の列ラベルを持つFrame
で呼び出される場合、次の例外が発生します:
ClinicError:In args of (f: Frame[Any, Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])], int64, str_, float64]) -> Series[IndexHierarchy[Index[int64], IndexYearMonth], float64]└── Frame[Any, Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])], int64, str_, float64] └── Annotated[Index[str_], LabelsOrder(['permno', <lambda>], 'yyyymm', ...), LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>])] └── LabelsMatch([{'Mom12m', 'LRreversal', 'Mom6m'}, <lambda>]) └── Expected label to match frozenset({'Mom12m', 'LRreversal', 'Mom6m'}), no provided match
型の表現力への貢献 – TypeVarTuple
上記のように、TypeVarTuple
はゼロ個以上の異なる列タイプを指定できます。たとえば、2つの浮動小数点数または6つの混在タイプのFrame
の型ヒントを提供することができます:
>>> from typing import Any>>> from static_frame import Frame, Index>>> f1: sf.Frame[Any, Any, np.float64, np.float64]>>> f2: sf.Frame[Any, Any, np.bool_, np.float64, np.int8, np.int8, np.str_, np.datetime64]
これにより、さまざまなデータフレームを扱うことができますが、数百の列を持つような幅広いデータフレームの型ヒントは扱いづらいでしょう。Python 3.11では、TypeVarTuple
のジェネリックに対して変数範囲の型を提供するための新しい構文が導入されています:tuple
ジェネリックのエイリアスのスターエクスプレッションです。たとえば、日付のインデックス、文字列の列ラベル、および列のタイプの任意の構成でFrame
の型ヒントを指定するには、ゼロ個以上のAll
をtuple
でスターアンパックすることができます:
>>> from typing import Any>>> from static_frame import Frame, Index>>> f: sf.Frame[Index[np.datetime64], Index[np.str_], *tuple[All, ...]]
タプルスターエクスプレッションは、型のリスト内のどこにでも配置することができますが、1つだけです。たとえば、以下の型ヒントでは、Boolean列と文字列列で始まり、後続のnp.float64列の任意の数の柔軟な仕様を持つFrame
を定義しています。
>>> from typing import Any>>> from static_frame import Frame>>> f: sf.Frame[Any, Any, np.bool_, np.str_, *tuple[np.float64, ...]]
型ヒントのためのユーティリティ
このような詳細な型ヒントを扱うことは難しい場合があります。そのため、StaticFrameではランタイム型ヒントと検証のための便利なユーティリティを提供しています。StaticFrame 2のすべてのコンテナには、via_type_clinic
インターフェースが備わっており、TypeClinic
の機能にアクセスすることができます。
まず、Frame
などのコンテナを型ヒントに変換するためのユーティリティが提供されています。via_type_clinic
インターフェースの文字列表現は、コンテナの型ヒントの文字列表現を提供します。また、to_hint()
メソッドは完全なジェネリックエイリアスオブジェクトを返します。
>>> import static_frame as sf>>> f = sf.Frame.from_records(([3, '192004', 0.3], [3, '192005', -0.4]), columns=('permno', 'yyyymm', 'Mom3m'))>>> f.via_type_clinicFrame[Index[int64], Index[str_], int64, str_, float64]>>> f.via_type_clinic.to_hint()static_frame.core.frame.Frame[static_frame.core.index.Index[numpy.int64], static_frame.core.index.Index[numpy.str_], numpy.int64, numpy.str_, numpy.float64]
次に、ランタイム型ヒントのテストのためのユーティリティが提供されています。via_type_clinic.check()
関数を使用して、コンテナを指定された型ヒントと照合することができます。
>>> f.via_type_clinic.check(sf.Frame[sf.Index[np.str_], sf.TIndexAny, *tuple[tp.Any, ...]])ClinicError:In Frame[Index[str_], Index[Any], Unpack[Tuple[Any, ...]]]└── Index[str_] └── Expected str_, provided int64 invalid
逐次的な型付けをサポートするために、StaticFrameでは、各コンポーネントタイプに対してAny
で構成されたいくつかのジェネリックエイリアスを定義しています。たとえば、TFrameAny
はどのFrame
にも使用でき、TSeriesAny
はどのSeries
にも使用できます。予想どおり、TFrameAny
は上記で作成したFrame
を検証します。
>>> f.via_type_clinic.check(sf.TFrameAny)
結論
DataFramesのためのより良い型ヒントが必要とされています。モダンなPythonの型ヒントツールと、イミュータブルなデータモデルに基づくDataFrameを備えたStaticFrame 2は、メンテナンス性と検証性を重視するエンジニアにとって、強力なリソースを提供します。
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles