bank-fraud / scripts /generate_fixtures.py
root
init
942b115
Raw
History Blame Contribute Delete
9.07 kB
"""Generates the hand-labeled fixture set consumed by
tests/unit/test_labeled_fixtures.py (TEST-03).
Each entry is a deliberately constructed, individually-reasoned scenario
(not a random sample) covering obvious fraud and obvious legitimate
patterns, spread across simulated dates (steps) so date/time-derived
features aren't exercised by only a single moment. "Obvious" here means
built from the same structural patterns the Phase 1 EDA identified: fraud
confined to TRANSFER/CASH_OUT with a full-balance drain (amount ==
oldbalanceOrg, newbalanceOrig == 0) versus legitimate transactions that
leave a residual balance or are CASH_IN/PAYMENT/DEBIT (fraud rate 0 in the
training data for those types).
Run: python -m scripts.generate_fixtures
Writes: tests/fixtures/labeled_transactions.json
"""
from __future__ import annotations
import json
from pathlib import Path
OUTPUT_PATH = Path("tests/fixtures/labeled_transactions.json")
HOURS_PER_DAY = 24
def _step(day: int, hour: int) -> int:
return max(1, day * HOURS_PER_DAY + hour)
def _txn(
*,
id_: str,
label: str,
description: str,
day: int,
hour: int,
type_: str,
amount: float,
name_orig: str,
old_orig: float,
new_orig: float,
name_dest: str,
old_dest: float,
new_dest: float,
) -> dict:
return {
"id": id_,
"expected_label": label,
"description": description,
"simulated_day": day,
"simulated_hour": hour,
"step": _step(day, hour),
"type": type_,
"amount": amount,
"nameOrig": name_orig,
"oldbalanceOrg": old_orig,
"newbalanceOrig": new_orig,
"nameDest": name_dest,
"oldbalanceDest": old_dest,
"newbalanceDest": new_dest,
}
def build_fixtures() -> list[dict]:
fixtures: list[dict] = []
# ---- Obvious fraud: full-balance-drain TRANSFER, varied amounts/times ----
fraud_transfer_amounts = [
181.0, 2500.0, 9999.99, 50000.0, 120000.0, 275000.5, 999999.0, 45.0, 733.21, 18500.0,
]
for i, amount in enumerate(fraud_transfer_amounts):
day = i * 3
hour = (i * 7) % 24
fixtures.append(
_txn(
id_=f"fraud-transfer-drain-{i:02d}",
label="fraud",
description="Full-balance TRANSFER drain (amount == oldbalanceOrg, newbalanceOrig == 0) -- the PaySim fraud signature.",
day=day,
hour=hour,
type_="TRANSFER",
amount=amount,
name_orig=f"CFXFRAUD{i:03d}A",
old_orig=amount,
new_orig=0.0,
name_dest=f"CFXFRAUD{i:03d}B",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious fraud: full-balance-drain CASH_OUT, varied amounts/times ----
fraud_cashout_amounts = [
300.0, 4750.0, 15000.0, 62000.0, 210000.0, 890000.0, 99.99, 3333.33, 27500.0, 145000.0,
]
for i, amount in enumerate(fraud_cashout_amounts):
day = 30 + i * 2
hour = (i * 5 + 1) % 24
fixtures.append(
_txn(
id_=f"fraud-cashout-drain-{i:02d}",
label="fraud",
description="Full-balance CASH_OUT drain -- same signature as TRANSFER fraud, second fraud-prone type identified in EDA.",
day=day,
hour=hour,
type_="CASH_OUT",
amount=amount,
name_orig=f"CFXFRAUD{i:03d}C",
old_orig=amount,
new_orig=0.0,
name_dest=f"CFXFRAUD{i:03d}D",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious fraud: large drains at night hours, brand-new accounts ----
night_amounts = [8200.0, 41000.0, 176000.0, 630000.0, 12500.5, 55555.0, 320000.0, 7770.0]
for i, amount in enumerate(night_amounts):
day = 60 + i * 4
hour = [1, 2, 3, 23][i % 4]
fixtures.append(
_txn(
id_=f"fraud-night-drain-{i:02d}",
label="fraud",
description="Full-balance drain at a late-night simulated hour, single-use account (no prior history).",
day=day,
hour=hour,
type_="TRANSFER" if i % 2 == 0 else "CASH_OUT",
amount=amount,
name_orig=f"CFXFRAUDN{i:03d}A",
old_orig=amount,
new_orig=0.0,
name_dest=f"CFXFRAUDN{i:03d}B",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious legit: PAYMENT to merchant, partial balance, varied days ----
payment_cases = [
(25.50, 500.0), (89.99, 1200.0), (14.00, 300.0), (250.0, 3000.0), (5.75, 80.0),
(999.0, 15000.0), (42.30, 600.0), (150.0, 2200.0), (7.25, 100.0), (620.0, 9000.0),
]
for i, (amount, balance) in enumerate(payment_cases):
day = i * 6
hour = (i * 3 + 8) % 24
fixtures.append(
_txn(
id_=f"legit-payment-{i:02d}",
label="legit",
description="Small PAYMENT to a merchant, balance not drained -- fraud rate 0 for PAYMENT in the training data.",
day=day,
hour=hour,
type_="PAYMENT",
amount=amount,
name_orig=f"CFXLEGITP{i:03d}",
old_orig=balance,
new_orig=balance - amount,
name_dest=f"MFXMERCH{i:03d}",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious legit: CASH_IN deposits (never fraud in PaySim) ----
cashin_cases = [
(200.0, 1000.0), (5000.0, 20000.0), (75.0, 400.0), (1200.0, 8000.0), (33.0, 150.0),
(9000.0, 50000.0), (450.0, 3000.0), (60.0, 900.0),
]
for i, (amount, balance) in enumerate(cashin_cases):
day = 10 + i * 5
hour = (i * 2 + 6) % 24
fixtures.append(
_txn(
id_=f"legit-cashin-{i:02d}",
label="legit",
description="CASH_IN deposit -- always legitimate in PaySim (fraud confined to TRANSFER/CASH_OUT).",
day=day,
hour=hour,
type_="CASH_IN",
amount=amount,
name_orig=f"CFXLEGITCI{i:03d}",
old_orig=balance,
new_orig=balance + amount,
name_dest=f"CFXLEGITCID{i:03d}",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious legit: DEBIT, small amounts ----
debit_cases = [(20.0, 300.0), (55.0, 700.0), (8.5, 120.0), (310.0, 4000.0), (17.25, 250.0)]
for i, (amount, balance) in enumerate(debit_cases):
day = 20 + i * 7
hour = (i * 4 + 12) % 24
fixtures.append(
_txn(
id_=f"legit-debit-{i:02d}",
label="legit",
description="Small DEBIT transaction -- fraud rate 0 for DEBIT in the training data.",
day=day,
hour=hour,
type_="DEBIT",
amount=amount,
name_orig=f"CFXLEGITD{i:03d}",
old_orig=balance,
new_orig=balance - amount,
name_dest=f"MFXDEBIT{i:03d}",
old_dest=0.0,
new_dest=0.0,
)
)
# ---- Obvious legit: partial TRANSFER/CASH_OUT, balance survives ----
partial_cases = [
("TRANSFER", 500.0, 5000.0), ("CASH_OUT", 1200.0, 8000.0), ("TRANSFER", 75.0, 900.0),
("CASH_OUT", 3000.0, 40000.0), ("TRANSFER", 220.0, 3000.0), ("CASH_OUT", 60.0, 500.0),
("TRANSFER", 9000.0, 100000.0),
]
for i, (type_, amount, balance) in enumerate(partial_cases):
day = 45 + i * 3
hour = (i * 6 + 9) % 24
fixtures.append(
_txn(
id_=f"legit-partial-{type_.lower()}-{i:02d}",
label="legit",
description=f"Partial {type_} -- amount well under balance, account not drained (newbalanceOrig > 0).",
day=day,
hour=hour,
type_=type_,
amount=amount,
name_orig=f"CFXLEGITPART{i:03d}A",
old_orig=balance,
new_orig=balance - amount,
name_dest=f"CFXLEGITPART{i:03d}B",
old_dest=0.0,
new_dest=amount,
)
)
return fixtures
def main() -> None:
fixtures = build_fixtures()
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(json.dumps(fixtures, indent=2))
fraud_count = sum(1 for f in fixtures if f["expected_label"] == "fraud")
legit_count = sum(1 for f in fixtures if f["expected_label"] == "legit")
print(f"Wrote {len(fixtures)} fixtures ({fraud_count} fraud, {legit_count} legit) to {OUTPUT_PATH}")
if __name__ == "__main__":
main()