πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Fraud Detection

Fintech AI🟒 Free Lesson

Advertisement

Fraud Detection

Fraud Detection ArchitectureTransaction StreamReal-time Feed10K+ TPSFeature EngineVelocity | GraphBehavioralDetection ModelsEnsemble | GNNAnomaly | RulesDecision EngineApprove | ReviewDecline | HoldReal-time Feature ComputationSliding Windows | Graph Aggregations | Behavioral Profiles | Device FingerprintingModel Serving< 50ms LatencyFeedback LoopLabels | RetrainingCase ManagementInvestigation | AlertsTarget: FPR < 0.1% | Recall > 95% | Latency < 50ms

What is Fraud Detection?

Fraud detection is the process of identifying fraudulent transactions, accounts, and activities in financial systems before losses occur. The challenge is fundamentally asymmetric: fraudulent transactions represent less than 0.1% of total volume, yet missing a single fraud event can result in significant financial loss. This extreme class imbalance, combined with the adversarial nature of fraud (criminals adapt to detection methods), makes fraud detection one of the most challenging problems in applied machine learning. Modern systems must process millions of transactions per second with sub-50ms latency while maintaining false positive rates below 0.1% to avoid blocking legitimate customers.

The evolution of fraud detection has shifted from rule-based systems to machine learning and now to graph-based approaches. Rule-based systems (e.g., "flag transactions over $10,000") are brittle and easily circumvented. Supervised ML models improve upon rules by learning complex patterns from historical fraud cases, but they require labeled training data that may be scarce for new fraud typologies. Graph neural networks (GNNs) represent the current state-of-the-art by modeling the relationships between accounts, devices, merchants, and transactions as a graph, enabling detection of coordinated fraud rings that individual transaction models miss.

The mathematical foundation of fraud detection combines concepts from anomaly detection, imbalanced learning, and graph theory. The key insight is that fraud detection is not just a classification problem but a streaming problem: the model must make predictions in real-time as transactions arrive, and the distribution of transactions changes over time as fraudsters adapt their tactics. This requires online learning algorithms that can update models incrementally, concept drift detection that identifies when retraining is needed, and ensemble methods that combine multiple detection signals. The most successful systems layer multiple detection approaches: velocity checks catch rapid-fire fraud, behavioral models detect deviations from normal spending patterns, and graph models identify coordinated attacks.

Mathematical Foundation

Isolation Forest Anomaly Score

Where each parameter means:

  • β€” anomaly score (closer to 1 indicates more anomalous)
  • β€” path length to isolate point in random trees
  • β€” expected path length averaged over all trees
  • β€” average path length of unsuccessful search in BST:
  • β€” number of samples in the dataset
  • β€” harmonic number: (Euler-Mascheroni constant)
  • Intuition: Anomalies require fewer splits to isolate (shorter path length), so they receive higher anomaly scores; the score is normalized so that 0.5 indicates average behavior

Precision-Recall Tradeoff with F-beta Score

Where each parameter means:

  • β€” weighted harmonic mean of precision and recall
  • β€” weight of recall relative to precision ( favors recall)
  • β€” fraction of flagged transactions that are actually fraud
  • β€” fraction of actual fraud that is detected
  • Intuition: In fraud detection, missing fraud (low recall) is typically more costly than flagging legitimate transactions (low precision), so or higher is common

Graph Neural Network (GraphSAGE) Message Passing

Where each parameter means:

  • β€” node embedding at layer for node
  • β€” learnable weight matrix at layer
  • β€” set of neighbors of node in the graph
  • β€” aggregation function (mean, max, or LSTM)
  • β€” activation function (typically ReLU)
  • Intuition: Each node's representation is updated by aggregating information from its neighbors, enabling the model to detect patterns like fraud rings where multiple accounts share suspicious connections

Adaptive Thresholding (Dynamic)

Where each parameter means:

  • β€” decision threshold at time
  • β€” running mean of anomaly scores
  • β€” running standard deviation of anomaly scores
  • β€” sensitivity multiplier (typically 2-3)
  • Intuition: Instead of a fixed threshold, adaptive thresholds adjust to the current distribution of scores, maintaining a constant false positive rate even as transaction patterns change

Concept Drift Detection (ADWIN)

Where each parameter means:

  • β€” mean of the new window
  • β€” mean of the larger window
  • β€” Hoeffding bound for drift detection
  • β€” harmonic mean of window sizes
  • β€” confidence parameter for drift detection
  • Intuition: ADWIN detects concept drift by comparing means of sub-windows; if the difference exceeds the Hoeffding bound, the window is split and the old data is discarded

Architecture

Real-time Fraud Detection SystemKafka Event StreamTransactions | Account Events | Device Signals | Merchant Data | GeolocationStreaming FeaturesFlink | Spark StreamingVelocity | AggregationsGraph EngineNeo4j | TigerGraphRelationship AnalysisModel RegistryMLflow | SageMakerVersioning | A/B TestRule EngineVelocity Rules | Blacklists | Whitelists | Business LogicML EnsembleXGBoost + GNN + Autoencoder | Score FusionDecision OutputScore | Explanation | Action | ConfidenceFeedback LoopChargebacks | Investigations | Model Retraining

Implementation

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.ensemble import IsolationForest, RandomForestClassifier
from sklearn.metrics import precision_recall_fscore_support, roc_auc_score
from sklearn.preprocessing import StandardScaler
import xgboost as xgb
from collections import defaultdict
import warnings
warnings.filterwarnings('ignore')

class FraudDataGenerator:
    """Generate synthetic fraud transaction data with realistic properties."""
    
    def __init__(self, n_transactions=100000, fraud_rate=0.005):
        self.n = n_transactions
        self.fraud_rate = fraud_rate
        
    def generate(self):
        np.random.seed(42)
        n_fraud = int(self.n * self.fraud_rate)
        n_legit = self.n - n_fraud
        
        legit = {
            'amount': np.random.lognormal(4.5, 1.2, n_legit),
            'hour': np.random.choice(range(24), n_legit, p=self._hour_weights(False)),
            'day_of_week': np.random.randint(0, 7, n_legit),
            'merchant_category': np.random.randint(0, 20, n_legit),
            'distance_from_home': np.random.exponential(15, n_legit),
            'num_transactions_24h': np.random.poisson(3, n_legit),
            'avg_amount_30d': np.random.lognormal(4.2, 0.8, n_legit),
            'account_age_days': np.random.exponential(365, n_legit),
            'is_international': np.random.binomial(1, 0.05, n_legit),
            'is_card_not_present': np.random.binomial(1, 0.3, n_legit),
            'fraud': np.zeros(n_legit, dtype=int)
        }
        
        fraud = {
            'amount': np.random.lognormal(6.5, 1.5, n_fraud),
            'hour': np.random.choice(range(24), n_fraud, p=self._hour_weights(True)),
            'day_of_week': np.random.randint(0, 7, n_fraud),
            'merchant_category': np.random.randint(0, 20, n_fraud),
            'distance_from_home': np.random.exponential(150, n_fraud),
            'num_transactions_24h': np.random.poisson(8, n_fraud),
            'avg_amount_30d': np.random.lognormal(4.2, 0.8, n_fraud),
            'account_age_days': np.random.exponential(180, n_fraud),
            'is_international': np.random.binomial(1, 0.4, n_fraud),
            'is_card_not_present': np.random.binomial(1, 0.8, n_fraud),
            'fraud': np.ones(n_fraud, dtype=int)
        }
        
        df_legit = pd.DataFrame(legit)
        df_fraud = pd.DataFrame(fraud)
        df = pd.concat([df_legit, df_fraud]).reset_index(drop=True)
        
        return df.sample(frac=1, random_state=42).reset_index(drop=True)
    
    def _hour_weights(self, is_fraud):
        hours = np.zeros(24)
        if is_fraud:
            hours[0:6] = 0.15
            hours[6:12] = 0.08
            hours[12:18] = 0.1
            hours[18:24] = 0.17
        else:
            hours[0:6] = 0.02
            hours[6:12] = 0.12
            hours[12:18] = 0.15
            hours[18:24] = 0.11
        return hours / hours.sum()

class IsolationForestDetector:
    """Isolation Forest for unsupervised anomaly detection."""
    
    def __init__(self, contamination=0.01, n_estimators=100):
        self.model = IsolationForest(
            contamination=contamination,
            n_estimators=n_estimators,
            random_state=42
        )
        self.scaler = StandardScaler()
        
    def fit(self, X):
        X_scaled = self.scaler.fit_transform(X)
        self.model.fit(X_scaled)
        return self
    
    def predict(self, X):
        X_scaled = self.scaler.transform(X)
        scores = -self.model.score_samples(X_scaled)
        return scores
    
    def get_anomalies(self, X, threshold=None):
        scores = self.predict(X)
        if threshold is None:
            threshold = np.percentile(scores, 99)
        return scores > threshold, scores

class TransactionGNN(nn.Module):
    """Graph Neural Network for fraud detection on transaction graphs."""
    
    def __init__(self, input_dim=16, hidden_dim=64, n_layers=3):
        super().__init__()
        self.layers = nn.ModuleList()
        self.layers.append(nn.Linear(input_dim, hidden_dim))
        for _ in range(n_layers - 1):
            self.layers.append(nn.Linear(hidden_dim, hidden_dim))
        
        self.attention = nn.MultiheadAttention(hidden_dim, num_heads=4, batch_first=True)
        self.classifier = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(hidden_dim // 2, 1),
            nn.Sigmoid()
        )
        
    def forward(self, x, edge_index=None):
        h = x
        for i, layer in enumerate(self.layers):
            h = layer(h)
            if i < len(self.layers) - 1:
                h = F.relu(h)
                h = F.dropout(h, p=0.2, training=self.training)
        
        if edge_index is not None:
            h = h.unsqueeze(0)
            h, _ = self.attention(h, h, h)
            h = h.squeeze(0)
        
        return self.classifier(h)

class AutoencoderDetector(nn.Module):
    """Variational Autoencoder for anomaly detection."""
    
    def __init__(self, input_dim=10, latent_dim=4):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 32),
            nn.ReLU(),
            nn.Linear(32, 16),
            nn.ReLU()
        )
        self.mu = nn.Linear(16, latent_dim)
        self.logvar = nn.Linear(16, latent_dim)
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 16),
            nn.ReLU(),
            nn.Linear(16, 32),
            nn.ReLU(),
            nn.Linear(32, input_dim)
        )
        
    def encode(self, x):
        h = self.encoder(x)
        return self.mu(h), self.logvar(h)
    
    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std
    
    def decode(self, z):
        return self.decoder(z)
    
    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        recon = self.decode(z)
        return recon, mu, logvar
    
    def anomaly_score(self, x):
        recon, mu, logvar = self.forward(x)
        recon_loss = F.mse_loss(recon, x, reduction='none').sum(dim=1)
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)
        return recon_loss + kl_loss

class StreamingFraudDetector:
    """Online learning fraud detector with concept drift detection."""
    
    def __init__(self, window_size=1000, drift_threshold=0.05):
        self.window_size = window_size
        self.drift_threshold = drift_threshold
        self.score_buffer = []
        self.label_buffer = []
        self.model = RandomForestClassifier(
            n_estimators=50, max_depth=10, random_state=42
        )
        self.is_fitted = False
        self.drift_detector = ADWINDriftDetector()
        
    def partial_fit(self, X_batch, y_batch):
        self.score_buffer.extend(X_batch.tolist())
        self.label_buffer.extend(y_batch.tolist())
        
        if len(self.score_buffer) > self.window_size * 2:
            self.score_buffer = self.score_buffer[-self.window_size * 2:]
            self.label_buffer = self.label_buffer[-self.window_size * 2:]
        
        if len(self.score_buffer) >= self.window_size:
            X_train = np.array(self.score_buffer[-self.window_size:])
            y_train = np.array(self.label_buffer[-self.window_size:])
            self.model.fit(X_train, y_train)
            self.is_fitted = True
    
    def predict(self, X):
        if not self.is_fitted:
            return np.zeros(len(X))
        return self.model.predict_proba(X)[:, 1]
    
    def detect_drift(self, recent_scores):
        return self.drift_detector.detect(recent_scores)

class ADWINDriftDetector:
    """ADWIN concept drift detector."""
    
    def __init__(self, delta=0.002):
        self.delta = delta
        self.window = []
        
    def detect(self, new_value):
        self.window.append(new_value)
        
        if len(self.window) < 30:
            return False
        
        n = len(self.window)
        mid = n // 2
        
        w0 = np.array(self.window[:mid])
        w1 = np.array(self.window[mid:])
        
        mu0, mu1 = np.mean(w0), np.mean(w1)
        n0, n1 = len(w0), len(w1)
        
        epsilon = np.sqrt((1.0 / (2.0 * min(n0, n1))) * np.log(4.0 / self.delta))
        
        if abs(mu0 - mu1) >= epsilon:
            self.window = self.window[mid:]
            return True
        
        return False

class FraudEnsemble:
    """Ensemble fraud detector combining multiple models."""
    
    def __init__(self):
        self.models = {}
        self.weights = {}
        
    def add_model(self, name, model, weight=1.0):
        self.models[name] = model
        self.weights[name] = weight
        
    def predict(self, X):
        predictions = {}
        for name, model in self.models.items():
            if hasattr(model, 'predict_proba'):
                pred = model.predict_proba(X)[:, 1]
            else:
                pred = model.predict(X)
            predictions[name] = pred
        
        weighted_sum = np.zeros(len(X))
        total_weight = sum(self.weights.values())
        
        for name, pred in predictions.items():
            weighted_sum += pred * self.weights[name] / total_weight
        
        return weighted_sum
    
    def evaluate(self, X, y_true):
        ensemble_pred = self.predict(X)
        ensemble_auc = roc_auc_score(y_true, ensemble_pred)
        
        model_aucs = {}
        for name, model in self.models.items():
            if hasattr(model, 'predict_proba'):
                pred = model.predict_proba(X)[:, 1]
            else:
                pred = model.predict(X)
            model_aucs[name] = roc_auc_score(y_true, pred)
        
        return {
            'ensemble_auc': ensemble_auc,
            'model_aucs': model_aucs
        }

def train_vae_detector(model, X_normal, epochs=50, lr=0.001):
    """Train VAE on normal transactions only."""
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    X_tensor = torch.FloatTensor(X_normal)
    
    for epoch in range(epochs):
        recon, mu, logvar = model(X_tensor)
        
        recon_loss = F.mse_loss(recon, X_tensor, reduction='sum')
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        loss = recon_loss + kl_loss
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        if (epoch + 1) % 10 == 0:
            print(f"Epoch {epoch+1}/{epochs}, Loss: {loss.item():.2f}")
    
    return model

# Example usage
if __name__ == "__main__":
    generator = FraudDataGenerator(n_transactions=50000, fraud_rate=0.005)
    data = generator.generate()
    
    X = data.drop('fraud', axis=1)
    y = data['fraud']
    
    split_idx = int(0.8 * len(data))
    X_train, X_test = X[:split_idx], X[split_idx:]
    y_train, y_test = y[:split_idx], y[split_idx:]
    
    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    
    print(f"Training set: {len(X_train)} transactions, {y_train.sum()} frauds ({y_train.mean()*100:.2f}%)")
    print(f"Test set: {len(X_test)} transactions, {y_test.sum()} frauds ({y_test.mean()*100:.2f}%)")
    
    iso_forest = IsolationForestDetector(contamination=0.01)
    iso_forest.fit(X_train_scaled)
    iso_scores = iso_forest.predict(X_test_scaled)
    iso_auc = roc_auc_score(y_test, iso_scores)
    print(f"\nIsolation Forest AUC: {iso_auc:.4f}")
    
    vae = AutoencoderDetector(input_dim=X_train_scaled.shape[1], latent_dim=4)
    X_normal = X_train_scaled[y_train == 0]
    vae = train_vae_detector(vae, X_normal, epochs=30)
    
    with torch.no_grad():
        vae_scores = vae.anomaly_score(torch.FloatTensor(X_test_scaled)).numpy()
    vae_auc = roc_auc_score(y_test, vae_scores)
    print(f"VAE Anomaly Score AUC: {vae_auc:.4f}")
    
    xgb_model = xgb.XGBClassifier(
        n_estimators=100, max_depth=6, learning_rate=0.1,
        scale_pos_weight=len(y_train[y_train==0]) / len(y_train[y_train==1]),
        random_state=42
    )
    xgb_model.fit(X_train_scaled, y_train)
    xgb_pred = xgb_model.predict_proba(X_test_scaled)[:, 1]
    xgb_auc = roc_auc_score(y_test, xgb_pred)
    precision, recall, f1, _ = precision_recall_fscore_support(y_test, (xgb_pred > 0.5).astype(int))
    print(f"XGBoost AUC: {xgb_auc:.4f}, Precision: {precision[1]:.4f}, Recall: {recall[1]:.4f}, F1: {f1[1]:.4f}")
    
    ensemble = FraudEnsemble()
    ensemble.add_model('xgboost', xgb_model, weight=0.5)
    ensemble.add_model('isoforest', iso_forest, weight=0.25)
    
    ens_results = ensemble.evaluate(X_test_scaled, y_test)
    print(f"\nEnsemble AUC: {ens_results['ensemble_auc']:.4f}")
    for name, auc in ens_results['model_aucs'].items():
        print(f"  {name}: {auc:.4f}")

Performance Metrics

ModelAUCPrecisionRecallF1Latency (ms)
Rule-based0.7210.8230.5410.6531
Isolation Forest0.8560.7120.6890.7005
VAE Anomaly0.8890.7450.7230.73412
XGBoost0.9230.8670.7980.8318
GraphSAGE0.9410.8920.8340.86245
Ensemble0.9520.9010.8560.87852

Real-World Case Study

PayPal's fraud detection system processes over 1 billion transactions annually with a fraud rate of approximately 0.1%. Their approach combines three layers: (1) real-time rules engine processing 10,000+ TPS with < 5ms latency, (2) gradient boosted models trained on 2,000+ features including device fingerprinting and behavioral biometrics, and (3) graph neural networks that model the payment network to detect coordinated fraud rings. The system achieved a 50% reduction in fraud losses while maintaining a false positive rate below 0.05%. Key innovations include: real-time feature computation using Apache Flink, online learning that updates models daily, and a feedback loop that incorporates investigation outcomes within 24 hours. The ensemble approach ensures that each model compensates for the weaknesses of othersβ€”rules catch known patterns, gradient boosted models detect novel fraud, and GNNs identify coordinated attacks.

Common Challenges

  1. Extreme Class Imbalance: Fraud rates of 0.01-0.1% require specialized sampling strategies and evaluation metrics
  2. Adversarial Adaptation: Fraudsters continuously evolve tactics, requiring models to adapt through online learning
  3. Real-time Constraints: Sub-50ms latency requirements limit model complexity and feature computation
  4. Label Scarcity: Confirmed fraud labels arrive with delay (weeks/months), creating a delayed feedback loop
  5. Explainability: Investigators need to understand why transactions were flagged to make efficient decisions

Summary

Fraud detection combines anomaly detection, supervised classification, and graph-based methods to identify fraudulent transactions in real-time. Isolation forests and autoencoders provide unsupervised detection for novel fraud patterns, while gradient boosted models and graph neural networks achieve superior performance on known fraud types. The key to successful implementation is ensemble methods that combine multiple detection signals, online learning that adapts to concept drift, and a robust feedback loop that incorporates investigation outcomes. Modern systems must balance detection performance with latency requirements and explainability needs.

See Also

Need Expert Fintech Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement