EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

panderaを用いてデータフレームのバリデーションをしてみる

こんにちは、エモーションテックでSREチームに所属しているsugawaraです。 以前panderaについての記事を書かせていただいたのですが、今回もpanderaについて書いてみます。

この記事は エモーションテック Advent Calendar 2022 の 15 日目の記事です。

はじめに

データフレームを扱う際、データのバリデーションを実施したいことがあると思います。panderaではそのような機能が提供されているのですが、どういうバリデーションができるのか具体的に見ていきます。

サンプルケース①

以下のようなデータフレームがあるとします。

上記のデータフレームに対して、以下のようなバリデーションを実施したいとします。

  • カラム名のprefixが「str」であるカラムについて
    • 「A」「B」「C」いずれかの値を取り、各値を取る行が一つ以上存在する
    • Nullは許容しない
  • カラム名のprefixが「int」であるカラムについて
    • 以下-3 ~ 3範囲のint型の値を取る
    • 0より大きい値を取る行が一つ以上存在する
    • 0より小さい値を取る行が一つ以上存在する
    • Nullは許容しない

上記を実現するのが以下コードになります。

import pandera as pa

schema = pa.DataFrameSchema({
    "str.+": pa.Column(
        str,
        checks=[
            pa.Check(lambda series: series.isin(["A", "B", "C"]), name="check_str_col_1"),
            pa.Check(lambda series: (series == "A").sum() > 0, name="check_str_col_2"),
            pa.Check(lambda series: (series == "B").sum() > 0, name="check_str_col_3"),
            pa.Check(lambda series: (series == "C").sum() > 0, name="check_str_col_4"),
        ],
        nullable=False,
        regex=True,
    ),
    "int.+": pa.Column(
        int,
        checks=[
            pa.Check(lambda series: (-3 <= series) & (series <= 3), name="check_int_col_1"),
            pa.Check(lambda series: (series > 0).sum() > 0, name="check_int_col_2"),
            pa.Check(lambda series: (series < 0).sum() > 0, name="check_int_col_3"),
        ],
        nullable=False,
        regex=True,
    ),
})

schema.validate(df)

実行結果はこちら。

SchemaError: <Schema Column(name=int_3, type=DataType(int64))> failed series or dataframe validator 2:
<Check check_int_col_3>

エラーメッセージから「int_3」カラムのデータが「check_int_col_3」のチェックで失敗していることがわかります。入力のデータフレームを確認すると、「int_3」カラムのデータは全て正の値なので、バリデーションは期待した通りの結果となっています。

サンプルケース②

先ほどと同じデータフレームがあるとします。

上記のデータフレームに対して、以下のようなバリデーションを実施したいとします。

  • カラム名が「str_1」であるカラムについて
    • 「A」「B」「C」いずれかの値を取る
    • Nullは許容しない
  • カラム名のprefixが「int」であるカラムについて
    • 以下-3 ~ 3範囲のint型の値を取る
    • 「str_1」カラムが取りうる値ごとに0より大きい値を取る行が一つ以上存在する
    • 「str_1」カラムが取りうる値ごとに0より小さい値を取る行が一つ以上存在する
    • Nullは許容しない

上記を実現するのが以下コードになります。 今回groupbyを用いているのですがその際の注意点として、groupbyで指定しているカラムはDataFrameSchemaで定義してある必要があります。

import pandera as pa

schema = pa.DataFrameSchema({    "str_1": pa.Column(
        str,
        checks=[pa.Check(lambda series: series.isin(["A", "B", "C"]), name="check_str_col_1")],
        nullable=False,
    ),
    "int.+": pa.Column(
        int,
        checks=[
            pa.Check(lambda series: (-3 <= series) & (series <= 3), name="check_int_col_1"),
            pa.Check(lambda d: (d["A"] > 0).sum() > 0, name="check_int_col_2_1", groupby="str_1"),
            pa.Check(lambda d: (d["B"] > 0).sum() > 0, name="check_int_col_2_2", groupby="str_1"),
            pa.Check(lambda d: (d["C"] > 0).sum() > 0, name="check_int_col_2_3", groupby="str_1"),
            pa.Check(lambda d: (d["A"] < 0).sum() > 0, name="check_int_col_3_1", groupby="str_1"),
            pa.Check(lambda d: (d["B"] < 0).sum() > 0, name="check_int_col_3_2", groupby="str_1"),
            pa.Check(lambda d: (d["C"] < 0).sum() > 0, name="check_int_col_3_3", groupby="str_1"),
        ],
        nullable=False,
        regex=True,
    ),
})

schema.validate(df)

もう少しスマートな書き方ができないか試行錯誤してみたのですが諦めました。 実行結果はこちら。

SchemaError: <Schema Column(name=int_2, type=DataType(int64))> failed series or dataframe validator 6:
<Check check_int_col_3_3>

エラーメッセージから「int_2」カラムのデータが「check_int_col_3_3」のチェックで失敗していることがわかります。入力のデータフレームを確認すると、「str_1」カラムが「C」のデータは「int_2」カラムの値がどちらも正の値なので、バリデーションは期待した通りの結果となっています。

まとめ

いかがでしたでしょうか。 個人的には、groupbyを使えば複雑な条件のバリデーションを実現できるのはなかなか良いなと思いました。

We're Hiring!

エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、ぜひ採用ページからご応募をお願いいたします。

hrmos.co

参考リンク

https://pandera.readthedocs.io/en/stable/checks.html