panderaを用いてデータフレームのバリデーションを実施する方法

## はじめに

データ分析や機械学習モデルの構築を行う際、データのバリデーションは信頼性を保つために非常に重要なステップとなります。
例えば、機械学習モデルを定期的に構築・更新する過程で、予期しない値を含むデータを検出せずに使用してしまうと、処理がエラーで中断したり、モデルの精度が悪化したりする可能性があります。
これを防ぐためには、データフレームに対する効率的なバリデーション方法が求められます。
今回は、その一例としてpanderaのDataFrameModelを用いたバリデーション手法を紹介します。

## 使用したバージョン
- python 3.9.7
- pandas 3.2.0
- pandera 2.0.2

## インストール方法
```
pip install pandera
```

## 基本的な使い方
以下は、'id'、'年齢(age)'、'所得(income)'、'性別(gender)'、そして'職業(job)'という項目を持つpandasのDataFrameに対してバリデーションを実施する例を示します。
この例では、データフレームの各列（スキーマ）の定義を含むUserSchemaという名前のクラスを作成しています。

```python
import pandas as pd

import pandera as pa
from pandera.typing import Series

# バリデーション対象のデータフレームの作成
data = {
    "id": ["No_0001", "No_0002", "No_0003", "No_0004", "No_0005"],
    "age": [25, 27, 31, "45", 52],
    "income": [50000, 62000, 10000, 87000, 92000],
    "gender": ["male", "male", "female", "male", "female"],
    "job": ["engineer", "doctor", None, "engineer", "teacher"],
}

df = pd.DataFrame(data)


class UserSchema(pa.DataFrameModel):
    # 各フィールドの定義と基本的なチェックを設定。
    id: Series[str] = pa.Field()  # idは文字列である
    age: Series[int] = pa.Field(ge=18, le=60, coerce=True)  # 年齢は18以上60以下で、数値に強制変換する
    income: Series[int] = pa.Field(ge=1, le=100000)  # 収入は1以上100000以下である
    gender: Series[str] = pa.Field(isin=["male", "female"])  # 性別は"male"か"female"
    job: Series[str] = pa.Field(nullable=True)  # 職業は文字列で、欠損値（None）も許容する

    @pa.check("id")
    def id_split_check(cls, series: Series[str]) -> Series[bool]:
        """'_'でidを分割したときに2つの要素になることを確認する"""
        return series.str.split("_", expand=True).shape[1] == 2

    @pa.check("id")
    def id_length_check(cls, series: Series[str]) -> Series[bool]:
        """'_'でidを分割したときに、その2つの要素の文字数がそれぞれ2文字と4文字であることを確認する"""
        split_series = series.str.split("_", expand=True)
        return (split_series[0].str.len() == 2) & (split_series[1].str.len() == 4)


# データフレームのバリデーションを実行
validated_df = UserSchema.validate(df)
print("Before-----------------")
print(df.dtypes)
print("After------------------")
print(validated_df.dtypes)

```

バリデーションが問題なく完了すればエラーが吐かれず処理を完遂することができます。
```:出力
Before-----------------
id        object
age       object
income     int64
gender    object
job       object
dtype: object

After------------------
id        object
age        int64
income     int64
gender    object
job       object
dtype: object

```
ageの要素3のみあえて文字として入力しておりましたが、coerce=Trueとすることで強制的に指定した型(int)に変換が行われています。


例えば上記のスキーマの定義で、異常なデータ(incomeが-10000等)が混入していた場合はSchemaErrorが出ます。
incomeのindexの2番に異常値が含まれていると以下のように表示されます。
```:出力
pandera.errors.SchemaError: <Schema Column(name=income, type=DataType(int64))> failed element-wise validator 0:
<Check greater_than_or_equal_to: greater_than_or_equal_to(1)>
failure cases:
   index  failure_case
0      2        -10000
```


panderaのチェック機能では、各列（pandasのSeries）を入力とし、条件式を適用してブール値（True/False）のSeriesを出力します。このため、チェックをパスするためには出力されたブール値のSeriesの全ての値がTrueである必要があります。


## pandera.Fieldで指定可能な引数
pandera.Fieldで指定可能な引数を一部抜粋しました。

| 引数 | 説明 | 引数の値の指定方法 |
| ---- | ---- | ---- |
| eq | 列の値が指定した値と等しいかをチェック | 任意の値 |
| ne | 列の値が指定した値と等しくないかをチェック | 任意の値 |
| gt | 列の値が指定した値より大きいかをチェック | 数値 |
| ge | 列の値が指定した値以上かをチェック | 数値 |
| lt | 列の値が指定した値より小さいかをチェック | 数値 |
| le | 列の値が指定した値以下かをチェック | 数値 |
| in_range | 列の値が指定した範囲内にあるかをチェック | 辞書型で上限と下限を指定。{"min_value": 1, "max_value": 100} のように指定。|
| isin | 列の値が指定したリストに含まれているかをチェック | リストまたはイテラブルオブジェクト |
| notin | 列の値が指定したリストに含まれていないかをチェック | リストまたはイテラブルオブジェクト |
| str_contains | 文字列列が指定した文字列を含むかをチェック | 文字列 |
| str_endswith | 文字列列が指定した文字列で終わるかをチェック | 文字列 |
| str_length | 文字列列の長さが指定した条件を満たすかをチェック | 辞書型で文字列の長さの上限と下限を指定。str_length={"min_value": 1, "max_value": 6}のように指定。 |
| str_matches | 文字列列が指定した正規表現にマッチするかをチェック | 正規表現の文字列 |
| str_startswith | 文字列列が指定した文字列で始まるかをチェック | 文字列 |
| nullable | 列がnull値を含んでも良いかどうかを指定 | ブール値 |
| unique | 列の値が一意であるべきかを指定 | ブール値 |
| coerce | 列のデータ型を強制的に変換するかを指定 | ブール値 |
| regex | フィールド名またはエイリアスが正規表現パターンであるかを指定 | ブール値 |
| ignore_na | チェックでnull値を無視するかを指定 | ブール値 |
| raise_warning | 例外ではなく警告を発するかを指定 | ブール値 |
| n_failure_cases | 最初のn件のユニークな失敗ケースを報告。Noneの場合、全ての失敗ケースを報告 | 整数またはNone |


## おわりに
この記事では、panderaのDataFrameModelを用いたデータフレームのバリデーションについて説明しました。
データのバリデーションは機械学習モデルを構築し実際に推論を行うまでの一連の行程に必ずしも必要というわけではないため軽視されがちですが、モデルによる推論結果の信頼性を担保するために不可欠な工程だと思います。
特に定期的に連携されてくるデータを基にモデルで推論を行うなどのケースだと、データの中に予期していない新たな値が追加されていたり、処理異常でデータが一部欠損しているなどの事象は稀に起こります。このような異常にいち早く気づけるようにpanderaのバリデーション機能を積極的に使用していこうと思います。




## 参考
- [公式ドキュメント](https://pandera.readthedocs.io/en/stable/)