Feature Engineering: The Art of Creating Powerful Features
π What You'll Learn
By the end of this lesson, you will be able to:
- Transform numerical features with scaling, binning, log/power transforms, and outlier handling
- Encode categorical variables using one-hot, ordinal, frequency, and target encoding
- Extract features from datetime and text data (cyclical time features, TF-IDF, and more)
- Create interaction and polynomial features to capture non-linear relationships
- Apply advanced techniques such as embeddings and dimensionality-aware feature construction
- Automate repeatable feature pipelines and validate that new features actually improve the model
β±οΈ Estimated Time: 45β60 minutes
π― Project: Take a raw tabular dataset and engineer a richer feature set β numeric transforms, encoded categoricals, and datetime features β then measure the lift on a baseline model.
The Secret Weapon of Machine Learning π οΈ
"Coming up with features is difficult, time-consuming, requires expert knowledge. 'Applied machine learning' is basically feature engineering." - Andrew Ng. The quality of your features often matters more than the choice of algorithm. Master feature engineering, and you'll dramatically improve your model's performance!
The Feature Engineering Pipeline
# Essential imports for feature engineering
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import (StandardScaler, MinMaxScaler, RobustScaler,
PolynomialFeatures, OneHotEncoder, LabelEncoder,
Binarizer, KBinsDiscretizer)
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')
# Set visualization style
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
print("="*60)
print("FEATURE ENGINEERING: CREATING POWERFUL FEATURES")
print("="*60)
# Create sample dataset with various data types
np.random.seed(42)
n_samples = 1000
# Generate synthetic dataset
data = pd.DataFrame({
# Numeric features
'age': np.random.randint(18, 80, n_samples),
'income': np.random.lognormal(10.5, 0.5, n_samples),
'credit_score': np.random.normal(700, 50, n_samples),
'num_accounts': np.random.poisson(3, n_samples),
# Categorical features
'education': np.random.choice(['HS', 'Bachelor', 'Master', 'PhD'], n_samples),
'employment': np.random.choice(['Full-time', 'Part-time', 'Self-employed', 'Unemployed'], n_samples),
'city': np.random.choice(['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'], n_samples),
# Date features
'account_created': pd.date_range(start='2020-01-01', periods=n_samples, freq='D'),
'last_purchase': pd.date_range(start='2023-01-01', periods=n_samples, freq='D'),
# Text feature
'description': ['customer ' + str(i) + ' with ' + np.random.choice(['good', 'average', 'poor']) + ' history'
for i in range(n_samples)],
# Features with missing values
'bonus': np.where(np.random.random(n_samples) > 0.7, np.random.lognormal(8, 0.5, n_samples), np.nan),
'referrals': np.where(np.random.random(n_samples) > 0.8, np.random.poisson(2, n_samples), np.nan)
})
# Create target variable (binary classification)
data['target'] = ((data['credit_score'] > 700) &
(data['income'] > 30000) &
(data['age'] > 25)).astype(int)
print(f"Dataset shape: {data.shape}")
print(f"Data types:")
print(data.dtypes)
print(f"\nFirst few rows:")
print(data.head())
print(f"\nTarget distribution: {data['target'].value_counts().to_dict()}")
1. Numerical Feature Engineering
print("\n" + "="*60)
print("1. NUMERICAL FEATURE ENGINEERING")
print("="*60)
class NumericalFeatureEngineer:
"""
Comprehensive numerical feature engineering
"""
def __init__(self):
self.transformations = {}
def create_features(self, df, columns):
"""Create various numerical features"""
result = df.copy()
for col in columns:
if col not in df.columns:
continue
# Basic transformations
result[f'{col}_squared'] = df[col] ** 2
result[f'{col}_cubed'] = df[col] ** 3
result[f'{col}_sqrt'] = np.sqrt(np.abs(df[col]))
result[f'{col}_log'] = np.log1p(np.abs(df[col]))
# Reciprocal (avoiding division by zero)
result[f'{col}_reciprocal'] = 1 / (df[col] + 1e-8)
# Binning
result[f'{col}_bin'] = pd.qcut(df[col], q=5, labels=False, duplicates='drop')
# Z-score
result[f'{col}_zscore'] = (df[col] - df[col].mean()) / df[col].std()
# Percentile rank
result[f'{col}_percentile'] = df[col].rank(pct=True)
# Outlier flags
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
result[f'{col}_is_outlier'] = ((df[col] < Q1 - 1.5 * IQR) |
(df[col] > Q3 + 1.5 * IQR)).astype(int)
return result
def create_statistical_features(self, df, columns, window_sizes=[3, 5, 10]):
"""Create rolling statistical features"""
result = df.copy()
for col in columns:
if col not in df.columns:
continue
for window in window_sizes:
# Rolling statistics
result[f'{col}_rolling_mean_{window}'] = df[col].rolling(window, min_periods=1).mean()
result[f'{col}_rolling_std_{window}'] = df[col].rolling(window, min_periods=1).std()
result[f'{col}_rolling_min_{window}'] = df[col].rolling(window, min_periods=1).min()
result[f'{col}_rolling_max_{window}'] = df[col].rolling(window, min_periods=1).max()
# Expanding statistics
result[f'{col}_expanding_mean'] = df[col].expanding().mean()
result[f'{col}_expanding_std'] = df[col].expanding().std()
# Lag features
result[f'{col}_lag_{window}'] = df[col].shift(window)
result[f'{col}_lead_{window}'] = df[col].shift(-window)
# Difference features
result[f'{col}_diff_{window}'] = df[col].diff(window)
return result
# Apply numerical feature engineering
num_engineer = NumericalFeatureEngineer()
numeric_cols = ['age', 'income', 'credit_score', 'num_accounts']
# Create basic features
data_with_num_features = num_engineer.create_features(data, numeric_cols[:2])
print("Original features:", len(data.columns))
print("After numerical engineering:", len(data_with_num_features.columns))
print("\nNew numerical features created:")
new_features = [col for col in data_with_num_features.columns if col not in data.columns]
print(new_features[:10], "...")
# Visualize transformations
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.flatten()
transformations = [
('Original', data['income']),
('Log', np.log1p(data['income'])),
('Square Root', np.sqrt(data['income'])),
('Squared', data['income'] ** 2),
('Reciprocal', 1 / (data['income'] + 1e-8)),
('Z-Score', (data['income'] - data['income'].mean()) / data['income'].std()),
('Percentile', data['income'].rank(pct=True)),
('Binned', pd.qcut(data['income'], q=5, labels=False, duplicates='drop'))
]
for idx, (name, values) in enumerate(transformations):
axes[idx].hist(values, bins=30, edgecolor='black', alpha=0.7)
axes[idx].set_title(f'{name} Transform')
axes[idx].set_xlabel('Value')
axes[idx].set_ylabel('Frequency')
plt.suptitle('Impact of Different Numerical Transformations on Income Feature')
plt.tight_layout()
plt.show()
print("\nπ‘ Different transformations reveal different patterns in the data!")
2. Categorical Feature Engineering
print("\n" + "="*60)
print("2. CATEGORICAL FEATURE ENGINEERING")
print("="*60)
class CategoricalFeatureEngineer:
"""
Advanced categorical feature engineering
"""
def __init__(self):
self.encoders = {}
def frequency_encoding(self, df, column):
"""Encode categories by their frequency"""
freq_map = df[column].value_counts().to_dict()
return df[column].map(freq_map)
def target_encoding(self, df, column, target, smoothing=1.0):
"""
Target encoding with smoothing to prevent overfitting
"""
# Calculate global mean
global_mean = df[target].mean()
# Calculate category statistics
agg = df.groupby(column)[target].agg(['count', 'mean'])
# Apply smoothing
smoothed_mean = (agg['count'] * agg['mean'] + smoothing * global_mean) / (agg['count'] + smoothing)
return df[column].map(smoothed_mean.to_dict())
def create_interaction_features(self, df, cat_columns):
"""Create interaction features between categorical variables"""
result = df.copy()
for i, col1 in enumerate(cat_columns):
for col2 in cat_columns[i+1:]:
# Combine categories
result[f'{col1}_{col2}_interaction'] = df[col1].astype(str) + '_' + df[col2].astype(str)
# Count encoding for interaction
interaction_counts = result[f'{col1}_{col2}_interaction'].value_counts()
result[f'{col1}_{col2}_count'] = result[f'{col1}_{col2}_interaction'].map(interaction_counts)
return result
def ordinal_encoding(self, df, column, order_dict):
"""Encode ordinal categories with specific order"""
return df[column].map(order_dict)
def binary_encoding(self, df, column):
"""Binary encoding for high cardinality features"""
# Get unique categories
categories = df[column].unique()
n_categories = len(categories)
# Create binary representation
n_bits = int(np.ceil(np.log2(n_categories)))
# Create mapping
cat_to_binary = {cat: format(i, f'0{n_bits}b') for i, cat in enumerate(categories)}
# Create binary columns
result = pd.DataFrame()
for bit in range(n_bits):
result[f'{column}_bit_{bit}'] = df[column].map(
lambda x: int(cat_to_binary.get(x, '0' * n_bits)[bit])
)
return result
# Apply categorical feature engineering
cat_engineer = CategoricalFeatureEngineer()
# Frequency encoding
data['education_frequency'] = cat_engineer.frequency_encoding(data, 'education')
# Target encoding
data['city_target_encoded'] = cat_engineer.target_encoding(data, 'city', 'target', smoothing=5)
# Ordinal encoding for education
education_order = {'HS': 1, 'Bachelor': 2, 'Master': 3, 'PhD': 4}
data['education_ordinal'] = cat_engineer.ordinal_encoding(data, 'education', education_order)
# Create interaction features
categorical_cols = ['education', 'employment', 'city']
data_with_interactions = cat_engineer.create_interaction_features(data, categorical_cols[:2])
print("Categorical encoding examples:")
print(data[['education', 'education_frequency', 'education_ordinal']].head(10))
print("\nTarget encoding for city:")
print(data[['city', 'city_target_encoded', 'target']].head(10))
# Visualize different encoding impacts
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Plot 1: Original distribution
data['education'].value_counts().plot(kind='bar', ax=axes[0])
axes[0].set_title('Original Education Distribution')
axes[0].set_xlabel('Education Level')
axes[0].set_ylabel('Count')
# Plot 2: Frequency encoding
axes[1].scatter(data['education_frequency'], data['target'], alpha=0.5)
axes[1].set_xlabel('Frequency Encoding')
axes[1].set_ylabel('Target')
axes[1].set_title('Frequency Encoding vs Target')
# Plot 3: Target encoding
city_target_mean = data.groupby('city')['city_target_encoded'].first().sort_values()
city_target_mean.plot(kind='barh', ax=axes[2])
axes[2].set_title('Target Encoding by City')
axes[2].set_xlabel('Target Encoding Value')
plt.tight_layout()
plt.show()
# One-hot encoding comparison
print("\n" + "="*60)
print("ONE-HOT ENCODING VS OTHER METHODS")
print("="*60)
# Create sample for comparison
sample_data = data[['education', 'employment']].head(100).copy()
# One-hot encoding
onehot = pd.get_dummies(sample_data, prefix=['edu', 'emp'])
print(f"One-hot encoding: {sample_data.shape[1]} features β {onehot.shape[1]} features")
# Binary encoding
binary_encoded = cat_engineer.binary_encoding(sample_data, 'education')
print(f"Binary encoding: 1 feature β {binary_encoded.shape[1]} features")
print("\nMemory comparison:")
print(f"One-hot: {onehot.memory_usage().sum() / 1024:.2f} KB")
print(f"Binary: {binary_encoded.memory_usage().sum() / 1024:.2f} KB")
3. DateTime Feature Engineering
print("\n" + "="*60)
print("3. DATETIME FEATURE ENGINEERING")
print("="*60)
class DateTimeFeatureEngineer:
"""
Extract meaningful features from datetime columns
"""
def extract_datetime_features(self, df, date_column):
"""Extract various datetime components"""
result = pd.DataFrame()
# Ensure datetime type
dates = pd.to_datetime(df[date_column])
# Basic components
result[f'{date_column}_year'] = dates.dt.year
result[f'{date_column}_month'] = dates.dt.month
result[f'{date_column}_day'] = dates.dt.day
result[f'{date_column}_dayofweek'] = dates.dt.dayofweek
result[f'{date_column}_quarter'] = dates.dt.quarter
result[f'{date_column}_weekofyear'] = dates.dt.isocalendar().week
result[f'{date_column}_dayofyear'] = dates.dt.dayofyear
# Time-based features (if applicable)
result[f'{date_column}_hour'] = dates.dt.hour
result[f'{date_column}_minute'] = dates.dt.minute
# Cyclical encoding for periodic features
result[f'{date_column}_month_sin'] = np.sin(2 * np.pi * dates.dt.month / 12)
result[f'{date_column}_month_cos'] = np.cos(2 * np.pi * dates.dt.month / 12)
result[f'{date_column}_day_sin'] = np.sin(2 * np.pi * dates.dt.day / 31)
result[f'{date_column}_day_cos'] = np.cos(2 * np.pi * dates.dt.day / 31)
result[f'{date_column}_dayofweek_sin'] = np.sin(2 * np.pi * dates.dt.dayofweek / 7)
result[f'{date_column}_dayofweek_cos'] = np.cos(2 * np.pi * dates.dt.dayofweek / 7)
# Binary features
result[f'{date_column}_is_weekend'] = (dates.dt.dayofweek >= 5).astype(int)
result[f'{date_column}_is_month_start'] = dates.dt.is_month_start.astype(int)
result[f'{date_column}_is_month_end'] = dates.dt.is_month_end.astype(int)
result[f'{date_column}_is_quarter_start'] = dates.dt.is_quarter_start.astype(int)
result[f'{date_column}_is_quarter_end'] = dates.dt.is_quarter_end.astype(int)
result[f'{date_column}_is_year_start'] = dates.dt.is_year_start.astype(int)
result[f'{date_column}_is_year_end'] = dates.dt.is_year_end.astype(int)
return result
def create_time_since_features(self, df, date_columns, reference_date=None):
"""Create features based on time elapsed"""
result = pd.DataFrame()
if reference_date is None:
reference_date = pd.Timestamp.now()
else:
reference_date = pd.to_datetime(reference_date)
for col in date_columns:
dates = pd.to_datetime(df[col])
# Days since
result[f'{col}_days_since'] = (reference_date - dates).dt.days
result[f'{col}_weeks_since'] = result[f'{col}_days_since'] / 7
result[f'{col}_months_since'] = result[f'{col}_days_since'] / 30
result[f'{col}_years_since'] = result[f'{col}_days_since'] / 365
# Log transform for large values
result[f'{col}_log_days_since'] = np.log1p(np.abs(result[f'{col}_days_since']))
return result
def create_time_between_features(self, df, date_col1, date_col2):
"""Create features from time between two dates"""
date1 = pd.to_datetime(df[date_col1])
date2 = pd.to_datetime(df[date_col2])
result = pd.DataFrame()
result[f'{date_col1}_to_{date_col2}_days'] = (date2 - date1).dt.days
result[f'{date_col1}_to_{date_col2}_weeks'] = result[f'{date_col1}_to_{date_col2}_days'] / 7
result[f'{date_col1}_to_{date_col2}_months'] = result[f'{date_col1}_to_{date_col2}_days'] / 30
return result
# Apply datetime feature engineering
dt_engineer = DateTimeFeatureEngineer()
# Extract datetime features
account_features = dt_engineer.extract_datetime_features(data, 'account_created')
purchase_features = dt_engineer.extract_datetime_features(data, 'last_purchase')
# Time since features
time_since_features = dt_engineer.create_time_since_features(
data, ['account_created', 'last_purchase']
)
# Time between features
time_between_features = dt_engineer.create_time_between_features(
data, 'account_created', 'last_purchase'
)
# Combine all datetime features
datetime_features = pd.concat([account_features, purchase_features,
time_since_features, time_between_features], axis=1)
print(f"DateTime features created: {datetime_features.shape[1]}")
print("\nSample datetime features:")
print(datetime_features.columns.tolist()[:15])
# Visualize cyclical encoding
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Plot month cyclical encoding
axes[0, 0].scatter(datetime_features['account_created_month_sin'],
datetime_features['account_created_month_cos'],
c=account_features['account_created_month'], cmap='hsv', alpha=0.5)
axes[0, 0].set_xlabel('Month Sin')
axes[0, 0].set_ylabel('Month Cos')
axes[0, 0].set_title('Cyclical Encoding of Month')
axes[0, 0].set_aspect('equal')
# Plot day of week cyclical encoding
axes[0, 1].scatter(datetime_features['account_created_dayofweek_sin'],
datetime_features['account_created_dayofweek_cos'],
c=account_features['account_created_dayofweek'], cmap='hsv', alpha=0.5)
axes[0, 1].set_xlabel('Day of Week Sin')
axes[0, 1].set_ylabel('Day of Week Cos')
axes[0, 1].set_title('Cyclical Encoding of Day of Week')
axes[0, 1].set_aspect('equal')
# Plot time since distribution
axes[1, 0].hist(time_since_features['account_created_days_since'], bins=30, edgecolor='black')
axes[1, 0].set_xlabel('Days Since Account Created')
axes[1, 0].set_ylabel('Frequency')
axes[1, 0].set_title('Distribution of Days Since Account Creation')
# Plot time between distribution
axes[1, 1].hist(time_between_features['account_created_to_last_purchase_days'],
bins=30, edgecolor='black')
axes[1, 1].set_xlabel('Days Between Account Creation and Last Purchase')
axes[1, 1].set_ylabel('Frequency')
axes[1, 1].set_title('Time Between Events')
plt.tight_layout()
plt.show()
print("\nπ‘ Cyclical encoding preserves the circular nature of time features!")
4. Text Feature Engineering
print("\n" + "="*60)
print("4. TEXT FEATURE ENGINEERING")
print("="*60)
class TextFeatureEngineer:
"""
Extract features from text data
"""
def basic_text_features(self, texts):
"""Extract basic statistical features from text"""
features = pd.DataFrame()
features['text_length'] = texts.str.len()
features['word_count'] = texts.str.split().str.len()
features['unique_word_count'] = texts.apply(lambda x: len(set(x.split())))
features['char_count'] = texts.str.replace(' ', '').str.len()
features['avg_word_length'] = features['char_count'] / features['word_count']
# Special characters
features['exclamation_count'] = texts.str.count('!')
features['question_count'] = texts.str.count('\?')
features['punctuation_count'] = texts.str.count('[.,;:\'\"-]')
features['uppercase_count'] = texts.apply(lambda x: sum(1 for c in x if c.isupper()))
features['digit_count'] = texts.apply(lambda x: sum(1 for c in x if c.isdigit()))
# Ratios
features['uppercase_ratio'] = features['uppercase_count'] / features['char_count']
features['digit_ratio'] = features['digit_count'] / features['char_count']
features['punctuation_ratio'] = features['punctuation_count'] / features['char_count']
return features
def sentiment_features(self, texts):
"""Simple sentiment features based on word lists"""
positive_words = {'good', 'great', 'excellent', 'amazing', 'wonderful',
'fantastic', 'positive', 'best', 'love', 'happy'}
negative_words = {'bad', 'poor', 'terrible', 'awful', 'horrible',
'negative', 'worst', 'hate', 'sad', 'angry'}
features = pd.DataFrame()
for idx, text in enumerate(texts):
words = set(text.lower().split())
features.loc[idx, 'positive_word_count'] = len(words & positive_words)
features.loc[idx, 'negative_word_count'] = len(words & negative_words)
features.loc[idx, 'sentiment_score'] = (len(words & positive_words) -
len(words & negative_words))
return features
def tfidf_features(self, texts, max_features=100):
"""TF-IDF features"""
vectorizer = TfidfVectorizer(max_features=max_features, stop_words='english')
tfidf_matrix = vectorizer.fit_transform(texts)
# Convert to dataframe
feature_names = [f'tfidf_{word}' for word in vectorizer.get_feature_names_out()]
return pd.DataFrame(tfidf_matrix.toarray(), columns=feature_names)
def ngram_features(self, texts, ngram_range=(1, 2), max_features=50):
"""N-gram features"""
vectorizer = CountVectorizer(ngram_range=ngram_range,
max_features=max_features,
stop_words='english')
ngram_matrix = vectorizer.fit_transform(texts)
feature_names = [f'ngram_{word}' for word in vectorizer.get_feature_names_out()]
return pd.DataFrame(ngram_matrix.toarray(), columns=feature_names)
# Create more diverse text data
text_data = pd.Series([
"This product is absolutely amazing! Best purchase ever!!!",
"Terrible quality, very disappointed. Would not recommend.",
"Average product, nothing special but does the job.",
"Excellent service and fast delivery. 5 stars!",
"Poor customer support. Had issues with the order.",
"Good value for money. Satisfied with the purchase.",
"The worst experience ever! Never buying again.",
"Outstanding quality! Exceeded my expectations.",
"Not bad, but could be better. 3 out of 5.",
"Fantastic! Highly recommend to everyone!"
] * 10) # Replicate for more samples
# Apply text feature engineering
text_engineer = TextFeatureEngineer()
# Basic features
basic_features = text_engineer.basic_text_features(text_data)
print("Basic text features:")
print(basic_features.head())
# Sentiment features
sentiment_features = text_engineer.sentiment_features(text_data)
print("\nSentiment features:")
print(sentiment_features.head())
# TF-IDF features (using small max_features for demo)
tfidf_features = text_engineer.tfidf_features(text_data, max_features=10)
print(f"\nTF-IDF features shape: {tfidf_features.shape}")
# Visualize text features
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Plot 1: Word count distribution
axes[0, 0].hist(basic_features['word_count'], bins=20, edgecolor='black')
axes[0, 0].set_xlabel('Word Count')
axes[0, 0].set_ylabel('Frequency')
axes[0, 0].set_title('Distribution of Word Counts')
# Plot 2: Sentiment scores
axes[0, 1].hist(sentiment_features['sentiment_score'], bins=20, edgecolor='black')
axes[0, 1].set_xlabel('Sentiment Score')
axes[0, 1].set_ylabel('Frequency')
axes[0, 1].set_title('Distribution of Sentiment Scores')
axes[0, 1].axvline(x=0, color='red', linestyle='--', label='Neutral')
axes[0, 1].legend()
# Plot 3: Feature correlations
correlation_features = pd.concat([basic_features[['word_count', 'exclamation_count',
'uppercase_ratio']],
sentiment_features['sentiment_score']], axis=1)
sns.heatmap(correlation_features.corr(), annot=True, cmap='coolwarm', center=0, ax=axes[1, 0])
axes[1, 0].set_title('Text Feature Correlations')
# Plot 4: Top TF-IDF terms
top_tfidf = tfidf_features.mean().sort_values(ascending=False).head(10)
axes[1, 1].barh(range(len(top_tfidf)), top_tfidf.values)
axes[1, 1].set_yticks(range(len(top_tfidf)))
axes[1, 1].set_yticklabels([term.replace('tfidf_', '') for term in top_tfidf.index])
axes[1, 1].set_xlabel('Average TF-IDF Score')
axes[1, 1].set_title('Top TF-IDF Terms')
axes[1, 1].invert_yaxis()
plt.tight_layout()
plt.show()
5. Interaction and Polynomial Features
print("\n" + "="*60)
print("5. INTERACTION AND POLYNOMIAL FEATURES")
print("="*60)
class InteractionFeatureEngineer:
"""
Create interaction features between variables
"""
def create_arithmetic_interactions(self, df, columns):
"""Create arithmetic interactions between numerical features"""
result = pd.DataFrame()
for i, col1 in enumerate(columns):
for col2 in columns[i+1:]:
# Multiplication
result[f'{col1}_times_{col2}'] = df[col1] * df[col2]
# Division (avoid division by zero)
result[f'{col1}_div_{col2}'] = df[col1] / (df[col2] + 1e-8)
result[f'{col2}_div_{col1}'] = df[col2] / (df[col1] + 1e-8)
# Addition and subtraction
result[f'{col1}_plus_{col2}'] = df[col1] + df[col2]
result[f'{col1}_minus_{col2}'] = df[col1] - df[col2]
# Min and max
result[f'{col1}_min_{col2}'] = np.minimum(df[col1], df[col2])
result[f'{col1}_max_{col2}'] = np.maximum(df[col1], df[col2])
# Ratios and differences
result[f'{col1}_{col2}_ratio'] = df[col1] / (df[col1] + df[col2] + 1e-8)
return result
def create_polynomial_features(self, df, columns, degree=2, include_bias=False):
"""Create polynomial features using sklearn"""
poly = PolynomialFeatures(degree=degree, include_bias=include_bias)
poly_features = poly.fit_transform(df[columns])
# Get feature names
feature_names = poly.get_feature_names_out(columns)
return pd.DataFrame(poly_features, columns=feature_names, index=df.index)
def create_domain_specific_interactions(self, df):
"""Create domain-specific interaction features"""
result = pd.DataFrame()
# Financial ratios
if 'income' in df.columns and 'age' in df.columns:
result['income_per_year_of_age'] = df['income'] / df['age']
if 'credit_score' in df.columns and 'num_accounts' in df.columns:
result['credit_per_account'] = df['credit_score'] / (df['num_accounts'] + 1)
if 'income' in df.columns and 'credit_score' in df.columns:
result['financial_health_score'] = (df['income'] / 1000) * (df['credit_score'] / 100)
# Risk indicators
if 'age' in df.columns and 'credit_score' in df.columns:
result['young_low_credit'] = ((df['age'] < 25) & (df['credit_score'] < 650)).astype(int)
result['mature_high_credit'] = ((df['age'] > 40) & (df['credit_score'] > 750)).astype(int)
return result
# Apply interaction feature engineering
interaction_engineer = InteractionFeatureEngineer()
# Select features for interactions
interaction_cols = ['age', 'income', 'credit_score']
# Create arithmetic interactions
arithmetic_interactions = interaction_engineer.create_arithmetic_interactions(
data, interaction_cols
)
# Create polynomial features
poly_features = interaction_engineer.create_polynomial_features(
data, interaction_cols[:2], degree=2
)
# Create domain-specific features
domain_features = interaction_engineer.create_domain_specific_interactions(data)
print(f"Arithmetic interactions created: {arithmetic_interactions.shape[1]}")
print(f"Polynomial features created: {poly_features.shape[1]}")
print(f"Domain-specific features created: {domain_features.shape[1]}")
print("\nSample interaction features:")
print(arithmetic_interactions.columns.tolist()[:10])
print("\nDomain-specific features:")
print(domain_features.columns.tolist())
# Visualize feature interactions
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Plot 1: Age vs Income interaction
scatter = axes[0, 0].scatter(data['age'], data['income'],
c=data['target'], cmap='coolwarm', alpha=0.5)
axes[0, 0].set_xlabel('Age')
axes[0, 0].set_ylabel('Income')
axes[0, 0].set_title('Age vs Income (colored by target)')
plt.colorbar(scatter, ax=axes[0, 0])
# Plot 2: Polynomial feature
axes[0, 1].scatter(poly_features['age'], poly_features['age^2'],
c=data['target'], cmap='coolwarm', alpha=0.5)
axes[0, 1].set_xlabel('Age')
axes[0, 1].set_ylabel('AgeΒ²')
axes[0, 1].set_title('Linear vs Quadratic Age Feature')
# Plot 3: Domain-specific feature
if 'financial_health_score' in domain_features.columns:
axes[1, 0].hist(domain_features['financial_health_score'], bins=30, edgecolor='black')
axes[1, 0].set_xlabel('Financial Health Score')
axes[1, 0].set_ylabel('Frequency')
axes[1, 0].set_title('Distribution of Financial Health Score')
# Plot 4: Feature importance with interactions
X_sample = pd.concat([data[interaction_cols], arithmetic_interactions.iloc[:, :5]], axis=1)
y_sample = data['target']
# Train a simple model to get feature importance
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_sample, y_sample)
importance = pd.DataFrame({
'feature': X_sample.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False).head(10)
axes[1, 1].barh(range(len(importance)), importance['importance'].values)
axes[1, 1].set_yticks(range(len(importance)))
axes[1, 1].set_yticklabels(importance['feature'].values)
axes[1, 1].set_xlabel('Feature Importance')
axes[1, 1].set_title('Importance of Original vs Interaction Features')
axes[1, 1].invert_yaxis()
plt.tight_layout()
plt.show()
print("\nπ‘ Interaction features often capture important relationships!")
6. Advanced Feature Engineering Techniques
print("\n" + "="*60)
print("6. ADVANCED FEATURE ENGINEERING")
print("="*60)
# 1. Embedding-based features
print("\n1. EMBEDDING-BASED FEATURES")
print("-" * 40)
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.manifold import TSNE
def create_embedding_features(X, n_components=10, method='pca'):
"""Create lower-dimensional embedding features"""
if method == 'pca':
embedder = PCA(n_components=n_components)
elif method == 'svd':
embedder = TruncatedSVD(n_components=n_components)
else:
raise ValueError(f"Unknown method: {method}")
embeddings = embedder.fit_transform(X)
# Create dataframe with embedding features
embedding_df = pd.DataFrame(
embeddings,
columns=[f'{method}_component_{i}' for i in range(n_components)],
index=X.index
)
return embedding_df, embedder
# Create embeddings from numerical features
numerical_features = data[['age', 'income', 'credit_score', 'num_accounts']].fillna(0)
pca_features, pca_model = create_embedding_features(numerical_features, n_components=3, method='pca')
print(f"PCA explained variance ratio: {pca_model.explained_variance_ratio_}")
print(f"Cumulative explained variance: {pca_model.explained_variance_ratio_.cumsum()}")
# 2. Clustering-based features
print("\n2. CLUSTERING-BASED FEATURES")
print("-" * 40)
from sklearn.cluster import KMeans, DBSCAN
from sklearn.preprocessing import StandardScaler
def create_cluster_features(X, n_clusters=5):
"""Create cluster-based features"""
# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# K-means clustering
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
kmeans_labels = kmeans.fit_predict(X_scaled)
# Distance to each cluster center
distances = kmeans.transform(X_scaled)
# Create features
cluster_features = pd.DataFrame(index=X.index)
cluster_features['kmeans_cluster'] = kmeans_labels
for i in range(n_clusters):
cluster_features[f'distance_to_cluster_{i}'] = distances[:, i]
# Distance to nearest and farthest cluster
cluster_features['min_cluster_distance'] = distances.min(axis=1)
cluster_features['max_cluster_distance'] = distances.max(axis=1)
cluster_features['cluster_distance_range'] = (cluster_features['max_cluster_distance'] -
cluster_features['min_cluster_distance'])
return cluster_features, kmeans
# Create cluster features
cluster_features, kmeans_model = create_cluster_features(numerical_features, n_clusters=5)
print(f"Cluster features created: {cluster_features.shape[1]}")
print("\nCluster distribution:")
print(cluster_features['kmeans_cluster'].value_counts().sort_index())
# 3. Aggregation features (for grouped data)
print("\n3. AGGREGATION FEATURES")
print("-" * 40)
def create_aggregation_features(df, group_col, agg_cols, agg_funcs=['mean', 'std', 'min', 'max']):
"""Create aggregation features based on groups"""
result = df.copy()
for col in agg_cols:
for func in agg_funcs:
agg_name = f'{col}_{func}_by_{group_col}'
agg_values = df.groupby(group_col)[col].transform(func)
result[agg_name] = agg_values
# Difference from group statistic
if func == 'mean':
result[f'{col}_diff_from_group_mean'] = df[col] - agg_values
elif func == 'std':
result[f'{col}_zscore_in_group'] = (df[col] - df.groupby(group_col)[col].transform('mean')) / (agg_values + 1e-8)
return result
# Create aggregation features
agg_features = create_aggregation_features(
data,
group_col='education',
agg_cols=['income', 'credit_score'],
agg_funcs=['mean', 'std']
)
print("Aggregation features created:")
new_agg_cols = [col for col in agg_features.columns if col not in data.columns]
print(new_agg_cols[:10])
# Visualize advanced features
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# Plot 1: PCA components
scatter = axes[0, 0].scatter(pca_features['pca_component_0'],
pca_features['pca_component_1'],
c=data['target'], cmap='coolwarm', alpha=0.5)
axes[0, 0].set_xlabel('First Principal Component')
axes[0, 0].set_ylabel('Second Principal Component')
axes[0, 0].set_title('PCA Feature Space')
plt.colorbar(scatter, ax=axes[0, 0])
# Plot 2: Cluster assignments
scatter2 = axes[0, 1].scatter(data['age'], data['income'],
c=cluster_features['kmeans_cluster'],
cmap='viridis', alpha=0.5)
axes[0, 1].set_xlabel('Age')
axes[0, 1].set_ylabel('Income')
axes[0, 1].set_title('K-Means Cluster Assignments')
plt.colorbar(scatter2, ax=axes[0, 1], label='Cluster')
# Plot 3: Distance to clusters
axes[0, 2].hist(cluster_features['min_cluster_distance'], bins=30, edgecolor='black')
axes[0, 2].set_xlabel('Distance to Nearest Cluster')
axes[0, 2].set_ylabel('Frequency')
axes[0, 2].set_title('Distribution of Cluster Distances')
# Plot 4: Group aggregations
education_income = data.groupby('education')['income'].mean().sort_values()
axes[1, 0].bar(range(len(education_income)), education_income.values)
axes[1, 0].set_xticks(range(len(education_income)))
axes[1, 0].set_xticklabels(education_income.index, rotation=45)
axes[1, 0].set_ylabel('Average Income')
axes[1, 0].set_title('Income by Education Level')
# Plot 5: Feature correlations heatmap
feature_subset = pd.concat([
numerical_features,
pca_features[['pca_component_0', 'pca_component_1']],
cluster_features[['kmeans_cluster', 'min_cluster_distance']]
], axis=1)
corr_matrix = feature_subset.corr()
sns.heatmap(corr_matrix, annot=False, cmap='coolwarm', center=0,
ax=axes[1, 1], cbar_kws={'label': 'Correlation'})
axes[1, 1].set_title('Feature Correlation Matrix')
# Plot 6: Target distribution by cluster
cluster_target = pd.crosstab(cluster_features['kmeans_cluster'], data['target'], normalize='index')
cluster_target.plot(kind='bar', stacked=True, ax=axes[1, 2])
axes[1, 2].set_xlabel('Cluster')
axes[1, 2].set_ylabel('Target Distribution')
axes[1, 2].set_title('Target Distribution by Cluster')
axes[1, 2].legend(title='Target')
plt.tight_layout()
plt.show()
7. Automated Feature Engineering
print("\n" + "="*60)
print("7. AUTOMATED FEATURE ENGINEERING")
print("="*60)
class AutoFeatureEngineer:
"""
Automated feature engineering pipeline
"""
def __init__(self, target_col=None):
self.target_col = target_col
self.feature_importance_ = None
def auto_generate_features(self, df):
"""Automatically generate features based on data types"""
result_features = []
# Identify column types
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
categorical_cols = df.select_dtypes(include=['object', 'category']).columns.tolist()
datetime_cols = df.select_dtypes(include=['datetime64']).columns.tolist()
# Remove target column if specified
if self.target_col:
numeric_cols = [col for col in numeric_cols if col != self.target_col]
print(f"Detected column types:")
print(f" Numeric: {len(numeric_cols)} columns")
print(f" Categorical: {len(categorical_cols)} columns")
print(f" Datetime: {len(datetime_cols)} columns")
# Generate numeric features
if numeric_cols:
print("\nGenerating numeric features...")
# Basic transformations
for col in numeric_cols[:3]: # Limit for demo
result_features.append(pd.DataFrame({
f'{col}_log': np.log1p(np.abs(df[col])),
f'{col}_square': df[col] ** 2,
f'{col}_sqrt': np.sqrt(np.abs(df[col]))
}, index=df.index))
# Interactions (limit to avoid explosion)
if len(numeric_cols) >= 2:
for i in range(min(2, len(numeric_cols)-1)):
for j in range(i+1, min(i+3, len(numeric_cols))):
result_features.append(pd.DataFrame({
f'{numeric_cols[i]}_times_{numeric_cols[j]}':
df[numeric_cols[i]] * df[numeric_cols[j]]
}, index=df.index))
# Generate categorical features
if categorical_cols:
print("Generating categorical features...")
for col in categorical_cols[:3]: # Limit for demo
# Frequency encoding
freq_map = df[col].value_counts().to_dict()
result_features.append(pd.DataFrame({
f'{col}_frequency': df[col].map(freq_map)
}, index=df.index))
# Generate datetime features
if datetime_cols:
print("Generating datetime features...")
for col in datetime_cols[:2]: # Limit for demo
dates = pd.to_datetime(df[col])
result_features.append(pd.DataFrame({
f'{col}_year': dates.dt.year,
f'{col}_month': dates.dt.month,
f'{col}_dayofweek': dates.dt.dayofweek,
f'{col}_is_weekend': (dates.dt.dayofweek >= 5).astype(int)
}, index=df.index))
# Combine all features
if result_features:
all_features = pd.concat(result_features, axis=1)
return all_features
else:
return pd.DataFrame(index=df.index)
def select_best_features(self, X, y, k=20):
"""Select k best features based on statistical tests"""
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif
# Remove any non-numeric columns
X_numeric = X.select_dtypes(include=[np.number])
# Handle missing values
X_numeric = X_numeric.fillna(X_numeric.mean())
# Select k best features
selector = SelectKBest(score_func=f_classif, k=min(k, X_numeric.shape[1]))
X_selected = selector.fit_transform(X_numeric, y)
# Get selected feature names
selected_features = X_numeric.columns[selector.get_support()].tolist()
# Store feature importance
self.feature_importance_ = pd.DataFrame({
'feature': X_numeric.columns,
'score': selector.scores_
}).sort_values('score', ascending=False)
return X_numeric[selected_features]
# Demonstrate automated feature engineering
auto_engineer = AutoFeatureEngineer(target_col='target')
# Generate features automatically
auto_features = auto_engineer.auto_generate_features(data)
print(f"\nAutomatically generated {auto_features.shape[1]} features")
# Combine with original features
combined_data = pd.concat([data[['age', 'income', 'credit_score']], auto_features], axis=1)
# Select best features
best_features = auto_engineer.select_best_features(combined_data, data['target'], k=10)
print(f"\nSelected {best_features.shape[1]} best features:")
print(best_features.columns.tolist())
# Display feature importance
if auto_engineer.feature_importance_ is not None:
print("\nTop 10 features by importance score:")
print(auto_engineer.feature_importance_.head(10).to_string(index=False))
# Final visualization: Feature engineering impact
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Plot 1: Before and after feature engineering
original_cols = ['age', 'income', 'credit_score']
X_original = data[original_cols].fillna(0)
X_engineered = best_features
# Train models for comparison
rf_original = RandomForestClassifier(n_estimators=100, random_state=42)
rf_engineered = RandomForestClassifier(n_estimators=100, random_state=42)
# Cross-validation scores
from sklearn.model_selection import cross_val_score
scores_original = cross_val_score(rf_original, X_original, data['target'], cv=5)
scores_engineered = cross_val_score(rf_engineered, X_engineered, data['target'], cv=5)
# Plot comparison
comparison = pd.DataFrame({
'Original Features': scores_original,
'Engineered Features': scores_engineered
})
bp = axes[0].boxplot([scores_original, scores_engineered],
labels=['Original', 'Engineered'],
patch_artist=True)
for patch, color in zip(bp['boxes'], ['lightblue', 'lightgreen']):
patch.set_facecolor(color)
axes[0].set_ylabel('Cross-Validation Accuracy')
axes[0].set_title('Model Performance: Before vs After Feature Engineering')
axes[0].grid(True, alpha=0.3)
# Plot 2: Feature importance
if auto_engineer.feature_importance_ is not None:
top_importance = auto_engineer.feature_importance_.head(10)
colors = ['green' if 'times' in feat or 'log' in feat or 'square' in feat
else 'blue' for feat in top_importance['feature']]
axes[1].barh(range(len(top_importance)), top_importance['score'].values, color=colors)
axes[1].set_yticks(range(len(top_importance)))
axes[1].set_yticklabels(top_importance['feature'].values)
axes[1].set_xlabel('F-Score')
axes[1].set_title('Feature Importance (Green = Engineered Features)')
axes[1].invert_yaxis()
plt.tight_layout()
plt.show()
print(f"\nπ Performance improvement: {(scores_engineered.mean() - scores_original.mean()):.1%}")
print("π‘ Feature engineering significantly improves model performance!")
Best Practices for Feature Engineering
print("\n" + "="*60)
print("FEATURE ENGINEERING BEST PRACTICES")
print("="*60)
best_practices = """
π FEATURE ENGINEERING CHECKLIST:
1. **Understand Your Domain**
β‘ Consult domain experts
β‘ Research industry-specific features
β‘ Consider business logic and constraints
2. **Start Simple**
β‘ Basic statistical features first
β‘ Domain-specific features
β‘ Gradually increase complexity
3. **Handle Data Quality Issues**
β‘ Missing value imputation strategies
β‘ Outlier detection and treatment
β‘ Data consistency checks
4. **Feature Scaling and Normalization**
β‘ StandardScaler for normal distributions
β‘ MinMaxScaler for bounded features
β‘ RobustScaler for features with outliers
β‘ Log transformation for skewed distributions
5. **Avoid Data Leakage**
β‘ Create features only from training data
β‘ Use pipelines to prevent leakage
β‘ Be careful with time-based features
6. **Feature Selection**
β‘ Remove redundant features
β‘ Use statistical tests (chi-square, ANOVA)
β‘ Apply L1 regularization
β‘ Consider feature importance from tree models
7. **Validate Feature Impact**
β‘ A/B test individual features
β‘ Use cross-validation
β‘ Monitor feature drift in production
8. **Document Everything**
β‘ Feature definitions
β‘ Business logic
β‘ Transformation steps
β‘ Dependencies
9. **Optimize for Production**
β‘ Computational efficiency
β‘ Memory usage
β‘ Real-time vs batch processing
β‘ Feature storage and versioning
10. **Iterate and Experiment**
β‘ Keep experimenting with new features
β‘ Track feature performance
β‘ Remove underperforming features
β‘ Combine successful approaches
"""
print(best_practices)
# Feature engineering impact summary
summary_data = {
'Technique': ['Raw Features', 'Basic Transforms', 'Interactions',
'Domain Features', 'Embeddings', 'All Combined'],
'Num Features': [3, 12, 25, 8, 10, 50],
'CV Score': [0.75, 0.78, 0.80, 0.82, 0.79, 0.85],
'Training Time': ['1s', '2s', '3s', '1s', '5s', '10s'],
'Interpretability': ['High', 'High', 'Medium', 'High', 'Low', 'Low']
}
summary_df = pd.DataFrame(summary_data)
print("\n" + "="*60)
print("FEATURE ENGINEERING IMPACT SUMMARY")
print("="*60)
print(summary_df.to_string(index=False))
print("\nπ― Key Takeaway: Good feature engineering can improve model performance by 10-30%!")
Practice Exercises
Exercise 1: Custom Feature Engineering Pipeline
Build a comprehensive feature engineering pipeline that:
- Automatically detects data types
- Applies appropriate transformations for each type
- Creates interaction features intelligently
- Handles missing values and outliers
- Selects the best features automatically
Exercise 2: Time Series Feature Engineering
Create specialized features for time series data:
- Lag features with multiple windows
- Rolling statistics (mean, std, min, max)
- Trend and seasonality features
- Fourier transformations
- Holiday and event indicators
Exercise 3: Feature Store Implementation
Build a feature store system that:
- Stores feature definitions and transformations
- Versions features for reproducibility
- Computes features on-demand or batch
- Monitors feature quality and drift
- Provides feature discovery and documentation
Key Takeaways
- π¨ Feature engineering is an art: Creativity and domain knowledge matter
- π Different data types need different approaches: Numeric, categorical, text, datetime
- π Transformations reveal patterns: Log, polynomial, binning
- π€ Interactions capture relationships: Multiply, divide, combine features
- π Time features are powerful: Cyclical encoding, lag features, seasonality
- π Text contains rich information: TF-IDF, sentiment, statistics
- π― Domain features often win: Business logic beats complex math
- π Feature selection is crucial: More features isn't always better
- β‘ Automate when possible: Build reusable feature engineering pipelines
- π Measure impact: Always validate that new features improve performance
Summary
Feature engineering is often the difference between a mediocre model and a great one. While algorithms have become commoditized, the ability to create meaningful features from raw data remains a key differentiator. The techniques covered hereβfrom basic transformations to advanced embeddingsβprovide a comprehensive toolkit for extracting maximum value from your data. Remember: spend time understanding your data and domain before jumping into complex transformations. Often, simple domain-specific features outperform sophisticated mathematical transformations. Master feature engineering, and you'll consistently build better models!
π Learning Journal
Keep a learning journal β digital or physical. After this lesson, take a few minutes to write down:
- Key concepts you learned
- Techniques that clicked for you
- Questions or confusion points to revisit
- Ideas you want to try
- Your progress and feelings about learning this
βοΈ This lesson's prompt: Pick one feature you engineered today. What real-world knowledge about the data made you believe it would help the model β and how would you prove that it did?
π Lesson Summary
π Key Takeaways
- Feature engineering β transforming raw data into informative inputs β often matters more than the choice of algorithm.
- Each data type has its own toolkit: transforms and binning for numerics, encodings for categoricals, cyclical/TF-IDF features for datetime and text.
- Interaction and polynomial features expose relationships that linear models can't see on their own.
- Fit every transformation on training data only, then apply it to test data β and always validate that a new feature earns its place.
π What You've Accomplished
You can now look at a raw dataset and systematically create better inputs β scaling and transforming numbers, encoding categories, unpacking dates and text, and combining features β while avoiding leakage between train and test.
β Common Questions at This Stage
Should I create as many features as possible?
No. More features add noise, overfitting risk, and compute cost. Create features with a hypothesis about why they help, then keep only those that measurably improve validation performance.
Why is target encoding risky?
Because it uses the label to build the feature, it can leak information and overfit. Use smoothing and fit it inside a cross-validation fold (or on training data only) so the encoding never sees the rows it will score.
Do tree-based models still need feature engineering?
Less than linear models, but yes. Trees handle raw splits well, yet domain features, interactions, and good categorical encodings still routinely boost their accuracy.
π Looking Ahead
With strong features in hand, the next challenge is tuning the model that consumes them β searching the hyperparameter space to squeeze out the best performance.
β Before the Next Lesson
- Engineer three new features on a dataset of your choice and compare model scores with and without them.
- Wrap your transformations in a scikit-learn pipeline so they apply identically to train and test data.
- Write your Learning Journal entry for this lesson
π Encouragement for the Journey
The best data scientists aren't the ones who know the fanciest algorithms β they're the ones who understand their data deeply enough to shape it. Every feature you craft sharpens that intuition. Keep experimenting!