Master advanced ensemble techniques including multi-level stacking, meta-learner selection, and blending strategies. Learn to combine diverse models for superior predictive performance.
Stacking (Stacked Generalization) and blending represent the pinnacle of ensemble learning, consistently producing winning solutions in machine learning competitions. While bagging and boosting combine similar models, stacking combines diverse models with different strengths, using a meta-learner to optimally weight their predictions. This two-level (or multi-level) architecture captures complementary patterns that single-level ensembles miss.
The power of stacking lies in its ability to leverage model diversity. A Random Forest might excel at capturing non-linear interactions, while Logistic Regression provides well-calibrated probabilities, and Gradient Boosting excels at precise predictions. Stacking learns how to optimally combine these strengths, often achieving performance gains of 1-5% over the best individual model—a massive improvement in competitive settings.
In this comprehensive lesson, you'll master both stacking and blending techniques, understanding when to use each, how to prevent overfitting through proper cross-validation, selecting optimal meta-learners, and implementing production-grade systems. You'll learn the critical difference between stacking (using out-of-fold predictions) and blending (using hold-out predictions), and how to build multi-level ensembles that consistently outperform single models.
Stacking creates a two-level (or multi-level) ensemble where base models' predictions become features for a meta-learner. Understanding this architecture prevents overfitting and enables optimal combination.
Basic Stacking Architecture:
1Level 0 (Base Models):
2 Model 1 (e.g., Random Forest)
3 Model 2 (e.g., Gradient Boosting) → Predictions → Level 1
4 Model 3 (e.g., Neural Network) (Meta-Learner)
5 Model 4 (e.g., SVM) → Final Prediction
6
7Training Flow:
81. Train base models on training data
92. Generate predictions (meta-features)
103. Train meta-learner on meta-features
114. Final prediction from meta-learnerKey Insight: Out-of-Fold Predictions
The critical challenge: If base models predict on their own training data, the meta-learner will overfit to their mistakes. Solution: Use out-of-fold (OOF) predictions.
Out-of-Fold Prediction Strategy:
1from sklearn.model_selection import KFold
2import numpy as np
3
4def get_oof_predictions(model, X_train, y_train, X_test, n_folds=5):
5 """
6 Generate out-of-fold predictions for stacking.
7
8 Returns:
9 oof_train: Out-of-fold predictions on training set
10 oof_test: Averaged predictions on test set
11 """
12 kf = KFold(n_splits=n_folds, shuffle=True, random_state=42)
13
14 oof_train = np.zeros(len(X_train))
15 oof_test = np.zeros(len(X_test))
16 oof_test_skf = np.zeros((len(X_test), n_folds))
17
18 for fold, (train_idx, val_idx) in enumerate(kf.split(X_train)):
19 # Split data
20 X_tr = X_train[train_idx]
21 y_tr = y_train[train_idx]
22 X_val = X_train[val_idx]
23
24 # Clone model for this fold
25 model_clone = clone(model)
26
27 # Train on fold
28 model_clone.fit(X_tr, y_tr)
29
30 # Predict on validation fold (out-of-fold)
31 oof_train[val_idx] = model_clone.predict_proba(X_val)[:, 1]
32
33 # Predict on test set
34 oof_test_skf[:, fold] = model_clone.predict_proba(X_test)[:, 1]
35
36 # Average test predictions across folds
37 oof_test = oof_test_skf.mean(axis=1)
38
39 return oof_train, oof_testComplete Stacking Implementation:
1from sklearn.model_selection import KFold
2from sklearn.base import clone
3from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
4from sklearn.linear_model import LogisticRegression
5from sklearn.svm import SVC
6import numpy as np
7
8class StackingClassifier:
9 """Complete stacking implementation with OOF predictions"""
10
11 def __init__(self, base_models, meta_model, n_folds=5):
12 self.base_models = base_models
13 self.meta_model = meta_model
14 self.n_folds = n_folds
15 self.fitted_base_models = []
16
17 def fit(self, X, y):
18 """Fit stacking ensemble"""
19 # Store meta-features
20 meta_features = np.zeros((len(X), len(self.base_models)))
21
22 # For each base model
23 for i, model in enumerate(self.base_models):
24 print(f"Training base model {i+1}/{len(self.base_models)}: {model.__class__.__name__}")
25
26 kf = KFold(n_splits=self.n_folds, shuffle=True, random_state=42)
27 oof_predictions = np.zeros(len(X))
28 fold_models = []
29
30 # OOF prediction for this model
31 for fold, (train_idx, val_idx) in enumerate(kf.split(X)):
32 X_tr, X_val = X[train_idx], X[val_idx]
33 y_tr = y[train_idx]
34
35 # Clone and train
36 model_clone = clone(model)
37 model_clone.fit(X_tr, y_tr)
38 fold_models.append(model_clone)
39
40 # OOF prediction
41 if hasattr(model_clone, 'predict_proba'):
42 oof_predictions[val_idx] = model_clone.predict_proba(X_val)[:, 1]
43 else:
44 oof_predictions[val_idx] = model_clone.decision_function(X_val)
45
46 # Store OOF predictions as meta-features
47 meta_features[:, i] = oof_predictions
48
49 # Store fitted models for this base model
50 self.fitted_base_models.append(fold_models)
51
52 # Train meta-model on meta-features
53 print(f"Training meta-model: {self.meta_model.__class__.__name__}")
54 self.meta_model.fit(meta_features, y)
55
56 return self
57
58 def predict_proba(self, X):
59 """Predict using stacking ensemble"""
60 # Generate meta-features from base models
61 meta_features = np.zeros((len(X), len(self.base_models)))
62
63 for i, fold_models in enumerate(self.fitted_base_models):
64 # Average predictions across folds
65 fold_predictions = np.zeros((len(X), len(fold_models)))
66
67 for fold, model in enumerate(fold_models):
68 if hasattr(model, 'predict_proba'):
69 fold_predictions[:, fold] = model.predict_proba(X)[:, 1]
70 else:
71 fold_predictions[:, fold] = model.decision_function(X)
72
73 meta_features[:, i] = fold_predictions.mean(axis=1)
74
75 # Meta-model prediction
76 return self.meta_model.predict_proba(meta_features)
77
78 def predict(self, X):
79 return (self.predict_proba(X)[:, 1] > 0.5).astype(int)
80
81
82# Example usage
83base_models = [
84 RandomForestClassifier(n_estimators=100, random_state=42),
85 GradientBoostingClassifier(n_estimators=100, random_state=42),
86 SVC(probability=True, random_state=42)
87]
88
89meta_model = LogisticRegression()
90
91stacker = StackingClassifier(base_models, meta_model, n_folds=5)
92stacker.fit(X_train, y_train)
93predictions = stacker.predict(X_test)Why Out-of-Fold Matters:
Without OOF:
With OOF:
Best Practices:
The meta-learner combines base model predictions into the final prediction. Choosing and optimizing the right meta-learner is crucial for stacking success.
Meta-Learner Options:
1. Linear Models (Most Common):
Logistic Regression:
1from sklearn.linear_model import LogisticRegression
2
3meta_model = LogisticRegression(
4 C=1.0, # Regularization strength (lower = more regularization)
5 penalty='l2', # 'l1', 'l2', 'elasticnet', 'none'
6 solver='lbfgs', # 'lbfgs', 'liblinear', 'saga'
7 max_iter=1000,
8 random_state=42
9)Ridge Regression:
1from sklearn.linear_model import Ridge
2
3meta_model = Ridge(
4 alpha=1.0, # Regularization strength
5 random_state=42
6)2. Tree-Based Meta-Learners:
Decision Tree:
1from sklearn.tree import DecisionTreeClassifier
2
3meta_model = DecisionTreeClassifier(
4 max_depth=3, # Shallow tree to prevent overfitting
5 min_samples_split=20,
6 min_samples_leaf=10,
7 random_state=42
8)Random Forest (Meta-Forest):
1from sklearn.ensemble import RandomForestClassifier
2
3meta_model = RandomForestClassifier(
4 n_estimators=100,
5 max_depth=3, # Shallow trees
6 min_samples_split=20,
7 random_state=42
8)3. Gradient Boosting Meta-Learner:
XGBoost/LightGBM:
1import xgboost as xgb
2
3meta_model = xgb.XGBClassifier(
4 max_depth=2, # Very shallow
5 n_estimators=50, # Few trees
6 learning_rate=0.1,
7 subsample=0.8,
8 colsample_bytree=0.8,
9 random_state=42
10)4. Neural Network Meta-Learner:
MLPClassifier:
1from sklearn.neural_network import MLPClassifier
2
3meta_model = MLPClassifier(
4 hidden_layer_sizes=(10,), # Small network
5 activation='relu',
6 alpha=0.01, # L2 regularization
7 max_iter=1000,
8 random_state=42
9)Selecting the Optimal Meta-Learner:
1from sklearn.model_selection import cross_val_score
2from sklearn.linear_model import LogisticRegression, Ridge
3from sklearn.tree import DecisionTreeClassifier
4from sklearn.ensemble import RandomForestClassifier
5import xgboost as xgb
6
7def compare_meta_learners(meta_features, y, cv=5):
8 """Compare different meta-learners"""
9
10 meta_models = {
11 'Logistic Regression': LogisticRegression(max_iter=1000),
12 'Ridge': Ridge(),
13 'Decision Tree': DecisionTreeClassifier(max_depth=3),
14 'Random Forest': RandomForestClassifier(n_estimators=50, max_depth=3),
15 'XGBoost': xgb.XGBClassifier(max_depth=2, n_estimators=50)
16 }
17
18 results = {}
19
20 for name, model in meta_models.items():
21 scores = cross_val_score(
22 model, meta_features, y,
23 cv=cv,
24 scoring='roc_auc',
25 n_jobs=-1
26 )
27 results[name] = {
28 'mean': scores.mean(),
29 'std': scores.std()
30 }
31 print(f"{name:20s}: {scores.mean():.4f} (+/- {scores.std():.4f})")
32
33 # Return best meta-learner
34 best_name = max(results, key=lambda k: results[k]['mean'])
35 return meta_models[best_name]
36
37# Usage
38best_meta = compare_meta_learners(meta_features_train, y_train)Meta-Learner Feature Engineering:
1. Include Original Features:
1# Combine meta-features with original features
2meta_features_enhanced = np.hstack([
3 meta_features, # Base model predictions
4 X_train # Original features
5])
6
7meta_model.fit(meta_features_enhanced, y_train)2. Feature Interactions:
1from sklearn.preprocessing import PolynomialFeatures
2
3# Add interaction terms between base model predictions
4poly = PolynomialFeatures(degree=2, include_bias=False)
5meta_features_poly = poly.fit_transform(meta_features)
6
7meta_model.fit(meta_features_poly, y_train)3. Prediction Statistics:
1# Add statistics across base model predictions
2meta_features_stats = np.column_stack([
3 meta_features,
4 meta_features.mean(axis=1), # Average prediction
5 meta_features.std(axis=1), # Prediction variance
6 meta_features.min(axis=1), # Min prediction
7 meta_features.max(axis=1) # Max prediction
8])Regularization for Meta-Learners:
L1/L2 Regularization:
1from sklearn.linear_model import LogisticRegressionCV
2
3# Automatic regularization tuning
4meta_model = LogisticRegressionCV(
5 Cs=10, # Test 10 regularization values
6 cv=5,
7 penalty='l2',
8 scoring='roc_auc',
9 random_state=42
10)Feature Selection:
1from sklearn.feature_selection import SelectFromModel
2from sklearn.linear_model import LassoCV
3
4# Select most important base models
5selector = SelectFromModel(LassoCV(cv=5))
6selector.fit(meta_features, y_train)
7meta_features_selected = selector.transform(meta_features)Best Practices:
Common Mistakes:
Proper cross-validation is critical for stacking to prevent overfitting and produce reliable performance estimates. Understanding nested CV and stratified folding ensures robust ensembles.
The Overfitting Challenge:
Stacking has two levels where overfitting can occur:
Solution: Nested Cross-Validation
Nested CV Architecture:
1Outer CV (Model Evaluation):
2 Fold 1: Test Set
3 Inner CV (Stacking):
4 Train base models with 5-fold CV
5 Generate OOF predictions
6 Train meta-model
7 Evaluate on Fold 1
8
9 Fold 2: Test Set
10 Inner CV (Stacking):
11 Train base models with 5-fold CV
12 Generate OOF predictions
13 Train meta-model
14 Evaluate on Fold 2
15
16 ... (Repeat for all folds)
17
18Final Performance: Average across outer foldsImplementing Nested Cross-Validation:
1from sklearn.model_selection import KFold, cross_val_score
2from sklearn.base import clone
3import numpy as np
4
5class NestedStackingCV:
6 """Nested cross-validation for stacking with proper evaluation"""
7
8 def __init__(self, base_models, meta_model,
9 outer_cv=5, inner_cv=5):
10 self.base_models = base_models
11 self.meta_model = meta_model
12 self.outer_cv = outer_cv
13 self.inner_cv = inner_cv
14
15 def evaluate(self, X, y, scoring='roc_auc'):
16 """
17 Evaluate stacking with nested CV.
18
19 Returns unbiased performance estimate.
20 """
21 outer_kf = KFold(n_splits=self.outer_cv,
22 shuffle=True, random_state=42)
23
24 outer_scores = []
25
26 for outer_fold, (train_idx, test_idx) in enumerate(outer_kf.split(X)):
27 print(f"
28Outer Fold {outer_fold + 1}/{self.outer_cv}")
29
30 # Outer split
31 X_train_outer = X[train_idx]
32 y_train_outer = y[train_idx]
33 X_test_outer = X[test_idx]
34 y_test_outer = y[test_idx]
35
36 # Inner stacking with CV
37 meta_features_train = self._get_meta_features(
38 X_train_outer, y_train_outer
39 )
40 meta_features_test = self._get_meta_features_test(
41 X_train_outer, y_train_outer, X_test_outer
42 )
43
44 # Train meta-model on inner OOF predictions
45 meta_model_clone = clone(self.meta_model)
46 meta_model_clone.fit(meta_features_train, y_train_outer)
47
48 # Evaluate on outer test fold
49 if scoring == 'roc_auc':
50 from sklearn.metrics import roc_auc_score
51 y_pred = meta_model_clone.predict_proba(meta_features_test)[:, 1]
52 score = roc_auc_score(y_test_outer, y_pred)
53 else:
54 from sklearn.metrics import accuracy_score
55 y_pred = meta_model_clone.predict(meta_features_test)
56 score = accuracy_score(y_test_outer, y_pred)
57
58 outer_scores.append(score)
59 print(f" Fold Score: {score:.4f}")
60
61 mean_score = np.mean(outer_scores)
62 std_score = np.std(outer_scores)
63
64 print(f"
65Nested CV Score: {mean_score:.4f} (+/- {std_score:.4f})")
66
67 return mean_score, std_score
68
69 def _get_meta_features(self, X, y):
70 """Generate OOF meta-features using inner CV"""
71 inner_kf = KFold(n_splits=self.inner_cv,
72 shuffle=True, random_state=42)
73
74 meta_features = np.zeros((len(X), len(self.base_models)))
75
76 for i, model in enumerate(self.base_models):
77 oof_preds = np.zeros(len(X))
78
79 for train_idx, val_idx in inner_kf.split(X):
80 X_tr, X_val = X[train_idx], X[val_idx]
81 y_tr = y[train_idx]
82
83 model_clone = clone(model)
84 model_clone.fit(X_tr, y_tr)
85
86 if hasattr(model_clone, 'predict_proba'):
87 oof_preds[val_idx] = model_clone.predict_proba(X_val)[:, 1]
88 else:
89 oof_preds[val_idx] = model_clone.decision_function(X_val)
90
91 meta_features[:, i] = oof_preds
92
93 return meta_features
94
95 def _get_meta_features_test(self, X_train, y_train, X_test):
96 """Generate test meta-features"""
97 inner_kf = KFold(n_splits=self.inner_cv,
98 shuffle=True, random_state=42)
99
100 meta_features = np.zeros((len(X_test), len(self.base_models)))
101
102 for i, model in enumerate(self.base_models):
103 test_preds = []
104
105 for train_idx, _ in inner_kf.split(X_train):
106 X_tr = X_train[train_idx]
107 y_tr = y_train[train_idx]
108
109 model_clone = clone(model)
110 model_clone.fit(X_tr, y_tr)
111
112 if hasattr(model_clone, 'predict_proba'):
113 preds = model_clone.predict_proba(X_test)[:, 1]
114 else:
115 preds = model_clone.decision_function(X_test)
116
117 test_preds.append(preds)
118
119 # Average predictions across inner folds
120 meta_features[:, i] = np.mean(test_preds, axis=0)
121
122 return meta_features
123
124
125# Usage
126base_models = [
127 RandomForestClassifier(n_estimators=100),
128 GradientBoostingClassifier(n_estimators=100),
129 SVC(probability=True)
130]
131
132meta_model = LogisticRegression()
133
134nested_cv = NestedStackingCV(
135 base_models=base_models,
136 meta_model=meta_model,
137 outer_cv=5,
138 inner_cv=5
139)
140
141mean_score, std_score = nested_cv.evaluate(X, y, scoring='roc_auc')Stratified Cross-Validation for Imbalanced Data:
1from sklearn.model_selection import StratifiedKFold
2
3# Maintains class distribution in each fold
4stratified_kf = StratifiedKFold(
5 n_splits=5,
6 shuffle=True,
7 random_state=42
8)
9
10# Use in stacking
11for train_idx, val_idx in stratified_kf.split(X, y):
12 X_train_fold = X[train_idx]
13 y_train_fold = y[train_idx]
14 # ... train base modelsTime Series Cross-Validation:
For time series data, use forward chaining:
1from sklearn.model_selection import TimeSeriesSplit
2
3# Preserves temporal order
4ts_cv = TimeSeriesSplit(n_splits=5)
5
6for train_idx, val_idx in ts_cv.split(X):
7 # Ensures no future data in training
8 X_train_fold = X[train_idx]
9 y_train_fold = y[train_idx]
10 # ... train base modelsRepeated Cross-Validation for Stability:
1from sklearn.model_selection import RepeatedStratifiedKFold
2
3# Repeat CV multiple times with different seeds
4repeated_cv = RepeatedStratifiedKFold(
5 n_splits=5,
6 n_repeats=3, # Repeat 3 times
7 random_state=42
8)
9
10# More stable estimates
11scores = cross_val_score(
12 stacking_model,
13 X, y,
14 cv=repeated_cv,
15 scoring='roc_auc'
16)Monitoring Overfitting:
1def monitor_stacking_overfitting(base_models, meta_model,
2 X_train, y_train,
3 X_test, y_test):
4 """Monitor train/test gap in stacking"""
5
6 # Get meta-features
7 meta_train = get_oof_meta_features(base_models, X_train, y_train)
8 meta_test = get_test_meta_features(base_models, X_train, y_train, X_test)
9
10 # Train meta-model
11 meta_model.fit(meta_train, y_train)
12
13 # Evaluate on both train and test meta-features
14 train_score = roc_auc_score(
15 y_train,
16 meta_model.predict_proba(meta_train)[:, 1]
17 )
18
19 test_score = roc_auc_score(
20 y_test,
21 meta_model.predict_proba(meta_test)[:, 1]
22 )
23
24 gap = train_score - test_score
25
26 print(f"Train AUC: {train_score:.4f}")
27 print(f"Test AUC: {test_score:.4f}")
28 print(f"Overfitting Gap: {gap:.4f}")
29
30 if gap > 0.05:
31 print("WARNING: Significant overfitting detected!")
32 print("Consider: Simpler meta-model, more regularization, fewer base models")
33
34 return train_score, test_score, gapBest Practices:
Common Pitfalls:
Blending is a simpler alternative to stacking that uses hold-out validation instead of cross-validation. While slightly less powerful than stacking, blending is faster and easier to implement.
Blending vs. Stacking:
Blending:
Stacking:
When to Use Blending:
When to Use Stacking:
Blending Implementation:
1from sklearn.model_selection import train_test_split
2from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
3from sklearn.linear_model import LogisticRegression
4import numpy as np
5
6class BlendingClassifier:
7 """Simple blending ensemble"""
8
9 def __init__(self, base_models, meta_model, blend_size=0.2):
10 self.base_models = base_models
11 self.meta_model = meta_model
12 self.blend_size = blend_size
13
14 def fit(self, X, y):
15 """
16 Fit blending ensemble.
17
18 Splits data into:
19 - Training set: Train base models
20 - Blending set: Train meta-model
21 """
22 # Split into training and blending sets
23 X_train, X_blend, y_train, y_blend = train_test_split(
24 X, y,
25 test_size=self.blend_size,
26 random_state=42,
27 stratify=y
28 )
29
30 print(f"Training set: {len(X_train)} samples")
31 print(f"Blending set: {len(X_blend)} samples")
32
33 # Train base models on training set
34 self.fitted_base_models = []
35 meta_features_blend = np.zeros((len(X_blend), len(self.base_models)))
36
37 for i, model in enumerate(self.base_models):
38 print(f"
39Training base model {i+1}: {model.__class__.__name__}")
40
41 # Train on training set
42 model.fit(X_train, y_train)
43 self.fitted_base_models.append(model)
44
45 # Predict on blending set
46 if hasattr(model, 'predict_proba'):
47 meta_features_blend[:, i] = model.predict_proba(X_blend)[:, 1]
48 else:
49 meta_features_blend[:, i] = model.decision_function(X_blend)
50
51 # Train meta-model on blending predictions
52 print(f"
53Training meta-model: {self.meta_model.__class__.__name__}")
54 self.meta_model.fit(meta_features_blend, y_blend)
55
56 return self
57
58 def predict_proba(self, X):
59 """Predict using blending ensemble"""
60 # Generate meta-features from base models
61 meta_features = np.zeros((len(X), len(self.fitted_base_models)))
62
63 for i, model in enumerate(self.fitted_base_models):
64 if hasattr(model, 'predict_proba'):
65 meta_features[:, i] = model.predict_proba(X)[:, 1]
66 else:
67 meta_features[:, i] = model.decision_function(X)
68
69 # Meta-model prediction
70 return self.meta_model.predict_proba(meta_features)
71
72 def predict(self, X):
73 return (self.predict_proba(X)[:, 1] > 0.5).astype(int)
74
75
76# Example usage
77base_models = [
78 RandomForestClassifier(n_estimators=100, random_state=42),
79 GradientBoostingClassifier(n_estimators=100, random_state=42)
80]
81
82meta_model = LogisticRegression()
83
84blender = BlendingClassifier(
85 base_models=base_models,
86 meta_model=meta_model,
87 blend_size=0.2 # 20% for blending set
88)
89
90blender.fit(X_train, y_train)
91predictions = blender.predict(X_test)Optimizing Blending Set Size:
1from sklearn.metrics import roc_auc_score
2
3def optimize_blend_size(base_models, meta_model, X, y):
4 """Find optimal blending set size"""
5
6 blend_sizes = [0.1, 0.15, 0.2, 0.25, 0.3]
7 results = []
8
9 for blend_size in blend_sizes:
10 # Split
11 X_train, X_blend, y_train, y_blend = train_test_split(
12 X, y,
13 test_size=blend_size,
14 random_state=42,
15 stratify=y
16 )
17
18 # Further split for evaluation
19 X_blend_train, X_blend_test, y_blend_train, y_blend_test = train_test_split(
20 X_blend, y_blend,
21 test_size=0.5,
22 random_state=42,
23 stratify=y_blend
24 )
25
26 # Train base models
27 meta_features = []
28 for model in base_models:
29 model.fit(X_train, y_train)
30 preds = model.predict_proba(X_blend_train)[:, 1]
31 meta_features.append(preds)
32
33 meta_features = np.column_stack(meta_features)
34
35 # Train meta-model
36 meta_model_clone = clone(meta_model)
37 meta_model_clone.fit(meta_features, y_blend_train)
38
39 # Evaluate
40 test_meta_features = []
41 for model in base_models:
42 preds = model.predict_proba(X_blend_test)[:, 1]
43 test_meta_features.append(preds)
44
45 test_meta_features = np.column_stack(test_meta_features)
46 y_pred = meta_model_clone.predict_proba(test_meta_features)[:, 1]
47 score = roc_auc_score(y_blend_test, y_pred)
48
49 results.append({
50 'blend_size': blend_size,
51 'score': score
52 })
53
54 print(f"Blend size {blend_size:.2f}: AUC = {score:.4f}")
55
56 # Find best size
57 best = max(results, key=lambda x: x['score'])
58 print(f"
59Optimal blend size: {best['blend_size']:.2f}")
60
61 return best['blend_size']Multi-Stage Blending:
1class MultiStageBlending:
2 """
3 Multi-stage blending with multiple hold-out sets.
4
5 Reduces overfitting by using multiple independent blending sets.
6 """
7
8 def __init__(self, base_models, meta_model, n_stages=3):
9 self.base_models = base_models
10 self.meta_model = meta_model
11 self.n_stages = n_stages
12
13 def fit(self, X, y):
14 # Split into stages
15 stage_size = 1.0 / (self.n_stages + 1)
16
17 X_train = X
18 y_train = y
19
20 all_meta_features = []
21 all_meta_targets = []
22
23 for stage in range(self.n_stages):
24 # Split off a blending set
25 X_train, X_blend, y_train, y_blend = train_test_split(
26 X_train, y_train,
27 test_size=stage_size,
28 random_state=stage,
29 stratify=y_train
30 )
31
32 # Train base models on remaining training data
33 stage_meta_features = []
34
35 for model in self.base_models:
36 model_clone = clone(model)
37 model_clone.fit(X_train, y_train)
38
39 preds = model_clone.predict_proba(X_blend)[:, 1]
40 stage_meta_features.append(preds)
41
42 all_meta_features.append(np.column_stack(stage_meta_features))
43 all_meta_targets.append(y_blend)
44
45 # Combine all blending sets
46 combined_meta_features = np.vstack(all_meta_features)
47 combined_targets = np.concatenate(all_meta_targets)
48
49 # Train meta-model
50 self.meta_model.fit(combined_meta_features, combined_targets)
51
52 # Train final base models on all training data
53 self.fitted_base_models = []
54 for model in self.base_models:
55 model_clone = clone(model)
56 model_clone.fit(X_train, y_train)
57 self.fitted_base_models.append(model_clone)
58
59 return selfWeighted Blending:
1from scipy.optimize import minimize
2
3def optimize_blend_weights(predictions, y_true):
4 """
5 Find optimal linear weights for blending.
6
7 predictions: (n_samples, n_models) array of base model predictions
8 y_true: True labels
9 """
10 n_models = predictions.shape[1]
11
12 # Objective: Minimize negative AUC
13 def objective(weights):
14 # Ensure weights sum to 1
15 weights = weights / weights.sum()
16 blended = (predictions * weights).sum(axis=1)
17 return -roc_auc_score(y_true, blended)
18
19 # Constraints: weights between 0 and 1, sum to 1
20 constraints = ({'type': 'eq', 'fun': lambda w: w.sum() - 1})
21 bounds = [(0, 1) for _ in range(n_models)]
22
23 # Initial guess: equal weights
24 initial_weights = np.ones(n_models) / n_models
25
26 # Optimize
27 result = minimize(
28 objective,
29 initial_weights,
30 method='SLSQP',
31 bounds=bounds,
32 constraints=constraints
33 )
34
35 optimal_weights = result.x / result.x.sum()
36
37 return optimal_weights
38
39
40# Usage
41blend_predictions = np.column_stack([
42 model1.predict_proba(X_val)[:, 1],
43 model2.predict_proba(X_val)[:, 1],
44 model3.predict_proba(X_val)[:, 1]
45])
46
47weights = optimize_blend_weights(blend_predictions, y_val)
48print(f"Optimal weights: {weights}")
49
50# Final prediction
51final_pred = (blend_predictions * weights).sum(axis=1)Best Practices:
Blending for Kaggle:
Deploying stacked ensembles in production requires careful consideration of latency, memory, versioning, and maintenance. Understanding these practical aspects ensures successful real-world applications.
Production Challenges:
1. Latency:
2. Memory:
3. Complexity:
4. Versioning:
Optimizing Inference Speed:
1. Model Pruning:
1from sklearn.feature_selection import SelectFromModel
2
3def prune_weak_base_models(base_models, meta_model, X_val, y_val,
4 threshold_percentile=25):
5 """
6 Remove weak base models that don't contribute to ensemble.
7
8 Returns: Pruned list of base models
9 """
10 # Get meta-model feature importances (if available)
11 if hasattr(meta_model, 'coef_'):
12 importances = np.abs(meta_model.coef_[0])
13 elif hasattr(meta_model, 'feature_importances_'):
14 importances = meta_model.feature_importances_
15 else:
16 print("Meta-model doesn't support importance extraction")
17 return base_models
18
19 # Calculate threshold
20 threshold = np.percentile(importances, threshold_percentile)
21
22 # Keep important models
23 pruned_models = [
24 model for i, model in enumerate(base_models)
25 if importances[i] >= threshold
26 ]
27
28 print(f"Pruned {len(base_models) - len(pruned_models)} weak models")
29 print(f"Kept {len(pruned_models)} important models")
30
31 return pruned_models2. Parallel Prediction:
1from joblib import Parallel, delayed
2
3class FastStackingClassifier:
4 """Stacking with parallel base model prediction"""
5
6 def __init__(self, base_models, meta_model, n_jobs=-1):
7 self.base_models = base_models
8 self.meta_model = meta_model
9 self.n_jobs = n_jobs
10
11 def predict_proba(self, X):
12 """Parallel prediction from base models"""
13
14 # Predict from all base models in parallel
15 base_predictions = Parallel(n_jobs=self.n_jobs)(
16 delayed(self._predict_single_model)(model, X)
17 for model in self.base_models
18 )
19
20 # Stack predictions
21 meta_features = np.column_stack(base_predictions)
22
23 # Meta-model prediction
24 return self.meta_model.predict_proba(meta_features)
25
26 def _predict_single_model(self, model, X):
27 if hasattr(model, 'predict_proba'):
28 return model.predict_proba(X)[:, 1]
29 else:
30 return model.decision_function(X)3. Model Caching:
1from functools import lru_cache
2import hashlib
3
4class CachedStackingClassifier:
5 """Stacking with prediction caching"""
6
7 def __init__(self, base_models, meta_model, cache_size=1000):
8 self.base_models = base_models
9 self.meta_model = meta_model
10 self.cache_size = cache_size
11
12 @lru_cache(maxsize=cache_size)
13 def _predict_cached(self, X_hash):
14 """Cache predictions for identical inputs"""
15 # ... actual prediction logic
16 pass
17
18 def predict_proba(self, X):
19 # Hash input for caching
20 X_hash = hashlib.md5(X.tobytes()).hexdigest()
21
22 return self._predict_cached(X_hash)4. Model Distillation:
Train single model to mimic stacked ensemble:
1from sklearn.tree import DecisionTreeClassifier
2
3def distill_ensemble(stacking_model, X_train, X_unlabeled):
4 """
5 Distill stacked ensemble into single fast model.
6
7 Uses ensemble to label unlabeled data, then trains simple model.
8 """
9 # Get ensemble predictions on unlabeled data
10 y_pseudo = stacking_model.predict_proba(X_unlabeled)[:, 1]
11
12 # Train simple model on pseudo-labels
13 distilled_model = DecisionTreeClassifier(max_depth=10)
14 distilled_model.fit(X_unlabeled, (y_pseudo > 0.5).astype(int))
15
16 return distilled_model
17
18# Usage
19# 1. Train stacked ensemble on labeled data
20# 2. Generate pseudo-labels on large unlabeled set
21# 3. Train fast model on pseudo-labels
22# 4. Deploy fast model instead of full ensembleModel Versioning and Deployment:
1import joblib
2import json
3from datetime import datetime
4
5class VersionedStackingEnsemble:
6 """Stacking with versioning and metadata"""
7
8 def __init__(self, base_models, meta_model):
9 self.base_models = base_models
10 self.meta_model = meta_model
11 self.version = datetime.now().strftime("%Y%m%d_%H%M%S")
12 self.metadata = {
13 'version': self.version,
14 'created_at': datetime.now().isoformat(),
15 'n_base_models': len(base_models),
16 'base_model_types': [type(m).__name__ for m in base_models],
17 'meta_model_type': type(meta_model).__name__
18 }
19
20 def save(self, path):
21 """Save ensemble with version metadata"""
22 # Save models
23 model_data = {
24 'base_models': self.base_models,
25 'meta_model': self.meta_model,
26 'metadata': self.metadata
27 }
28
29 joblib.dump(model_data, f"{path}/ensemble_v{self.version}.pkl")
30
31 # Save metadata separately
32 with open(f"{path}/metadata_v{self.version}.json", 'w') as f:
33 json.dump(self.metadata, f, indent=2)
34
35 print(f"Saved ensemble version {self.version}")
36
37 @staticmethod
38 def load(path, version=None):
39 """Load specific ensemble version"""
40 if version:
41 model_file = f"{path}/ensemble_v{version}.pkl"
42 else:
43 # Load latest
44 import glob
45 model_files = glob.glob(f"{path}/ensemble_v*.pkl")
46 model_file = max(model_files) # Latest by name
47
48 model_data = joblib.load(model_file)
49
50 ensemble = VersionedStackingEnsemble(
51 model_data['base_models'],
52 model_data['meta_model']
53 )
54 ensemble.metadata = model_data['metadata']
55
56 print(f"Loaded ensemble version {ensemble.version}")
57
58 return ensembleMonitoring in Production:
1class MonitoredStackingClassifier:
2 """Stacking with prediction monitoring"""
3
4 def __init__(self, base_models, meta_model):
5 self.base_models = base_models
6 self.meta_model = meta_model
7 self.prediction_log = []
8
9 def predict_proba(self, X):
10 """Predict with monitoring"""
11 import time
12
13 # Time base model predictions
14 base_times = []
15 base_predictions = []
16
17 for model in self.base_models:
18 start = time.time()
19 pred = model.predict_proba(X)[:, 1]
20 elapsed = time.time() - start
21
22 base_times.append(elapsed)
23 base_predictions.append(pred)
24
25 meta_features = np.column_stack(base_predictions)
26
27 # Time meta-model
28 start = time.time()
29 final_pred = self.meta_model.predict_proba(meta_features)
30 meta_time = time.time() - start
31
32 # Log metrics
33 log_entry = {
34 'timestamp': datetime.now().isoformat(),
35 'n_samples': len(X),
36 'base_times': base_times,
37 'meta_time': meta_time,
38 'total_time': sum(base_times) + meta_time,
39 'avg_time_per_sample': (sum(base_times) + meta_time) / len(X)
40 }
41
42 self.prediction_log.append(log_entry)
43
44 # Alert if too slow
45 if log_entry['avg_time_per_sample'] > 0.1: # 100ms threshold
46 print(f"WARNING: Slow prediction: {log_entry['avg_time_per_sample']*1000:.2f}ms/sample")
47
48 return final_pred
49
50 def get_performance_summary(self):
51 """Get performance statistics"""
52 if not self.prediction_log:
53 return {}
54
55 total_times = [entry['total_time'] for entry in self.prediction_log]
56
57 return {
58 'n_predictions': len(self.prediction_log),
59 'avg_total_time': np.mean(total_times),
60 'p95_total_time': np.percentile(total_times, 95),
61 'p99_total_time': np.percentile(total_times, 99),
62 'max_total_time': np.max(total_times)
63 }Best Practices:
Development:
Pre-Production:
Production:
Common Production Issues:
Cost-Benefit Analysis:
Before deploying stacking:
Stacking is worth it when:
Comprehensive implementation demonstrating both stacking and blending with nested cross-validation, meta-learner optimization, performance monitoring, and production deployment features.
1# Complete Stacking and Blending System
2import numpy as np
3import pandas as pd
4from sklearn.datasets import make_classification
5from sklearn.model_selection import train_test_split, KFold, StratifiedKFold
6from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, ExtraTreesClassifier
7from sklearn.linear_model import LogisticRegression, Ridge
8from sklearn.svm import SVC
9from sklearn.neighbors import KNeighborsClassifier
10from sklearn.metrics import accuracy_score, roc_auc_score, f1_score, roc_curve
11from sklearn.base import clone
12import matplotlib.pyplot as plt
13import seaborn as sns
14import time
15import warnings
16warnings.filterwarnings('ignore')
17
18# Set style
19sns.set_style('whitegrid')
20plt.rcParams['figure.figsize'] = (16, 12)
21
22print("=" * 80)
23print("COMPLETE STACKING AND BLENDING SYSTEM")
24print("=" * 80)
25
26# ============================================================================
27# 1. GENERATE DATASET
28# ============================================================================
29print("
301. Generating Synthetic Dataset...")
31print("-" * 80)
32
33X, y = make_classification(
34 n_samples=5000,
35 n_features=30,
36 n_informative=20,
37 n_redundant=5,
38 n_classes=2,
39 weights=[0.65, 0.35],
40 flip_y=0.05,
41 random_state=42
42)
43
44X_train, X_test, y_train, y_test = train_test_split(
45 X, y, test_size=0.2, random_state=42, stratify=y
46)
47
48print(f"Dataset shape: {X.shape}")
49print(f"Training set: {X_train.shape}")
50print(f"Test set: {X_test.shape}")
51print(f"Class distribution: {np.bincount(y_train)}")
52
53# ============================================================================
54# 2. DEFINE BASE MODELS
55# ============================================================================
56print("
572. Defining Diverse Base Models...")
58print("-" * 80)
59
60base_models = [
61 ('Random Forest', RandomForestClassifier(
62 n_estimators=100,
63 max_depth=10,
64 random_state=42,
65 n_jobs=-1
66 )),
67 ('Gradient Boosting', GradientBoostingClassifier(
68 n_estimators=100,
69 max_depth=3,
70 learning_rate=0.1,
71 random_state=42
72 )),
73 ('Extra Trees', ExtraTreesClassifier(
74 n_estimators=100,
75 max_depth=10,
76 random_state=42,
77 n_jobs=-1
78 )),
79 ('SVM', SVC(
80 kernel='rbf',
81 probability=True,
82 random_state=42
83 )),
84 ('KNN', KNeighborsClassifier(
85 n_neighbors=5,
86 n_jobs=-1
87 ))
88]
89
90for name, model in base_models:
91 print(f" - {name}")
92
93# ============================================================================
94# 3. IMPLEMENT STACKING WITH OUT-OF-FOLD PREDICTIONS
95# ============================================================================
96print("
973. Stacking Implementation (Out-of-Fold)...")
98print("-" * 80)
99
100def get_oof_predictions(models, X_train, y_train, X_test, n_folds=5):
101 """Generate out-of-fold predictions for stacking"""
102
103 n_models = len(models)
104 n_train = X_train.shape[0]
105 n_test = X_test.shape[0]
106
107 # Initialize arrays
108 oof_train = np.zeros((n_train, n_models))
109 oof_test = np.zeros((n_test, n_models))
110
111 # Stratified K-Fold
112 skf = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
113
114 for i, (name, model) in enumerate(models):
115 print(f"
116 Processing {name}...")
117 oof_test_folds = np.zeros((n_test, n_folds))
118
119 for fold, (train_idx, val_idx) in enumerate(skf.split(X_train, y_train)):
120 # Split data
121 X_tr = X_train[train_idx]
122 y_tr = y_train[train_idx]
123 X_val = X_train[val_idx]
124
125 # Clone and train model
126 model_clone = clone(model)
127 model_clone.fit(X_tr, y_tr)
128
129 # OOF predictions
130 oof_train[val_idx, i] = model_clone.predict_proba(X_val)[:, 1]
131
132 # Test predictions
133 oof_test_folds[:, fold] = model_clone.predict_proba(X_test)[:, 1]
134
135 # Average test predictions across folds
136 oof_test[:, i] = oof_test_folds.mean(axis=1)
137
138 # Model performance
139 fold_auc = roc_auc_score(y_train, oof_train[:, i])
140 print(f" OOF AUC: {fold_auc:.4f}")
141
142 return oof_train, oof_test
143
144# Generate OOF predictions
145start_time = time.time()
146oof_train, oof_test = get_oof_predictions(base_models, X_train, y_train, X_test, n_folds=5)
147stacking_time = time.time() - start_time
148
149print(f"
150 Meta-features shape (train): {oof_train.shape}")
151print(f" Meta-features shape (test): {oof_test.shape}")
152print(f" Time: {stacking_time:.2f}s")
153
154# ============================================================================
155# 4. COMPARE META-LEARNERS
156# ============================================================================
157print("
1584. Meta-Learner Selection...")
159print("-" * 80)
160
161meta_models = {
162 'Logistic Regression': LogisticRegression(max_iter=1000, random_state=42),
163 'Ridge Regression': Ridge(random_state=42),
164 'Random Forest': RandomForestClassifier(n_estimators=50, max_depth=3, random_state=42)
165}
166
167meta_results = {}
168
169for name, meta_model in meta_models.items():
170 # Train meta-model
171 meta_model.fit(oof_train, y_train)
172
173 # Predictions
174 meta_pred_proba = meta_model.predict_proba(oof_test)[:, 1] if hasattr(meta_model, 'predict_proba') else meta_model.predict(oof_test)
175 meta_pred = (meta_pred_proba > 0.5).astype(int)
176
177 # Metrics
178 accuracy = accuracy_score(y_test, meta_pred)
179 auc = roc_auc_score(y_test, meta_pred_proba)
180 f1 = f1_score(y_test, meta_pred)
181
182 meta_results[name] = {
183 'accuracy': accuracy,
184 'auc': auc,
185 'f1': f1,
186 'model': meta_model,
187 'predictions': meta_pred_proba
188 }
189
190 print(f"
191 {name}:")
192 print(f" Accuracy: {accuracy:.4f}")
193 print(f" AUC: {auc:.4f}")
194 print(f" F1: {f1:.4f}")
195
196# Select best meta-learner
197best_meta_name = max(meta_results, key=lambda k: meta_results[k]['auc'])
198best_meta_model = meta_results[best_meta_name]['model']
199
200print(f"
201 Best Meta-Learner: {best_meta_name} (AUC: {meta_results[best_meta_name]['auc']:.4f})")
202
203# ============================================================================
204# 5. IMPLEMENT BLENDING
205# ============================================================================
206print("
2075. Blending Implementation (Hold-out)...")
208print("-" * 80)
209
210# Split training data into train and blend sets
211X_train_blend, X_blend, y_train_blend, y_blend = train_test_split(
212 X_train, y_train,
213 test_size=0.2,
214 random_state=42,
215 stratify=y_train
216)
217
218print(f" Blend training set: {X_train_blend.shape}")
219print(f" Blend validation set: {X_blend.shape}")
220
221# Train base models on blend training set
222blend_train_preds = []
223blend_test_preds = []
224fitted_blend_models = []
225
226for name, model in base_models:
227 print(f"
228 Training {name}...")
229
230 # Train on blend training set
231 model_clone = clone(model)
232 model_clone.fit(X_train_blend, y_train_blend)
233 fitted_blend_models.append((name, model_clone))
234
235 # Predictions on blend set
236 blend_train_preds.append(model_clone.predict_proba(X_blend)[:, 1])
237
238 # Predictions on test set
239 blend_test_preds.append(model_clone.predict_proba(X_test)[:, 1])
240
241# Stack predictions
242blend_train_meta = np.column_stack(blend_train_preds)
243blend_test_meta = np.column_stack(blend_test_preds)
244
245# Train meta-model
246blend_meta_model = LogisticRegression(max_iter=1000, random_state=42)
247blend_meta_model.fit(blend_train_meta, y_blend)
248
249# Predictions
250blend_pred_proba = blend_meta_model.predict_proba(blend_test_meta)[:, 1]
251blend_pred = (blend_pred_proba > 0.5).astype(int)
252
253blend_accuracy = accuracy_score(y_test, blend_pred)
254blend_auc = roc_auc_score(y_test, blend_pred_proba)
255
256print(f"
257 Blending Results:")
258print(f" Accuracy: {blend_accuracy:.4f}")
259print(f" AUC: {blend_auc:.4f}")
260
261# ============================================================================
262# 6. COMPARE WITH INDIVIDUAL BASE MODELS
263# ============================================================================
264print("
2656. Baseline Comparison (Individual Models)...")
266print("-" * 80)
267
268baseline_results = {}
269
270for name, model in base_models:
271 # Train on full training set
272 model_clone = clone(model)
273 model_clone.fit(X_train, y_train)
274
275 # Predict
276 y_pred_proba = model_clone.predict_proba(X_test)[:, 1]
277 y_pred = (y_pred_proba > 0.5).astype(int)
278
279 # Metrics
280 baseline_results[name] = {
281 'accuracy': accuracy_score(y_test, y_pred),
282 'auc': roc_auc_score(y_test, y_pred_proba),
283 'predictions': y_pred_proba
284 }
285
286 print(f" {name}: AUC = {baseline_results[name]['auc']:.4f}")
287
288# ============================================================================
289# 7. RESULTS COMPARISON
290# ============================================================================
291print("
2927. Comprehensive Results Comparison...")
293print("-" * 80)
294
295# Compile all results
296all_results = pd.DataFrame({
297 'Model': list(baseline_results.keys()) + ['Stacking', 'Blending'],
298 'AUC': [baseline_results[k]['auc'] for k in baseline_results.keys()] +
299 [meta_results[best_meta_name]['auc'], blend_auc],
300 'Type': ['Base Model'] * len(baseline_results) + ['Ensemble', 'Ensemble']
301})
302
303print("
304All Results:")
305print(all_results.to_string(index=False))
306
307best_baseline_auc = max(baseline_results.values(), key=lambda x: x['auc'])['auc']
308stacking_improvement = ((meta_results[best_meta_name]['auc'] - best_baseline_auc) / best_baseline_auc) * 100
309blending_improvement = ((blend_auc - best_baseline_auc) / best_baseline_auc) * 100
310
311print(f"
312📊 Performance Gains:")
313print(f" Best Baseline: {best_baseline_auc:.4f}")
314print(f" Stacking: {meta_results[best_meta_name]['auc']:.4f} (+{stacking_improvement:.2f}%)")
315print(f" Blending: {blend_auc:.4f} (+{blending_improvement:.2f}%)")
316
317# ============================================================================
318# 8. VISUALIZATION
319# ============================================================================
320print("
3218. Creating Visualizations...")
322print("-" * 80)
323
324fig = plt.figure(figsize=(16, 12))
325
326# Model Performance Comparison
327ax1 = plt.subplot(2, 3, 1)
328all_results_sorted = all_results.sort_values('AUC', ascending=True)
329colors = ['skyblue' if t == 'Base Model' else 'lightcoral' for t in all_results_sorted['Type']]
330
331ax1.barh(range(len(all_results_sorted)), all_results_sorted['AUC'], color=colors, alpha=0.8)
332ax1.set_yticks(range(len(all_results_sorted)))
333ax1.set_yticklabels(all_results_sorted['Model'])
334ax1.set_xlabel('AUC Score')
335ax1.set_title('Model Performance Comparison')
336ax1.axvline(best_baseline_auc, color='red', linestyle='--', alpha=0.5, label='Best Baseline')
337ax1.legend()
338ax1.grid(True, alpha=0.3, axis='x')
339
340# ROC Curves
341ax2 = plt.subplot(2, 3, 2)
342
343# Best baseline
344best_baseline_name = max(baseline_results, key=lambda k: baseline_results[k]['auc'])
345fpr, tpr, _ = roc_curve(y_test, baseline_results[best_baseline_name]['predictions'])
346ax2.plot(fpr, tpr, label=f'{best_baseline_name} (AUC={baseline_results[best_baseline_name]["auc"]:.3f})',
347 linewidth=2, alpha=0.7)
348
349# Stacking
350fpr, tpr, _ = roc_curve(y_test, meta_results[best_meta_name]['predictions'])
351ax2.plot(fpr, tpr, label=f'Stacking (AUC={meta_results[best_meta_name]["auc"]:.3f})',
352 linewidth=2, alpha=0.7)
353
354# Blending
355fpr, tpr, _ = roc_curve(y_test, blend_pred_proba)
356ax2.plot(fpr, tpr, label=f'Blending (AUC={blend_auc:.3f})', linewidth=2, alpha=0.7)
357
358ax2.plot([0, 1], [0, 1], 'k--', alpha=0.3)
359ax2.set_xlabel('False Positive Rate')
360ax2.set_ylabel('True Positive Rate')
361ax2.set_title('ROC Curves')
362ax2.legend()
363ax2.grid(True, alpha=0.3)
364
365# Meta-Learner Comparison
366ax3 = plt.subplot(2, 3, 3)
367meta_names = list(meta_results.keys())
368meta_aucs = [meta_results[k]['auc'] for k in meta_names]
369
370ax3.bar(range(len(meta_names)), meta_aucs, color='lightgreen', alpha=0.8)
371ax3.set_xticks(range(len(meta_names)))
372ax3.set_xticklabels(meta_names, rotation=45, ha='right')
373ax3.set_ylabel('AUC Score')
374ax3.set_title('Meta-Learner Comparison')
375ax3.grid(True, alpha=0.3, axis='y')
376
377# Base Model Correlation
378ax4 = plt.subplot(2, 3, 4)
379correlation_matrix = np.corrcoef(oof_train.T)
380sns.heatmap(correlation_matrix,
381 annot=True,
382 fmt='.2f',
383 cmap='coolwarm',
384 xticklabels=[name for name, _ in base_models],
385 yticklabels=[name for name, _ in base_models],
386 ax=ax4,
387 cbar_kws={'label': 'Correlation'})
388ax4.set_title('Base Model Prediction Correlation')
389
390# Meta-Feature Importance (if logistic regression)
391ax5 = plt.subplot(2, 3, 5)
392if hasattr(best_meta_model, 'coef_'):
393 importances = np.abs(best_meta_model.coef_[0])
394 model_names = [name for name, _ in base_models]
395
396 ax5.barh(range(len(importances)), importances)
397 ax5.set_yticks(range(len(importances)))
398 ax5.set_yticklabels(model_names)
399 ax5.set_xlabel('Absolute Coefficient')
400 ax5.set_title(f'Meta-Model Feature Importance ({best_meta_name})')
401 ax5.grid(True, alpha=0.3, axis='x')
402
403# Ensemble Gain Summary
404ax6 = plt.subplot(2, 3, 6)
405improvements = [0, stacking_improvement, blending_improvement]
406methods = ['Best Baseline', 'Stacking', 'Blending']
407colors_gain = ['gray', 'green', 'blue']
408
409ax6.bar(methods, improvements, color=colors_gain, alpha=0.7)
410ax6.set_ylabel('Improvement over Best Baseline (%)')
411ax6.set_title('Ensemble Performance Gain')
412ax6.axhline(0, color='black', linewidth=0.5)
413ax6.grid(True, alpha=0.3, axis='y')
414
415plt.tight_layout()
416plt.show()
417
418# ============================================================================
419# FINAL SUMMARY
420# ============================================================================
421print("
422" + "=" * 80)
423print("STACKING AND BLENDING SUMMARY")
424print("=" * 80)
425
426print(f"
427📊 DATASET:")
428print(f" Total samples: {len(X)}")
429print(f" Features: {X.shape[1]}")
430print(f" Train/Test split: {len(X_train)}/{len(X_test)}")
431
432print(f"
433🏗️ ARCHITECTURE:")
434print(f" Base models: {len(base_models)}")
435print(f" Stacking folds: 5")
436print(f" Blending split: 80/20")
437print(f" Best meta-learner: {best_meta_name}")
438
439print(f"
440🎯 RESULTS:")
441print(f" Best baseline: {best_baseline_auc:.4f} ({best_baseline_name})")
442print(f" Stacking: {meta_results[best_meta_name]['auc']:.4f} (+{stacking_improvement:.2f}%)")
443print(f" Blending: {blend_auc:.4f} (+{blending_improvement:.2f}%)")
444
445print(f"
446⚡ PERFORMANCE:")
447print(f" Stacking training time: {stacking_time:.2f}s")
448
449print(f"
450💡 RECOMMENDATIONS:")
451if stacking_improvement > blending_improvement:
452 print(f" ✓ Use STACKING for production ({stacking_improvement:.2f}% gain)")
453else:
454 print(f" ✓ Use BLENDING for production ({blending_improvement:.2f}% gain)")
455
456print(f" ✓ Selected meta-learner: {best_meta_name}")
457print(f" ✓ Consider pruning weakest base models for speed")
458
459print("
460" + "=" * 80)This comprehensive demonstration showcases complete stacking and blending systems with production-grade features:
🎯 Complete System Architecture:
1. Dataset Preparation:
2. Diverse Base Models:
Why diversity matters: Different models make different errors. Stacking learns to leverage each model's strengths.
3. Stacking Implementation (Out-of-Fold):
OOF Generation Process:
Why OOF matters:
4. Meta-Learner Selection:
Tested Meta-Learners:
Selection Process:
5. Blending Implementation:
Blending Process:
Blending vs Stacking:
6. Baseline Comparison:
Individual Model Evaluation:
Why baseline matters: Shows actual performance improvement from ensembling, not just model quality.
7. Comprehensive Comparison:
Metrics Tracked:
Results Analysis:
8. Visualization Suite:
A. Performance Bar Chart:
B. ROC Curves:
C. Meta-Learner Comparison:
D. Base Model Correlation Matrix:
E. Meta-Model Feature Importance:
F. Ensemble Gain Summary:
🔬 Advanced Techniques Demonstrated:
Proper Cross-Validation:
Meta-Feature Engineering:
Model Diversity:
Performance Profiling:
📊 Production Insights:
When to Use Stacking:
When to Use Blending:
Deployment Considerations:
Optimization Strategies:
💡 Key Takeaways:
This showcase provides a complete template for implementing and evaluating stacked ensembles in real-world applications.
Excellent work mastering stacking and blending! You now understand how to combine diverse models for superior performance. In our next lesson, "Feature Engineering Fundamentals," you'll learn the critical skill of creating powerful features that make all your models (including ensembles) dramatically more effective. Get ready to master the art and science of feature engineering!
Your progress is saved in this browser, on this device only.