
Dreamforce 2026: What AIforce Changes for Enterprise AI ArchitectureRead More

In a machine learning task some years ago, I was debugging a recommendation engine that took 18 hours to retrain. The product team needed daily updates, but our weekly retraining schedule was not letting user engagement possible. After months of trying different algorithms and scaling approaches, I discovered something counterintuitive: removing 90% of our features made the model faster, more accurate, and dramatically easier to maintain.
This guide documents the specific techniques I have used to transform underperforming machine learning systems into production-ready solutions. Rather than presenting academic theory, I'll walk you through real problems I have encountered and the steps I took to solve them.
In my earlier days of machine learning study and experimentation, I had a customer churn prediction model with 847 features. The data science team was proud of their comprehensive feature engineering, but the operations team was frustrated with deployment challenges:
The wake-up call came during a production incident. When the model started predicting that 95% of customers would churn, it took us 8 hours to identify that a single corrupted feature was causing the issue. That's when I realized we had built a brittle system optimized for complexity rather than reliability.
When I first started working with high-dimensional datasets, I underestimated the dramatic impact of feature count on computational complexity. Here's what I learned the hard way:
Linear Models scale as O(n × d²), where n is the sample count and d is the feature count. When we reduced the number of features from 1,000 to 100 in our fraud detection system, matrix operations became 100X faster. The training time dropped from 45 minutes to 3 minutes.
Tree-Based Models scale as O(n × d × log(n)). For our customer segmentation model with 500,000 samples, reducing features from 200 to 50 cut training time from 2 hours to 20 minutes.
Neural Networks suffer differently. While the input layer shrinks linearly with features, the real benefit comes from faster convergence. Our image classification model with reduced features converged in 40 epochs instead of 120.
Beyond raw computation, features carry operational overhead that compounds over time:
Not every feature reduction improves models. I've made these mistakes so you don't have to:
I've tried dozens of feature selection approaches. Here's the framework that consistently delivers results:
Start here because these methods are fast and catch obvious problems:
# Real example from our e-commerce dataset
from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# Load your dataset
df = pd.read_csv('customer_data.csv')
original_features = df.shape[1]
# Remove features with zero variance
selector = VarianceThreshold(threshold=0)
df_filtered = selector.fit_transform(df)
print(f"Removed {original_features - df_filtered.shape[1]} zero-variance features")
# Output: Removed 23 zero-variance features
In our e-commerce dataset, 23 features had identical values across all customers (mostly legacy columns from database schema changes). Removing these was obvious and immediate.
# Find feature pairs with correlation > 0.95
corr_matrix = df.corr().abs()
upper_triangle = corr_matrix.where(
np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
)
high_corr_features = [column for column in upper_triangle.columns
if any(upper_triangle[column] > 0.95)]
print(f"Highly correlated features: {len(high_corr_features)}")
df_filtered = df.drop(columns=high_corr_features)In practice, I found that 'total_orders' and 'order_count' were perfectly correlated (correlation = 1.0), so we kept the more intuitive 'total_orders' feature.
from sklearn.feature_selection import chi2, SelectKBest
from sklearn.preprocessing import MinMaxScaler
# Ensure all features are positive for chi-square
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
# Select top 50 features based on chi-square scores
selector = SelectKBest(score_func=chi2, k=50)
X_selected = selector.fit_transform(X_scaled, y)
# Get selected feature names
selected_features = [features[i] for i in selector.get_support(indices=True)]
For Regression Problems: Mutual Information
from sklearn.feature_selection import mutual_info_regression
# Calculate mutual information scores
mi_scores = mutual_info_regression(X, y)
mi_scores = pd.Series(mi_scores, index=X.columns).sort_values(ascending=False)
# Select features with MI score > threshold
threshold = 0.1
selected_features = mi_scores[mi_scores > threshold].index.tolist()This is where you get the biggest accuracy improvements, but it's computationally expensive.
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
# Use Random Forest as the estimator
rf = RandomForestClassifier(n_estimators=100, random_state=42)
# Start with all features, eliminate down to optimal number
feature_counts = range(10, 101, 10) # Test 10, 20, 30... 100 features
cv_scores = []
for n_features in feature_counts:
rfe = RFE(estimator=rf, n_features_to_select=n_features, step=10)
rfe.fit(X_train, y_train)
# Evaluate with cross-validation
scores = cross_val_score(rf, X_train[rfe.support_], y_train, cv=5)
cv_scores.append(scores.mean())
print(f"{n_features} features: CV score = {scores.mean():.4f}")
# Find optimal number of features
optimal_features = feature_counts[np.argmax(cv_scores)]
Tree-Based Feature Importance
# Train a Random Forest to get feature importances
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
# Get feature importances
importances = pd.DataFrame({
'feature': X_train.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
# Select top features based on cumulative importance
cumulative_importance = importances['importance'].cumsum()
n_features = (cumulative_importance <= 0.95).sum() # Features explaining 95% of importance
selected_features = importances.head(n_features)['feature'].tolist()Let me walk you through how I applied this framework to a customer churn model with 234 features:
Step 1: Filter Methods (Reduced to 180 features)
Step 2: Statistical Tests (Reduced to 89 features)
Step 3: RFE with Random Forest (Final 34 features)
Results:
I've encountered datasets where basic feature selection didn't work well. Here are specialized techniques I've developed for challenging scenarios:
Most data scientists make the mistake of applying numerical feature selection methods to categorical data. Here's what actually works:
Problem: One-hot encoded categorical features create artificial correlation patterns.
Solution: Group-based feature selection
def select_categorical_features(df, target, max_categories_per_feature=5):
"""
Select categorical features while preserving their group structure.
"""
categorical_groups = {}
# Group one-hot encoded features back together
for col in df.columns:
if col.endswith('_'): # Assuming one-hot encoded columns end with '_'
base_name = col.rsplit('_', 1)[0]
if base_name not in categorical_groups:
categorical_groups[base_name] = []
categorical_groups[base_name].append(col)
# Evaluate each categorical feature group as a whole
selected_groups = []
for group_name, columns in categorical_groups.items():
# Create temporary model with only this categorical feature
temp_X = df[columns]
cv_score = cross_val_score(RandomForestClassifier(), temp_X, target, cv=3).mean()
if cv_score > 0.55: # Threshold based on baseline model performance
selected_groups.extend(columns)
return selected_groupsTime series data requires special consideration because features have temporal dependencies.
Mistake I Made: Applying standard correlation analysis to time series features without considering lag relationships.
Better Approach: Lag-aware feature selection
def time_series_feature_selection(df, target_col, max_lag=12):
"""
Select features considering temporal relationships.
"""
from statsmodels.tsa.stattools import grangercausalitytests
selected_features = []
for feature in df.columns:
if feature == target_col:
continue
# Test Granger causality at different lags
try:
# Prepare data for Granger causality test
test_data = df[[target_col, feature]].dropna()
# Test causality up to max_lag periods
gc_results = grangercausalitytests(test_data, max_lag, verbose=False)
# Check if any lag shows significant causality (p < 0.05)
significant = any(
gc_results[lag][0]['ssr_ftest'][1] < 0.05
for lag in range(1, max_lag + 1)
)
if significant:
selected_features.append(feature)
except Exception as e:
print(f"Could not test {feature}: {e}")
continue
return selected_featuresThe Problem: Features like user_id or product_id with thousands of unique values.
My Solution: Smart aggregation before selection
def handle_high_cardinality(df, feature_col, target_col, min_frequency=100):
"""
Convert high-cardinality categorical features into useful numeric features.
"""
# Calculate target statistics for each category
feature_stats = df.groupby(feature_col)[target_col].agg([
'mean', 'count', 'std'
]).reset_index()
# Only keep categories with sufficient frequency
frequent_categories = feature_stats[
feature_stats['count'] >= min_frequency
][feature_col].tolist()
# Create new features
df[f'{feature_col}_target_mean'] = df[feature_col].map(
feature_stats.set_index(feature_col)['mean']
)
df[f'{feature_col}_frequency'] = df[feature_col].map(
feature_stats.set_index(feature_col)['count']
)
df[f'{feature_col}_target_std'] = df[feature_col].map(
feature_stats.set_index(feature_col)['std']
)
# Fill missing values (categories with low frequency)
df[f'{feature_col}_target_mean'].fillna(df[target_col].mean(), inplace=True)
df[f'{feature_col}_frequency'].fillna(1, inplace=True)
df[f'{feature_col}_target_std'].fillna(df[target_col].std(), inplace=True)
return [f'{feature_col}_target_mean', f'{feature_col}_frequency', f'{feature_col}_target_std']Standard feature selection methods can be biased toward the majority class. Here's my approach for imbalanced data:
def balanced_feature_selection(X, y, n_features=50):
"""
Feature selection that considers class imbalance.
"""
from sklearn.utils import resample
from sklearn.feature_selection import mutual_info_classif
# Identify minority and majority classes
class_counts = pd.Series(y).value_counts()
minority_class = class_counts.idxmin()
majority_class = class_counts.idxmax()
# Separate classes
minority_indices = np.where(y == minority_class)[0]
majority_indices = np.where(y == majority_class)[0]
# Bootstrap sampling to create balanced subsets
n_bootstrap = 10
feature_scores = np.zeros(X.shape[1])
for i in range(n_bootstrap):
# Sample equal numbers from each class
sampled_majority = resample(
majority_indices,
n_samples=len(minority_indices),
random_state=i
)
balanced_indices = np.concatenate([minority_indices, sampled_majority])
X_balanced = X.iloc[balanced_indices]
y_balanced = y[balanced_indices]
# Calculate feature importance on balanced subset
scores = mutual_info_classif(X_balanced, y_balanced, random_state=i)
feature_scores += scores
# Average scores across bootstrap samples
feature_scores /= n_bootstrap
# Select top features
top_features = np.argsort(feature_scores)[-n_features:]
return X.columns[top_features].tolist(), feature_scores
The biggest mistake I made early in my career was deploying a feature selection strategy and never monitoring it. Features that were important during training can become irrelevant over time.
import json
from datetime import datetime
import numpy as np
class FeatureDriftMonitor:
def __init__(self, baseline_importances, alert_threshold=0.3):
self.baseline_importances = baseline_importances
self.alert_threshold = alert_threshold
self.importance_history = []
def check_drift(self, current_importances):
"""
Compare current feature importances to baseline.
Returns True if significant drift detected.
"""
drift_scores = {}
for feature in self.baseline_importances:
if feature in current_importances:
baseline_imp = self.baseline_importances[feature]
current_imp = current_importances[feature]
# Calculate relative change
if baseline_imp > 0:
drift = abs(current_imp - baseline_imp) / baseline_imp
else:
drift = current_imp
drift_scores[feature] = drift
# Log current state
self.importance_history.append({
'timestamp': datetime.now().isoformat(),
'importances': current_importances,
'drift_scores': drift_scores
})
# Check for significant drift
significant_drifts = {
feature: score for feature, score in drift_scores.items()
if score > self.alert_threshold
}
if significant_drifts:
print(f"ALERT: Feature drift detected for {len(significant_drifts)} features")
for feature, score in significant_drifts.items():
print(f" {feature}: {score:.3f} drift")
return True
return False
def get_drift_report(self):
"""Generate a detailed drift report."""
if not self.importance_history:
return "No monitoring data available"
latest = self.importance_history[-1]
return {
'last_check': latest['timestamp'],
'features_monitored': len(self.baseline_importances),
'significant_drifts': sum(
1 for score in latest['drift_scores'].values()
if score > self.alert_threshold
)
}When drift is detected, you need an automated system to re-evaluate feature selection:
def automated_reselection_pipeline(new_data, target, baseline_features):
"""
Automated pipeline that triggers when feature drift is detected.
"""
# Step 1: Quick validation of data quality
data_quality_check = validate_data_quality(new_data)
if not data_quality_check['passed']:
return {'status': 'failed', 'reason': 'Data quality issues'}
# Step 2: Apply same selection methodology as original
selected_features = apply_feature_selection_pipeline(new_data, target)
# Step 3: Compare with baseline
feature_overlap = len(set(selected_features) & set(baseline_features))
overlap_percentage = feature_overlap / len(baseline_features)
# Step 4: Decide whether to update
if overlap_percentage < 0.7: # Less than 70% overlap
return {
'status': 'significant_change',
'new_features': selected_features,
'overlap_percentage': overlap_percentage,
'recommendation': 'manual_review_required'
}
return {
'status': 'minor_update',
'new_features': selected_features,
'overlap_percentage': overlap_percentage,
'recommendation': 'auto_update_approved'
}
def validate_data_quality(df):
"""Basic data quality checks."""
checks = {
'missing_data': df.isnull().sum().sum() / df.size < 0.1,
'duplicate_rows': df.duplicated().sum() / len(df) < 0.05,
'feature_count_stable': abs(df.shape[1] - expected_feature_count) < 5
}
return {
'passed': all(checks.values()),
'details': checks
}Never deploy feature selection changes to 100% of traffic immediately. Here's my A/B testing framework:
class FeatureSelectionABTest:
def __init__(self, control_features, treatment_features, traffic_split=0.1):
self.control_features = control_features
self.treatment_features = treatment_features
self.traffic_split = traffic_split
self.results = {'control': [], 'treatment': []}
def get_feature_set(self, user_id):
"""Deterministically assign users to control or treatment."""
# Use hash of user_id for consistent assignment
hash_value = hash(str(user_id)) % 100
if hash_value < self.traffic_split * 100:
return self.treatment_features, 'treatment'
else:
return self.control_features, 'control'
def log_prediction_result(self, user_id, predicted_value, actual_value, group):
"""Log prediction results for analysis."""
self.results[group].append({
'user_id': user_id,
'predicted': predicted_value,
'actual': actual_value,
'timestamp': datetime.now().isoformat()
})
def analyze_results(self):
"""Analyze A/B test results."""
if len(self.results['control']) < 100 or len(self.results['treatment']) < 100:
return {'status': 'insufficient_data'}
# Calculate metrics for both groups
control_accuracy = self.calculate_accuracy(self.results['control'])
treatment_accuracy = self.calculate_accuracy(self.results['treatment'])
# Statistical significance test
p_value = self.statistical_test(self.results['control'], self.results['treatment'])
return {
'control_accuracy': control_accuracy,
'treatment_accuracy': treatment_accuracy,
'improvement': treatment_accuracy - control_accuracy,
'p_value': p_value,
'significant': p_value < 0.05,
'recommendation': 'deploy_treatment' if (
treatment_accuracy > control_accuracy and p_value < 0.05
) else 'keep_control'
}
The Challenge: Our recommendation system had 1,847 features derived from user behavior, product attributes, and contextual data. Training took 18 hours, making it impossible to incorporate daily sales data.
My Approach:
Code Implementation:
# Business-constrained RFE
def business_aware_rfe(X, y, business_groups, min_per_group=1):
"""
RFE that ensures representation from each business category.
"""
selected_features = []
# First, select minimum features per business group
for group_name, features in business_groups.items():
group_X = X[features]
# Use simple univariate selection within group
selector = SelectKBest(f_classif, k=min_per_group)
selector.fit(group_X, y)
selected_from_group = [features[i] for i in selector.get_support(indices=True)]
selected_features.extend(selected_from_group)
# Then, use RFE on remaining features
remaining_features = [f for f in X.columns if f not in selected_features]
if remaining_features:
rfe = RFE(RandomForestClassifier(), n_features_to_select=30)
rfe.fit(X[remaining_features], y)
selected_features.extend([remaining_features[i] for i in rfe.get_support(indices=True)])
return selected_features
Results:
The Surprise: Removing features actually improved recommendation quality. The model was previously overfitting to noise in rarely-used features.
The Problem: 634 features from transaction data, user profiles, and device fingerprinting. Model accuracy was good (92%) but inference latency was 300ms, too slow for real-time fraud detection.
What Didn't Work:
What Actually Worked:
# Fraud-specific feature selection
def fraud_aware_selection(X, y, fraud_threshold=0.1):
"""
Feature selection that considers class imbalance in fraud detection.
"""
fraud_mask = y == 1
normal_mask = y == 0
fraud_specific_features = []
for feature in X.columns:
# Calculate feature statistics for both classes
fraud_mean = X.loc[fraud_mask, feature].mean()
normal_mean = X.loc[normal_mask, feature].mean()
fraud_std = X.loc[fraud_mask, feature].std()
normal_std = X.loc[normal_mask, feature].std()
# Look for features with clear separation between classes
if fraud_std > 0 and normal_std > 0:
separation_score = abs(fraud_mean - normal_mean) / (fraud_std + normal_std)
if separation_score > fraud_threshold:
fraud_specific_features.append(feature)
return fraud_specific_featuresImplementation Details:
Results:
The Unique Challenge: 2,100 features from lab results, medical history, and imaging data. Privacy regulations required explainable decisions.
Regulatory Constraints:
My Specialized Approach:
# Medical domain-aware selection
def medical_feature_selection(X, y, clinical_groups, stability_threshold=0.8):
"""
Feature selection with medical domain constraints.
"""
# Phase 1: Clinical relevance filtering
clinically_relevant = []
for group_name, features in clinical_groups.items():
# Only include if clinically validated
if group_name in ['vital_signs', 'core_lab_values', 'symptoms']:
clinically_relevant.extend(features)
# Phase 2: Stability testing across time periods
stable_features = []
for feature in clinically_relevant:
# Test feature stability across different time periods
stability_scores = []
for period in ['q1', 'q2', 'q3', 'q4']:
period_data = X[X['quarter'] == period]
if len(period_data) > 100:
importance = calculate_feature_importance(period_data, y)
stability_scores.append(importance.get(feature, 0))
if len(stability_scores) > 2:
stability = 1 - (np.std(stability_scores) / np.mean(stability_scores))
if stability > stability_threshold:
stable_features.append(feature)
return stable_features
Results:
Never begin feature selection without understanding business requirements. In finance, certain features are legally required. In healthcare, clinical validity matters more than statistical significance.
A model that maintains 85% accuracy consistently is better than one that fluctuates between 90% and 75% as data changes.
# Production feature monitoring
class ProductionFeatureMonitor:
def __init__(self):
self.feature_usage = {}
self.performance_log = []
def log_prediction(self, features_used, prediction, actual):
# Track which features were actually used
for feature in features_used:
if feature not in self.feature_usage:
self.feature_usage[feature] = 0
self.feature_usage[feature] += 1
# Log performance
self.performance_log.append({
'features_count': len(features_used),
'correct': prediction == actual,
'timestamp': datetime.now()
})
def get_underused_features(self, threshold=0.01):
"""Find features used in less than 1% of predictions."""
total_predictions = len(self.performance_log)
return [
feature for feature, count in self.feature_usage.items()
if count / total_predictions < threshold
]
Document the operational impact, not just model metrics:
| Metric | Before | After | Business Impact |
|---|---|---|---|
| Training Time | 6 hours | 45 minutes | Daily model updates possible |
| Infrastructure Cost | $12,000/month | $4,800/month | $7,200/month savings |
| Debugging Time | 4 hours/incident | 30 minutes/incident | Faster incident resolution |
| Model Explainability | Complex | Simple | Easier stakeholder buy-in |
# WRONG - causes data leakage
X_selected = feature_selector.fit_transform(X_full, y_full)
X_train, X_test = train_test_split(X_selected, y_full)
# CORRECT - selection only on training data
X_train, X_test, y_train, y_test = train_test_split(X_full, y_full)
X_train_selected = feature_selector.fit_transform(X_train, y_train)
X_test_selected = feature_selector.transform(X_test)
Always consider which features were created together and might have dependencies.
Use validation data that the feature selection process hasn't seen to evaluate the final feature sets.
# Essential libraries for feature selection
from sklearn.feature_selection import (
VarianceThreshold, SelectKBest, chi2, f_classif,
mutual_info_classif, RFE, SelectFromModel
)
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LassoCV
import pandas as pd
import numpy as npStarter Template
class FeatureSelectionPipeline:
def __init__(self, correlation_threshold=0.95, variance_threshold=0.01):
self.correlation_threshold = correlation_threshold
self.variance_threshold = variance_threshold
self.selected_features = None
self.selection_history = {}
def fit(self, X, y):
"""Complete feature selection pipeline."""
print(f"Starting with {X.shape[1]} features")
# Stage 1: Variance filtering
X_filtered = self._variance_filter(X)
print(f"After variance filter: {X_filtered.shape[1]} features")
# Stage 2: Correlation filtering
X_filtered = self._correlation_filter(X_filtered, y)
print(f"After correlation filter: {X_filtered.shape[1]} features")
# Stage 3: Statistical selection
X_filtered = self._statistical_selection(X_filtered, y)
print(f"After statistical selection: {X_filtered.shape[1]} features")
# Stage 4: Model-based selection
self.selected_features = self._model_based_selection(X_filtered, y)
print(f"Final feature count: {len(self.selected_features)}")
return self
def transform(self, X):
"""Transform data using selected features."""
return X[self.selected_features]
def _variance_filter(self, X):
"""Remove low-variance features."""
selector = VarianceThreshold(self.variance_threshold)
X_filtered = X.loc[:, selector.fit(X).get_support()]
self.selection_history['variance'] = X_filtered.columns.tolist()
return X_filtered
def _correlation_filter(self, X, y):
"""Remove highly correlated features."""
corr_matrix = X.corr().abs()
upper_triangle = corr_matrix.where(
np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
)
high_corr_features = [column for column in upper_triangle.columns
if any(upper_triangle[column] > self.correlation_threshold)]
X_filtered = X.drop(columns=high_corr_features)
self.selection_history['correlation'] = X_filtered.columns.tolist()
return X_filtered
def _statistical_selection(self, X, y, k=100):
"""Select top k features based on statistical tests."""
if len(np.unique(y)) == 2: # Binary classification
selector = SelectKBest(chi2, k=min(k, X.shape[1]))
else: # Regression
selector = SelectKBest(f_classif, k=min(k, X.shape[1]))
X_filtered = X.loc[:, selector.fit(X, y).get_support()]
self.selection_history['statistical'] = X_filtered.columns.tolist()
return X_filtered
def _model_based_selection(self, X, y, n_features=50):
"""Final selection using model-based importance."""
rfe = RFE(RandomForestClassifier(n_estimators=50),
n_features_to_select=min(n_features, X.shape[1]))
rfe.fit(X, y)
selected = X.columns[rfe.support_].tolist()
self.selection_history['model_based'] = selected
return selected
In my experience, feature selection delivers some of the highest ROI improvements in machine learning projects. While algorithm improvements might give you 5-10% accuracy gains, thoughtful feature selection can:
The key insight that changed my approach: Feature selection is not about removing "bad" features—it's about finding the minimal set of features that captures the essential patterns in your data.
Start simple, measure everything, and let the data guide your decisions. The techniques in this guide have saved my teams hundreds of hours and thousands of dollars in infrastructure costs. More importantly, they've made our machine learning systems more reliable and easier to maintain.
Remember: A model that works reliably in production with 50 features is infinitely more valuable than a model that achieves slightly higher accuracy in development with 500 features but fails in the real world.
Trusted by top platforms for our transformative solutions and exceptional results:






