Spaces:
Sleeping
Sleeping
| """ | |
| app.py - Gradio interface for PyTorch fraud detection demo | |
| Requirements: | |
| pip install torch scikit-learn pandas numpy gradio | |
| Run: | |
| python app.py | |
| Then open the URL that Gradio prints (usually http://127.0.0.1:7860) | |
| """ | |
| import os | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.preprocessing import StandardScaler | |
| import torch | |
| import torch.nn as nn | |
| from torch.utils.data import Dataset, DataLoader | |
| import gradio as gr | |
| # ========================= | |
| # 1. Load or create data | |
| # ========================= | |
| CSV_PATH = "synthetic_fraud_transactions_1000.csv" # adjust if needed | |
| def load_or_create_data(path: str) -> pd.DataFrame: | |
| if os.path.exists(path): | |
| print(f"[INFO] Loading existing CSV: {path}") | |
| return pd.read_csv(path) | |
| print(f"[INFO] CSV not found at {path}. Generating synthetic data...") | |
| np.random.seed(42) | |
| n = 1000 | |
| # Generate synthetic features | |
| amount = np.random.lognormal(mean=3, sigma=0.7, size=n) # skewed positive | |
| amount_log = np.log1p(amount) | |
| is_foreign_currency = np.random.binomial(1, 0.1, size=n) | |
| txn_hour_of_day = np.random.randint(0, 24, size=n) | |
| # Time since last txn in seconds (0 to 2 days, skewed toward small) | |
| time_since_last_txn_sec = np.random.exponential(scale=2 * 60 * 60, size=n) | |
| time_since_last_txn_sec = np.clip(time_since_last_txn_sec, 10, 2 * 24 * 60 * 60).astype(int) | |
| # Distance between transactions in km (0–10,000) | |
| geo_distance_from_last_txn_km = np.random.exponential(scale=50, size=n) | |
| geo_distance_from_last_txn_km = np.clip(geo_distance_from_last_txn_km, 0, 10000) | |
| # Transactions and spend in windows | |
| txn_count_last_10min = np.random.poisson(lam=0.3, size=n) | |
| total_spend_last_24h = np.random.gamma(shape=2.0, scale=50.0, size=n) | |
| is_new_merchant_for_card = np.random.binomial(1, 0.2, size=n) | |
| merchant_fraud_rate_30d = np.random.beta(a=1.2, b=50, size=n) # mostly low rates | |
| is_new_device_for_card = np.random.binomial(1, 0.15, size=n) | |
| num_cards_on_device_24h = np.random.poisson(lam=1.1, size=n) | |
| # Simple fraud label: higher probability when risky patterns occur | |
| base_prob = 0.02 | |
| risk_score = ( | |
| 0.03 * (time_since_last_txn_sec < 120) | |
| + 0.04 * (geo_distance_from_last_txn_km > 500) | |
| + 0.05 * is_new_device_for_card | |
| + 0.04 * is_new_merchant_for_card | |
| + 0.03 * (num_cards_on_device_24h > 3) | |
| + 0.03 * is_foreign_currency | |
| ) | |
| fraud_prob = np.clip(base_prob + risk_score, 0, 0.9) | |
| fraud_label = (np.random.rand(n) < fraud_prob).astype(int) | |
| df = pd.DataFrame( | |
| { | |
| "amount": amount.round(2), | |
| "amount_log": amount_log.round(4), | |
| "is_foreign_currency": is_foreign_currency, | |
| "txn_hour_of_day": txn_hour_of_day, | |
| "time_since_last_txn_sec": time_since_last_txn_sec, | |
| "geo_distance_from_last_txn_km": geo_distance_from_last_txn_km.round(2), | |
| "txn_count_last_10min": txn_count_last_10min, | |
| "total_spend_last_24h": total_spend_last_24h.round(2), | |
| "is_new_merchant_for_card": is_new_merchant_for_card, | |
| "merchant_fraud_rate_30d": merchant_fraud_rate_30d.round(4), | |
| "is_new_device_for_card": is_new_device_for_card, | |
| "num_cards_on_device_24h": num_cards_on_device_24h, | |
| "fraud_label": fraud_label, | |
| } | |
| ) | |
| # Save so future runs reuse the same data | |
| df.to_csv(path, index=False) | |
| print(f"[INFO] Synthetic dataset saved to: {path}") | |
| return df | |
| df = load_or_create_data(CSV_PATH) | |
| feature_cols = [ | |
| "amount", | |
| "amount_log", | |
| "is_foreign_currency", | |
| "txn_hour_of_day", | |
| "time_since_last_txn_sec", | |
| "geo_distance_from_last_txn_km", | |
| "txn_count_last_10min", | |
| "total_spend_last_24h", | |
| "is_new_merchant_for_card", | |
| "merchant_fraud_rate_30d", | |
| "is_new_device_for_card", | |
| "num_cards_on_device_24h", | |
| ] | |
| target_col = "fraud_label" | |
| X = df[feature_cols].values.astype(np.float32) | |
| y = df[target_col].values.astype(np.float32) | |
| X_train, X_val, y_train, y_val = train_test_split( | |
| X, y, test_size=0.2, random_state=42, stratify=y | |
| ) | |
| scaler = StandardScaler() | |
| X_train_scaled = scaler.fit_transform(X_train) | |
| X_val_scaled = scaler.transform(X_val) | |
| # ========================= | |
| # 2. PyTorch Dataset & Model | |
| # ========================= | |
| class FraudDataset(Dataset): | |
| def __init__(self, X, y): | |
| self.X = torch.from_numpy(X).float() | |
| self.y = torch.from_numpy(y).float() | |
| def __len__(self): | |
| return len(self.X) | |
| def __getitem__(self, idx): | |
| return self.X[idx], self.y[idx] | |
| train_dataset = FraudDataset(X_train_scaled, y_train) | |
| val_dataset = FraudDataset(X_val_scaled, y_val) | |
| train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) | |
| val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False) | |
| class FraudNet(nn.Module): | |
| def __init__(self, input_dim): | |
| super(FraudNet, self).__init__() | |
| self.net = nn.Sequential( | |
| nn.Linear(input_dim, 64), | |
| nn.BatchNorm1d(64), | |
| nn.ReLU(), | |
| nn.Dropout(0.2), | |
| nn.Linear(64, 32), | |
| nn.BatchNorm1d(32), | |
| nn.ReLU(), | |
| nn.Dropout(0.2), | |
| nn.Linear(32, 1), # binary logit | |
| ) | |
| def forward(self, x): | |
| return self.net(x).squeeze(1) | |
| input_dim = len(feature_cols) | |
| model = FraudNet(input_dim) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model.to(device) | |
| # Handle class imbalance with pos_weight | |
| pos_weight_value = (len(y_train) - y_train.sum()) / (y_train.sum() + 1e-6) | |
| pos_weight = torch.tensor(pos_weight_value, dtype=torch.float32).to(device) | |
| criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) | |
| optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) | |
| def train_one_epoch(model, loader, optimizer, criterion, device): | |
| model.train() | |
| running_loss = 0.0 | |
| for X_batch, y_batch in loader: | |
| X_batch = X_batch.to(device) | |
| y_batch = y_batch.to(device) | |
| optimizer.zero_grad() | |
| logits = model(X_batch) | |
| loss = criterion(logits, y_batch) | |
| loss.backward() | |
| optimizer.step() | |
| running_loss += loss.item() * X_batch.size(0) | |
| return running_loss / len(loader.dataset) | |
| def evaluate(model, loader, criterion, device): | |
| model.eval() | |
| running_loss = 0.0 | |
| all_logits = [] | |
| all_labels = [] | |
| with torch.no_grad(): | |
| for X_batch, y_batch in loader: | |
| X_batch = X_batch.to(device) | |
| y_batch = y_batch.to(device) | |
| logits = model(X_batch) | |
| loss = criterion(logits, y_batch) | |
| running_loss += loss.item() * X_batch.size(0) | |
| all_logits.append(logits.cpu()) | |
| all_labels.append(y_batch.cpu()) | |
| all_logits = torch.cat(all_logits) | |
| all_labels = torch.cat(all_labels) | |
| probs = torch.sigmoid(all_logits) | |
| preds = (probs >= 0.5).int() | |
| accuracy = (preds == all_labels.int()).float().mean().item() | |
| return running_loss / len(loader.dataset), accuracy | |
| # ========================= | |
| # 3. Train the model once | |
| # ========================= | |
| EPOCHS = 15 # keep small so startup is fast | |
| for epoch in range(1, EPOCHS + 1): | |
| train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device) | |
| val_loss, val_acc = evaluate(model, val_loader, criterion, device) | |
| print( | |
| f"Epoch {epoch:02d} | " | |
| f"Train Loss: {train_loss:.4f} | " | |
| f"Val Loss: {val_loss:.4f} | " | |
| f"Val Acc: {val_acc:.4f}" | |
| ) | |
| model.eval() # set evaluation mode | |
| # ========================= | |
| # 4. Inference function | |
| # ========================= | |
| def predict_fraud( | |
| amount, | |
| is_foreign_currency, | |
| txn_hour_of_day, | |
| time_since_last_txn_min, | |
| geo_distance_from_last_txn_km, | |
| txn_count_last_10min, | |
| total_spend_last_24h, | |
| is_new_merchant_for_card, | |
| merchant_fraud_rate_30d, | |
| is_new_device_for_card, | |
| num_cards_on_device_24h, | |
| ): | |
| """ | |
| This function will be called by the Gradio interface. | |
| It builds a feature vector, scales it, runs the model, | |
| and returns a probability + label string. | |
| """ | |
| # Derived features | |
| amount_log = np.log1p(amount) | |
| time_since_last_txn_sec = time_since_last_txn_min * 60.0 | |
| # Build feature vector in the SAME ORDER as feature_cols | |
| x = np.array( | |
| [ | |
| amount, | |
| amount_log, | |
| float(is_foreign_currency), | |
| float(txn_hour_of_day), | |
| time_since_last_txn_sec, | |
| geo_distance_from_last_txn_km, | |
| float(txn_count_last_10min), | |
| total_spend_last_24h, | |
| float(is_new_merchant_for_card), | |
| merchant_fraud_rate_30d, | |
| float(is_new_device_for_card), | |
| float(num_cards_on_device_24h), | |
| ], | |
| dtype=np.float32, | |
| ).reshape(1, -1) | |
| # Scale | |
| x_scaled = scaler.transform(x) | |
| # To torch | |
| x_tensor = torch.from_numpy(x_scaled).float().to(device) | |
| with torch.no_grad(): | |
| logit = model(x_tensor) | |
| prob = torch.sigmoid(logit).item() | |
| label = "FRAUD" if prob >= 0.5 else "LEGIT" | |
| explanation = ( | |
| f"Predicted probability of fraud: **{prob:.3f}**\n\n" | |
| f"Model classification: **{label}** (threshold = 0.5)" | |
| ) | |
| return label, prob, explanation | |
| # ========================= | |
| # 5. Gradio UI | |
| # ========================= | |
| with gr.Blocks(title="Fraud Detection Demo (PyTorch + Gradio)") as demo: | |
| gr.Markdown( | |
| "# 💳 Fraud Detection Demo\n" | |
| "Play with transaction features and see how the model classifies them." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| amount = gr.Slider( | |
| minimum=1, | |
| maximum=2000, | |
| value=50, | |
| step=1, | |
| label="Transaction amount (in card currency)", | |
| ) | |
| is_foreign_currency = gr.Checkbox( | |
| value=False, label="Foreign currency?" | |
| ) | |
| txn_hour_of_day = gr.Slider( | |
| minimum=0, | |
| maximum=23, | |
| value=14, | |
| step=1, | |
| label="Transaction hour of day (0–23)", | |
| ) | |
| time_since_last_txn_min = gr.Slider( | |
| minimum=0.1, | |
| maximum=2880, | |
| value=60, | |
| step=1, | |
| label="Time since last transaction (minutes, up to 2 days)", | |
| ) | |
| geo_distance_from_last_txn_km = gr.Slider( | |
| minimum=0, | |
| maximum=10000, | |
| value=10, | |
| step=1, | |
| label="Distance from last transaction (km)", | |
| ) | |
| txn_count_last_10min = gr.Slider( | |
| minimum=0, | |
| maximum=20, | |
| value=1, | |
| step=1, | |
| label="Number of transactions in last 10 minutes", | |
| ) | |
| with gr.Column(): | |
| total_spend_last_24h = gr.Slider( | |
| minimum=0, | |
| maximum=10000, | |
| value=200, | |
| step=10, | |
| label="Total spend in last 24h", | |
| ) | |
| is_new_merchant_for_card = gr.Checkbox( | |
| value=False, label="Is this a new merchant for this card?" | |
| ) | |
| merchant_fraud_rate_30d = gr.Slider( | |
| minimum=0.0, | |
| maximum=0.5, | |
| value=0.02, | |
| step=0.01, | |
| label="Merchant fraud rate (last 30 days)", | |
| ) | |
| is_new_device_for_card = gr.Checkbox( | |
| value=False, label="Is this a new device for this card?" | |
| ) | |
| num_cards_on_device_24h = gr.Slider( | |
| minimum=1, | |
| maximum=20, | |
| value=1, | |
| step=1, | |
| label="Number of different cards on this device in last 24h", | |
| ) | |
| predict_btn = gr.Button("Predict Fraud Risk") | |
| label_out = gr.Textbox(label="Model Label (FRAUD / LEGIT)", interactive=False) | |
| prob_out = gr.Number(label="Fraud Probability", precision=4, interactive=False) | |
| explanation_out = gr.Markdown(label="Explanation") | |
| predict_btn.click( | |
| fn=predict_fraud, | |
| inputs=[ | |
| amount, | |
| is_foreign_currency, | |
| txn_hour_of_day, | |
| time_since_last_txn_min, | |
| geo_distance_from_last_txn_km, | |
| txn_count_last_10min, | |
| total_spend_last_24h, | |
| is_new_merchant_for_card, | |
| merchant_fraud_rate_30d, | |
| is_new_device_for_card, | |
| num_cards_on_device_24h, | |
| ], | |
| outputs=[label_out, prob_out, explanation_out], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |