-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloader.py
More file actions
150 lines (121 loc) · 5.55 KB
/
Copy pathloader.py
File metadata and controls
150 lines (121 loc) · 5.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""
Multi-source, multi-rail data loader.
Sources: synthetic Parquet | UCI San Diego CSV | IEEE-CIS CSV | real_org CSV.
IEEE-CIS columns (V*, C*, D*, card1-card6) are OPAQUE — never mapped to canonical schema.
Only harmonised fields: amount_usd, timestamp_utc, is_fraud, DeviceType, ProductCD.
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional
import polars as pl
# ── Source adapters ───────────────────────────────────────────────────────────
def load_synthetic(path: Path) -> pl.DataFrame:
"""Load synthetic Parquet (already in canonical schema)."""
df = pl.read_parquet(path)
return df.with_columns(pl.lit("synthetic").alias("data_source"))
def load_uci_san_diego(path: Path) -> pl.DataFrame:
"""
UCI creditcard.csv (Kaggle).
Columns: Time, V1-V28 (PCA), Amount, Class
No merchant, geo, or device columns available.
"""
df = pl.read_csv(path)
df = df.rename({"Class": "is_fraud", "Amount": "amount_usd"})
# Time is seconds from first transaction — convert to relative offset only
df = df.with_columns([
pl.col("amount_usd").cast(pl.Float64),
pl.col("is_fraud").cast(pl.Int8),
pl.lit("uc_san_diego").alias("data_source"),
pl.lit("cards").alias("rail"),
pl.lit("card_not_present").alias("channel"),
pl.lit("IN").alias("merchant_country"),
pl.lit("USD").alias("currency_code"),
(pl.col("amount_usd") * 84.0).alias("amount_inr"), # approx USD→INR
])
# Keep V1-V28 as-is in source namespace
return df
def load_ieee_cis(
identity_path: Path,
transaction_path: Path,
) -> pl.DataFrame:
"""
IEEE-CIS Fraud Detection dataset.
Columns: TransactionID, isFraud, TransactionDT, TransactionAmt,
ProductCD, card1-card6, addr1-addr2, dist1-dist2,
P_emaildomain, R_emaildomain, C1-C14, D1-D15, M1-M9,
V1-V339, DeviceType, DeviceInfo (from identity)
Opaque columns (V*, C*, D*, card*) kept in ieee_cis namespace.
Only harmonise: amount_usd, timestamp_utc relative, is_fraud.
"""
txn = pl.read_csv(transaction_path)
idt = pl.read_csv(identity_path, ignore_errors=True)
df = txn.join(idt, on="TransactionID", how="left")
df = df.rename({
"isFraud": "is_fraud",
"TransactionAmt": "amount_usd",
})
df = df.with_columns([
pl.col("amount_usd").cast(pl.Float64),
pl.col("is_fraud").cast(pl.Int8),
(pl.col("amount_usd") * 84.0).alias("amount_inr"),
pl.lit("ieee_cis").alias("data_source"),
pl.lit("cards").alias("rail"),
pl.lit("USD").alias("currency_code"),
pl.lit("IN").alias("merchant_country"),
# ProductCD → channel proxy
pl.when(pl.col("ProductCD") == "W").then(pl.lit("ecommerce"))
.when(pl.col("ProductCD") == "S").then(pl.lit("card_present"))
.otherwise(pl.lit("card_not_present")).alias("channel"),
])
return df
def load_real_org(path: Path, column_map: Optional[dict] = None) -> pl.DataFrame:
"""
Real org transactions (anonymised CSV or Parquet).
column_map: maps org-specific column names → canonical names.
"""
if path.suffix == ".parquet":
df = pl.read_parquet(path)
else:
df = pl.read_csv(path, ignore_errors=True)
if column_map:
df = df.rename(column_map)
return df.with_columns(pl.lit("real_org").alias("data_source"))
# ── Multi-source combiner ─────────────────────────────────────────────────────
COMMON_COLUMNS = [
"transaction_id", "data_source", "rail", "channel",
"amount_inr", "amount_usd", "currency_code",
"is_fraud", "merchant_country",
]
def combine(frames: list[pl.DataFrame]) -> pl.DataFrame:
"""
Combine frames from multiple sources.
Only common columns kept — source-specific feature columns (V*, C*) dropped here.
For full IEEE-CIS feature use, pass the frame directly to feature engineering.
"""
result_frames = []
for df in frames:
available = [c for c in COMMON_COLUMNS if c in df.columns]
result_frames.append(df.select(available))
return pl.concat(result_frames, how="diagonal_relaxed")
# ── Entrypoint ────────────────────────────────────────────────────────────────
def load_all(
synthetic_path: Optional[Path] = None,
uci_path: Optional[Path] = None,
ieee_txn_path: Optional[Path] = None,
ieee_idt_path: Optional[Path] = None,
real_org_path: Optional[Path] = None,
real_org_column_map: Optional[dict] = None,
) -> pl.DataFrame:
"""Load all available sources and combine into one DataFrame."""
frames: list[pl.DataFrame] = []
if synthetic_path and synthetic_path.exists():
frames.append(load_synthetic(synthetic_path))
if uci_path and uci_path.exists():
frames.append(load_uci_san_diego(uci_path))
if ieee_txn_path and ieee_idt_path and ieee_txn_path.exists() and ieee_idt_path.exists():
frames.append(load_ieee_cis(ieee_idt_path, ieee_txn_path))
if real_org_path and real_org_path.exists():
frames.append(load_real_org(real_org_path, real_org_column_map))
if not frames:
raise FileNotFoundError("No data sources found. Run generator first.")
return combine(frames)