""" audit_dataset_a.py ------------------ Audits Dataset A transaction-level model dataset (IEEE-CIS derived). Performs structural and statistical verification: - target distribution - missingness per column - duplicates - chronological ordering - train/validation/test time boundaries - fraud distribution by split - amount distribution by split - categorical cardinality - constant columns - potential target leakage columns Output: - data/processed/dataset_a_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("audit-dataset-a") def audit_dataset_a(parquet_path: Path | None = None) -> dict[str, Any]: if parquet_path is None: parquet_path = PROCESSED_DIR / "dataset_a_model.parquet" if not parquet_path.exists(): raise FileNotFoundError(f"Dataset A file not found: {parquet_path}") LOGGER.info("Loading Dataset A from %s ...", parquet_path) df = pd.read_parquet(parquet_path) # 1. Target distribution total_rows = len(df) fraud_count = int(df["isFraud"].sum()) non_fraud_count = total_rows - fraud_count fraud_pct = round(float(fraud_count / total_rows * 100), 4) # 2. Missingness per column missing_counts = df.isna().sum().to_dict() missing_pcts = (df.isna().mean() * 100).round(4).to_dict() missingness = { col: {"count": int(missing_counts[col]), "percentage": float(missing_pcts[col])} for col in df.columns } # 3. Duplicates dup_tx_ids = int(df["TransactionID"].duplicated().sum()) # 4. Chronological ordering is_ordered = bool(df["event_time"].is_monotonic_increasing) # 5. Train/Val/Test boundaries & fraud distribution splits = {} amount_by_split = {} fraud_by_split = {} for split_name in ["train", "validation", "test"]: sub = df[df["split"] == split_name] if not sub.empty: s_min = str(sub["event_time"].min()) s_max = str(sub["event_time"].max()) s_fraud = int(sub["isFraud"].sum()) s_total = len(sub) s_fraud_pct = round(float(s_fraud / s_total * 100), 4) splits[split_name] = { "rows": s_total, "min_event_time": s_min, "max_event_time": s_max, "fraud_count": s_fraud, "fraud_percentage": s_fraud_pct, } amt_series = sub["amount"] amount_by_split[split_name] = { "min": round(float(amt_series.min()), 2), "max": round(float(amt_series.max()), 2), "mean": round(float(amt_series.mean()), 2), "std": round(float(amt_series.std()), 2), "median": round(float(amt_series.median()), 2), } # Verify split boundary ordering train_max = df[df["split"] == "train"]["event_time"].max() val_min = df[df["split"] == "validation"]["event_time"].min() val_max = df[df["split"] == "validation"]["event_time"].max() test_min = df[df["split"] == "test"]["event_time"].min() boundary_valid = (train_max <= val_min) and (val_max <= test_min) # 6. Categorical cardinality cat_cols = [ col for col in [ "ProductCD", "card1", "card2", "card3", "card4", "card5", "card6", "addr1", "addr2", "P_emaildomain", "R_emaildomain", "DeviceType", "DeviceInfo", "customer_proxy_id", "device_proxy_id" ] if col in df.columns ] cardinality = {col: int(df[col].nunique(dropna=False)) for col in cat_cols} # 7. Constant columns constant_columns = [col for col in df.columns if df[col].nunique(dropna=False) <= 1] # 8. Potential leakage columns (|corr| > 0.95 with target) num_cols = df.select_dtypes(include=[np.number]).columns potential_leakage = [] for col in num_cols: if col != "isFraud": corr = float(df[col].corr(df["isFraud"])) if not np.isnan(corr) and abs(corr) > 0.95: potential_leakage.append({"column": col, "correlation": round(corr, 4)}) audit_json = { "dataset": "Dataset A (IEEE-CIS Model Dataset)", "total_rows": total_rows, "total_columns": len(df.columns), "target_distribution": { "fraud_count": fraud_count, "non_fraud_count": non_fraud_count, "fraud_percentage": fraud_pct, }, "duplicate_transaction_ids": dup_tx_ids, "chronological_ordering_valid": is_ordered, "split_boundary_valid": boundary_valid, "splits": splits, "amount_distribution_by_split": amount_by_split, "missingness": missingness, "categorical_cardinality": cardinality, "constant_columns": constant_columns, "potential_leakage_columns": potential_leakage, } json_path = PROCESSED_DIR / "dataset_a_audit.json" with json_path.open("w", encoding="utf-8") as f: json.dump(audit_json, f, indent=2) LOGGER.info("Dataset A audit JSON written to %s", json_path) return audit_json if __name__ == "__main__": audit_dataset_a()