loanguard / src /data /splitter.py
adwitiyashukla's picture
update repo
44cd54e
Raw
History Blame Contribute Delete
2.81 kB
"""Time-aware and stratified data splitters.
Out-of-time validation is essential for credit / fraud models - random
splits hugely overestimate generalisation because borrower behaviour
drifts and macro conditions change. We default to time-based splits
and use stratified random only for unit tests.
"""
from __future__ import annotations
from typing import Tuple
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from ..utils.logging import get_logger
log = get_logger(__name__)
def time_based_split(
df: pd.DataFrame,
date_col: str = "issue_d",
val_size: float = 0.15,
test_size: float = 0.15,
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
"""Sort by date, then chronologically slice into train/val/test.
Older loans go to train, the next chunk to val, the latest to test.
This mirrors how a credit model actually gets deployed.
"""
if date_col not in df.columns:
raise ValueError(f"Date column '{date_col}' not in DataFrame")
df_sorted = df.sort_values(date_col).reset_index(drop=True)
n = len(df_sorted)
test_start = int(n * (1 - test_size))
val_start = int(n * (1 - test_size - val_size))
train = df_sorted.iloc[:val_start].reset_index(drop=True)
val = df_sorted.iloc[val_start:test_start].reset_index(drop=True)
test = df_sorted.iloc[test_start:].reset_index(drop=True)
def _span(d: pd.DataFrame) -> str:
if d.empty or d[date_col].isna().all():
return "empty"
return f"{d[date_col].min().date()}{d[date_col].max().date()}"
log.info(
f"Time-split: train={len(train):,} ({_span(train)}), "
f"val={len(val):,} ({_span(val)}), test={len(test):,} ({_span(test)})"
)
return train, val, test
def stratified_split(
df: pd.DataFrame,
target_col: str = "is_fraud",
val_size: float = 0.15,
test_size: float = 0.15,
random_seed: int = 42,
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
"""Stratified random split - fallback when timestamps aren't available."""
train_val, test = train_test_split(
df, test_size=test_size, stratify=df[target_col], random_state=random_seed
)
relative_val = val_size / (1 - test_size)
train, val = train_test_split(
train_val,
test_size=relative_val,
stratify=train_val[target_col],
random_state=random_seed,
)
log.info(
f"Stratified split: train={len(train):,}, val={len(val):,}, test={len(test):,} | "
f"fraud rate train={train[target_col].mean():.2%}, "
f"val={val[target_col].mean():.2%}, test={test[target_col].mean():.2%}"
)
return (
train.reset_index(drop=True),
val.reset_index(drop=True),
test.reset_index(drop=True),
)