Back to README · See also: Feature Catalog · Training Guide
| Source | Volume | Fraud Rate | Format | Use |
|---|---|---|---|---|
| Real org transactions | Varies | 0.05–0.5% | Custom CSV → canonical | Primary training signal |
| Synthetic generator | Configurable (1M+) | 0.3% (tunable per rail) | Parquet | Cold start + augmentation |
| IEEE-CIS (Kaggle) | 590K | 3.5% | CSV (opaque columns) | GBM training supplement |
| UCI San Diego | 284K | 0.172% | CSV (PCA-anonymised) | Autoencoder pretraining |
IEEE-CIS columns (V1–V339, C1–C14, D1–D15, card1–card6) are opaque — never map to canonical fields. Keep in source namespace (ieee_cis.V1 etc).
Only harmonise: TransactionAmt → amount_usd, TransactionDT → timestamp_utc, isFraud → is_fraud, DeviceType, ProductCD.
Why: Column semantics are unknown (PCA-transformed by Vesta). Mapping V23 → user_account_age_days introduces false semantics and training-serving skew.
Code: src/data/loader.py:load_ieee_cis
All data normalised to TransactionRecord (Pydantic v2). Multi-rail with rail-specific required fields.
class TransactionRecord(BaseModel):
# ── Identity ──────────────────────────────────────────────────────────────
transaction_id: str # UUIDv4 recommended
timestamp_utc: datetime # always UTC; validator enforces
data_source: Literal["ieee_cis", "uc_san_diego", "synthetic", "real_org"]
schema_version: str = "2.0.0"
# ── Rail ─────────────────────────────────────────────────────────────────
rail: Literal["upi", "aeps", "dmt", "cards", "loans", "utility"]
channel: str # see channel list in API Reference
# ── Amount ───────────────────────────────────────────────────────────────
amount_inr: Decimal # in INR, >= 0
currency_code: str = "INR" # ISO 4217
# ── Parties ──────────────────────────────────────────────────────────────
customer_id_hash: str # SHA-256, 64 hex chars
merchant_id: str
merchant_country: str = "IN" # ISO 3166-1 alpha-2
# ── Card (PCI-DSS — no raw PAN) ──────────────────────────────────────────
card_token: Optional[str] # tokenized; never raw PAN
bin_8: Optional[str] # first 8 digits
pan_last4: Optional[str] # last 4 only
card_type: Optional[Literal["debit", "credit", "prepaid", "corporate", "rupay"]]
cvv_result: Optional[Literal["match", "no_match", "not_provided"]]
three_ds_result: Optional[Literal["authenticated", "not_authenticated", "not_attempted"]]
is_prepaid: bool = False
# ── UPI-specific ─────────────────────────────────────────────────────────
vpa_sender_hash: Optional[str] # required for rail=upi
vpa_receiver_hash: Optional[str]
upi_app: Optional[str]
consent_timestamp_utc: Optional[datetime]
# ── AEPS-specific (Aadhaar Act §29 compliant) ─────────────────────────────
aadhaar_last4: Optional[str] # last 4 only; no full Aadhaar
agent_id: Optional[str] # required for rail=aeps
biometric_quality_score: Optional[float] # UIDAI quality 0–100; NOT raw biometric
biometric_attempt_count: Optional[int]
session_duration_seconds: Optional[float]
# ── DMT-specific ─────────────────────────────────────────────────────────
beneficiary_account_hash: Optional[str]
beneficiary_bank_ifsc: Optional[str]
beneficiary_is_new: Optional[bool]
# ── Device / Network ─────────────────────────────────────────────────────
device_id_hash: Optional[str] # SHA-256 of device fingerprint
ip_address_hash: Optional[str] # SHA-256+salt; never raw IP
device_firmware_version: Optional[str]
txn_lat: Optional[float]
txn_lon: Optional[float]
# ── Label ────────────────────────────────────────────────────────────────
is_fraud: Optional[int] # 0 or 1; absent for inference
label_source: Optional[str] # "confirmed_chargeback" (gold standard)
label_confirmed_at_utc: Optional[datetime]| Validator | Rule |
|---|---|
validate_hash_format |
customer_id_hash, device_id_hash, ip_address_hash must be 64 or 128 hex chars |
validate_rail_fields |
UPI requires vpa_sender_hash; AEPS requires agent_id; DMT requires beneficiary_account_hash |
no_full_aadhaar |
aadhaar_last4 must be exactly 4 digits; rejects 12-digit input |
timestamp_utc |
Converts naive datetime to UTC; rejects future timestamps > 5 minutes |
Code: src/data/schema.py
Generates realistic per-rail transaction data with configurable fraud injection.
python -m fraud_detection.src.data.generator \
--n_transactions 1_000_000 \
--output fraud_detection/data/synthetic/txns.parquet \
--seed 42 \
--n_customers 50_000| Rail | Fraud Rate | Amount Distribution | Fraud Patterns |
|---|---|---|---|
| UPI | 0.3% | Log-normal μ=7.5, σ=1.2 (INR) | Phishing, fake QR, SIM swap |
| AEPS | 0.15% | Bimodal (₹100–₹2000 withdrawals) | Ghost txn, rogue agent |
| DMT | 0.5% | Log-normal, spike at ₹97K–₹99K | Structuring, mule |
| Cards | 0.8% | Log-normal μ=8.0, σ=1.5 | CNP, BIN attack |
- Poisson arrivals with IST peak hours (10am–1pm and 5pm–8pm higher rate)
- Month-end spike (salary day: 1st–3rd and 28th–31st)
- Weekend suppression for corporate/DMT channels
- Random seed → fully reproducible
_inject_aeps_ghost() # session < 10s, quality=100, attempts=1
_inject_dmt_structuring() # amount in ₹97K–₹99.5K band, same-day repeat
_inject_card_cnp() # ecommerce, no 3DS, new merchant, foreign BINCode: src/data/generator.py
Applied to all sources before training. Idempotent — safe to run multiple times.
| Step | Function | Description |
|---|---|---|
| 1 | dedup |
Drop exact-duplicate transaction_id rows; keep first |
| 2 | normalise_timestamps |
Convert all timestamps to UTC; drop rows with null timestamp |
| 3 | filter_test_transactions |
Drop transaction_id containing test, mock, sandbox |
| 4 | filter_amount_outliers |
Drop amount = 0; cap at 99.9th percentile per rail |
| 5 | filter_label_maturity |
Drop rows where label_confirmed_at_utc > cutoff - 14d (labels not yet mature) |
| 6 | coerce_schema |
Cast all columns to canonical types; fill missing with rail-appropriate defaults |
| 7 | clean |
Master pipeline: runs steps 1–6 in sequence; returns cleaned Polars DataFrame |
# Exclude last 14 days from training set
cutoff = df["timestamp_utc"].max() - timedelta(days=14)
df = df.filter(pl.col("timestamp_utc") <= cutoff)Chargebacks arrive up to 45 days after transaction. Labels in the last 14 days have high false-negative rate (fraud not yet confirmed). Including them trains the model to think recent fraud is legitimate.
Code: src/data/cleaner.py
from fraud_detection.src.data.loader import load_all, combine
dfs = load_all(
real_org_path="data/raw/org_txns.csv",
ieee_cis_path="data/external/ieee_cis/",
uci_path="data/external/uc_san_diego/creditcard.csv",
synthetic_path="data/synthetic/txns.parquet",
)
combined = combine(dfs) # unified canonical DataFrame| Function | Reads | Normalises |
|---|---|---|
load_synthetic |
Parquet | Already canonical |
load_uci_san_diego |
CSV | V1–V28, Amount, Class → canonical amount + label |
load_ieee_cis |
CSV directory | TransactionAmt, isFraud → canonical; V*/C*/D* kept as-is |
load_real_org |
CSV | Custom columns → canonical via configurable mapping |
load_all |
All sources | Returns dict keyed by source name |
combine |
dict | Concatenates, adds data_source column |
Code: src/data/loader.py
fraud_detection/data/
├── raw/ # Real org transactions — anonymised before commit
│ └── .gitkeep
├── processed/ # Cleaned, point-in-time feature snapshots
│ └── .gitkeep
├── synthetic/ # Generator output — can be regenerated; not committed
│ └── .gitkeep
└── external/ # Kaggle datasets — download separately
├── .gitkeep
├── ieee_cis/ # Place Kaggle IEEE-CIS files here
│ ├── train_transaction.csv
│ └── train_identity.csv
└── uc_san_diego/ # Place UCI credit card dataset here
└── creditcard.csv
All data directories in .gitignore. Data managed outside git (S3 / DVC for large files).
# IEEE-CIS Fraud Detection (Kaggle)
kaggle competitions download -c ieee-fraud-detection -p fraud_detection/data/external/ieee_cis/
# UCI Credit Card Fraud (Kaggle)
kaggle datasets download -d mlg-ulb/creditcardfraud -p fraud_detection/data/external/uc_san_diego/Requires kaggle CLI + ~/.kaggle/kaggle.json credentials.
For training, velocity features are pre-materialised as-of each transaction's timestamp to prevent training-serving skew:
processed/
├── features_train.parquet # features computed as-of txn timestamp
├── features_val.parquet
├── features_test.parquet
└── feature_schema_v2.json # feature names + types for version tracking
Why not compute live from raw logs? Computing 14-window velocity for 1M training examples from raw logs takes hours. Pre-materialise once, reuse for all HPO trials.
Next: Feature Catalog · Training Guide