Machine learning / Feature engineering and preprocessing
When Feature Scaling Matters in Machine Learning
Contents
- Understanding Feature Scaling Fundamentals
- Scale-Sensitive Algorithms Implementation
- Scale-Invariant Algorithms
- Custom Scaling Implementation
- Real-world Application: Credit Card Fraud Detection
- Performance Impact Analysis
- Scale-Invariant Feature Engineering
- Automated Feature Scaling Pipeline
- Mathematical Foundations of Scaling Methods
- Real-world Application: Time Series Feature Scaling
- Gradient Impact Analysis
- Online Feature Scaling Implementation
- Cross-Validation with Feature Scaling
- Additional Resources
Understanding Feature Scaling Fundamentals
Feature scaling transforms data into a specific range, typically [0,1] or [-1,1], to normalize the influence of features with different magnitudes. This process involves mathematical transformations that preserve relative relationships while standardizing scale differences between variables.
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler
# Sample dataset with different scales
data = np.array([[1000, 0.5],
[2000, 0.8],
[3000, 0.2]])
# MinMaxScaler transforms features to [0,1] range
minmax_scaler = MinMaxScaler()
minmax_scaled = minmax_scaler.fit_transform(data)
# StandardScaler transforms to zero mean and unit variance
standard_scaler = StandardScaler()
standard_scaled = standard_scaler.fit_transform(data)
print("Original data:\n", data)
print("\nMinMax scaled:\n", minmax_scaled)
print("\nStandard scaled:\n", standard_scaled)
Scale-Sensitive Algorithms Implementation
Scale-sensitive algorithms like Support Vector Machines and k-Nearest Neighbors require feature scaling for optimal performance. This implementation demonstrates the impact of scaling on SVM classification using a practical example with mixed-scale features.
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# Generate sample data with different scales
np.random.seed(42)
X = np.column_stack([
np.random.normal(1000, 100, 1000), # Feature 1: large scale
np.random.normal(0, 0.1, 1000) # Feature 2: small scale
])
y = (X[:, 0] * 10 + X[:, 1] > 10000).astype(int)
# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Without scaling
svm_no_scale = SVC(kernel='rbf')
svm_no_scale.fit(X_train, y_train)
pred_no_scale = svm_no_scale.predict(X_test)
# With scaling
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
svm_scaled = SVC(kernel='rbf')
svm_scaled.fit(X_train_scaled, y_train)
pred_scaled = svm_scaled.predict(X_test_scaled)
print(f"Accuracy without scaling: {accuracy_score(y_test, pred_no_scale):.3f}")
print(f"Accuracy with scaling: {accuracy_score(y_test, pred_scaled):.3f}")
Scale-Invariant Algorithms
Decision trees and random forests inherently handle different feature scales due to their splitting mechanism based on individual feature thresholds. This implementation compares scaled and unscaled data performance with decision trees.
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import make_classification
# Create dataset with mixed scales
X, y = make_classification(n_samples=1000, n_features=4, random_state=42)
X[:, 0] *= 1000 # Scale up first feature
X[:, 1] *= 0.001 # Scale down second feature
# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Without scaling
dt_no_scale = DecisionTreeClassifier(random_state=42)
dt_no_scale.fit(X_train, y_train)
pred_no_scale = dt_no_scale.predict(X_test)
# With scaling
X_train_scaled = StandardScaler().fit_transform(X_train)
X_test_scaled = StandardScaler().fit_transform(X_test)
dt_scaled = DecisionTreeClassifier(random_state=42)
dt_scaled.fit(X_train_scaled, y_train)
pred_scaled = dt_scaled.predict(X_test_scaled)
print(f"Decision Tree accuracy without scaling: {accuracy_score(y_test, pred_no_scale):.3f}")
print(f"Decision Tree accuracy with scaling: {accuracy_score(y_test, pred_scaled):.3f}")
Custom Scaling Implementation
Understanding the mathematics behind scaling methods enables custom implementation for specific requirements. This implementation creates a robust scaler that handles outliers using the interquartile range method.
class RobustCustomScaler:
def __init__(self):
self.q1 = None
self.q3 = None
self.iqr = None
self.median = None
def fit(self, X):
self.q1 = np.percentile(X, 25, axis=0)
self.q3 = np.percentile(X, 75, axis=0)
self.iqr = self.q3 - self.q1
self.median = np.median(X, axis=0)
return self
def transform(self, X):
return (X - self.median) / (self.iqr + 1e-8)
def fit_transform(self, X):
self.fit(X)
return self.transform(X)
# Example usage
X = np.random.normal(100, 20, (1000, 2))
X[0] = [1000, 1000] # Add outliers
scaler = RobustCustomScaler()
X_scaled = scaler.fit_transform(X)
print("Original data statistics:")
print(f"Mean: {np.mean(X, axis=0)}")
print(f"Std: {np.std(X, axis=0)}")
print("\nScaled data statistics:")
print(f"Mean: {np.mean(X_scaled, axis=0)}")
print(f"Std: {np.std(X_scaled, axis=0)}")
Real-world Application: Credit Card Fraud Detection
Credit card fraud detection involves features with vastly different scales, from transaction amounts to time deltas. This implementation demonstrates proper scaling in a fraud detection scenario with imbalanced classes.
import pandas as pd
from sklearn.preprocessing import RobustScaler
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import GradientBoostingClassifier
from imblearn.over_sampling import SMOTE
# Generate synthetic credit card data
np.random.seed(42)
n_samples = 10000
data = {
'amount': np.random.lognormal(3, 1, n_samples),
'time_delta': np.random.normal(3600, 1800, n_samples),
'merchant_score': np.random.uniform(0, 1, n_samples),
'is_fraud': np.random.binomial(1, 0.001, n_samples)
}
df = pd.DataFrame(data)
# Separate features and target
X = df.drop('is_fraud', axis=1)
y = df['is_fraud']
# Scale features
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)
# Apply SMOTE for class imbalance
smote = SMOTE(random_state=42)
X_balanced, y_balanced = smote.fit_resample(X_scaled, y)
# Train and evaluate model
model = GradientBoostingClassifier(random_state=42)
scores = cross_val_score(model, X_balanced, y_balanced, cv=5, scoring='roc_auc')
print(f"ROC-AUC scores: {scores}")
print(f"Mean ROC-AUC: {scores.mean():.3f} ± {scores.std():.3f}")
Performance Impact Analysis
When working with neural networks and gradient-based optimization, feature scaling significantly impacts convergence speed and final model performance. This experiment quantifies the impact using a controlled comparison.
from sklearn.neural_network import MLPClassifier
import time
from sklearn.datasets import make_classification
# Generate dataset with extreme feature scales
X, y = make_classification(n_samples=5000, n_features=20, random_state=42)
X[:, :10] *= 1000 # Scale up first 10 features
X[:, 10:] *= 0.001 # Scale down last 10 features
# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Train without scaling
start_time = time.time()
mlp_no_scale = MLPClassifier(hidden_layer_sizes=(100, 50), max_iter=1000)
mlp_no_scale.fit(X_train, y_train)
time_no_scale = time.time() - start_time
score_no_scale = mlp_no_scale.score(X_test, y_test)
# Train with scaling
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
start_time = time.time()
mlp_scaled = MLPClassifier(hidden_layer_sizes=(100, 50), max_iter=1000)
mlp_scaled.fit(X_train_scaled, y_train)
time_scaled = time.time() - start_time
score_scaled = mlp_scaled.score(X_test_scaled, y_test)
print(f"Without scaling - Time: {time_no_scale:.2f}s, Accuracy: {score_no_scale:.3f}")
print(f"With scaling - Time: {time_scaled:.2f}s, Accuracy: {score_scaled:.3f}")
print(f"Convergence iterations - No scaling: {mlp_no_scale.n_iter_}, Scaling: {mlp_scaled.n_iter_}")
Scale-Invariant Feature Engineering
Creating scale-invariant features through ratio-based transformations can eliminate the need for scaling while preserving important relationships in the data. This implementation demonstrates effective feature engineering techniques.
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
# Create sample financial dataset
np.random.seed(42)
n_samples = 1000
data = {
'total_assets': np.random.lognormal(10, 1, n_samples),
'current_assets': np.random.lognormal(8, 1, n_samples),
'total_liabilities': np.random.lognormal(9, 1, n_samples),
'revenue': np.random.lognormal(8, 1, n_samples),
'net_income': np.random.lognormal(6, 1, n_samples)
}
df = pd.DataFrame(data)
# Create scale-invariant ratios
df['current_ratio'] = df['current_assets'] / df['total_assets']
df['debt_ratio'] = df['total_liabilities'] / df['total_assets']
df['profit_margin'] = df['net_income'] / df['revenue']
# Create target variable (simplified example)
df['performance_good'] = (df['profit_margin'] > df['profit_margin'].median()).astype(int)
# Compare models with original vs ratio features
X_original = df[['total_assets', 'current_assets', 'total_liabilities', 'revenue', 'net_income']]
X_ratios = df[['current_ratio', 'debt_ratio', 'profit_margin']]
y = df['performance_good']
# Evaluate both feature sets
rf_original = RandomForestClassifier(random_state=42)
rf_ratios = RandomForestClassifier(random_state=42)
scores_original = cross_val_score(rf_original, X_original, y, cv=5)
scores_ratios = cross_val_score(rf_ratios, X_ratios, y, cv=5)
print("Original features accuracy: {:.3f} ± {:.3f}".format(
scores_original.mean(), scores_original.std()))
print("Ratio features accuracy: {:.3f} ± {:.3f}".format(
scores_ratios.mean(), scores_ratios.std()))
Automated Feature Scaling Pipeline
Implementing an automated pipeline that selectively applies scaling based on algorithm requirements ensures optimal preprocessing while maintaining efficiency. This implementation creates a smart scaling pipeline.
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.base import BaseEstimator, TransformerMixin
class ScaleSelector(BaseEstimator, TransformerMixin):
def __init__(self, scale_threshold=5):
self.scale_threshold = scale_threshold
self.requires_scaling = None
def fit(self, X, y=None):
# Determine which features need scaling based on variance ratio
variances = np.var(X, axis=0)
max_var = np.max(variances)
self.requires_scaling = variances / max_var < 1/self.scale_threshold
return self
def transform(self, X):
X = np.array(X)
X_transformed = X.copy()
scaler = StandardScaler()
X_transformed[:, self.requires_scaling] = scaler.fit_transform(
X[:, self.requires_scaling])
return X_transformed
# Example usage with mixed algorithm types
def create_smart_pipeline(algorithm):
if isinstance(algorithm, (SVC, MLPClassifier, KNeighborsClassifier)):
return Pipeline([
('scaler', StandardScaler()),
('classifier', algorithm)
])
else:
return Pipeline([
('scale_selector', ScaleSelector()),
('classifier', algorithm)
])
# Test with different algorithms
algorithms = {
'svm': SVC(),
'rf': RandomForestClassifier(),
'mlp': MLPClassifier()
}
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X[:, :5] *= 1000 # Create scale differences
results = {}
for name, algo in algorithms.items():
pipeline = create_smart_pipeline(algo)
scores = cross_val_score(pipeline, X, y, cv=5)
results[name] = (scores.mean(), scores.std())
for name, (mean, std) in results.items():
print(f"{name}: {mean:.3f} ± {std:.3f}")
Mathematical Foundations of Scaling Methods
Understanding the mathematical transformations behind different scaling methods enables better selection for specific use cases. This implementation demonstrates the relationship between various scaling approaches.
class ScalingMethods:
@staticmethod
def minmax_scale(X):
"""
$$x_{scaled} = \frac{x - x_{min}}{x_{max} - x_{min}}$$
"""
return (X - np.min(X)) / (np.max(X) - np.min(X))
@staticmethod
def standard_scale(X):
"""
$$x_{scaled} = \frac{x - \mu}{\sigma}$$
"""
return (X - np.mean(X)) / np.std(X)
@staticmethod
def robust_scale(X):
"""
$$x_{scaled} = \frac{x - median}{IQR}$$
"""
q1 = np.percentile(X, 25)
q3 = np.percentile(X, 75)
iqr = q3 - q1
return (X - np.median(X)) / iqr
@staticmethod
def max_abs_scale(X):
"""
$$x_{scaled} = \frac{x}{|x|_{max}}$$
"""
return X / np.max(np.abs(X))
# Demonstrate scaling methods with outliers
np.random.seed(42)
X = np.random.normal(100, 15, 1000)
X = np.append(X, [0, 200]) # Add outliers
methods = {
'MinMax': ScalingMethods.minmax_scale,
'Standard': ScalingMethods.standard_scale,
'Robust': ScalingMethods.robust_scale,
'MaxAbs': ScalingMethods.max_abs_scale
}
results = {name: method(X.copy()) for name, method in methods.items()}
for name, scaled_data in results.items():
print(f"\n{name} Scaling Statistics:")
print(f"Mean: {np.mean(scaled_data):.3f}")
print(f"Std: {np.std(scaled_data):.3f}")
print(f"Range: [{np.min(scaled_data):.3f}, {np.max(scaled_data):.3f}]")
Real-world Application: Time Series Feature Scaling
Time series data requires special consideration for feature scaling to prevent data leakage and maintain temporal relationships. This implementation shows proper scaling techniques for time series forecasting.
import pandas as pd
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
class TimeSeriesScaler:
def __init__(self, window_size=30):
self.window_size = window_size
self.scalers = {}
def transform(self, data, sequence=True):
scaled_data = np.zeros_like(data, dtype=np.float32)
if sequence:
# Rolling window scaling
for i in range(len(data)):
start_idx = max(0, i - self.window_size)
scaler = StandardScaler()
window_data = data[start_idx:i+1]
scaler.fit(window_data.reshape(-1, 1))
scaled_data[i] = scaler.transform([[data[i]]])[0]
else:
# Global scaling
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data.reshape(-1, 1)).ravel()
return scaled_data
# Generate sample time series data
np.random.seed(42)
t = np.linspace(0, 365, 1000)
trend = 0.01 * t
seasonality = 10 * np.sin(2 * np.pi * t / 365)
noise = np.random.normal(0, 1, 1000)
y = trend + seasonality + noise
# Compare rolling vs global scaling
ts_scaler = TimeSeriesScaler(window_size=30)
rolled_scaled = ts_scaler.transform(y, sequence=True)
global_scaled = ts_scaler.transform(y, sequence=False)
# Plot results
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.plot(y[:100], label='Original', alpha=0.7)
plt.plot(rolled_scaled[:100], label='Rolling Scale', alpha=0.7)
plt.plot(global_scaled[:100], label='Global Scale', alpha=0.7)
plt.legend()
plt.title('Time Series Scaling Comparison')
print("Rolling scale stats:", np.mean(rolled_scaled), np.std(rolled_scaled))
print("Global scale stats:", np.mean(global_scaled), np.std(global_scaled))
Gradient Impact Analysis
Feature scaling significantly affects gradient-based optimization. This implementation visualizes the impact of scaling on gradient descent convergence paths for logistic regression.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
class GradientVisualizer:
def __init__(self, X, y, scale=False):
self.X = X.copy()
if scale:
self.X = StandardScaler().fit_transform(self.X)
self.y = y
self.weights_history = []
def custom_gradient_descent(self, learning_rate=0.01, n_iterations=100):
n_samples, n_features = self.X.shape
weights = np.zeros(n_features)
for _ in range(n_iterations):
# Compute predictions
z = np.dot(self.X, weights)
predictions = 1 / (1 + np.exp(-z))
# Compute gradients
gradients = np.dot(self.X.T, (predictions - self.y)) / n_samples
# Update weights
weights -= learning_rate * gradients
self.weights_history.append(weights.copy())
return np.array(self.weights_history)
# Generate dataset with different scales
np.random.seed(42)
X = np.random.randn(1000, 2)
X[:, 0] *= 100 # Scale up first feature
y = (X[:, 0] + X[:, 1] > 0).astype(int)
# Compare convergence paths
viz_unscaled = GradientVisualizer(X, y, scale=False)
viz_scaled = GradientVisualizer(X, y, scale=True)
paths_unscaled = viz_unscaled.custom_gradient_descent()
paths_scaled = viz_scaled.custom_gradient_descent()
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(paths_unscaled[:, 0], paths_unscaled[:, 1], 'r-', label='Unscaled')
plt.title('Convergence Path (Unscaled)')
plt.xlabel('Weight 1')
plt.ylabel('Weight 2')
plt.subplot(1, 2, 2)
plt.plot(paths_scaled[:, 0], paths_scaled[:, 1], 'b-', label='Scaled')
plt.title('Convergence Path (Scaled)')
plt.xlabel('Weight 1')
plt.ylabel('Weight 2')
print("Final weights (unscaled):", paths_unscaled[-1])
print("Final weights (scaled):", paths_scaled[-1])
Online Feature Scaling Implementation
Online feature scaling is crucial for streaming data where the full dataset is not available upfront. This implementation demonstrates an adaptive scaling approach for real-time data processing.
class OnlineScaler:
def __init__(self, alpha=0.1):
self.alpha = alpha
self.mean = None
self.variance = None
self.n_samples = 0
def partial_fit(self, x):
x = np.array(x).reshape(-1)
if self.mean is None:
self.mean = np.zeros_like(x, dtype=float)
self.variance = np.ones_like(x, dtype=float)
self.n_samples += 1
# Update running mean
delta = x - self.mean
self.mean += self.alpha * delta
# Update running variance
delta2 = x - self.mean
self.variance = (1 - self.alpha) * self.variance + \
self.alpha * (delta * delta2)
def transform(self, x):
return (x - self.mean) / np.sqrt(self.variance + 1e-8)
def partial_fit_transform(self, x):
self.partial_fit(x)
return self.transform(x)
# Simulate streaming data
np.random.seed(42)
stream_size = 1000
online_scaler = OnlineScaler(alpha=0.1)
batch_scaler = StandardScaler()
# Generate data with drift
t = np.linspace(0, 10, stream_size)
drift = 0.1 * t
streaming_data = np.random.normal(0, 1, stream_size) + drift
# Process data in streaming fashion
online_scaled = np.zeros(stream_size)
batch_scaled = np.zeros(stream_size)
for i in range(stream_size):
online_scaled[i] = online_scaler.partial_fit_transform(streaming_data[i])
# For comparison, compute batch scaling at each step
batch_scaled[i] = batch_scaler.fit_transform(
streaming_data[:i+1].reshape(-1, 1))[-1]
# Compare results
print("Online Scaling Statistics:")
print(f"Mean: {np.mean(online_scaled):.3f}")
print(f"Std: {np.std(online_scaled):.3f}")
print("\nBatch Scaling Statistics:")
print(f"Mean: {np.mean(batch_scaled):.3f}")
print(f"Std: {np.std(batch_scaled):.3f}")
Cross-Validation with Feature Scaling
Proper implementation of feature scaling in cross-validation requires careful handling to prevent data leakage. This implementation demonstrates correct scaling within cross-validation folds.
from sklearn.model_selection import KFold
from sklearn.base import clone
class LeakFreeScalingCV:
def __init__(self, estimator, scaler, n_splits=5):
self.estimator = estimator
self.scaler = scaler
self.n_splits = n_splits
def evaluate(self, X, y):
kf = KFold(n_splits=self.n_splits, shuffle=True, random_state=42)
scores_correct = []
scores_leaky = []
# Incorrect approach (leaky)
X_scaled_leaky = self.scaler.fit_transform(X)
for train_idx, test_idx in kf.split(X):
# Correct approach
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# Scale within fold
scaler = clone(self.scaler)
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Train and evaluate (correct approach)
model = clone(self.estimator)
model.fit(X_train_scaled, y_train)
scores_correct.append(model.score(X_test_scaled, y_test))
# Train and evaluate (leaky approach)
model_leaky = clone(self.estimator)
model_leaky.fit(X_scaled_leaky[train_idx], y_train)
scores_leaky.append(model_leaky.score(X_scaled_leaky[test_idx], y_test))
return np.mean(scores_correct), np.mean(scores_leaky)
# Example usage
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
X[:, :10] *= 1000 # Create scale differences
cv_evaluator = LeakFreeScalingCV(
estimator=LogisticRegression(),
scaler=StandardScaler(),
n_splits=5
)
score_correct, score_leaky = cv_evaluator.evaluate(X, y)
print("Cross-validation results:")
print(f"Correct scaling implementation: {score_correct:.3f}")
print(f"Leaky scaling implementation: {score_leaky:.3f}")
print(f"Performance difference: {abs(score_correct - score_leaky):.3f}")
Additional Resources
- https://arxiv.org/abs/1811.03600 - "On Feature Scaling Methods for Neural Networks"
- https://arxiv.org/abs/2002.11321 - "The Effect of Feature Scaling on Deep Neural Network Training"
- https://arxiv.org/abs/1908.08160 - "An Analysis of Feature Scaling Methods for Support Vector Machines"
- https://arxiv.org/abs/2106.11342 - "Adaptive Feature Scaling for Deep Learning Models"
- https://arxiv.org/abs/2003.03253 - "Feature Scaling in High-Dimensional Machine Learning Problems"