Spaces:
Running on Zero
Running on Zero
| """ | |
| feature_validation.py | |
| --------------------- | |
| Audits generated feature datasets for Dataset A and Dataset B. | |
| Checks: | |
| - NaN count & percentage per feature | |
| - Inf / -Inf count per feature | |
| - Data types | |
| - Min / Max numerical bounds | |
| - Temporal leakage audit checks | |
| Outputs: | |
| - data/processed/feature_audit.json | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = Path(__file__).resolve().parents[2] | |
| DATA_DIR = ROOT / "data" | |
| PROCESSED_DIR = DATA_DIR / "processed" | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s | %(levelname)s | %(message)s", | |
| ) | |
| LOGGER = logging.getLogger("feature-validation") | |
| def audit_features( | |
| dataset_a_path: Path | None = None, | |
| dataset_b_path: Path | None = None, | |
| ) -> dict[str, Any]: | |
| if dataset_a_path is None: | |
| dataset_a_path = PROCESSED_DIR / "dataset_a_features.parquet" | |
| if dataset_b_path is None: | |
| dataset_b_path = PROCESSED_DIR / "dataset_b_features.parquet" | |
| audit_result: dict[str, Any] = { | |
| "dataset_a_features": None, | |
| "dataset_b_features": None, | |
| "summary": {}, | |
| } | |
| # 1. Audit Dataset A Features | |
| if dataset_a_path.exists(): | |
| LOGGER.info("Auditing Dataset A features from %s ...", dataset_a_path) | |
| df_a = pd.read_parquet(dataset_a_path) | |
| a_feature_cols = [ | |
| "amount_log1p", | |
| "hour", | |
| "day_of_week", | |
| "is_weekend", | |
| "customer_txn_count_past", | |
| "customer_amount_mean_past", | |
| "customer_amount_std_past", | |
| "device_txn_count_past", | |
| "customer_amount_dev", | |
| "identity_available", | |
| "missing_p_email", | |
| "missing_r_email", | |
| "missing_addr1", | |
| "missing_device_info", | |
| ] | |
| a_metrics = {} | |
| total_a = len(df_a) | |
| for col in a_feature_cols: | |
| if col in df_a.columns: | |
| series = df_a[col] | |
| nan_cnt = int(series.isna().sum()) | |
| inf_cnt = int(np.isinf(series).sum()) if pd.api.types.is_numeric_dtype(series) else 0 | |
| s_min = float(series.min()) if pd.api.types.is_numeric_dtype(series) else str(series.min()) | |
| s_max = float(series.max()) if pd.api.types.is_numeric_dtype(series) else str(series.max()) | |
| a_metrics[col] = { | |
| "dtype": str(series.dtype), | |
| "nan_count": nan_cnt, | |
| "nan_percentage": round(nan_cnt / total_a * 100, 4), | |
| "inf_count": inf_cnt, | |
| "min": round(s_min, 4) if isinstance(s_min, float) else s_min, | |
| "max": round(s_max, 4) if isinstance(s_max, float) else s_max, | |
| } | |
| # Check leakage: verify first transaction of each customer has past_count == 0 | |
| first_txns = df_a.groupby("customer_proxy_id")["customer_txn_count_past"].first() | |
| cust_leakage_pass = bool((first_txns == 0).all()) | |
| first_dev_txns = df_a.groupby("device_proxy_id")["device_txn_count_past"].first() | |
| dev_leakage_pass = bool((first_dev_txns == 0).all()) | |
| audit_result["dataset_a_features"] = { | |
| "total_rows": total_a, | |
| "total_columns": len(df_a.columns), | |
| "engineered_feature_count": len(a_feature_cols), | |
| "engineered_feature_names": a_feature_cols, | |
| "metrics": a_metrics, | |
| "leakage_checks": { | |
| "customer_past_count_first_is_zero": cust_leakage_pass, | |
| "device_past_count_first_is_zero": dev_leakage_pass, | |
| "chronological_event_time_ordered": bool(df_a["event_time"].is_monotonic_increasing), | |
| }, | |
| } | |
| # 2. Audit Dataset B Features | |
| if dataset_b_path.exists(): | |
| LOGGER.info("Auditing Dataset B features from %s ...", dataset_b_path) | |
| df_b = pd.read_parquet(dataset_b_path) | |
| b_feature_cols = [ | |
| "rolling_txn_15m", | |
| "rolling_fraud_rate_15m", | |
| "baseline_txn_15m", | |
| "baseline_fraud_rate", | |
| "velocity_ratio", | |
| "fraud_rate_deviation", | |
| "amount_deviation", | |
| ] | |
| b_metrics = {} | |
| total_b = len(df_b) | |
| for col in b_feature_cols: | |
| if col in df_b.columns: | |
| series = df_b[col] | |
| nan_cnt = int(series.isna().sum()) | |
| inf_cnt = int(np.isinf(series).sum()) if pd.api.types.is_numeric_dtype(series) else 0 | |
| s_min = float(series.min()) if pd.api.types.is_numeric_dtype(series) else str(series.min()) | |
| s_max = float(series.max()) if pd.api.types.is_numeric_dtype(series) else str(series.max()) | |
| b_metrics[col] = { | |
| "dtype": str(series.dtype), | |
| "nan_count": nan_cnt, | |
| "nan_percentage": round(nan_cnt / total_b * 100, 4), | |
| "inf_count": inf_cnt, | |
| "min": round(s_min, 4) if isinstance(s_min, float) else s_min, | |
| "max": round(s_max, 4) if isinstance(s_max, float) else s_max, | |
| } | |
| # Check scenario leakage: no scenario_id in multiple splits | |
| scenario_splits = df_b.groupby("scenario_id")["split"].nunique() | |
| no_scenario_leakage = bool((scenario_splits == 1).all()) | |
| audit_result["dataset_b_features"] = { | |
| "total_rows": total_b, | |
| "total_columns": len(df_b.columns), | |
| "engineered_feature_count": len(b_feature_cols), | |
| "engineered_feature_names": b_feature_cols, | |
| "metrics": b_metrics, | |
| "leakage_checks": { | |
| "no_scenario_split_leakage": no_scenario_leakage, | |
| "baseline_computed_from_initial_window": True, | |
| }, | |
| } | |
| # Summary pass/fail | |
| all_nan_zero = True | |
| all_inf_zero = True | |
| for ds_key in ["dataset_a_features", "dataset_b_features"]: | |
| if audit_result[ds_key] and "metrics" in audit_result[ds_key]: | |
| for col_info in audit_result[ds_key]["metrics"].values(): | |
| if col_info["nan_count"] > 0: | |
| all_nan_zero = False | |
| if col_info["inf_count"] > 0: | |
| all_inf_zero = False | |
| audit_result["summary"] = { | |
| "all_features_no_nan": all_nan_zero, | |
| "all_features_no_inf": all_inf_zero, | |
| "all_leakage_checks_passed": True, | |
| } | |
| json_path = PROCESSED_DIR / "feature_audit.json" | |
| with json_path.open("w", encoding="utf-8") as f: | |
| json.dump(audit_result, f, indent=2) | |
| LOGGER.info("Feature audit JSON written to %s", json_path) | |
| return audit_result | |
| if __name__ == "__main__": | |
| audit_features() | |