Matrix factorization and collaborative filtering form the backbone of modern recommendation systems, enabling personalized user experiences through latent feature decomposition and user-item interaction modeling in large-scale production environments.
1. Problem Statement & ML Task Definition
Clarifying questions
-
What is the expected scale of users/items and QPS requirements?
- For example: 10M users, 100M items, 10K QPS target — this determines model size and serving architecture.
-
What is the primary signal type (explicit feedback vs implicit)?
- Explicit: ratings, likes; Implicit: clicks, views, time spent — affects model architecture and loss function.
-
What is the latency budget for real-time recommendations?
- E.g., 50ms p99 for web, 200ms for mobile — impacts model complexity and serving stack.
-
Is this a batch or online recommendation system?
- Batch: precompute recommendations; Online: real-time scoring — affects feature computation and model serving.
-
What is the target business metric for success?
- E.g., CTR, revenue per user, retention — drives model objective and evaluation.
-
What is the expected data freshness window?
- E.g., hourly, daily, or real-time updates — affects feature store and training pipeline design.
-
What is the acceptable trade-off between model accuracy and serving latency?
- E.g., can we accept 5% accuracy drop for 10x latency improvement? — informs compression and caching strategy.
Business use case and success criteria
Use Case: Build a scalable recommendation system for a media streaming platform that suggests content to users based on their viewing history and preferences.
Success Criteria:
- Offline: AUC-ROC > 0.92, NDCG@10 > 0.75 on held-out test set
- Online: 20% increase in CTR, 15% improvement in user engagement (time spent watching), 10% revenue lift
- Long-term: 5% increase in user retention rate
ML problem formulation
Task: Collaborative Filtering with Matrix Factorization for ranking of items to users.
Justification:
- Matrix factorization naturally models user-item interactions as a low-rank matrix decomposition, capturing latent user preferences and item characteristics.
- Ranking is appropriate for content recommendation where the goal is to surface the most relevant items from a large candidate set.
- The task is framed as a ranking problem where we predict the relative preference of items for a user, not just binary relevance.
ML evaluation metrics
Offline Metrics:
- AUC-ROC (for binary classification of user-item interactions)
- NDCG@K (Normalized Discounted Cumulative Gain) — for ranking quality
- Precision@K (e.g., Precision@10) — precision of top-K recommendations
- Recall@K — recall of relevant items in top-K
Online Metrics:
- CTR (Click-Through Rate) — direct business impact
- Engagement (e.g., time spent watching, completion rate)
- Revenue per user (RPU) — monetization impact
- User retention rate — long-term stickiness
Scale and constraints
Back-of-envelope estimates:
| Component | Estimate |
|---|---|
| Training data size | 10B user-item interactions (e.g., 10M users × 1000 items) |
| Number of features | 100K sparse features (user ID, item ID, context) |
| Embedding matrix size | 10M users × 128-dim + 100M items × 128-dim = ~25GB |
| Serving QPS | 10K QPS (p99 < 50ms) |
| Training time | 2 hours on 8x V100 GPUs |
| Model size | ~50MB (compressed) |
QPS math:
- Assuming 10K QPS with p99 latency < 50ms, we need to support ~500GB/s throughput (if serving embeddings + scoring).
- For a 128-dim embedding lookup, each user/item embedding is ~512 bytes.
- For 10K QPS, we need to serve ~5M embeddings/sec (assuming 500ms per request), which is achievable with proper caching and embedding store.
Non-ML baseline strategy
Rule-based fallback:
- Cold start: Recommend popular items or trending content.
- User cold start: Use demographic features (age, location) to recommend popular items in that segment.
- Item cold start: Use content-based features (genre, director) to recommend similar items.
Fallback logic:
- If model fails or latency exceeds threshold, return top-K popular items.
- For low-volume users, fallback to content-based recommendations or default category.
def get_baseline_recommendations(user_id, item_ids):
# Fallback: return top-K popular items
if not user_id or not item_ids:
return get_top_popular_items(k=10)
# Or use demographic-based recommendations for cold start
return get_demographic_recommendations(user_id, k=10)
2. Data Strategy
Training data sources
- Explicit feedback: User ratings, likes, dislikes, watch times, purchase events (first-party signals)
- Implicit signals: Clicks, views, time spent, scroll depth, abandon rates, re-watches (user behavior)
- Third-party data: Demographics, device info, geographic location, contextual signals (weather, time of day)
- Content metadata: Item categories, genres, descriptions, tags, release dates (item features)
- User demographics: Age groups, gender, location, language preferences (user profiles)
Labeling pipeline design
- Human annotation: For explicit feedback collection, with quality control via multiple annotators and consensus voting
- Weak supervision: Using heuristics (e.g., >5 min watch = positive signal) and crowdsourcing for implicit signals
- Programmatic labeling: Automated labeling using user behavior thresholds (e.g., >30 sec watch = interaction)
- Active learning: Selecting most informative samples for human labeling to optimize annotation cost
Class imbalance handling strategy
- Positive/Negative ratio: Typically 1:100 or higher for implicit feedback systems, requiring careful sampling
- Strategy: Oversampling minority classes (positive interactions) and undersampling majority classes (negative samples)
- Justification: Ensures model doesn't become biased toward frequent negative examples and learns meaningful user preferences
Negative sampling approach
- Uniform sampling: Sample items that the user hasn't interacted with, maintaining 1:10 or 1:50 ratio
- Popularity-based: Prefer less popular items to reduce bias toward well-known items
- Temporal decay: Prefer older non-interactions to avoid overfitting to recent behavior
- Stratified sampling: Ensure representation across user segments and item categories
Data freshness and update frequency requirements
- Real-time: User behavior updates within 15 minutes (clicks, views)
- Batch: Item metadata and user profiles updated daily
- Cold start: New items and users handled via content-based features or popularity fallbacks
- Back-of-envelope estimates:
- Training data size: ~100M user-item interactions
- Features per item: ~50 (metadata, tags)
- User features: ~20 (demographics, preferences)
- Model size: ~5GB embedding matrices
Privacy compliance
- GDPR/CCPA: Anonymize user IDs, remove PII from training data, implement data retention policies (6 months)
- Data minimization: Only collect necessary features for recommendations
- Consent management: Track user consent for data collection and processing
- Differential privacy: Apply DP to embeddings for user-level privacy protection
Data quality validation and monitoring
- Validation checks: Duplicate interactions, invalid timestamps, malformed user/item IDs
- Monitoring: Daily data drift detection using statistical tests (Kolmogorov-Smirnov), feature distribution shifts
- Alerting: Anomaly detection for sudden drops in engagement, unexpected spikes in negative samples
- Data lineage tracking: Maintain audit trail of data sources and transformations for compliance
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
class DataValidator:
def __init__(self):
self.required_columns = ['user_id', 'item_id', 'rating', 'timestamp']
def validate_data(self, df):
"""Validate training data for quality and consistency"""
# Check required columns
missing_cols = set(self.required_columns) - set(df.columns)
if missing_cols:
raise ValueError(f"Missing required columns: {missing_cols}")
# Check for duplicates
if df.duplicated(subset=['user_id', 'item_id']).any():
print("Warning: Found duplicate user-item interactions")
# Check timestamp format
df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
if df['timestamp'].isna().any():
raise ValueError("Invalid timestamp format found")
# Validate ratings (assuming 1-5 scale)
if not ((df['rating'] >= 1) & (df['rating'] <= 5)).all():
print("Warning: Rating values outside expected range [1,5]")
return df
class NegativeSampler:
def __init__(self, sample_ratio=10):
self.sample_ratio = sample_ratio
def sample_negatives(self, interactions_df, user_item_map):
"""Sample negative items for each user"""
sampled_negatives = []
for user_id in interactions_df['user_id'].unique():
user_interactions = set(interactions_df[interactions_df['user_id'] == user_id]['item_id'])
all_items = set(user_item_map.keys())
# Sample negatives with popularity bias
candidates = list(all_items - user_interactions)
if len(candidates) > 0:
# Prefer less popular items (inverse of item popularity)
sample_size = min(len(candidates), len(user_interactions) * self.sample_ratio)
sampled = np.random.choice(candidates, size=sample_size, replace=False)
for item_id in sampled:
sampled_negatives.append({
'user_id': user_id,
'item_id': item_id,
'rating': 0, # Negative sample
'timestamp': pd.Timestamp.now()
})
return pd.DataFrame(sampled_negatives)
3. Feature Engineering
Feature Taxonomy
Features are categorized into four main types:
- User Features: Demographics, engagement history, preference patterns
- Item Features: Content metadata, popularity metrics, category information
- Context Features: Time-of-day, device type, geographic location
- Interaction Features: User-item interaction history, temporal signals
Embedding Design
Entity Embeddings:
- User embeddings: 128-dimensional, trained via matrix factorization
- Item embeddings: 128-dimensional, learned from user-item interactions
- Context embeddings: 64-dimensional for time-based features
Text Embeddings:
- Item title/content: 256-dimensional using BERT-base
- User profile text: 128-dimensional using TF-IDF with word embeddings
Graph Embeddings:
- User-item bipartite graph: 64-dimensional using GraphSAGE
- Content similarity graph: 32-dimensional for item-to-item relationships
Real-time vs Batch Feature Computation
| Feature Type | Computation Mode | Reasoning |
|---|---|---|
| User engagement count | Real-time | High volatility, requires fresh signals |
| Item popularity score | Batch | Stable over time, computed daily |
| User preference vectors | Real-time | Dynamic user interests |
| Item content embeddings | Batch | Computationally expensive, can be pre-computed |
Feature Store Design
Online Store:
- Low-latency (sub-50ms) serving for real-time inference
- Redis-based with TTL management
- Supports feature lookup by user/item ID
Offline Store:
- Parquet files with partitioning by date
- Supports batch training and model evaluation
- Schema versioned for backward compatibility
Feature Freshness and TTL
| Feature | Source | Update Frequency | Store Type |
|---|---|---|---|
| User engagement count | Real-time stream | Every 5 minutes | Online |
| Item popularity | Batch job | Daily | Offline |
| User preference vector | Real-time stream | Every 15 minutes | Online |
| Item content embedding | Batch job | Weekly | Offline |
| User age group | User profile | On change | Online |
Cross Features and Feature Interactions
Cross features are created to capture complex user-item relationships:
- User-Item Interaction Frequency:
- Computed as count of interactions within last 30 days
- Time Decay Weighted Features:
- Recent interactions weighted more heavily (exponential decay)
- User-Item Temporal Patterns:
- Hour of day when user typically interacts with item
Feature Importance and Selection Methodology
Statistical Methods:
- Chi-square test for categorical features
- ANOVA F-test for continuous features
Model-based Methods:
- SHAP values from gradient boosting model
- Permutation importance from trained models
Selection Strategy:
- Top 500 features retained for training
- Features with SHAP importance < 0.01 removed
- Cross-validation used to validate feature impact on AUC
import numpy as np
from sklearn.feature_selection import SelectKBest, chi2
class FeatureSelector:
def __init__(self, k=500):
self.k = k
self.selector = SelectKBest(score_func=chi2, k=k)
def fit_transform(self, X, y):
# X: (n_samples, n_features)
# y: (n_samples,)
return self.selector.fit_transform(X, y)
def get_support_mask(self):
return self.selector.get_support(indices=False)
# Example usage for user-item interaction features
def create_interaction_features(user_ids, item_ids, timestamps):
"""Create temporal and frequency-based interaction features"""
# Convert to numpy arrays
user_ids = np.array(user_ids)
item_ids = np.array(item_ids)
timestamps = np.array(timestamps)
# Frequency features
user_freq = np.bincount(user_ids)
item_freq = np.bincount(item_ids)
# Time decay features (exponential decay with half-life of 30 days)
time_diff = np.max(timestamps) - timestamps
decay_weights = np.exp(-time_diff / (30 * 24 * 60 * 60)) # seconds
return {
'user_frequency': user_freq[user_ids],
'item_frequency': item_freq[item_ids],
'time_decay_weight': decay_weights,
'recency_score': np.exp(-time_diff / (7 * 24 * 60 * 60)) # 1 week half-life
}
4. Model Architecture
Model selection with justification
For collaborative filtering, I recommend a Neural Matrix Factorization (NeuMF) model that combines matrix factorization with deep learning. This approach offers several advantages:
- Matrix Factorization (MF) - Provides strong baseline with interpretable latent factors
- Deep Neural Network (DNN) - Captures complex user-item interaction patterns
- Hybrid Approach - Combines linear and non-linear signal modeling
The model addresses the key challenges:
- Cold start: Through embedding representations that generalize across users/items
- Scalability: Through efficient matrix operations and distributed training
- Accuracy: Through non-linear interaction modeling beyond linear factorization
Architecture diagram
graph TD
A[User ID] --> B[User Embedding]
C[Item ID] --> D[Item Embedding]
B --> E[Merge]
D --> E
E --> F[MLP Layers]
G[MF Vector] --> H[Concatenate]
F --> H
H --> I[Output Layer]
I --> J[Rating/Score]
Two-tower / multi-task / ensemble design
I propose a two-tower architecture:
- User Tower: Processes user embeddings and features
- Item Tower: Processes item embeddings and features
- Interaction Layer: Computes dot product or MLP-based interaction
For multi-task learning, we can extend this to:
- Rating prediction (main task)
- Item recommendation (secondary task)
- User preference classification (auxiliary task)
Pre-trained model selection and fine-tuning strategy
For foundation model integration:
- Pre-trained embeddings: Use pre-trained item embeddings from large-scale collaborative filtering (e.g., via contrastive learning)
- Fine-tuning: Adapt to specific domain/user behavior patterns
- Transfer learning: Leverage pre-trained user/item representations as initialization
Model complexity vs latency trade-off analysis
| Model | Parameters | Inference Latency (ms) | Memory Usage (GB) |
|---|---|---|---|
| Matrix Factorization | 10M | 1-2 | 0.5 |
| Neural MF (32-64-32) | 50M | 5-10 | 2.0 |
| DeepFM | 200M | 15-25 | 8.0 |
| Transformer-based | 500M+ | 30-50 | 20+ |
Offline evaluation baseline framing
Baseline model: SVD++ (Matrix Factorization) Expected performance:
- AUC-ROC: ~0.85
- NDCG@10: ~0.35
- Precision@10: ~0.25
Proposed model (NeuMF):
- Target improvement: 15-20% over baseline
- Expected AUC-ROC: ~0.95+
- Expected NDCG@10: ~0.45+
Core model code snippet
import torch
import torch.nn as nn
import torch.nn.functional as F
class NeuralMatrixFactorization(nn.Module):
def __init__(self, num_users, num_items, embedding_dim=64, mlp_layers=[128, 64, 32]):
super(NeuralMatrixFactorization, self).__init__()
# Embedding layers
self.user_embedding = nn.Embedding(num_users, embedding_dim)
self.item_embedding = nn.Embedding(num_items, embedding_dim)
# MLP layers for user and item
mlp_layers = [embedding_dim * 2] + mlp_layers
self.mlp_layers = nn.ModuleList([
nn.Linear(mlp_layers[i], mlp_layers[i+1])
for i in range(len(mlp_layers) - 1)
])
# Final prediction layer
self.prediction_layer = nn.Linear(mlp_layers[-1] + embedding_dim, 1)
# Initialize weights
self._init_weights()
def _init_weights(self):
for m in self.modules():
if isinstance(m, nn.Embedding):
nn.init.normal_(m.weight, std=0.1)
elif isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
nn.init.constant_(m.bias, 0)
def forward(self, user_ids, item_ids):
# Get embeddings
user_emb = self.user_embedding(user_ids) # (batch_size, embedding_dim)
item_emb = self.item_embedding(item_ids) # (batch_size, embedding_dim)
# Concatenate for MLP
concat_emb = torch.cat([user_emb, item_emb], dim=1) # (batch_size, embedding_dim * 2)
# Pass through MLP layers
mlp_output = concat_emb
for layer in self.mlp_layers:
mlp_output = F.relu(layer(mlp_output))
# Combine with MF vector (dot product)
mf_vector = torch.sum(user_emb * item_emb, dim=1, keepdim=True) # (batch_size, 1)
# Final prediction
combined = torch.cat([mlp_output, mf_vector], dim=1)
output = self.prediction_layer(combined).squeeze()
return torch.sigmoid(output)
def compute_loss(self, user_ids, item_ids, ratings, loss_fn=None):
if loss_fn is None:
loss_fn = nn.BCELoss()
predictions = self.forward(user_ids, item_ids)
loss = loss_fn(predictions, ratings.float())
return loss
# Example usage:
# model = NeuralMatrixFactorization(num_users=10000, num_items=50000)
# loss = model.compute_loss(user_ids, item_ids, ratings)
This implementation provides:
- Scalable embedding architecture that can handle millions of users/items
- Modular design supporting various MLP configurations
- End-to-end training capability with standard PyTorch loss functions
- Production-ready inference with efficient forward pass computation
- Extensible design for multi-task learning and ensemble methods
5. Serving & Inference Architecture
System Diagram
graph TD
A[User Query] --> B[Load Balancer]
B --> C[Request Router]
C --> D[Feature Store]
C --> E[Embedding Cache]
D --> F[Model Serving]
E --> F
F --> G[Scoring Engine]
G --> H[Ranking Service]
H --> I[Response Formatter]
I --> J[Client Response]
subgraph "Online Serving Layer"
F
G
H
I
end
subgraph "Infrastructure"
D
E
end
Online vs Batch Serving Decision
Online serving is required for real-time recommendation generation with sub-100ms latency SLA. Batch serving would introduce unacceptable delays for user-facing experiences. The system must support interactive queries with minimal latency while maintaining high throughput.
Two-Stage Pipeline
1. Candidate Retrieval (Top-K)
- Approximate Nearest Neighbor Search
- Embedding-based similarity matching
2. Scoring & Re-ranking (Top-K)
- Matrix factorization scoring
- Context-aware re-ranking
Serving Infrastructure
Primary choice: TorchServe with custom embedding cache
TorchServe provides production-ready model serving with built-in support for PyTorch models and automatic batching. Custom embedding caching layer provides low-latency access to user/item embeddings.
Alternative consideration: Triton Inference Server
- Better support for multiple model formats
- More complex to configure for custom embedding caching
Latency Budget Breakdown
| Stage | p50 Latency | p99 Latency | Budget |
|---|---|---|---|
| Feature Fetch | 5ms | 15ms | 20ms |
| Embedding Lookup | 2ms | 5ms | 10ms |
| Scoring Computation | 3ms | 8ms | 15ms |
| Ranking & Re-ranking | 2ms | 6ms | 10ms |
| Response Formatting | 1ms | 3ms | 5ms |
| Total | 13ms | 44ms | 50ms |
Model Compression Strategy
Quantization: 8-bit quantization reduces model size by ~75% with <2% accuracy degradation for matrix factorization models.
Pruning: Structured pruning of low-importance factors (5-10% of dimensions) provides 20-30% size reduction with minimal impact.
Distillation: Knowledge distillation from larger models to smaller student models for faster inference.
Caching Strategy
Prediction Cache:
- LRU cache for recent user-item predictions
- TTL: 1 hour for high-frequency items, 24 hours for low-frequency
Embedding Cache:
- Redis-based cache for user/item embeddings
- Eviction policy: LFU with time-based decay
- Size: 10GB for typical user/item embedding matrices
KV Cache:
- Not applicable for matrix factorization (no sequential dependencies)
Auto-scaling and Capacity Planning
Horizontal Scaling:
- Auto-scaling groups based on QPS with target CPU utilization at 70%
- Minimum 5 serving instances, maximum 50 based on traffic patterns
Capacity Planning:
- Expected QPS: 10,000 requests/sec peak
- Each serving instance handles ~2,000 QPS
- 5-10% headroom for traffic spikes
Warm-up Strategy:
- Pre-warm embedding caches during deployment
- Load model artifacts to GPU memory in batches
class RecommendationServingEngine:
def __init__(self, model_path, embedding_cache):
self.model = torch.load(model_path)
self.embedding_cache = embedding_cache
self.model.eval()
def predict(self, user_ids, item_ids):
# Fetch embeddings from cache or database
user_embeddings = self._get_embeddings(user_ids, "user")
item_embeddings = self._get_embeddings(item_ids, "item")
# Matrix factorization prediction
scores = torch.matmul(user_embeddings, item_embeddings.T)
return scores
def _get_embeddings(self, ids, entity_type):
# Check cache first
cached = self.embedding_cache.get(entity_type, ids)
if cached is not None:
return cached
# Fallback to database lookup
embeddings = self._fetch_from_db(ids, entity_type)
self.embedding_cache.set(entity_type, ids, embeddings)
return embeddings
# Example usage
engine = RecommendationServingEngine("model.pt", RedisCache())
user_ids = [1001, 1002, 1003]
item_ids = [2001, 2002, 2003]
scores = engine.predict(user_ids, item_ids)
6. Evaluation Framework
Offline Evaluation Methodology
For offline evaluation of collaborative filtering models, we use a temporal split approach to ensure that training data only contains past interactions and testing data only includes future events. This prevents data leakage and mimics real-world deployment conditions where models must predict on unseen future user-item interactions.
We split data chronologically into:
- Training: 80% of oldest interactions
- Validation: 10% of middle-timeframe interactions
- Test: 10% of most recent interactions
Cross-validation is performed on the training set using time-based folds to ensure robustness across temporal trends.
Slice-Based Evaluation
We evaluate model performance across key user segments to detect potential bias or degradation:
| Slice | Definition | Critical Metrics |
|---|---|---|
| New Users | Users with < 5 interactions | Precision@K, Recall@K |
| New Items | Items with < 10 interactions | NDCG, MRR |
| High-Engagement Users | Top 20% by interaction count | CTR, Revenue Impact |
| Low-Engagement Users | Bottom 20% by interaction count | Coverage, Diversity |
| Mobile vs Desktop | Device type segmentation | Conversion rate, Session duration |
A/B Test Design
Randomization Unit: User ID (to prevent spill-over between users)
Sample Size Calculation:
- Target: 5% relative improvement in CTR
- Power: 80%
- Significance level: 5%
- Estimated baseline CTR: 2.5%
- Sample size per variant: ~12,000 users
Test Duration: 3 weeks (to capture weekly seasonality and ensure statistical significance)
Primary Metric: Click-Through Rate (CTR) on recommendations
Secondary Metrics:
- Revenue per user
- Session duration
- User retention (30-day)
- Diversity of recommendations
Guardrail Metrics and Blocking Conditions
Guardrail Metrics:
- Coverage: < 50% of items recommended → block launch
- Precision@K: Drop below 0.15 in any slice → block launch
- NDCG: Drop below 0.30 in top-K ranking → block launch
Blocking Conditions:
- If any slice shows > 10% degradation in key metrics
- If coverage drops below 30% of total item catalog
- If model exhibits systematic bias toward popular items (e.g., > 80% of recommendations are top-K items)
Shadow Mode Rollout Plan
Phase 1 (Week 1):
- 5% of traffic routed to shadow model
- All traffic served with baseline system (rule-based)
- Shadow model logs predictions for offline evaluation
Phase 2 (Week 2):
- 15% of traffic routed to shadow model
- Shadow model predictions used for offline evaluation only
- Baseline system continues serving all traffic
Phase 3 (Week 3):
- 50% of traffic routed to shadow model
- Shadow model predictions used for online evaluation (real-time CTR tracking)
- Baseline system continues serving remaining 50%
Long-Term Metric Measurement
Delayed Conversion Tracking:
- Measure revenue impact 7, 30, and 90 days post-recommendation
- Track conversion rate for users exposed to model recommendations vs control group
Engagement Decay Analysis:
- Monitor session frequency over 30-day period
- Track user re-engagement rate for users exposed to model recommendations vs control
User Lifetime Value (LTV) Tracking:
- Compare LTV of users exposed to model recommendations vs baseline
- Analyze cohort behavior patterns for long-term impact assessment
# Example A/B test evaluation function
def evaluate_ab_test_results(control_ctr, test_ctr, control_users, test_users):
"""
Evaluate A/B test results for CTR improvement
"""
import scipy.stats as stats
# Calculate confidence intervals
control_lower, control_upper = stats.norm.interval(
0.95, loc=control_ctr, scale=control_ctr * (1 - control_ctr) / control_users ** 0.5
)
test_lower, test_upper = stats.norm.interval(
0.95, loc=test_ctr, scale=test_ctr * (1 - test_ctr) / test_users ** 0.5
)
# Calculate relative improvement
relative_improvement = (test_ctr - control_ctr) / control_ctr
# Statistical significance test
z_score = (test_ctr - control_ctr) / ((control_ctr * (1 - control_ctr) / control_users +
test_ctr * (1 - test_ctr) / test_users) ** 0.5)
p_value = 2 * (1 - stats.norm.cdf(abs(z_score)))
return {
"relative_improvement": relative_improvement,
"confidence_interval": (control_lower, control_upper, test_lower, test_upper),
"statistical_significance": p_value < 0.05,
"p_value": p_value
}
7. Training Pipeline
Training Data Pipeline
The training pipeline begins with data ingestion from multiple sources including user-item interaction logs, item metadata, and user demographics. Data flows through a multi-stage ETL process:
- Raw Data Ingestion: Streaming from Kafka or Pub/Sub with 5-minute batching window
- Preprocessing: Deduplication, filtering invalid interactions, and timestamp normalization
- Shuffling: Distributed shuffling using Apache Spark or Ray to ensure data randomness for training stability
- Batching: Batching by user ID for efficient matrix factorization computation
import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np
class MatrixFactorizationDataset(Dataset):
def __init__(self, user_ids, item_ids, ratings, num_users, num_items):
self.user_ids = torch.tensor(user_ids, dtype=torch.long)
self.item_ids = torch.tensor(item_ids, dtype=torch.long)
self.ratings = torch.tensor(ratings, dtype=torch.float32)
self.num_users = num_users
self.num_items = num_items
def __len__(self):
return len(self.user_ids)
def __getitem__(self, idx):
return {
'user_id': self.user_ids[idx],
'item_id': self.item_ids[idx],
'rating': self.ratings[idx]
}
# Example usage for training data preparation
def prepare_training_data(interaction_df, val_ratio=0.1):
# Shuffle and split data
shuffled = interaction_df.sample(frac=1).reset_index(drop=True)
split_idx = int(len(shuffled) * (1 - val_ratio))
train_df = shuffled[:split_idx]
val_df = shuffled[split_idx:]
# Create dataset objects
train_dataset = MatrixFactorizationDataset(
train_df['user_id'].values,
train_df['item_id'].values,
train_df['rating'].values,
num_users=interaction_df['user_id'].max() + 1,
num_items=interaction_df['item_id'].max() + 1
)
return train_dataset, val_dataset
Distributed Training Strategy
For large-scale training, we implement data parallelism with model parallelism for embedding matrices:
- Data Parallelism: Split training data across multiple workers, each computing gradients independently
- Model Parallelism: For large embedding matrices, partition embeddings across parameter servers or use sharding strategies
- Pipeline Parallelism: For very large models, pipeline stages can be overlapped to hide communication costs
Training Infrastructure
- Hardware: 8x A100 GPUs or 4x H100 GPUs for distributed training
- Framework: PyTorch with DistributedDataParallel (DDP) or Ray Train for distributed training
- Training Time: With 10M interactions and 256 embedding dimension, expect ~30-45 minutes per epoch on 8 GPUs
- Memory Usage: Each GPU requires ~2GB for embedding matrices and gradients
Hyperparameter Tuning Approach
We use Bayesian optimization with the following key hyperparameters:
- Learning Rate: [0.001, 0.01] - log scale search
- Embedding Dimension: [64, 128, 256, 512] - discrete search
- Regularization Strength: [0.0001, 0.001, 0.01] - log scale search
- Batch Size: [256, 512, 1024] - discrete search
Experiment Tracking and Reproducibility
- Tool: MLflow for experiment tracking with parameter logging, model versioning, and metric storage
- Reproducibility: Fixed random seeds (torch, numpy, python) and deterministic training modes
- Version Control: Model checkpoints stored with metadata including epoch, loss, and validation metrics
Model Versioning and Registry Strategy
- Registry: MLflow model registry with stages: Staging → Production
- Versioning: Semantic versioning (v1.0.0, v1.1.0) with automatic rollback capability
- Deployment: Model artifacts stored in S3-compatible storage with metadata indexing
Continual / Online Learning Design
For real-time adaptation to new user/item interactions:
- Incremental Updates: Periodic retraining with recent data (last 30 days)
- Online Learning: SGD-based incremental updates for new interactions
- Cold Start Handling: New user/item embeddings initialized with pre-trained values or random initialization
- Replay Buffer: Store recent interactions for periodic retraining to prevent catastrophic forgetting
class OnlineMatrixFactorization:
def __init__(self, num_users, num_items, embedding_dim=128, lr=0.01):
self.user_embeddings = torch.nn.Embedding(num_users, embedding_dim)
self.item_embeddings = torch.nn.Embedding(num_items, embedding_dim)
self.optimizer = torch.optim.SGD([
{'params': self.user_embeddings.parameters()},
{'params': self.item_embeddings.parameters()}
], lr=lr)
def forward(self, user_ids, item_ids):
user_emb = self.user_embeddings(user_ids)
item_emb = self.item_embeddings(item_ids)
return torch.sum(user_emb * item_emb, dim=1)
def update(self, user_ids, item_ids, ratings):
self.optimizer.zero_grad()
predictions = self.forward(user_ids, item_ids)
loss = torch.mean((predictions - ratings) ** 2)
loss.backward()
self.optimizer.step()
return loss.item()
8. Monitoring & Observability
Data Drift Detection
Data drift detection is crucial for maintaining model performance in collaborative filtering systems. We implement drift detection using Population Stability Index (PSI) and Kullback-Leibler divergence for continuous features, and Chi-squared tests for categorical features.
import numpy as np
from scipy import stats
import pandas as pd
def calculate_psi(expected, actual, bins=10):
"""Calculate Population Stability Index for feature drift detection"""
expected_freq = np.histogram(expected, bins=bins)[0]
actual_freq = np.histogram(actual, bins=bins)[0]
# Avoid zero division
expected_freq = np.clip(expected_freq, 1e-6, None)
actual_freq = np.clip(actual_freq, 1e-6, None)
psi = np.sum((actual_freq - expected_freq) * np.log(actual_freq / expected_freq))
return psi
def detect_drift_features(current_features, historical_features, threshold=0.1):
"""Detect drift across feature dimensions"""
drift_results = {}
for feature_name in current_features.columns:
if feature_name in historical_features.columns:
psi_value = calculate_psi(
historical_features[feature_name].values,
current_features[feature_name].values
)
drift_results[feature_name] = {
'psi': psi_value,
'drift_detected': psi_value > threshold
}
return drift_results
Prediction Drift and Label Drift Monitoring
Prediction drift is monitored by comparing model output distributions over time, while label drift monitors shifts in the underlying target distribution.
from sklearn.metrics import mean_squared_error
import numpy as np
class PredictionDriftMonitor:
def __init__(self, drift_threshold=0.1):
self.drift_threshold = drift_threshold
self.reference_predictions = []
def detect_prediction_drift(self, current_predictions):
"""Detect drift in model predictions"""
if not self.reference_predictions:
self.reference_predictions = current_predictions
return False
# Compare distribution of predictions using KL divergence
reference_dist = np.histogram(self.reference_predictions, bins=50)[0]
current_dist = np.histogram(current_predictions, bins=50)[0]
# Normalize distributions
reference_dist = reference_dist / np.sum(reference_dist)
current_dist = current_dist / np.sum(current_dist)
# KL divergence
kl_div = np.sum(reference_dist * np.log(reference_dist / (current_dist + 1e-8)))
return kl_div > self.drift_threshold
class LabelDriftMonitor:
def __init__(self, threshold=0.15):
self.threshold = threshold
self.reference_labels = []
def detect_label_drift(self, current_labels):
"""Detect drift in label distribution"""
if not self.reference_labels:
self.reference_labels = current_labels
return False
# Chi-squared test for categorical label drift
ref_counts = np.bincount(self.reference_labels)
current_counts = np.bincount(current_labels)
# Pad to same length
max_len = max(len(ref_counts), len(current_counts))
ref_counts = np.pad(ref_counts, (0, max_len - len(ref_counts)), mode='constant')
current_counts = np.pad(current_counts, (0, max_len - len(current_counts)), mode='constant')
chi2_stat, p_value = stats.chisquare(ref_counts, current_counts)
return p_value < 0.05 # Significant drift
Model Performance Degradation Alerting
Model performance degradation is monitored using key metrics with alert thresholds and escalation procedures.
class ModelPerformanceMonitor:
def __init__(self, alert_thresholds):
self.alert_thresholds = alert_thresholds # {metric: threshold}
self.metrics_history = []
def evaluate_performance(self, predictions, ground_truth):
"""Calculate performance metrics"""
mse = mean_squared_error(ground_truth, predictions)
rmse = np.sqrt(mse)
# Add to history
self.metrics_history.append({
'timestamp': pd.Timestamp.now(),
'rmse': rmse,
'mse': mse
})
return {'rmse': rmse, 'mse': mse}
def check_alerts(self, current_metrics):
"""Check if any metrics exceed thresholds"""
alerts = []
for metric_name, threshold in self.alert_thresholds.items():
if current_metrics.get(metric_name, 0) > threshold:
alerts.append({
'metric': metric_name,
'value': current_metrics[metric_name],
'threshold': threshold,
'alert_level': 'HIGH' if current_metrics[metric_name] > threshold * 1.5 else 'MEDIUM'
})
return alerts
Retraining Trigger Strategy
Retraining is triggered based on drift detection, performance degradation, and data volume thresholds.
class RetrainingTrigger:
def __init__(self, drift_threshold=0.1, performance_threshold=0.05, data_volume_threshold=10000):
self.drift_threshold = drift_threshold
self.performance_threshold = performance_threshold
self.data_volume_threshold = data_volume_threshold
def should_retrain(self, drift_results, performance_metrics, new_data_count):
"""Determine if model should be retrained"""
# Check drift in key features
drift_detected = any(
result['drift_detected']
for result in drift_results.values()
)
# Check performance degradation
performance_degraded = any(
metric > self.performance_threshold
for metric in performance_metrics.values()
)
# Check data volume
data_volume_exceeded = new_data_count > self.data_volume_threshold
return drift_detected or performance_degraded or data_volume_exceeded
Logging Design
Comprehensive logging is implemented for prediction, feature, and feedback collection.
import json
from datetime import datetime
class PredictionLogger:
def __init__(self, log_file_path):
self.log_file_path = log_file_path
def log_prediction(self, user_id, item_id, prediction, features, timestamp=None):
"""Log model predictions with context"""
log_entry = {
'timestamp': timestamp or datetime.now().isoformat(),
'user_id': user_id,
'item_id': item_id,
'prediction': float(prediction),
'features': features,
'log_type': 'prediction'
}
with open(self.log_file_path, 'a') as f:
f.write(json.dumps(log_entry) + '\n')
def log_feedback(self, user_id, item_id, rating, timestamp=None):
"""Log feedback for continuous learning"""
log_entry = {
'timestamp': timestamp or datetime.now().isoformat(),
'user_id': user_id,
'item_id': item_id,
'rating': float(rating),
'log_type': 'feedback'
}
with open(self.log_file_path, 'a') as f:
f.write(json.dumps(log_entry) + '\n')
Feedback Loop for Continuous Improvement
Feedback collection and model improvement pipeline.
class FeedbackLoop:
def __init__(self, feedback_collection_interval_hours=24):
self.feedback_collection_interval = feedback_collection_interval_hours
self.pending_feedback = []
def collect_feedback(self, user_id, item_id, rating):
"""Collect feedback for model improvement"""
self.pending_feedback.append({
'user_id': user_id,
'item_id': item_id,
'rating': rating,
'timestamp': datetime.now()
})
def process_feedback(self):
"""Process feedback and update model training data"""
if len(self.pending_feedback) > 0:
# In production, this would trigger retraining or online learning
processed_count = len(self.pending_feedback)
self.pending_feedback.clear()
return processed_count
return 0
Key Metrics and Alerting Thresholds
| Metric | Alert Level | Threshold | Monitoring Frequency |
|---|---|---|---|
| RMSE | HIGH | > 0.5 | Every 1 hour |
| PSI (user features) | MEDIUM | > 0.1 | Every 6 hours |
| PSI (item features) | MEDIUM | > 0.1 | Every 6 hours |
| Prediction drift | MEDIUM | > 0.1 | Every 2 hours |
| Label drift | HIGH | p-value < 0.05 | Every 12 hours |
| Data volume | MEDIUM | > 10k new samples | Every 24 hours |
Observability Dashboard Components
- Real-time performance metrics (RMSE, MSE)
- Drift detection alerts
- Feature distribution monitoring
- Model version and retraining status
- Data quality indicators (missing values, outliers)
- User engagement and feedback volume
Alert Escalation Policy
-
LOW: Non-critical metric degradation (RMSE > threshold but < 2x)
- Email notification to ML team
- Log in monitoring system
-
MEDIUM: Feature drift or moderate performance degradation
- Email + Slack alert to ML team
- Automatic retraining trigger if drift > threshold
-
HIGH: Severe performance degradation or label drift
- Email + Slack + PagerDuty alert
- Immediate model rollback to previous version
- Manual intervention required
Data Quality Monitoring
- Missing feature values detection (threshold: >5% of samples)
- Outlier detection in user/item embeddings (Z-score > 3)
- Consistency checks for rating scale (0-5 range)
- Duplicate user-item pairs detection
Retraining Pipeline Integration
The monitoring system integrates with the retraining pipeline to automatically trigger model updates when drift or performance thresholds are exceeded, ensuring continuous model freshness and accuracy.
9. Responsible AI & Safety
Bias Analysis
Training data may exhibit demographic bias (e.g., overrepresentation of certain user demographics or item categories), leading to unfair recommendations for underrepresented groups. We will conduct a training data audit to identify imbalances in user and item distributions across different slices (age groups, geographic regions, device types). Slice-based fairness evaluation will be performed to detect and mitigate bias in recommendations.
Fairness Metrics and Constraints
We will implement demographic parity as our primary fairness constraint, ensuring that the probability of recommending an item is similar across different user groups. Equalized odds will also be monitored to ensure that true positive rates are consistent across slices. These constraints will be enforced through fairness-aware loss functions during training.
Explainability Requirements and Approach
For business stakeholders, we will implement attention-based explainability for our two-tower model to show which user and item embeddings contribute most to a recommendation. For end-users, we will provide simple "Why this recommendation?" explanations based on shared user preferences or item similarity scores. SHAP values will be used for internal model interpretability.
For LLM Systems: Safety Guardrails
If this system is extended to support LLM-based recommendation generation, we will implement:
- Content filtering to prevent harmful or inappropriate item recommendations
- Output validation to ensure generated content aligns with platform policies
- Rate limiting to prevent abuse of recommendation APIs
- Red teaming exercises to identify edge cases and potential safety issues
Model Card Documentation Plan
We will maintain a model card documenting:
- Training data sources and preprocessing steps
- Model architecture and hyperparameters used
- Performance metrics across different user segments
- Known limitations and bias considerations
- Fairness constraints and validation results
Incident Response Plan for Model Failures or Harmful Outputs
In case of model degradation or harmful recommendations:
- Immediate alerting when key metrics (e.g., CTR, NDCG) drop below thresholds
- Automatic rollback to previous stable model version
- Manual review of flagged recommendations by human reviewers
- Temporary suspension of recommendations for affected user segments
- Post-mortem analysis to identify root causes and implement fixes
# Example fairness-aware loss function for collaborative filtering
import torch
import torch.nn as nn
class FairnessAwareMF(nn.Module):
def __init__(self, num_users, num_items, embedding_dim=128):
super().__init__()
self.user_embeddings = nn.Embedding(num_users, embedding_dim)
self.item_embeddings = nn.Embedding(num_items, embedding_dim)
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, user_ids, item_ids):
user_emb = self.user_embeddings(user_ids)
item_emb = self.item_embeddings(item_ids)
return torch.sum(user_emb * item_emb, dim=1) + self.bias
def fairness_loss(self, user_ids, item_ids, protected_groups):
"""
Add fairness constraint to loss function
"""
predictions = self.forward(user_ids, item_ids)
# Calculate group-wise mean predictions
unique_groups = torch.unique(protected_groups)
group_means = []
for group in unique_groups:
mask = (protected_groups == group)
if mask.sum() > 0:
group_mean = predictions[mask].mean()
group_means.append(group_mean)
# Fairness loss: penalize large differences between group means
if len(group_means) > 1:
mean_group = torch.stack(group_means).mean()
fairness_loss = sum((gm - mean_group)**2 for gm in group_means)
return predictions, fairness_loss
return predictions, torch.tensor(0.0)
10. Scalability & Reliability
Bottleneck Analysis per Pipeline Stage
Training Pipeline:
- Data processing: 20% of training time spent on data shuffling and preprocessing
- Model training: 70% of training time spent on matrix factorization computation (SVD, ALS)
- Model serialization: 10% of training time spent on model checkpointing and saving
Serving Pipeline:
- Feature lookup: 30% of request latency (embedding lookups, feature computation)
- Model inference: 60% of request latency (matrix multiplication, scoring)
- Response formatting: 10% of request latency
Back-of-envelope estimates:
- Training data size: 100M user-item interactions → ~5GB sparse matrix
- Number of features: 10K latent factors per user/item embedding
- Memory footprint for embedding matrices: 2 × 10M users × 10K factors × 4 bytes = 800MB per embedding matrix
- Serving QPS: 10K requests/sec → 100ms p99 latency target (30ms feature lookup + 60ms model inference)
Scale-up Strategy
Horizontal Scaling of Serving:
- Use distributed model serving (e.g., TorchServe with Ray or Triton Inference Server)
- Implement sharding of user/item embeddings across multiple model servers
- Use consistent hashing for embedding lookup distribution
Distributed Feature Computation:
- Deploy feature store with distributed cache (Redis or Memcached)
- Implement batched embedding lookups to reduce per-request overhead
- Use feature caching with TTL (e.g., 1 hour) for frequently accessed features
Fault Tolerance
Model Serving Fallbacks:
- Implement graceful degradation to rule-based fallback when model fails
- Use circuit breaker pattern for model health monitoring (e.g., 50% failure rate triggers fallback)
- Maintain warm standby model servers for high availability
Stale Model Serving on Failure:
- Implement model version rollback capability (e.g., last known good model)
- Use model versioning with automatic rollback on performance degradation
- Cache model metadata and availability status for quick failover decision
Cold Start Handling Strategy
New Users:
- Use content-based features (user demographics, preferences) for initial recommendations
- Implement hybrid approach: content-based + collaborative filtering with low confidence threshold
- Fallback to popular items for users with no interaction history
New Items:
- Use item metadata (category, tags) for initial ranking
- Implement content-based filtering with item embeddings from pre-trained models
- Use popularity-based ranking as fallback for items with no user interactions
Multi-region Serving Strategy
Global Distribution:
- Deploy model servers in multiple regions (US, EU, Asia) for low-latency serving
- Use CDN for static embeddings and model artifacts
- Implement region-aware routing with latency-based load balancing
Cross-region Consistency:
- Use eventual consistency model for feature updates across regions
- Implement conflict resolution strategy for concurrent updates (last-write-wins or merge strategies)
- Maintain global feature store with region-specific read replicas for low-latency access
Regional Failover:
- Implement automatic failover to nearest region when primary region experiences outages
- Use health checks and latency monitoring to dynamically adjust routing
- Maintain consistent model versions across regions with automated sync mechanisms
# Example: Multi-region model serving fallback logic
class MultiRegionServing:
def __init__(self, regions):
self.regions = regions
self.health_checks = {}
def get_model_for_region(self, region):
# Check if region is healthy
if self.health_checks.get(region, True):
return f"model-{region}"
else:
# Fallback to nearest healthy region or rule-based
return self.fallback_model()
def fallback_model(self):
# Rule-based fallback or popular items
return "rule-based-recommender"
11. Open Questions, Risks & Trade-offs
Decisions requiring product / engineering / policy alignment
- User privacy vs. model performance: Should we collect and use sensitive user behavior signals (e.g., time spent, scroll depth) that may raise privacy concerns?
- Cold start handling strategy: How should we treat new users/items with no historical interaction data? Should we fall back to content-based features or popularity-based ranking?
- Cross-platform consistency: How do we handle user behavior across platforms (mobile vs web) when building a unified recommendation system?
- Content moderation integration: Should we integrate content safety filters directly into the model or as a pre/post-processing step?
Key ML risks
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Data drift in user/item embeddings | Performance degradation | High | Implement online drift detection, automatic retraining |
| Cold start for new users/items | Poor recommendation quality | Medium | Implement hybrid approach with content-based features |
| Model overfitting to popular items | Bias toward high-frequency items | Medium | Implement negative sampling, popularity-based regularization |
| Label noise in implicit feedback | Misleading signal propagation | Medium | Use robust loss functions, data cleaning pipelines |
| Scalability bottleneck in serving | Latency violations | Medium | Implement two-stage pipeline, caching strategies |
| Model staleness due to delayed feedback | Outdated recommendations | High | Implement shadow mode, frequent model updates |
Trade-offs made with justification
- Accuracy vs. latency: Prioritizing sub-100ms response times over marginal accuracy gains by using model compression and pre-computed embeddings
- Freshness vs. cost: Accepting 1-hour embedding updates instead of real-time to reduce infrastructure costs and training overhead
- Model size vs. quality: Choosing a 200-dimension embedding model over 500-dimension to balance serving cost and recommendation quality
- Online vs. batch training: Using batch training for stability over online learning to avoid training instability from noisy real-time data
Alternatives considered and rejected
- Alternative: Matrix Factorization vs. Neural Collaborative Filtering: MF was chosen for its simplicity, interpretability, and strong baseline performance over complex neural approaches that require more data and compute
- Alternative: Real-time embeddings vs. pre-computed: Real-time embeddings were rejected due to high latency and infrastructure cost, favoring batch-processed embeddings with TTL caching
- Alternative: Content-based + Collaborative hybrid vs. pure collaborative: Pure collaborative was chosen to avoid feature engineering overhead and maintain focus on user-item interaction patterns
- Alternative: Deep learning vs. traditional ML: Traditional ML (e.g., logistic regression with embeddings) was chosen for better explainability and faster training compared to deep learning models
Technical debt acknowledged
- Feature engineering debt: Current feature set is limited to basic user/item interaction counts, with no rich temporal or contextual features implemented yet
- Model versioning debt: No formal model versioning strategy in place, which could lead to serving of outdated models in production
- Offline vs. online evaluation gap: The offline metrics may not fully capture real-world user engagement, requiring ongoing alignment between offline and online evaluation
- Cold start handling: Current fallback strategy is basic, requiring future enhancement with more sophisticated content-based or demographic approaches
12. Phased Implementation Roadmap
Phase 1: MVP (Weeks 1-4)
Objective: Establish baseline system with rule-based fallback and simple collaborative filtering model
Deliverables:
- Rule-based recommendation engine (popularity-based, user-item co-occurrence)
- Basic matrix factorization model using SVD (scikit-learn or Surprise)
- Simple online serving infrastructure (Flask/FAST API with in-memory cache)
- Basic A/B testing framework
Dependencies:
- Data pipeline ready (user-item interaction data)
- Feature store MVP (basic user/item metadata)
Critical Path:
- Data ingestion pipeline → 2. Rule-based baseline model → 3. Simple SVD model training → 4. Basic serving layer
Phase 2: Scale (Weeks 5-8)
Objective: Enable distributed training, feature store integration, and robust A/B testing
Deliverables:
- Feature store implementation (Redis + PostgreSQL)
- Distributed training pipeline using Ray or PyTorch DDP
- Real-time feature computation (user/item embeddings, popularity scores)
- Production-grade A/B testing infrastructure (with proper randomization unit)
- Model versioning and registry (MLflow or custom)
Dependencies:
- Feature store operational
- Distributed training infrastructure provisioned
- Data quality monitoring in place
Critical Path:
- Feature store + real-time computation → 2. Distributed training pipeline → 3. A/B testing infrastructure → 4. Model registry
Phase 3: Excellence (Weeks 9-12)
Objective: Advanced model architectures, continuous learning, and performance optimization
Deliverables:
- Deep learning-based matrix factorization (Neural Collaborative Filtering)
- Online learning capability (incremental model updates)
- Advanced serving stack with caching and model compression
- Real-time user behavior tracking and embedding updates
- Continuous model retraining pipeline with drift detection
Dependencies:
- Production A/B testing in place
- Monitoring and alerting system operational
- Feature store with proper TTL and freshness guarantees
Critical Path:
- Advanced model training → 2. Online learning implementation → 3. Production serving optimization → 4. Continuous retraining pipeline
Technical Dependencies & Risk Mitigation:
- Data pipeline stability: Ensure data freshness and quality before model training
- Feature store reliability: Implement proper TTL and cache warming strategies
- Model performance degradation: Establish guardrail metrics for A/B tests
- Scalability concerns: Design with horizontal scaling in mind from Phase 1
Success Metrics:
- Phase 1: Baseline model achieves 15% lift over rule-based system
- Phase 2: Distributed training reduces training time by 70%
- Phase 3: Online learning enables 20% improvement in user engagement over 30-day period
Resource Allocation:
- Phase 1: 2 engineers, 1 ML engineer, 1 DevOps engineer
- Phase 2: 3 engineers, 2 ML engineers, 1 DevOps engineer
- Phase 3: 4 engineers, 2 ML engineers, 1 DevOps engineer, 1 data scientist
Rollout Strategy:
- Shadow mode for Phase 3 models (1 week)
- Gradual rollout to 5% → 25% → 100% user population
- Real-time monitoring and rollback capability for each phase