# Import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.linear_model import LinearRegression, Ridge, RidgeCV, Lasso, LassoCV, ElasticNet, ElasticNetCV
from sklearn.metrics import mean_squared_error, r2_score
import warnings
warnings.filterwarnings('ignore')
print("Packages loaded!")Day 2: Regularized Regression (LASSO, Ridge, Elastic Net)
WISE Workshop | Addis Ababa, Feb 2026
In this notebook, you’ll apply regularization techniques to prevent overfitting in supply chain demand prediction.
Setup
Part 1: Data Preparation
We’ll create a dataset with many features to see regularization in action.
# Create sample supply chain data with many features
np.random.seed(42)
n_rows = 1000
# Generate dates and basic features
dates = pd.date_range('2023-01-01', periods=n_rows, freq='D')
regions = np.random.choice(['Addis Ababa', 'Oromia', 'Amhara', 'SNNP', 'Tigray'], n_rows)
facility_types = np.random.choice(['Hospital', 'Health Center', 'Clinic'], n_rows, p=[0.2, 0.5, 0.3])
# Create demand with clear patterns
base_demand = 100
facility_effect = np.where(facility_types == 'Hospital', 80,
np.where(facility_types == 'Health Center', 30, 0))
region_effect = np.where(regions == 'Addis Ababa', 50,
np.where(regions == 'Oromia', 20, 0))
seasonal_effect = 25 * np.sin(2 * np.pi * dates.dayofyear / 365)
noise = np.random.normal(0, 15, n_rows)
demand = base_demand + facility_effect + region_effect + seasonal_effect + noise
demand = np.maximum(demand, 10)
df = pd.DataFrame({
'date': dates,
'region': regions,
'facility_type': facility_types,
'demand': demand.astype(int)
})
print(f"Data shape: {df.shape}")
df.head()# Feature Engineering: Create MANY features (some useful, some noise)
# Time-based features
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
df['quarter'] = df['date'].dt.quarter
df['day_of_year'] = df['date'].dt.dayofyear
df['week_of_year'] = df['date'].dt.isocalendar().week.astype(int)
df['is_weekend'] = (df['day_of_week'] >= 5).astype(int)
# Cyclical encoding for month (sine/cosine)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
# Create dummy variables for categorical features
region_dummies = pd.get_dummies(df['region'], prefix='region', drop_first=False)
facility_dummies = pd.get_dummies(df['facility_type'], prefix='facility', drop_first=False)
# Add some noise features (to see if LASSO eliminates them)
for i in range(5):
df[f'noise_{i}'] = np.random.normal(0, 1, n_rows)
# Combine all features
df = pd.concat([df, region_dummies, facility_dummies], axis=1)
print(f"After feature engineering: {df.shape}")
print(f"\nFeature columns:")
print([c for c in df.columns if c not in ['date', 'region', 'facility_type', 'demand']])# Define features and target
feature_cols = [c for c in df.columns if c not in ['date', 'region', 'facility_type', 'demand']]
X = df[feature_cols]
y = df['demand']
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# IMPORTANT: Scale features for regularization
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(f"Number of features: {len(feature_cols)}")
print(f"Training samples: {X_train.shape[0]}")
print(f"Test samples: {X_test.shape[0]}")Part 2: Ridge Regression
Ridge regression adds an L2 penalty: \(\lambda \sum \beta_j^2\)
This shrinks all coefficients toward zero but never sets them exactly to zero.
# Fit Ridge with different alpha (lambda) values
alphas = [0.001, 0.01, 0.1, 1, 10, 100, 1000]
ridge_coefs = []
for alpha in alphas:
ridge = Ridge(alpha=alpha)
ridge.fit(X_train_scaled, y_train)
ridge_coefs.append(ridge.coef_)
ridge_coefs = np.array(ridge_coefs)# Visualize coefficient shrinkage with Ridge
fig, ax = plt.subplots(figsize=(12, 6))
for i in range(ridge_coefs.shape[1]):
ax.plot(np.log10(alphas), ridge_coefs[:, i], linewidth=1)
ax.axhline(y=0, color='black', linestyle='--', alpha=0.5)
ax.set_xlabel('log10(alpha)', fontsize=12)
ax.set_ylabel('Coefficient Value', fontsize=12)
ax.set_title('Ridge Regression: Coefficient Paths\n(All coefficients shrink, but none reach zero)', fontsize=14)
plt.tight_layout()
plt.show()# Use cross-validation to find optimal alpha
alphas_cv = np.logspace(-3, 3, 50)
ridge_cv = RidgeCV(alphas=alphas_cv, cv=5, scoring='neg_mean_squared_error')
ridge_cv.fit(X_train_scaled, y_train)
print(f"Optimal alpha: {ridge_cv.alpha_:.4f}")
# Evaluate on test set
ridge_pred = ridge_cv.predict(X_test_scaled)
ridge_rmse = np.sqrt(mean_squared_error(y_test, ridge_pred))
ridge_r2 = r2_score(y_test, ridge_pred)
print(f"Ridge Test RMSE: {ridge_rmse:.2f}")
print(f"Ridge Test R²: {ridge_r2:.3f}")Part 3: LASSO Regression
LASSO adds an L1 penalty: \(\lambda \sum |\beta_j|\)
This shrinks coefficients AND can set them exactly to zero (feature selection!).
# Fit LASSO with different alpha values
alphas_lasso = [0.001, 0.01, 0.1, 0.5, 1, 2, 5, 10]
lasso_coefs = []
for alpha in alphas_lasso:
lasso = Lasso(alpha=alpha, max_iter=10000)
lasso.fit(X_train_scaled, y_train)
lasso_coefs.append(lasso.coef_)
lasso_coefs = np.array(lasso_coefs)# Visualize coefficient paths with LASSO
fig, ax = plt.subplots(figsize=(12, 6))
for i in range(lasso_coefs.shape[1]):
ax.plot(np.log10(alphas_lasso), lasso_coefs[:, i], linewidth=1)
ax.axhline(y=0, color='black', linestyle='--', alpha=0.5)
ax.set_xlabel('log10(alpha)', fontsize=12)
ax.set_ylabel('Coefficient Value', fontsize=12)
ax.set_title('LASSO Regression: Coefficient Paths\n(Watch coefficients go to EXACTLY zero!)', fontsize=14)
plt.tight_layout()
plt.show()# Use cross-validation to find optimal alpha for LASSO
lasso_cv = LassoCV(alphas=np.logspace(-3, 1, 50), cv=5, max_iter=10000)
lasso_cv.fit(X_train_scaled, y_train)
print(f"Optimal alpha: {lasso_cv.alpha_:.4f}")
# Evaluate on test set
lasso_pred = lasso_cv.predict(X_test_scaled)
lasso_rmse = np.sqrt(mean_squared_error(y_test, lasso_pred))
lasso_r2 = r2_score(y_test, lasso_pred)
print(f"LASSO Test RMSE: {lasso_rmse:.2f}")
print(f"LASSO Test R²: {lasso_r2:.3f}")# See which features LASSO selected (non-zero coefficients)
lasso_coef_df = pd.DataFrame({
'feature': feature_cols,
'coefficient': lasso_cv.coef_
})
# Count zero and non-zero coefficients
n_selected = (lasso_coef_df['coefficient'] != 0).sum()
n_eliminated = (lasso_coef_df['coefficient'] == 0).sum()
print(f"Features SELECTED by LASSO: {n_selected}")
print(f"Features ELIMINATED by LASSO: {n_eliminated}")
print(f"\nSelected features:")
print(lasso_coef_df[lasso_coef_df['coefficient'] != 0].sort_values('coefficient', key=abs, ascending=False))# Visualize LASSO feature selection
fig, ax = plt.subplots(figsize=(12, 6))
# Sort by absolute coefficient value
sorted_df = lasso_coef_df.copy()
sorted_df['abs_coef'] = sorted_df['coefficient'].abs()
sorted_df = sorted_df.sort_values('abs_coef', ascending=True)
colors = ['green' if c != 0 else 'gray' for c in sorted_df['coefficient']]
ax.barh(range(len(sorted_df)), sorted_df['coefficient'], color=colors)
ax.set_yticks(range(len(sorted_df)))
ax.set_yticklabels(sorted_df['feature'], fontsize=8)
ax.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
ax.set_xlabel('Coefficient Value', fontsize=12)
ax.set_title(f'LASSO Feature Selection\n(Green = Selected, Gray = Eliminated)', fontsize=14)
plt.tight_layout()
plt.show()🤔 Discussion
Look at which features LASSO selected: - Did it keep the region and facility features (which have real effects)? - Did it eliminate the noise features? - What about the cyclical month encoding?
Part 4: Elastic Net
Elastic Net combines L1 and L2 penalties:
\(\text{Loss} = \text{MSE} + \lambda_1 \sum |\beta_j| + \lambda_2 \sum \beta_j^2\)
This gives you the best of both worlds: feature selection (L1) + grouped selection of correlated features (L2).
# Fit Elastic Net with cross-validation
# l1_ratio controls the mix: 1 = LASSO, 0 = Ridge
elastic_cv = ElasticNetCV(
l1_ratio=[0.1, 0.5, 0.7, 0.9, 0.95, 0.99],
alphas=np.logspace(-3, 1, 30),
cv=5,
max_iter=10000
)
elastic_cv.fit(X_train_scaled, y_train)
print(f"Optimal alpha: {elastic_cv.alpha_:.4f}")
print(f"Optimal l1_ratio: {elastic_cv.l1_ratio_:.2f}")
# Evaluate on test set
elastic_pred = elastic_cv.predict(X_test_scaled)
elastic_rmse = np.sqrt(mean_squared_error(y_test, elastic_pred))
elastic_r2 = r2_score(y_test, elastic_pred)
print(f"Elastic Net Test RMSE: {elastic_rmse:.2f}")
print(f"Elastic Net Test R²: {elastic_r2:.3f}")# Count features selected by Elastic Net
n_selected_elastic = (elastic_cv.coef_ != 0).sum()
print(f"Features selected by Elastic Net: {n_selected_elastic}")Part 5: Model Comparison
# Fit OLS for baseline
ols = LinearRegression()
ols.fit(X_train_scaled, y_train)
ols_pred = ols.predict(X_test_scaled)
ols_rmse = np.sqrt(mean_squared_error(y_test, ols_pred))
ols_r2 = r2_score(y_test, ols_pred)# Summary comparison table
results = pd.DataFrame({
'Model': ['OLS (Baseline)', 'Ridge', 'LASSO', 'Elastic Net'],
'Test RMSE': [ols_rmse, ridge_rmse, lasso_rmse, elastic_rmse],
'Test R²': [ols_r2, ridge_r2, lasso_r2, elastic_r2],
'Features Used': [
len(feature_cols),
len(feature_cols), # Ridge uses all
n_selected,
n_selected_elastic
]
})
print("Model Comparison:")
display(results)# Visualize comparison
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# RMSE comparison
colors = ['gray', 'blue', 'green', 'orange']
axes[0].bar(results['Model'], results['Test RMSE'], color=colors)
axes[0].set_ylabel('Test RMSE')
axes[0].set_title('Model Performance (Lower is Better)')
axes[0].tick_params(axis='x', rotation=45)
# Features used comparison
axes[1].bar(results['Model'], results['Features Used'], color=colors)
axes[1].set_ylabel('Number of Features')
axes[1].set_title('Model Complexity (Features Used)')
axes[1].tick_params(axis='x', rotation=45)
plt.tight_layout()
plt.show()# Predictions vs Actual plot
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
models = [('OLS', ols_pred), ('Ridge', ridge_pred), ('LASSO', lasso_pred), ('Elastic Net', elastic_pred)]
for ax, (name, pred) in zip(axes.flat, models):
ax.scatter(y_test, pred, alpha=0.5)
ax.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
ax.set_xlabel('Actual Demand')
ax.set_ylabel('Predicted Demand')
ax.set_title(name)
plt.tight_layout()
plt.show()Summary
In this notebook, you:
- Created features including some noise features
- Applied Ridge regression and saw all coefficients shrink
- Applied LASSO regression and saw automatic feature selection
- Applied Elastic Net combining both approaches
- Compared performance across all methods
Key Takeaways
| Method | What it does | When to use |
|---|---|---|
| Ridge | Shrinks all coefficients | Many small/medium effects |
| LASSO | Sets some coefficients to zero | Feature selection needed |
| Elastic Net | Combines both | Correlated features + selection |
Connection to This Afternoon
You now have a LASSO baseline to compare against: - Decision Trees - Random Forests - Gradient Boosting
These tree-based methods handle nonlinearities differently but also have their own regularization (max_depth, min_samples_leaf, etc.).
Next: Tree-Based Methods notebook