Spaces:
Sleeping
Sleeping
| """ | |
| Production data loading and validation module. | |
| Handles reading transaction data from multiple sources (Parquet, CSV, Delta Lake) | |
| with schema validation, data quality checks, and logging. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional | |
| import numpy as np | |
| import pandas as pd | |
| logger = logging.getLogger(__name__) | |
| EXPECTED_SCHEMA = { | |
| "transaction_id": "object", | |
| "timestamp": "datetime64[ns]", | |
| "cardholder_id": "object", | |
| "card_type": "object", | |
| "merchant_id": "object", | |
| "merchant_category_code": "object", | |
| "merchant_risk_score": "float64", | |
| "transaction_amount": "float64", | |
| "entry_mode": "object", | |
| "country_code": "object", | |
| "is_fraud": "int64", | |
| } | |
| REQUIRED_COLUMNS = list(EXPECTED_SCHEMA.keys()) | |
| class DataValidationError(Exception): | |
| """Raised when data fails validation checks.""" | |
| class TransactionDataLoader: | |
| """Production-grade data loader with validation and quality checks.""" | |
| def __init__(self, data_path: str | Path): | |
| self.data_path = Path(data_path) | |
| if not self.data_path.exists(): | |
| raise FileNotFoundError(f"Data file not found: {self.data_path}") | |
| def load(self, validate: bool = True, sample_frac: Optional[float] = None) -> pd.DataFrame: | |
| """Load transaction data from file. | |
| Args: | |
| validate: Whether to run schema and quality validation. | |
| sample_frac: Optional fraction to sample for development/testing. | |
| Returns: | |
| DataFrame with validated transaction data. | |
| """ | |
| logger.info("Loading data from %s", self.data_path) | |
| if self.data_path.suffix == ".parquet": | |
| df = pd.read_parquet(self.data_path) | |
| elif self.data_path.suffix == ".csv": | |
| df = pd.read_csv(self.data_path, parse_dates=["timestamp"]) | |
| else: | |
| raise ValueError(f"Unsupported file format: {self.data_path.suffix}") | |
| logger.info("Loaded %d rows, %d columns", len(df), len(df.columns)) | |
| if sample_frac is not None: | |
| original_len = len(df) | |
| df = df.sample(frac=sample_frac, random_state=42).reset_index(drop=True) | |
| logger.info("Sampled %.1f%% β %d β %d rows", sample_frac * 100, original_len, len(df)) | |
| if validate: | |
| self._validate_schema(df) | |
| self._validate_quality(df) | |
| return df | |
| def _validate_schema(self, df: pd.DataFrame) -> None: | |
| """Check that all required columns are present.""" | |
| missing = set(REQUIRED_COLUMNS) - set(df.columns) | |
| if missing: | |
| raise DataValidationError(f"Missing required columns: {missing}") | |
| logger.info("Schema validation passed β all %d required columns present", len(REQUIRED_COLUMNS)) | |
| def _validate_quality(self, df: pd.DataFrame) -> None: | |
| """Run data quality checks and log warnings.""" | |
| issues = [] | |
| # Check for nulls in critical columns | |
| critical_cols = ["transaction_id", "cardholder_id", "transaction_amount", "timestamp"] | |
| for col in critical_cols: | |
| null_count = df[col].isna().sum() | |
| if null_count > 0: | |
| issues.append(f"Column '{col}' has {null_count} null values") | |
| # Check for duplicate transaction IDs | |
| dup_count = df["transaction_id"].duplicated().sum() | |
| if dup_count > 0: | |
| issues.append(f"{dup_count} duplicate transaction IDs found") | |
| # Check for negative amounts | |
| neg_count = (df["transaction_amount"] < 0).sum() | |
| if neg_count > 0: | |
| issues.append(f"{neg_count} transactions with negative amounts") | |
| # Check fraud rate is reasonable (0.1% - 10%) | |
| fraud_rate = df["is_fraud"].mean() | |
| if fraud_rate < 0.001 or fraud_rate > 0.10: | |
| issues.append(f"Unusual fraud rate: {fraud_rate:.4%}") | |
| # Check timestamp range | |
| ts_range = (df["timestamp"].max() - df["timestamp"].min()).days | |
| if ts_range < 1: | |
| issues.append(f"Very narrow timestamp range: {ts_range} days") | |
| if issues: | |
| for issue in issues: | |
| logger.warning("Data quality issue: %s", issue) | |
| else: | |
| logger.info("Data quality validation passed") | |
| def load_and_split( | |
| data_path: str | Path, | |
| test_size: float = 0.2, | |
| validation_size: float = 0.1, | |
| random_state: int = 42, | |
| stratify: bool = True, | |
| ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: | |
| """Load data and split into train/validation/test sets. | |
| Uses time-based splitting for production realism β train on older data, | |
| test on newer data to prevent data leakage. | |
| Args: | |
| data_path: Path to the transaction data file. | |
| test_size: Fraction of data for the test set. | |
| validation_size: Fraction of data for the validation set. | |
| random_state: Random seed. | |
| stratify: Whether to use time-based stratification. | |
| Returns: | |
| Tuple of (train_df, val_df, test_df). | |
| """ | |
| loader = TransactionDataLoader(data_path) | |
| df = loader.load(validate=True) | |
| # Time-based split to prevent data leakage | |
| df = df.sort_values("timestamp").reset_index(drop=True) | |
| n = len(df) | |
| train_end = int(n * (1 - test_size - validation_size)) | |
| val_end = int(n * (1 - test_size)) | |
| train_df = df.iloc[:train_end].copy() | |
| val_df = df.iloc[train_end:val_end].copy() | |
| test_df = df.iloc[val_end:].copy() | |
| logger.info( | |
| "Data split β Train: %d (%.1f%%), Val: %d (%.1f%%), Test: %d (%.1f%%)", | |
| len(train_df), len(train_df) / n * 100, | |
| len(val_df), len(val_df) / n * 100, | |
| len(test_df), len(test_df) / n * 100, | |
| ) | |
| # Log fraud rates per split | |
| for name, split_df in [("Train", train_df), ("Val", val_df), ("Test", test_df)]: | |
| fraud_rate = split_df["is_fraud"].mean() | |
| logger.info(" %s fraud rate: %.4f%%", name, fraud_rate * 100) | |
| return train_df, val_df, test_df | |