check_that, check_viorate

badge of tested backend

簡易なルールベースのデータ検証ツール

概要

 R言語の varidateパッケージの check_that() 関数などをオマージュした、ごく簡易なデータ検証関数です。

check_that(
    data: IntoFrameT,
    rule_dict: Union[Mapping[str, str], pd.Series],
    **kwargs: Any,
)

check_viorate(
    data: IntoFrameT,
    rule_dict: Union[Mapping[str, str], pd.Series],
    **kwargs: Any,
)

引数 Argument

  • dataIntoFrameT(必須)
     ルールに基づくデータ検証を行うデータセット。narwhals が受け入れ可能な DataFrame 互換オブジェクト
    (例:pandas.DataFramepolars.DataFramepyarrow.Table)を指定できます。
  • rule_dictdict or pd.Series of str(必須)
     pandas.eval() メソッドで実行した結果が論理値となるような expression の文字列を値とする辞書オブジェクト。詳細は使用例も参照してください。
  • to_native: bool
    True の場合、入力と同じ型のデータフレーム(e.g. pandas / polars / pyarrow)を返します。
    False の場合、narwhals.DataFrame を返します。デフォルトは True で、to_native = False は、主にライブラリ内部での利用や、バックエンドに依存しない後続処理を行う場合を想定したオプションです。
  • **kwargs
     pandas.eval() に渡す追加の引数。

返り値 Value

check_that(): データセット単位の検証結果の集計

次の列を含む、引数 data に代入されたデータフレームと同じ型の DataFrame が出力されます。

  • rule: 検証ルールの名前
  • item: ルールが検証対象とした項目の数。レコード(行)を検証単位とするルールの場合、itemdata の行数(rows)になります。一方、データセット全体を検証単位とするルール(例:集計量に基づく条件)の場合、item は 1 になります。
  • passes: 検証の結果、ルールを満たすと判定されたレコードの数。
  • fails: 検証の結果、ルールを満たさないと判定されたレコードの数。
  • countna: 欠測値によって、ルールの検証が行えなかったレコードの数。行(レコード)を検証単位とするルールでは、ルールの評価に使用された変数のいずれかに欠測値が含まれる場合、そのレコードは検証不能として NA 扱いされます。countna は、このように検証を正しく実施できなかったレコードの件数を表します。
  • expression: 検証ルールを表す文字列(expression)。

check_viorate(): レコード単位の検証結果

ルール名を列名として、レコード毎の違反を示す論理変数をもつ DataFrame が出力されます。

各列の要素の True は検証のルールへの違反、もしくは欠測値によって評価に失敗したことを表します。rule_dict で設定された各ルールに対応する列の他に、次の列が追加で出力されます。

  • any: 行内のいずれかのルールが違反または評価に失敗した場合に True となるブール値。
  • all: 行内の全ルールが違反または評価に失敗した場合に True となるブール値。

使用例 Examples

 ここでは py4st.check_that() 関数を使って Loo, Jonge(2022, p. 136)の結果を再現します。まずはR言語の validate パッケージに付属する retailers データを利用します。retailers は60件の小売業者の経営状況についてのデータで、従業員数、売上高とその他の収入、人件費、総費用、および利益がユーロ導入前の通貨単位である1000ギルダー単位で収録されています。

import py4stats as py4st
import pandas as pd

URL = 'https://raw.githubusercontent.com/data-cleaning/validate/master/pkg/data/retailers.csv'
retailers = pd.read_csv(URL, sep = ';')
retailers.columns = retailers.columns.to_series().str.replace('.', '_', regex = False)

 py4st.check_that() 関数は、第1引数にデータセットを、第2引数に検証ルールの辞書オブジェクトを代入して使用します。
 まずは、検証ルールの辞書オブジェクトを定義します。辞書オブジェクトの値には pandas.eval() メソッドで実行可能な expression の文字列を指定し、key に検証ルールの名前を指定します。検証ルールの名前は任意の値で構いませんが、 expression は結果が論理値となるものでなければなりません。

rule_dict =  {
    'to':'turnover > 0',                                     # 売上高は厳密に正である
    'sc':'staff_costs / staff < 50',                         # 従業員1人当たりの人件費は50,000ギルダー未満である
    'cd1':'staff_costs > 0 | ~(staff > 0)',                    # 従業員がいる場合、人件費は厳密に正である
    'cd2':py4st.implies_exper('staff > 0', 'staff_costs > 0'), # cd1 の別表現
    'bs':'turnover + other_rev == total_rev',                # 売上高とその他の収入の合計は総収入に等しい
    'mn':'profit.mean() > 0'                                 # セクター全体の平均的な利益はゼロよりも大きい
    }
pd.Series(rule_dict)
#> to                          turnover > 0
#> sc              staff_costs / staff < 50
#> cd1       staff_costs > 0 | ~(staff > 0)
#> cd2       staff_costs > 0 | ~(staff > 0)
#> bs     turnover + other_rev == total_rev
#> mn                     profit.mean() > 0
#> dtype: object

retailersrule_dictpy4st.check_that() に代入すると、rule_dict に指定したルールに基づいた検証が実行されます。item 列はその検証ルールで生成された論理値の個数(通常はデータセットの列数と一致します)を表し、passes 列は検証結果が True となったレコードの数を、fails は False となったレコードの数を表します。また、coutna はルールの検証に使用した変数(データセットの列)のいずれかが欠測値であったレコードの数です。

print(py4st.check_that(retailers, rule_dict))
#>   rule  item  passes  fails  coutna                         expression
#> 0   to    60      56      0       4                       turnover > 0
#> 1   sc    60      39      5      16           staff_costs / staff < 50
#> 2  cd1    60      44      0      16     staff_costs > 0 | ~(staff > 0)
#> 3  cd2    60      44      0      16     staff_costs > 0 | ~(staff > 0)
#> 4   bs    60      19      4      37  turnover + other_rev == total_rev
#> 5   mn     1       1      0       0                  profit.mean() > 0

前述の通り、py4st.check_that() 関数ではルール検証を pandas.eval() メソッドで実行しているため、検証ルールに自作関数や外部のモジュールからインポート関数を使うには、関数名の前に @ をつけて @func(…) と記述し、また **kwargs 引数に local_dict = locals() と指定してください。
 次のコードで定義している is_complete() 関数は、代入された pd.Series が全て欠測値ではなく、指定された変数に関して完全ケースであることを判定する関数です。turnover.notna() & total_rev.notna() & other_rev.notna() と記述しても同じ結果が得られますが、自作関数を使うことで若干簡潔に記述できます。

from pandas.api.types import is_numeric_dtype
def is_complete(*arg): return pd.concat(arg, axis = 'columns').notna().all(axis = 'columns')

pd.set_option('display.expand_frame_repr', False)

rule_dict2 =  {
    'to_num':'@is_numeric_dtype(turnover)',                      # 売上高は数値変数である
    'rev_complete':'@is_complete(turnover, total_rev, other_rev)', # 売上高と収入が全て観測されている
    }

print(py4st.check_that(
    retailers, rule_dict2, local_dict = locals()
    ))
#>            rule  item  passes  fails  coutna                                    expression
#> 0        to_num     1       1      0       0                   @is_numeric_dtype(turnover)
#> 1  rev_complete    60      23      0      37  @is_complete(turnover, total_rev, other_rev)

py4st.check_viorate() の使い方も py4st.check_that() と同様ですが、py4st.check_that() がデータセット全体での検証結果を出力するのに対し、py4st.check_viorate() ではレコード別の検証結果を表示します。py4st.check_viorate() から出力されるデータフレームでは、各列が検証ルールに、各行が元データの観測値に対応し、当該ルールが満たされていない場合、True と表示されます。また、any 列は複数あるルールのいずれか1つでも満たされていないことを、all 列は全てのルールが満たされていないことを示します。

rule_dict3 =  {
    'to':'turnover > 0',                                     # 売上高は厳密に正である
    'sc':'staff_costs / staff < 50',                         # 従業員1人当たりの人件費は50,000ギルダー未満である
    'rev_complete':'@is_complete(turnover, total_rev, other_rev)',# 売上高と収入が全て観測されている
    }
  
df_viorate = py4st.check_viorate(retailers, rule_dict3)
print(df_viorate.head())
#>       to     sc rev_complete   any    all
#> 0   True   True         True  True   True
#> 1  False  False         True  True  False
#> 2  False   True        False  True  False
#> 3  False   True        False  True  False
#> 4   True   True         True  True   True

df_viorate データフレームの各列は論理値であるため、次のように検証ルールを満たさない観測値を抽出することができます。

print(retailers.loc[df_viorate['to'], 'size':'turnover'])
#>   size  incl_prob  staff  turnover
#> 0  sc0       0.02   75.0       NaN
#> 4  sc3       0.14    NaN       NaN
#> 6  sc3       0.14    5.0       NaN

注意 Notes

本関数の内部実装は、 pd.DataFrame.eval() メソッドに依存しているため、実行時間の面で必ずしも最適化されていません。

参考文献

  • Loo, Mark van der, and Edwin de Jonge. (2022). 『統計的データクリーニングの理論と実践: Rによるデータ編集/欠測補完システム』. 共立出版. 地道 正行, 髙橋 雅夫, 藤野 友和, 安川 武彦〔訳〕

Return to Function reference.