Feature Engineering and Preprocessing Guide
Essential techniques for preparing data and maintaining machine learning models
Introduction
Feature engineering and preprocessing are critical steps in the machine learning pipeline that can significantly impact model performance. This guide covers essential techniques for handling missing data, encoding categorical variables, scaling features, feature selection, and model deployment.
Proper data preparation not only improves model accuracy but also ensures stability, reproducibility, and efficient operation in production environments.
Handling Missing Data
Missing data is a common challenge in real-world datasets. How we handle these gaps can significantly affect our model's performance.
Detection and Visualization
First, it's essential to understand the extent and pattern of missing values in your dataset:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Check for missing values
missing_values = df.isnull().sum()
missing_percent = (missing_values / len(df)) * 100
# Display results
missing_info = pd.DataFrame({
'Missing Values': missing_values,
'Percent Missing': missing_percent
})
print(missing_info[missing_info['Missing Values'] > 0])
# Visualize missing values
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=False, yticklabels=False, cmap='viridis')
plt.title('Missing Value Heatmap')
plt.show()
Handling Techniques
Several approaches can be used to handle missing data:
Deletion
Removing rows or columns with missing values:
# Drop rows with any missing values df_dropped = df.dropna() # Drop rows only if all columns are missing df_dropped = df.dropna(how='all') # Drop columns with more than 50% missing values threshold = len(df) * 0.5 df_dropped = df.dropna(axis=1, thresh=threshold)
Note: Deletion methods are simple but can lead to information loss, especially if missing data is not completely at random.
Imputation
Filling missing values with estimated values:
# Simple imputation
df_filled = df.copy()
df_filled['numeric_column'].fillna(df_filled['numeric_column'].mean(), inplace=True) # Mean
df_filled['numeric_column'].fillna(df_filled['numeric_column'].median(), inplace=True) # Median
df_filled['categorical_column'].fillna(df_filled['categorical_column'].mode()[0], inplace=True) # Mode
# Using scikit-learn's SimpleImputer
from sklearn.impute import SimpleImputer
# For numeric columns
numeric_imputer = SimpleImputer(strategy='mean')
df_numeric = pd.DataFrame(numeric_imputer.fit_transform(df[['age', 'income']]),
columns=['age', 'income'])
# For categorical columns
cat_imputer = SimpleImputer(strategy='most_frequent')
df_cat = pd.DataFrame(cat_imputer.fit_transform(df[['education', 'occupation']]),
columns=['education', 'occupation'])
Advanced Imputation
Using more sophisticated methods to estimate missing values:
# KNN Imputation
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
df_knn_imputed = pd.DataFrame(
imputer.fit_transform(df),
columns=df.columns
)
# Iterative Imputation (MICE)
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(max_iter=10, random_state=42)
df_mice_imputed = pd.DataFrame(
imputer.fit_transform(df),
columns=df.columns
)
Adding Missing Indicators
Creating binary flags to indicate where values were missing:
# Create missing indicators
for col in ['age', 'income', 'education']:
df[f'{col}_was_missing'] = df[col].isnull().astype(int)
# Using scikit-learn's MissingIndicator
from sklearn.impute import MissingIndicator
indicator = MissingIndicator()
missing_indicators = indicator.fit_transform(df)
indicator_df = pd.DataFrame(missing_indicators, columns=[f'{col}_missing' for col in df.columns])
Categorical Variable Encoding
Machine learning algorithms generally require numerical input, so categorical variables need to be converted to numbers.
Label Encoding
Assigning a unique integer to each category (suitable for ordinal data):
from sklearn.preprocessing import LabelEncoder
# Create encoder
le = LabelEncoder()
# Example with a single column
df['encoded_education'] = le.fit_transform(df['education'])
# Get mapping
mapping = dict(zip(le.classes_, le.transform(le.classes_)))
print("Mapping:", mapping)
# Example output (mapping):
# {'high school': 0, 'bachelor': 1, 'master': 2, 'phd': 3}
Warning: Label encoding can imply an ordinal relationship where none exists. For nominal variables, one-hot encoding is usually better.
One-Hot Encoding
Creating binary columns for each category (suitable for nominal data):
# Using pandas
df_one_hot = pd.get_dummies(df, columns=['education', 'occupation'], drop_first=True)
# Using scikit-learn's OneHotEncoder
from sklearn.preprocessing import OneHotEncoder
# Create encoder (sparse matrix output by default)
encoder = OneHotEncoder(sparse=False, drop='first') # drop='first' eliminates multicollinearity
encoded_data = encoder.fit_transform(df[['education', 'occupation']])
# Convert to DataFrame with column names
encoded_df = pd.DataFrame(
encoded_data,
columns=encoder.get_feature_names_out(['education', 'occupation'])
)
# Example output columns:
# ['education_bachelor', 'education_master', 'education_phd', 'occupation_engineer', ...]
Binary Encoding
More memory-efficient for high-cardinality features:
# Install category_encoders if needed: pip install category_encoders import category_encoders as ce # Binary encoding encoder = ce.BinaryEncoder(cols=['zipcode', 'product_id']) df_binary_encoded = encoder.fit_transform(df)
Target Encoding
Using target statistics to encode categories:
# Simple target mean encoding
mean_target = df.groupby('category')['target'].mean()
df['category_encoded'] = df['category'].map(mean_target)
# Using category_encoders
encoder = ce.TargetEncoder(cols=['category'])
df_target_encoded = encoder.fit_transform(df, df['target'])
Important: Always perform target encoding within cross-validation to prevent data leakage.
Feature Scaling
Standardizing or normalizing features to the same scale to improve model performance.
StandardScaler
Standardizes features by removing the mean and scaling to unit variance (z-score normalization):
from sklearn.preprocessing import StandardScaler
# Create scaler
scaler = StandardScaler()
# Fit and transform
X_scaled = scaler.fit_transform(X)
# For DataFrames, preserving column names
X_scaled_df = pd.DataFrame(
scaler.fit_transform(df[['age', 'income', 'experience']]),
columns=['age', 'income', 'experience'],
index=df.index
)
The formula applied is: z = (x - μ) / σ
This transformation results in features with a mean of 0 and a standard deviation of 1.
MinMaxScaler
Scales features to a specified range (default [0, 1]):
from sklearn.preprocessing import MinMaxScaler
# Create scaler with default range [0, 1]
scaler = MinMaxScaler()
# Or specify a custom range, e.g., [-1, 1]
custom_scaler = MinMaxScaler(feature_range=(-1, 1))
# Fit and transform
X_minmax = scaler.fit_transform(X)
# For DataFrames
X_minmax_df = pd.DataFrame(
scaler.fit_transform(df[['age', 'income', 'experience']]),
columns=['age', 'income', 'experience'],
index=df.index
)
The formula applied is: X_scaled = (X - X_min) / (X_max - X_min)
RobustScaler
Scales features using statistics that are robust to outliers:
from sklearn.preprocessing import RobustScaler # Create scaler scaler = RobustScaler() # Fit and transform X_robust = scaler.fit_transform(X)
This scaler uses the median and interquartile range instead of the mean and variance, making it robust to outliers.
When to Use Each Scaler
| Scaler | Best Used When | Considerations |
|---|---|---|
| StandardScaler | Data follows a normal distribution; For algorithms sensitive to feature magnitudes (e.g., SVM, K-means) | Sensitive to outliers |
| MinMaxScaler | Known bounds exist; Neural networks with sigmoid activation | Very sensitive to outliers |
| RobustScaler | Data contains outliers | May not scale as efficiently as others |
Feature Selection Techniques
Selecting the most relevant features to improve model performance and reduce overfitting.
Filter Methods
Using statistical tests to select features without involving the model:
# Variance Threshold (removing constant features)
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.1) # features with variance <= 0.1 will be removed
X_selected = selector.fit_transform(X)
# Correlation with target (for regression)
import pandas as pd
import numpy as np
# Calculate correlations
correlations = df.corr()['target'].sort_values(ascending=False)
print(correlations)
# Select features with correlation above threshold
correlation_threshold = 0.2
selected_features = correlations[abs(correlations) > correlation_threshold].index.tolist()
selected_features.remove('target') # Remove target itself
X_selected = df[selected_features]
# Chi-squared test (for classification with categorical features)
from sklearn.feature_selection import chi2, SelectKBest
selector = SelectKBest(chi2, k=10) # select top 10 features
X_selected = selector.fit_transform(X, y)
selected_feature_indices = selector.get_support(indices=True)
selected_features = [X.columns[i] for i in selected_feature_indices]
Wrapper Methods
Evaluating subsets of features using the model itself:
# Recursive Feature Elimination (RFE)
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
# Create the RFE object
estimator = RandomForestClassifier(n_estimators=100, random_state=42)
selector = RFE(estimator, n_features_to_select=10, step=1)
selector = selector.fit(X, y)
# Get selected features
selected_features = X.columns[selector.support_]
print("Selected features:", selected_features)
print("Feature ranking:", selector.ranking_)
# Forward Feature Selection
from mlxtend.feature_selection import SequentialFeatureSelector
from sklearn.linear_model import LogisticRegression
sfs = SequentialFeatureSelector(
LogisticRegression(),
k_features=10,
forward=True,
scoring='accuracy',
cv=5
)
sfs.fit(X, y)
selected_features = list(X.columns[list(sfs.k_feature_idx_)])
Embedded Methods
Using models that perform feature selection as part of their algorithm:
# LASSO Regression
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
# Scale features first
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Fit LASSO model
lasso = Lasso(alpha=0.1)
lasso.fit(X_scaled, y)
# Get feature importance
feature_importance = pd.DataFrame({
'Feature': X.columns,
'Coefficient': lasso.coef_
})
feature_importance = feature_importance.sort_values(by='Coefficient', key=abs, ascending=False)
print(feature_importance)
# Random Forest Feature Importance
from sklearn.ensemble import RandomForestClassifier
import matplotlib.pyplot as plt
# Train model
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
# Get feature importances
feature_importance = pd.DataFrame({
'Feature': X.columns,
'Importance': rf.feature_importances_
})
feature_importance = feature_importance.sort_values('Importance', ascending=False)
# Plot
plt.figure(figsize=(10, 6))
plt.barh(feature_importance['Feature'], feature_importance['Importance'])
plt.xlabel('Importance')
plt.title('Feature Importance')
plt.tight_layout()
plt.show()
Dimensionality Reduction
Techniques that transform the feature space:
# Principal Component Analysis (PCA)
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# Create PCA object
pca = PCA(n_components=0.95) # Keep components that explain 95% of variance
# Fit and transform
X_pca = pca.fit_transform(X_scaled) # Use scaled data
# Plot explained variance
plt.figure(figsize=(10, 6))
plt.plot(np.cumsum(pca.explained_variance_ratio_))
plt.xlabel('Number of Components')
plt.ylabel('Cumulative Explained Variance')
plt.grid(True)
plt.show()
# Check shape after PCA
print(f"Original shape: {X.shape}, PCA shape: {X_pca.shape}")
# t-SNE for visualization
from sklearn.manifold import TSNE
# Create t-SNE object
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X_scaled)
# Plot
plt.figure(figsize=(10, 8))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis')
plt.colorbar()
plt.title('t-SNE Visualization')
plt.show()