check_that, check_viorate
簡易なルールベースのデータ検証ツール
概要
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
data:IntoFrameT(必須)
ルールに基づくデータ検証を行うデータセット。narwhals が受け入れ可能な DataFrame 互換オブジェクト
(例:pandas.DataFrame、polars.DataFrame、pyarrow.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: ルールが検証対象とした項目の数。レコード(行)を検証単位とするルールの場合、
itemはdataの行数(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: objectretailers と rule_dict を py4st.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 Truedf_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によるデータ編集/欠測補完システム』. 共立出版. 地道 正行, 髙橋 雅夫, 藤野 友和, 安川 武彦〔訳〕
