Skip to main content

AI/ML System Design for Class Imbalance Handling in Production ML Pipelines

Comprehensive guide to implementing robust class imbalance solutions including SMOTE, oversampling, and class weight strategies for scalable machine learning systems.

Class imbalance presents a critical challenge in real-world machine learning deployments where minority classes are underrepresented, requiring sophisticated handling strategies to maintain model performance and fairness across all classes in production systems.

1. Problem Statement & ML Task Definition

Clarifying questions

  1. What is the target business outcome? Is this for fraud detection, customer churn prediction, or anomaly detection? This determines whether we're optimizing for precision/recall trade-offs or revenue impact.
  2. What is the ratio of minority to majority class? Is it 1:100 or 1:1000? This directly impacts the choice of sampling methods and class weight strategies.
  3. What is the latency budget for predictions? Is this a real-time system (e.g., <50ms) or can we afford batch processing with higher accuracy?
  4. Is this a batch or online serving system? Online systems require more careful handling of class imbalance in real-time inference.
  5. What is the acceptable trade-off between accuracy and speed? Can we accept a 10% drop in AUC for 50% faster inference?
  6. What is the scale of training data? Is it millions or billions of samples? This affects the choice between SMOTE and class weights.
  7. What are the business success criteria? Is it maximizing revenue, minimizing false positives, or ensuring high recall for rare events?

Business use case and success criteria

This system is designed to handle imbalanced datasets in a fraud detection or customer churn prediction use case. The primary business objective is to minimize missed fraud cases or churn events, which can have severe financial consequences. Success criteria include:

  • Minimize false negatives (missed fraud or churn) — critical for business impact
  • Maintain high precision to avoid unnecessary alerts or customer outreach
  • Ensure robustness across user segments — prevent bias in detection for specific demographics or user types
  • Support real-time decision-making with sub-50ms latency for fraud alerts or proactive customer retention

ML problem formulation

This is a binary classification task with a strong imbalance in class distribution (e.g., 1:1000 minority to majority ratio). The problem is framed as:

  • Task: Predict whether a transaction is fraudulent or a customer will churn (binary outcome)
  • Justification:
    • The rare event is more valuable to detect than the common case
    • Business impact is asymmetric (missed fraud = high cost, false positive = moderate cost)
    • Traditional accuracy metrics are misleading in imbalanced settings

ML evaluation metrics

Offline Evaluation Metrics:

  • AUC-ROC: Overall model discrimination capability
  • Precision@K: Critical for fraud alerting where top-K predictions matter most
  • F1-Score: Harmonic mean of precision and recall, balanced for imbalanced data
  • Recall@K: Ensures we capture the majority of rare events in top-K predictions

Online Evaluation Metrics:

  • Click-Through Rate (CTR): For recommendation systems where rare events are more valuable
  • Engagement: For churn prediction, whether users engage with retention offers
  • Revenue Impact: For fraud systems, the cost of missed fraud vs. false positives

Scale and constraints

Back-of-envelope estimates:

  • Training data size: 10 million samples (5,000 fraud cases, 9.995M legitimate)
  • Number of features: ~200 (user, item, context features)
  • Embedding matrix size: 1M users × 128-dim = ~500MB (for user embeddings)
  • Serving QPS: 10,000 requests/sec → 50ms p99 latency target

Latency math:

  • For a model with 200 features and 128-dim embeddings, serving 10K QPS requires sub-50ms inference time
  • This implies model size < 10MB and inference pipeline optimized for throughput

Non-ML baseline strategy

  • Rule-based fallback:
    • Flag transactions > $10,000 or from high-risk countries
    • Flag users with no activity in 30 days for churn prediction
  • Cold start handling:
    • Default to "not fraud" or "likely to stay" for new users/items
    • Use default class probability (e.g., 0.01) for new user segments
  • Static thresholds:
    • Use historical fraud rate (e.g., 0.1%) as baseline for classification threshold adjustment

2. Data Strategy

Training data sources

  • Explicit feedback: User ratings, clicks, purchases, and direct labels (e.g., spam detection)
  • Implicit signals: Session duration, scroll depth, time spent on page, click-through rate (CTR), and view-through rate (VTR)
  • Third-party data: Demographic information from partner platforms, contextual signals like weather or location, and external event data (e.g., holidays)

Labeling pipeline design

  • Human annotation: For high-value, low-volume labels (e.g., fraud detection or content moderation) with quality control via inter-annotator agreement and gold standard validation sets.
  • Weak supervision: Using labeling functions (LFs) for noisy but scalable labeling (e.g., rule-based heuristics or pre-trained classifiers).
  • Programmatic labeling: For large-scale data generation using domain knowledge (e.g., auto-labeling based on user behavior patterns or content metadata).

Class imbalance handling strategy with justification

Class imbalance is addressed through a multi-pronged approach:

  1. Class weights in model training: Apply inverse frequency weighting to penalize misclassifications of minority classes more heavily.
  2. Oversampling techniques: Use SMOTE (Synthetic Minority Oversampling Technique) to generate synthetic samples for underrepresented classes.
  3. Threshold tuning: Optimize decision thresholds during inference to improve recall of minority classes while maintaining precision.
  4. Ensemble methods: Combine multiple models trained on balanced subsets to improve generalization.

These strategies are chosen because they address both the statistical imbalance in training data and the downstream impact on model performance for rare events.

Negative sampling approach

  • Stratified negative sampling: Sample negatives in proportion to their distribution in the training set to maintain realistic class ratios.
  • Hard negative mining: Identify and include hard-to-classify examples from the model's current predictions to improve robustness.
  • Negative sampling ratio: Use a 1:3 or 1:5 ratio of positive to negative samples depending on the task (e.g., fraud detection may use 1:10).

Data freshness and update frequency requirements

  • Real-time updates: For user behavior signals (e.g., CTR, session duration) updated every 5–10 minutes.
  • Batch updates: For static features (e.g., user demographics) updated daily or weekly.
  • Freshness SLA: Ensure that 95% of training data is within 24 hours of the latest event timestamp.

Privacy compliance (GDPR, CCPA)

  • Anonymization: Remove or hash PII fields (e.g., user IDs, email addresses) before training.
  • Data retention: Retain data for 30 days post-event, with automatic deletion policies.
  • Consent management: Implement opt-out mechanisms for data collection and usage, aligned with GDPR/CCPA requirements.

Data quality validation and monitoring

  • Data validation checks: Validate feature ranges, missing values, and label consistency.
  • Drift detection: Monitor for distribution shifts in key features using statistical tests (e.g., Kolmogorov-Smirnov test).
  • Logging and alerting: Log anomalies in data distribution or label imbalance, with alerts triggered for drift exceeding thresholds (e.g., PSI > 0.1).
import numpy as np
from sklearn.utils import resample
from imblearn.over_sampling import SMOTE

def apply_oversampling(X, y):
    """
    Apply SMOTE oversampling to handle class imbalance.
    X: feature matrix (numpy array or pandas DataFrame)
    y: target labels (numpy array or pandas Series)
    Returns: balanced X and y
    """
    smote = SMOTE(random_state=42)
    X_resampled, y_resampled = smote.fit_resample(X, y)
    return X_resampled, y_resampled

def compute_class_weights(y):
    """
    Compute class weights for imbalanced datasets.
    y: target labels (numpy array or pandas Series)
    Returns: class weight dictionary
    """
    from sklearn.utils.class_weight import compute_class_weight
    classes = np.unique(y)
    weights = compute_class_weight('balanced', classes=classes, y=y)
    return dict(zip(classes, weights))

3. Feature Engineering

Feature Taxonomy

Features are categorized into four main types:

  1. User Features: User demographics, behavior patterns, historical engagement, preferences (e.g., age, gender, session count, time spent)
  2. Item Features: Content metadata, popularity metrics, category tags, item age (e.g., item category, price, rating)
  3. Context Features: Temporal signals, device information, geographic location, time of day (e.g., hour, weekday, country)
  4. Interaction Features: User-item interaction patterns, temporal decay factors, recency signals (e.g., last seen time, click-through rate)

Embedding Design

Entity Embeddings:

  • User ID embeddings: 128-dim, trained via collaborative filtering
  • Item ID embeddings: 128-dim, learned from user-item interactions

Text Embeddings:

  • Item titles/descriptions: 256-dim using BERT-base or sentence-transformers
  • User reviews: 128-dim using pre-trained sentence encoders

Graph Embeddings:

  • User-item interaction graph: 64-dim using Node2Vec or GraphSAGE
  • Content-aware graph: 128-dim for item similarity

Real-time vs Batch Feature Computation

Feature TypeComputation ModeReason
User session countReal-timeHigh volatility, requires low latency
Item popularityBatchStable over time, computed nightly
User engagement scoreReal-timeDynamic, affects ranking in real-time
Historical item ratingsBatchStable, computed once per day

Feature Store Design

Online Store:

  • Low-latency key-value store (e.g., Redis or DynamoDB)
  • Designed for sub-100ms retrieval
  • TTL of 5 minutes for volatile features

Offline Store:

  • Data warehouse (e.g., Snowflake or BigQuery)
  • Used for training and batch feature generation
  • Historical data retention: 2 years

Feature Freshness and TTL

FeatureSourceUpdate FrequencyStore Type
User session countReal-time streamEvery 5 secondsOnline
Item popularityBatch ETLDailyOffline
User engagement scoreReal-time streamEvery 30 secondsOnline
Item categoryBatch ETLWeeklyOffline
User last seen timeReal-time streamEvery 10 secondsOnline

Cross Features and Feature Interactions

Cross features are engineered to capture complex user-item dynamics:

  • User-Item Interaction: User age × Item category
  • Temporal Interactions: User session count × Time of day
  • Behavioral Patterns: User engagement score × Item popularity

Cross features are generated using:

  1. Feature crosses in model (e.g., embedding lookup combinations)
  2. Feature crosses via hash buckets for sparse features
  3. Polynomial feature generation for dense features

Feature Importance and Selection Methodology

Importance Metrics:

  • SHAP values for model-agnostic feature importance
  • Permutation importance from trained models (e.g., XGBoost)
  • Variance-based selection for numerical features

Selection Strategy:

  1. Variance Threshold: Remove features with low variance (< 0.01)
  2. Correlation Filtering: Remove highly correlated features (|r| > 0.9)
  3. Recursive Feature Elimination: Use model-based feature ranking to select top 500 features
import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.ensemble import RandomForestClassifier

def select_important_features(X_train, y_train, k=500):
    # Select top K features using ANOVA F-test
    selector = SelectKBest(score_func=f_classif, k=k)
    X_train_selected = selector.fit_transform(X_train, y_train)
    
    # Get feature importance from random forest
    rf = RandomForestClassifier(n_estimators=100, random_state=42)
    rf.fit(X_train_selected, y_train)
    
    # Get permutation importance for final selection
    feature_importance = rf.feature_importances_
    selected_indices = np.argsort(feature_importance)[-k:]
    
    return X_train_selected[:, selected_indices]

4. Model Architecture

Model selection with justification (logistic regression / GBM / DNN / Transformer — trade-offs)

For handling class imbalance, I'll select a Gradient Boosting Machine (GBM) as the primary model architecture. This choice balances several critical factors:

  1. Natural handling of class imbalance: GBMs can be configured with scale_pos_weight parameter to handle imbalanced classes effectively, adjusting the weight of positive samples during training.
  2. Interpretability: While not as interpretable as logistic regression, GBMs provide feature importance scores that help understand which signals drive predictions.
  3. Robustness: GBMs are less prone to overfitting than DNNs, especially with imbalanced data.
  4. Performance: GBMs typically outperform logistic regression on complex, real-world datasets with multiple features and interactions.
  5. Production readiness: Well-established in production environments, with mature ecosystem support (XGBoost, LightGBM).

Alternatives considered and rejected:

  • Logistic Regression: Too simple for complex feature interactions; requires extensive feature engineering.
  • DNNs: High risk of overfitting with imbalanced data; complex to tune for class imbalance.
  • SVM: Not ideal for large-scale production due to training complexity and poor handling of extreme imbalance.

Architecture diagram (Mermaid)

graph TD
    A[Input Features] --> B[GBM Classifier]
    B --> C{Class Prediction}
    C --> D[Output Score]
    C --> E[Probability]
    
    subgraph "Model Components"
        B --> F[Tree Ensemble]
        F --> G[Weak Learners]
    end
    
    subgraph "Training"
        H[Class Weights] --> B
        I[Sample Weights] --> B
    end
    
    subgraph "Inference"
        J[Online Serving] --> C
    end

Two-tower / multi-task / ensemble design if applicable

For this use case, I'll implement a single-task model with the following enhancements:

  1. Class-weighted training: Configure scale_pos_weight to balance positive vs negative class impact.
  2. Ensemble of GBMs: Use multiple GBM models trained with different random seeds to reduce variance.
  3. Threshold tuning: Apply optimal threshold selection (F1-maximizing or precision/recall trade-off) rather than default 0.5.

Pre-trained model selection and fine-tuning strategy (for LLM / foundation model tasks)

This system does not involve LLMs or foundation models, so pre-trained model selection is not applicable. However, if this were an LLM context, I would:

  • Consider BERT-based classification heads for text imbalance handling
  • Fine-tune with class-weighted loss functions
  • Apply gradient-based sampling techniques for rare classes

Model complexity vs latency trade-off analysis (table: model / metric / latency / size)

ModelMetricLatency (p99)Size
Logistic RegressionAUC-ROC1ms5KB
XGBoost (default)AUC-ROC10ms5MB
XGBoost (optimized)AUC-ROC5ms3MB
LightGBM (default)AUC-ROC8ms4MB
LightGBM (optimized)AUC-ROC3ms2MB

Offline evaluation baseline framing (expected baseline vs proposed model)

Baseline: Logistic Regression with balanced accuracy of ~0.75
Proposed: XGBoost with class weighting achieving >0.85 AUC-ROC and F1-score

Core model code snippet

import xgboost as xgb
from sklearn.metrics import roc_auc_score, f1_score
import numpy as np

class ImbalancedXGBClassifier:
    def __init__(self, n_estimators=100, max_depth=6, scale_pos_weight=1.0):
        self.model = xgb.XGBClassifier(
            n_estimators=n_estimators,
            max_depth=max_depth,
            scale_pos_weight=scale_pos_weight,
            random_state=42
        )
    
    def fit(self, X, y):
        self.model.fit(X, y)
        return self
    
    def predict_proba(self, X):
        return self.model.predict_proba(X)[:, 1]
    
    def predict(self, X, threshold=0.5):
        proba = self.predict_proba(X)
        return (proba >= threshold).astype(int)
    
    def evaluate(self, X_test, y_test):
        y_pred = self.predict(X_test)
        y_proba = self.predict_proba(X_test)
        
        auc_roc = roc_auc_score(y_test, y_proba)
        f1 = f1_score(y_test, y_pred)
        
        return {
            'auc_roc': auc_roc,
            'f1_score': f1
        }

# Example usage with class balancing:
# Assuming X_train, y_train are your training features and labels
# Calculate scale_pos_weight based on class distribution:
# pos_weight = sum(negatives) / sum(positives)
# This ensures the model pays more attention to minority class samples

# Example training:
# model = ImbalancedXGBClassifier(scale_pos_weight=5.0)  # Adjust based on imbalance ratio
# model.fit(X_train, y_train)

This implementation provides:

  1. Class-weighted training to handle imbalance
  2. Production-ready interface with predict_proba and predict methods
  3. Evaluation metrics for offline assessment
  4. Configurable threshold for business-specific trade-offs
  5. Scalable XGBoost backend that's optimized for real-world performance

5. Serving & Inference Architecture

System Diagram

graph TD
    A[User Request] --> B[Load Balancer]
    B --> C[Model Serving Gateway]
    C --> D{Online/Offline}
    D -->|Online| E[Feature Store]
    D -->|Offline| F[Batch Feature Store]
    E --> G[Embedding Cache]
    G --> H[Model Server]
    F --> I[Batch Feature Computation]
    I --> H
    H --> J[Scoring Engine]
    J --> K[Ranking Engine]
    K --> L[Response Formatter]
    L --> M[Return to Client]

Online vs Batch Serving Decision

For a recommendation system with class imbalance handling, we prioritize online serving due to:

  • Real-time user interaction requirements
  • Need for immediate feedback and personalization
  • Low latency (50ms p99) requirement for user experience
  • Dynamic feature updates and model re-ranking

Two-Stage Pipeline

  1. Candidate Retrieval Stage (Top-K selection)

    • Uses approximate nearest neighbor search with embedding vectors
    • Handles class imbalance via weighted candidate selection during retrieval
  2. Scoring & Re-ranking Stage

    • Applies model scores to filtered candidates
    • Implements class-aware re-ranking to address imbalance

Serving Infrastructure

We use TorchServe for model serving with the following configuration:

  • Model format: TorchScript (for faster inference)
  • GPU acceleration (2x V100s per instance)
  • Auto-scaling based on QPS and latency metrics
  • Model versioning with rollback capability

Latency Budget Breakdown

Stagep50 Latencyp99 LatencyBudget
Feature Retrieval15ms30ms30ms
Model Inference20ms40ms40ms
Scoring/Re-ranking10ms25ms25ms
Response Formatting5ms10ms10ms
Total50ms105ms105ms

Model Compression Strategy

To maintain accuracy while reducing latency:

  • Quantization: INT8 quantization reduces model size by 4x with <2% accuracy drop
  • Pruning: Structured pruning (15%) to reduce computation by 30%
  • Distillation: Knowledge distillation from larger model to smaller student model (85% of original accuracy)

Caching Strategy

  1. Prediction Cache:

    • LRU cache with TTL of 30 minutes for user-item pairs
    • Reduces redundant scoring by ~60% for frequent users
  2. Embedding Cache:

    • Redis-based cache for user/item embeddings
    • Eviction policy: LRU with size limit of 10M entries
  3. KV Cache (for LLMs):

    • For content generation use cases, cache attention keys/values
    • Reduces recomputation in streaming scenarios

Auto-scaling and Capacity Planning

  • Horizontal Scaling: Auto-scale based on QPS (target: 1000 QPS per instance)
  • Vertical Scaling: GPU instances with 2x V100s for optimal throughput
  • Warm-up Strategy: Preload model and embeddings during low-traffic hours
  • Capacity Planning:
    • Expected peak QPS: 50,000
    • Target latency: <100ms p99
    • Required instances: ~50 with auto-scaling
# Example model serving class (simplified)
import torch
from torch import nn

class BalancedRecommendationModel(nn.Module):
    def __init__(self, embedding_dim=128, hidden_dim=256):
        super().__init__()
        self.user_embedding = nn.Embedding(100000, embedding_dim)
        self.item_embedding = nn.Embedding(500000, embedding_dim)
        self.classifier = nn.Sequential(
            nn.Linear(embedding_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1)
        )
        
    def forward(self, user_ids, item_ids):
        user_emb = self.user_embedding(user_ids)
        item_emb = self.item_embedding(item_ids)
        combined = torch.cat([user_emb, item_emb], dim=1)
        return torch.sigmoid(self.classifier(combined)).squeeze()

# Example scoring logic with class weight adjustment
def score_with_class_balance(model, user_ids, item_ids, class_weights=None):
    scores = model(user_ids, item_ids)
    
    if class_weights is not None:
        # Apply class weights to adjust for imbalance
        weights = torch.tensor(class_weights, dtype=torch.float32)
        # Assuming item_ids can be used to look up class weights
        weight_factors = weights[item_ids]
        scores = scores * weight_factors
        
    return scores

6. Evaluation Framework

Offline Evaluation Methodology

For imbalanced datasets, we must use robust offline evaluation techniques to avoid misleading performance metrics. We will implement temporal cross-validation with time-based splits to ensure that our model generalizes well across time and doesn't overfit to recent data patterns. This approach is critical for maintaining model stability in real-world deployment where class distributions may shift over time.

We will use holdout validation with a 70/15/15 split for training/validation/test sets, ensuring that the minority class is adequately represented in all splits. For highly imbalanced datasets, we will apply stratified sampling to maintain class proportions across folds.

Slice-Based Evaluation

We will evaluate model performance across critical user segments to detect potential bias or degradation in specific slices:

  • User demographics: Age groups, geographic regions, device types
  • Engagement levels: High vs low engagement users
  • Temporal slices: Recent vs older users, seasonal patterns
  • Content categories: Different content types that may have varying class distributions

Key slices to monitor include:

  • High-impact minority classes (e.g., rare events that drive revenue)
  • New user cohorts (cold start scenario)
  • Regional user bases with different distribution characteristics

A/B Test Design

We will design a robust A/B test framework to validate model improvements in production:

Randomization Unit: User-level randomization to avoid leakage between users and their interactions.

Sample Size Calculation:

  • Target: 5% relative improvement in key metric (e.g., CTR)
  • Power: 80%
  • Significance level: 5%
  • Expected baseline CTR: 2.5% → Target: 2.625%
  • Estimated variance: ~0.0005

Based on power analysis, we require approximately 12,000 users per variant for a 95% confidence level.

Test Duration: 30-day continuous test to capture seasonal patterns and ensure statistical significance.

Primary Metric: Click-through rate (CTR) for the minority class events, with secondary metrics including:

  • Revenue per user (RPU)
  • Conversion rate to target action
  • User engagement duration

Guardrail Metrics and Blocking Conditions

To ensure model safety, we will implement guardrails:

Blocking Conditions:

  • If CTR drops more than 15% in any slice, automatic rollback to baseline
  • If precision for majority class drops below 90%, halt deployment
  • If recall for minority class falls below 70%, trigger retraining

Guardrail Metrics:

  • Precision/recall trade-off curves for all slices
  • Distribution shift detection (PSI > 0.1)
  • Model confidence calibration (ECE < 5%)

Shadow Mode Rollout Plan

Before full A/B testing, we will implement shadow mode for 7 days:

  1. Traffic Split: 95% baseline, 5% new model (shadow)
  2. Logging: Capture all predictions and features for both variants
  3. Comparison: Compare offline performance between shadow and baseline
  4. Validation: Ensure shadow model outputs are consistent with expected behavior

Long-Term Metric Measurement

We will track delayed impact metrics to ensure model sustainability:

  • Delayed Conversion: 7-day and 30-day conversion rates for minority class events
  • Engagement Decay: User retention and re-engagement over time (30-day decay)
  • Revenue Impact: Time-to-revenue analysis for rare events that drive long-term value
  • User Saturation: Monitor for over-prediction in high-volume user segments

These metrics will be tracked using a dashboard with daily alerts for any significant deviations from expected patterns.

# Example evaluation code snippet for class imbalance handling
import numpy as np
from sklearn.metrics import classification_report, precision_recall_fscore_support
from sklearn.model_selection import train_test_split

def evaluate_imbalanced_model(y_true, y_pred_proba, minority_class=1):
    """
    Evaluate model performance on imbalanced dataset with focus on minority class
    """
    # Convert probabilities to predictions (threshold tuning)
    y_pred = (y_pred_proba[:, 1] >= 0.5).astype(int)
    
    # Get precision, recall, f1 for minority class
    precision, recall, f1, support = precision_recall_fscore_support(
        y_true, y_pred, labels=[minority_class], average='binary'
    )
    
    # Additional metrics for imbalanced evaluation
    report = classification_report(y_true, y_pred, labels=[minority_class], 
                                   target_names=['majority', 'minority'], output_dict=True)
    
    return {
        'precision': precision,
        'recall': recall,
        'f1_score': f1,
        'minority_support': support,
        'auc_roc': roc_auc_score(y_true, y_pred_proba[:, 1])
    }

# Example usage for A/B test evaluation
def ab_test_evaluation(control_y_true, control_y_pred_proba, 
                      treatment_y_true, treatment_y_pred_proba):
    """
    Compare model performance between control and treatment variants
    """
    control_metrics = evaluate_imbalanced_model(control_y_true, control_y_pred_proba)
    treatment_metrics = evaluate_imbalanced_model(treatment_y_true, treatment_y_pred_proba)
    
    # Statistical significance test (t-test or z-test)
    from scipy.stats import ttest_ind
    
    # Compare CTRs between variants
    control_ctr = np.mean(control_y_true)
    treatment_ctr = np.mean(treatment_y_true)
    
    # Confidence interval calculation
    p_value = ttest_ind(control_y_pred_proba[:, 1], treatment_y_pred_proba[:, 1]).pvalue
    
    return {
        'control_metrics': control_metrics,
        'treatment_metrics': treatment_metrics,
        'p_value': p_value,
        'effect_size': (treatment_ctr - control_ctr) / control_ctr
    }

7. Training Pipeline

Training Data Pipeline

The training pipeline follows a multi-stage ETL process designed to handle imbalanced datasets efficiently:

  1. Data Ingestion: Raw data flows from multiple sources (user events, item metadata, third-party signals) via Kafka streams
  2. Preprocessing:
    • Feature normalization and encoding (one-hot for categorical, standardization for numerical)
    • Handling missing values with median imputation or feature-specific strategies
    • Label balancing using synthetic sampling techniques (SMOTE, ADASYN)
  3. Sampling:
    • Oversampling minority classes using SMOTE with k=5 neighbors
    • Undersampling majority classes to maintain balanced ratio (1:1)
  4. Shuffling and Batching:
    • Stratified shuffling to preserve class distribution
    • Batch size of 256 for efficient GPU utilization

Distributed Training Strategy

For large-scale training, we implement data parallelism across multiple GPUs using PyTorch's DistributedDataParallel (DDP):

import torch
from torch.nn.parallel import DistributedDataParallel as DDP

# Initialize distributed training
torch.distributed.init_process_group(backend='nccl')
model = model.to(device)
model = DDP(model, device_ids=[local_rank])

# Training loop with gradient synchronization
for batch in dataloader:
    optimizer.zero_grad()
    outputs = model(batch)
    loss = criterion(outputs, batch['labels'])
    loss.backward()
    optimizer.step()

Training Infrastructure

  • Cluster Configuration: 4x A100 GPUs (80GB VRAM each) with NVLink connectivity
  • Training Time Estimate: ~2 hours for 1M samples using batch size 256
  • Memory Usage: ~4GB per GPU with mixed precision training (FP16)
  • Data Pipeline Optimization:
    • Prefetching with torch.utils.data.DataLoader with num_workers=4
    • Memory mapping for large datasets

Hyperparameter Tuning Approach

We use Bayesian optimization via Optuna to tune key hyperparameters:

import optuna

def objective(trial):
    # Hyperparameter search space
    lr = trial.suggest_float('lr', 1e-5, 1e-2, log=True)
    weight_decay = trial.suggest_float('weight_decay', 1e-6, 1e-2, log=True)
    class_weight = trial.suggest_categorical('class_weight', ['balanced', 'manual'])
    
    model = Model().to(device)
    optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
    
    # Training with class weights
    if class_weight == 'balanced':
        criterion = nn.CrossEntropyLoss(weight=get_balanced_weights())
    else:
        criterion = nn.CrossEntropyLoss()
    
    # Evaluate on validation set
    val_loss = evaluate(model, val_loader)
    return val_loss

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=50)

Key hyperparameters optimized:

  • Learning rate (1e-5 to 1e-2)
  • Weight decay (1e-6 to 1e-2)
  • Batch size (32, 64, 128, 256)
  • Class weight strategy (balanced vs manual)

Experiment Tracking and Reproducibility

We use MLflow for experiment tracking with:

  • Version Control: Git integration for model checkpoints and configuration files
  • Metrics Logging: Per-epoch training/validation loss, AUC-ROC, F1-score
  • Parameter Tracking: Hyperparameters, training time, dataset statistics

Model Versioning and Registry Strategy

  • Versioning: Semantic versioning (v1.0.0, v1.0.1) with Git tags
  • Registry: MLflow model registry with stages (Staging → Production)
  • Rollback Capability: Automatic rollback to previous stable version if performance degrades by >2%

Continual / Online Learning Design

For real-time adaptation to changing data distributions:

class OnlineLearningModel:
    def __init__(self, model, update_threshold=0.05):
        self.model = model
        self.update_threshold = update_threshold
        self.recent_losses = deque(maxlen=100)
        
    def adaptive_update(self, new_data_batch):
        # Calculate recent loss trend
        current_loss = self.compute_loss(new_data_batch)
        self.recent_losses.append(current_loss)
        
        # Trigger retraining if drift detected
        if len(self.recent_losses) >= 10 and self.is_drift_detected():
            self.retrain_model()
            
    def is_drift_detected(self):
        recent_avg = sum(self.recent_losses) / len(self.recent_losses)
        return abs(recent_avg - self.recent_losses[0]) > self.update_threshold

The system implements a hybrid approach where:

  • Scheduled Updates: Weekly retraining with new data batches
  • Drift Detection: Real-time monitoring of performance degradation
  • Incremental Updates: Online fine-tuning using recent samples with reduced learning rate

8. Monitoring & Observability

Data Drift Detection

Data drift detection is crucial for maintaining model performance over time. We'll implement drift detection using statistical tests and feature-level monitoring:

  • PSI (Population Stability Index) for categorical features
  • KL Divergence for continuous features
  • Chi-square test for categorical feature drift
  • Kolmogorov-Smirnov test for continuous feature drift
from scipy import stats
import numpy as np

def calculate_psi(expected, actual, bins=10):
    """Calculate Population Stability Index"""
    # Ensure both distributions have same bins
    _, bin_edges = np.histogram(expected, bins=bins)
    
    # Count occurrences in each bin
    expected_counts, _ = np.histogram(expected, bins=bin_edges)
    actual_counts, _ = np.histogram(actual, bins=bin_edges)
    
    # Avoid zero division
    expected_counts = np.clip(expected_counts, 1e-6, None)
    actual_counts = np.clip(actual_counts, 1e-6, None)
    
    psi = np.sum((actual_counts - expected_counts) * 
                 np.log(actual_counts / expected_counts))
    return psi

def detect_drift(expected_data, actual_data, feature_type='continuous'):
    """Detect drift between expected and actual data"""
    if feature_type == 'categorical':
        return stats.chi2_contingency([expected_data, actual_data])[0]
    else:
        return stats.ks_2samp(expected_data, actual_data)[0]

Prediction Drift and Label Drift Monitoring

We'll monitor prediction drift by comparing model outputs over time and label drift by tracking distribution shifts in training labels:

  • Prediction drift: Compare distribution of model confidence scores over time using KS test
  • Label drift: Track class distribution shifts in training labels using PSI

Model Performance Degradation Alerting

Implement alerting for performance degradation:

  • AUC-ROC drop > 5% over 30-day window
  • Precision@K drop > 10% for top-K recommendations
  • NDCG drop > 5% for ranking systems
from sklearn.metrics import roc_auc_score, ndcg_score

class ModelPerformanceMonitor:
    def __init__(self):
        self.performance_history = []
        
    def check_degradation(self, current_auc, current_ndcg, window_days=30):
        """Check for performance degradation"""
        if len(self.performance_history) < window_days:
            return False
            
        # Check AUC degradation
        recent_auc = [p['auc'] for p in self.performance_history[-window_days:]]
        if max(recent_auc) - current_auc > 0.05:
            return True
            
        # Check NDCG degradation  
        recent_ndcg = [p['ndcg'] for p in self.performance_history[-window_days:]]
        if max(recent_ndcg) - current_ndcg > 0.05:
            return True
            
        return False

Retraining Trigger Strategy

We'll implement a multi-trigger retraining strategy:

  • Scheduled retraining: Every 30 days or when drift exceeds thresholds
  • Drift-triggered: When PSI > 0.1 or KS test p-value < 0.05 for any feature
  • Performance-triggered: When AUC drops by > 5% over 30 days

Logging Design

Comprehensive logging for model observability:

  • Prediction logs: Model scores, features, timestamps, user IDs
  • Feature logs: Real-time feature values for drift detection
  • Label collection pipeline: Track label changes and feedback loops
import json
from datetime import datetime

class PredictionLogger:
    def __init__(self, log_file="predictions.log"):
        self.log_file = log_file
        
    def log_prediction(self, user_id, item_id, features, score, timestamp=None):
        """Log prediction with all relevant metadata"""
        if timestamp is None:
            timestamp = datetime.now().isoformat()
            
        log_entry = {
            "timestamp": timestamp,
            "user_id": user_id,
            "item_id": item_id,
            "features": features,
            "score": score,
            "model_version": "v1.2"
        }
        
        with open(self.log_file, "a") as f:
            f.write(json.dumps(log_entry) + "\n")

Feedback Loop for Continuous Improvement

Implement feedback collection:

  • User feedback: Explicit ratings, clicks, time spent on items
  • Label feedback: Re-labeling of predictions based on user behavior
  • Active learning: Identify high-value samples for retraining
class FeedbackCollector:
    def __init__(self):
        self.feedback_buffer = []
        
    def collect_feedback(self, user_id, item_id, feedback_type, timestamp):
        """Collect user feedback for model improvement"""
        feedback = {
            "user_id": user_id,
            "item_id": item_id,
            "feedback_type": feedback_type,  # "click", "rating", "dislike"
            "timestamp": timestamp,
            "confidence": self.get_model_confidence(user_id, item_id)
        }
        
        self.feedback_buffer.append(feedback)
        
        # Flush buffer when it reaches threshold
        if len(self.feedback_buffer) >= 1000:
            self.flush_feedback()
            
    def flush_feedback(self):
        """Flush feedback to training pipeline"""
        # In production, this would send to a queue or database
        print(f"Flushed {len(self.feedback_buffer)} feedback entries")
        self.feedback_buffer.clear()

Key Metrics for Monitoring

MetricAlert ThresholdMonitoring FrequencyPurpose
AUC-ROCDrop > 5% over 30dDailyOverall model performance
Precision@KDrop > 10% over 30dDailyTop-K recommendation quality
NDCGDrop > 5% over 30dDailyRanking quality
PSI> 0.1 for any featureWeeklyData drift detection
KS Test p-value< 0.05 for any featureWeeklyStatistical drift detection

Alerting and Escalation

  • Severity 1: AUC drop > 10% → immediate retraining trigger
  • Severity 2: PSI > 0.2 or KS p-value < 0.01 → alert to ML team
  • Severity 3: Performance degradation > 5% → weekly review

Observability Dashboard Components

  • Real-time performance dashboard: AUC, precision@K, NDCG trends
  • Drift monitoring dashboard: PSI and KS test results per feature
  • Prediction distribution dashboard: Model confidence distributions over time
  • Feedback analysis dashboard: User feedback trends and retraining signals

This monitoring framework ensures early detection of model degradation, enables proactive retraining, and supports continuous improvement through feedback loops. The system is designed to be lightweight for production deployment while providing comprehensive observability for ML model health.

9. Responsible AI & Safety

Bias Analysis and Fairness

Class imbalance can introduce significant bias, particularly in underrepresented classes. Our system must audit training data for demographic parity across user segments (age groups, geographic regions, device types) and item categories. We'll implement slice-based fairness evaluation using demographic parity difference (DPD) and equalized odds difference (EOD) as primary fairness metrics. For example, if 95% of training samples are from urban users, our model should not disproportionately favor urban over rural user predictions.

Fairness Metrics and Constraints

We will enforce fairness constraints using:

  • Demographic Parity Difference (DPD) < 0.05 for key user segments
  • Equalized Odds Difference (EOD) < 0.05 for positive class predictions

We will implement a fairness-aware loss function that penalizes violations of these constraints during training. This is particularly important for recommendation systems where underrepresentation of certain user groups could lead to unfair treatment.

Explainability Requirements

We will implement SHAP (SHapley Additive exPlanations) to understand feature importance for minority class predictions. For critical use cases, we'll provide attention weights in transformer-based models to show which user/item features influenced the model's decision for underrepresented classes.

Safety Guardrails and Content Filtering

For recommendation systems, we implement content filtering to prevent harmful or inappropriate item recommendations. This includes:

  • Pre-filtering items based on safety labels (e.g., content rating, moderation tags)
  • Rate limiting for high-risk user-item combinations
  • Red teaming with synthetic adversarial examples to test edge cases

Model Card Documentation Plan

We will maintain a comprehensive model card that includes:

  • Training data distribution by class and user segment
  • Fairness metrics across slices
  • Performance degradation on underrepresented classes
  • Known limitations and assumptions

Incident Response Plan

We'll establish a multi-tiered alerting system:

  • High-priority: >10% drop in recall for minority classes
  • Medium-priority: >5% increase in DPD/EOD violations
  • Low-priority: >20% decrease in model confidence for underrepresented samples

Model Robustness to Adversarial Inputs

We'll implement adversarial training techniques to make the model robust against input perturbations that could disproportionately affect minority classes. This includes:

  • Input gradient-based perturbations during training
  • Validation of model predictions against known attack patterns

Privacy and Data Protection

We'll ensure that any data augmentation techniques (SMOTE, oversampling) do not inadvertently reveal sensitive information about individuals in the training set. This includes:

  • Differential privacy techniques for synthetic data generation
  • Anonymization of user features before applying SMOTE
  • Regular audits of synthetic data to ensure it doesn't leak real user patterns

Bias Mitigation Strategy Implementation

import numpy as np
from sklearn.metrics import confusion_matrix, roc_auc_score

def fairness_metrics(y_true, y_pred, sensitive_groups):
    """
    Calculate fairness metrics for different user segments
    """
    # Demographic Parity Difference (DPD)
    dpd = {}
    for group in np.unique(sensitive_groups):
        mask = sensitive_groups == group
        pred_rate = np.mean(y_pred[mask])
        base_rate = np.mean(y_pred)
        dpd[group] = abs(pred_rate - base_rate)
    
    # Equalized Odds Difference (EOD) 
    eod = {}
    for group in np.unique(sensitive_groups):
        mask = sensitive_groups == group
        tn, fp, fn, tp = confusion_matrix(y_true[mask], y_pred[mask]).ravel()
        tpr_group = tp / (tp + fn) if (tp + fn) > 0 else 0
        tpr_base = np.sum(y_true) / len(y_true)
        eod[group] = abs(tpr_group - tpr_base)
    
    return {
        'dpd': dpd,
        'eod': eod
    }

# Example usage for fairness-aware training loss
def fairness_aware_loss(y_true, y_pred, sensitive_groups, alpha=0.1):
    """
    Loss function that penalizes fairness violations
    """
    base_loss = binary_crossentropy(y_true, y_pred)
    
    fairness_violations = 0
    for group in np.unique(sensitive_groups):
        mask = sensitive_groups == group
        pred_rate = np.mean(y_pred[mask])
        base_rate = np.mean(y_pred)
        fairness_violations += abs(pred_rate - base_rate)
    
    return base_loss + alpha * fairness_violations

10. Scalability & Reliability

Bottleneck Analysis per Pipeline Stage

StageBottleneck TypeBack-of-Envelope Estimate
Data IngestionI/O throughput10GB/s peak for 10M samples/sec
Feature ComputationCPU/memory500GB RAM for 10M users × 100 features
Model TrainingGPU memory8GB per GPU, 32GB total for 10M samples
Online ServingLatency50ms p99 for 100K QPS
Model UpdatesBatch processing2h training window for 1M samples

Scale-up Strategy

Horizontal Scaling of Serving:

  • Use Kubernetes with auto-scaling groups (min 10, max 50 replicas)
  • Implement consistent hashing for feature store lookups
  • Distribute model shards across multiple nodes with cache warming

Distributed Feature Computation:

  • Use Ray or Dask for distributed feature extraction
  • Partition training data by user ID hash to enable parallel processing
  • Implement incremental feature updates with delta sync

Fault Tolerance

Model Serving Fallbacks:

  • Implement fallback to previous model version on failure
  • Use circuit breaker pattern for degraded service (e.g., return default scores)
  • Maintain warm standby replicas in different availability zones

Stale Model Serving on Failure:

class FallbackModel:
    def __init__(self, primary_model_path, fallback_model_path):
        self.primary = load_model(primary_model_path)
        self.fallback = load_model(fallback_model_path)
        self.last_success = time.time()
    
    def predict(self, features):
        try:
            result = self.primary.predict(features)
            self.last_success = time.time()
            return result
        except Exception as e:
            # Fallback to previous model if failure within 5 min window
            if time.time() - self.last_success < 300:
                return self.fallback.predict(features)
            else:
                raise Exception("Model failure and no fallback available")

Cold Start Handling Strategy

New Users:

  • Use demographic-based default scores (e.g., age group, location)
  • Implement content-based filtering using item metadata
  • Apply user embedding clustering to find similar users

New Items:

  • Use popularity-based ranking for initial items
  • Apply content-based filtering using item text/image features
  • Implement candidate sampling from recent trending items

Multi-region Serving Strategy

class MultiRegionServing:
    def __init__(self, regions):
        self.regions = regions
        self.region_weights = {region: 1.0 for region in regions}
    
    def get_model_for_region(self, region):
        # Load model from nearest region or use fallback
        return self.load_model_from_region(region) or self.load_fallback()
    
    def route_request(self, user_location):
        # Route to nearest region with model availability
        return self.select_region_by_geo(user_location)

Region Selection Logic:

  • Route based on user's geographic location (latency + availability)
  • Implement cross-region failover with 500ms timeout
  • Maintain model consistency across regions using versioned model artifacts

Traffic Distribution:

  • Use weighted round-robin for load balancing
  • Implement region-specific model versions (e.g., language-specific)
  • Monitor regional QPS and latency to adjust routing weights dynamically

11. Open Questions, Risks & Trade-offs

Decisions requiring product / engineering / policy alignment

  • Class weight vs sampling strategy alignment: Should we prioritize class weights over oversampling techniques, or use a hybrid approach? This decision will impact model training cost and inference consistency.
  • Threshold tuning vs sampling strategy: Should we tune decision thresholds for imbalanced classes or rely on model-level adjustments? This affects downstream business logic and A/B test design.
  • Label quality vs sampling trade-off: How aggressively do we apply SMOTE or other synthetic sampling methods when training data labels are noisy or mislabeled?

Key ML risks

RiskImpactLikelihoodMitigation
Overfitting to minority classHigh - model performs well on training data but poorly in productionMediumImplement stratified cross-validation, use smaller minority class weights, apply early stopping
Synthetic data artifactsMedium - SMOTE-generated samples may not generalize well to real-world dataHighValidate synthetic samples with domain experts, limit SMOTE application to 2x minority class size
Performance degradation in productionHigh - model underperforms due to training-test distribution mismatchMediumImplement robust offline evaluation with temporal splits, shadow testing before full rollout
Bias amplification in underrepresented groupsHigh - model may disproportionately favor majority class during inferenceMediumMonitor fairness metrics across slices, implement demographic parity constraints
False positive rate inflationMedium - increased false positives in minority class may impact business KPIsHighUse precision-recall curves to optimize thresholds, implement cost-sensitive learning

Trade-offs made with justification

Trade-offJustification
Accuracy vs Precision/RecallPrioritizing recall over accuracy for minority class is critical in fraud detection or medical diagnosis use cases. Accuracy can be misleading when classes are imbalanced.
Training cost vs model performanceUsing SMOTE increases training time and memory usage, but can improve minority class detection. We balance this by applying SMOTE only to the most critical minority classes and monitoring training efficiency.
Synthetic data quality vs real data quantityWhile synthetic samples increase dataset size, they may introduce noise or unrealistic patterns. We limit synthetic sampling to 2x the original minority class size and validate with domain experts.
Online vs batch trainingBatch training allows for better synthetic sample generation but introduces latency. Online learning enables faster adaptation to new patterns but may amplify noise from synthetic samples. We use hybrid approach with periodic batch retraining.
Model size vs inference speedLarger models trained on balanced data may perform better but increase latency. We balance model size with p99 latency requirements by using model compression and pruning techniques.

Alternatives considered and rejected

AlternativeReason for rejection
Undersampling majority classRisk of losing valuable information and potentially introducing bias. Majority class patterns are important for generalization.
Ensemble of balanced modelsIncreased complexity and latency in serving. Simpler approaches like class weights or SMOTE provide similar gains with less operational overhead.
Cost-sensitive learning with custom lossMore complex to implement and tune. Class weights provide a simpler, effective alternative for most use cases.
Active learning with human reviewHigh operational cost and complexity. Not scalable for large-scale systems with high-volume data streams.
One-class classificationInappropriate for multi-class problems and may not capture the nuanced relationships between classes.

Technical debt acknowledged

  • Class weight handling inconsistency: Different training pipelines may use different class weight strategies, leading to inconsistent model behavior. Need for unified class weight management across systems.
  • SMOTE parameter sensitivity: The effectiveness of SMOTE depends heavily on hyperparameters (e.g., k-neighbors, sampling ratio) that are not consistently tuned across training jobs.
  • Feature interaction with synthetic samples: Synthetic feature combinations may not align with real-world patterns, creating technical debt in model interpretability and performance.
  • Evaluation metric drift: Offline metrics may not reflect real-world performance due to synthetic sample artifacts, requiring ongoing manual validation and metric refinement.
  • Threshold calibration drift: Decision thresholds optimized for training data may not generalize to production, requiring continuous re-evaluation and adjustment.

12. Phased Implementation Roadmap

Phase 1: MVP (Baseline to First ML Model)

Objective: Establish minimal viable system with rule-based fallback and basic ML model.

Deliverables:

  • Rule-based baseline system (e.g., majority class prediction for all samples)
  • Training pipeline with synthetic oversampled data (e.g., SMOTE) or class weights
  • Simple logistic regression model trained on balanced synthetic dataset
  • Basic feature store with user/item features (batch computation)
  • Online serving infrastructure for model scoring (e.g., TorchServe or custom endpoint)

Timeline: 2-3 weeks
Dependencies: Data pipeline, feature store, model training infrastructure

Phase 2: Scale (Feature Store & Distributed Training)

Objective: Enable scalable feature computation and distributed training for improved performance.

Deliverables:

  • Real-time feature computation pipeline (e.g., Kafka-based stream processing)
  • Distributed training infrastructure (e.g., Ray or PyTorch DDP)
  • Advanced model architectures (e.g., XGBoost, LightGBM, or DNNs with class weights)
  • A/B testing infrastructure (e.g., MLflow + custom A/B test service)
  • Online evaluation dashboard (e.g., Prometheus + Grafana)

Timeline: 4-6 weeks
Dependencies: Phase 1, feature store, model serving stack

Phase 3: Excellence (Advanced Models & Continuous Learning)

Objective: Deploy state-of-the-art models with real-time learning and adaptive class handling.

Deliverables:

  • Multi-task or ensemble model with attention to minority class performance
  • Online learning pipeline (e.g., incremental learning or periodic retraining with drift detection)
  • Dynamic class weight adjustment based on prediction drift or user feedback
  • Real-time feature interaction modeling (e.g., cross features, temporal trends)
  • Model retraining and deployment automation (CI/CD for ML models)

Timeline: 6-8 weeks
Dependencies: Phase 2, model monitoring, feedback loop system

Critical Path & Dependencies:

  1. Data Strategy must be established before any model training
  2. Feature Store is required for real-time serving and batch training consistency
  3. Serving Infrastructure must support low-latency, high-throughput scoring for production use
  4. Evaluation Framework is critical to validate model improvements and A/B test results

Risk Mitigation:

  • Start with synthetic oversampling in Phase 1 to validate model behavior before deploying class weight strategies
  • Use shadow mode for A/B tests in Phase 2 to prevent production impact from model drift or bias
  • Implement feedback loop for continuous retraining in Phase 3 to handle evolving class distributions

This phased approach enables rapid iteration, reduces risk of system-wide failure, and ensures each step builds upon the previous one with measurable impact.

Was this article helpful?

Topics in this article

More from AI/ML System Design

  • AI/ML System Design

    Large-Scale Logistic Regression Scoring System Design

    Comprehensive system design for scalable logistic regression inference at enterprise scale with distributed computing and real-time scoring capabilities.

  • AI/ML System Design

    Point-in-Time Correct Feature Retrieval System Design

    Comprehensive system design for ensuring feature data consistency and temporal correctness in machine learning pipelines.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.