SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  • Load and explore the capstone datasets using Python.

  • Perform data quality assessment – missing values, outliers, inconsistencies.

  • Conduct exploratory data analysis (EDA) to understand distributions, correlations, and patterns.

  • Identify relationships between features and the target (loan default).

  • Handle missing data and outliers using appropriate techniques.

  • Engineer new features to improve predictive power.

  • Visualise insights for stakeholder communication.

  • Prepare the data for modelling.


SECTION 2: DATA LOADING AND OVERVIEW

We’ll work with the following datasets:

  1. loan_applications.csv – Historical loan applications with default labels.

  2. customer_data.csv – Customer demographics and behaviour.

  3. transactions.csv – Customer transaction history.

  4. macro_data.csv – Quarterly macroeconomic indicators.

  5. loan_performance.csv – Historical loan performance.

Step 1: Load and Combine Data

We’ll load each dataset and merge them into a single analytical dataset for modelling.

python
# ===================================================================
# MODULE 9, LESSON 2: DATA EXPLORATION AND PREPROCESSING
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
from scipy.stats import skew, kurtosis
import warnings
warnings.filterwarnings('ignore')

# Set style
sns.set_style("whitegrid")
np.random.seed(42)

print("="*70)
print("CAPSTONE PROJECT – DATA EXPLORATION AND PREPROCESSING")
print("="*70)

# ----------------------------------------------------------------
# PART A: GENERATE SYNTHETIC DATA (SIMULATED)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART A: Generating Synthetic Capstone Data")
print("-"*60)

def generate_loan_applications(n=100000):
    """Generate synthetic loan application data."""
    np.random.seed(42)
    
    # Demographic features
    age = np.random.normal(42, 14, n).clip(18, 80).astype(int)
    income = np.random.gamma(5, 15, n) + 20  # in $000s
    credit_score = np.random.normal(700, 50, n).clip(550, 850).astype(int)
    dti = np.random.beta(2, 5, n) * 60
    loan_amount = np.random.gamma(4, 50, n) + 30  # in $000s
    loan_term = np.random.choice([12, 24, 36, 48, 60, 72], n, p=[0.05, 0.1, 0.2, 0.2, 0.2, 0.25])
    employment_years = np.random.gamma(3, 5, n).clip(0, 40).astype(int)
    home_owner = np.random.binomial(1, 0.65, n)
    marital_status = np.random.choice([0, 1, 2], n, p=[0.35, 0.45, 0.20])
    education = np.random.choice([0, 1, 2, 3], n, p=[0.15, 0.25, 0.35, 0.25])
    purpose = np.random.choice(['debt_consolidation', 'home_improvement', 'major_purchase', 
                                'medical', 'auto', 'other'], n, p=[0.25, 0.15, 0.2, 0.1, 0.15, 0.15])
    
    # Generate default based on features (logistic)
    log_odds = (-4.5 + 0.04 * dti - 0.005 * credit_score + 0.01 * (loan_amount/1000)
                + 0.02 * employment_years - 0.01 * age + 0.3 * home_owner
                - 0.5 * marital_status - 0.3 * education)
    
    # Add some non-linearity
    log_odds += 0.0003 * dti**2  # DTI has increasing effect
    
    # Add interaction
    log_odds -= 0.00001 * credit_score * dti  # High credit score mitigates DTI
    
    prob = 1 / (1 + np.exp(-log_odds))
    default = np.random.binomial(1, prob)
    
    # Create DataFrame
    df = pd.DataFrame({
        'application_id': range(1, n+1),
        'applicant_age': age,
        'income': income,
        'credit_score': credit_score,
        'dti': dti,
        'loan_amount': loan_amount,
        'loan_term': loan_term,
        'employment_years': employment_years,
        'home_owner': home_owner,
        'marital_status': marital_status,
        'education': education,
        'purpose': purpose,
        'default': default
    })
    
    return df

def generate_customer_data(n=50000):
    """Generate synthetic customer data."""
    np.random.seed(123)
    
    customer_ids = np.arange(1, n+1)
    age = np.random.normal(45, 15, n).clip(18, 85).astype(int)
    income = np.random.gamma(5, 20, n) + 25
    credit_score = np.random.normal(700, 50, n).clip(550, 850).astype(int)
    account_balance = np.random.gamma(3, 100, n).clip(0, 50000)
    transaction_count = np.random.gamma(2, 20, n).clip(0, 200).astype(int)
    avg_transaction_amount = np.random.gamma(2, 50, n).clip(10, 500)
    tenure = np.random.gamma(2, 5, n).clip(0, 20).astype(int)  # years
    churn = np.random.binomial(1, 0.05, n)
    
    return pd.DataFrame({
        'customer_id': customer_ids,
        'age': age,
        'income': income,
        'credit_score': credit_score,
        'account_balance': account_balance,
        'transaction_count': transaction_count,
        'avg_transaction_amount': avg_transaction_amount,
        'tenure': tenure,
        'churn': churn
    })

def generate_transactions(n=1000000):
    """Generate synthetic transaction data."""
    np.random.seed(456)
    
    customer_ids = np.random.choice(np.arange(1, 50001), n, replace=True)
    dates = pd.date_range(start='2021-01-01', end='2024-01-01', periods=n)
    amounts = np.random.lognormal(3, 1, n).clip(1, 1000)
    categories = np.random.choice(['groceries', 'dining', 'entertainment', 'utilities', 
                                   'transport', 'shopping', 'healthcare', 'education', 
                                   'travel', 'other'], n)
    merchants = np.random.choice(['Amazon', 'Target', 'Walmart', 'Starbucks', 'Uber', 
                                  'Netflix', 'Spotify', 'Apple', 'Google', 'Other'], n)
    channels = np.random.choice(['online', 'in-store', 'mobile', 'ATM'], n)
    
    return pd.DataFrame({
        'transaction_id': np.arange(1, n+1),
        'customer_id': customer_ids,
        'date': dates,
        'amount': amounts,
        'category': categories,
        'merchant': merchants,
        'channel': channels
    })

def generate_macro_data():
    """Generate synthetic macroeconomic data."""
    np.random.seed(789)
    
    dates = pd.date_range(start='2014-01-01', end='2024-01-01', freq='Q')
    n = len(dates)
    
    gdp_growth = np.random.normal(0.025, 0.01, n)
    unemployment = np.random.normal(0.05, 0.008, n).clip(0.03, 0.09)
    inflation = np.random.normal(0.02, 0.005, n).clip(0.01, 0.045)
    interest_rate = np.random.normal(0.035, 0.01, n).clip(0.01, 0.06)
    consumer_confidence = np.random.normal(100, 10, n).clip(70, 130).astype(int)
    
    return pd.DataFrame({
        'date': dates,
        'gdp_growth': gdp_growth,
        'unemployment': unemployment,
        'inflation': inflation,
        'interest_rate': interest_rate,
        'consumer_confidence': consumer_confidence
    })

def generate_loan_performance(n=50000):
    """Generate synthetic loan performance data."""
    np.random.seed(101)
    
    loan_ids = np.arange(1, n+1)
    origination_dates = pd.date_range(start='2020-01-01', end='2023-12-31', periods=n)
    maturity_dates = origination_dates + pd.Timedelta(days=np.random.choice([365, 730, 1095, 1460, 1825], n))
    current_balance = np.random.gamma(3, 50, n).clip(0, 200)
    delinquency_status = np.random.choice([0, 1, 2, 3], n, p=[0.75, 0.15, 0.07, 0.03])
    default_date = np.where(delinquency_status > 0, 
                           origination_dates + pd.Timedelta(days=np.random.randint(30, 365, n)), 
                           None)
    
    return pd.DataFrame({
        'loan_id': loan_ids,
        'origination_date': origination_dates,
        'maturity_date': maturity_dates,
        'current_balance': current_balance,
        'delinquency_status': delinquency_status,
        'default_date': default_date
    })

# Generate all datasets
print("Generating datasets...")
loan_apps = generate_loan_applications(10000)  # Smaller for demonstration
customers = generate_customer_data(5000)
transactions = generate_transactions(50000)
macro = generate_macro_data()
loan_perf = generate_loan_performance(5000)

print(f"Loan Applications: {len(loan_apps):,} rows")
print(f"Customer Data: {len(customers):,} rows")
print(f"Transactions: {len(transactions):,} rows")
print(f"Macro Data: {len(macro)} rows")
print(f"Loan Performance: {len(loan_perf):,} rows")

# Save to CSV (for use in the project)
loan_apps.to_csv('loan_applications.csv', index=False)
customers.to_csv('customer_data.csv', index=False)
transactions.to_csv('transactions.csv', index=False)
macro.to_csv('macro_data.csv', index=False)
loan_perf.to_csv('loan_performance.csv', index=False)

print("\nDatasets saved to CSV files.")

SECTION 3: EXPLORATORY DATA ANALYSIS (EDA)

python
# ----------------------------------------------------------------
# PART B: EXPLORATORY DATA ANALYSIS – LOAN APPLICATIONS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Exploratory Data Analysis – Loan Applications")
print("-"*60)

# Load the data
loan_apps = pd.read_csv('loan_applications.csv')
print(f"Dataset shape: {loan_apps.shape}")
print("\nFirst 5 rows:")
print(loan_apps.head())

print("\nData Types:")
print(loan_apps.dtypes)

print("\nSummary Statistics:")
print(loan_apps.describe())

print("\nMissing Values:")
print(loan_apps.isnull().sum())

# Default rate
default_rate = loan_apps['default'].mean()
print(f"\nDefault Rate: {default_rate:.2%}")

# ----------------------------------------------------------------
# PART C: VISUALISATIONS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Visualising the Data")
print("-"*60)

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# 1. Default Distribution
ax = axes[0, 0]
default_counts = loan_apps['default'].value_counts()
ax.pie(default_counts.values, labels=['No Default', 'Default'], autopct='%1.1f%%', 
       colors=['green', 'red'], startangle=90)
ax.set_title('Default Distribution')

# 2. Credit Score Distribution by Default
ax = axes[0, 1]
sns.histplot(data=loan_apps, x='credit_score', hue='default', bins=30, kde=True, ax=ax)
ax.set_title('Credit Score Distribution by Default')

# 3. DTI Distribution by Default
ax = axes[0, 2]
sns.boxplot(data=loan_apps, x='default', y='dti', ax=ax)
ax.set_title('DTI by Default')
ax.set_xticklabels(['No Default', 'Default'])

# 4. Income vs Loan Amount
ax = axes[1, 0]
scatter = ax.scatter(loan_apps['income'], loan_apps['loan_amount'], 
                     c=loan_apps['default'], cmap='RdYlGn_r', alpha=0.5, s=10)
ax.set_xlabel('Income ($000s)')
ax.set_ylabel('Loan Amount ($000s)')
ax.set_title('Income vs Loan Amount (colour = default)')
plt.colorbar(scatter, ax=ax)

# 5. Default by Home Ownership
ax = axes[1, 1]
default_by_home = loan_apps.groupby('home_owner')['default'].mean()
ax.bar(['Non-Owner', 'Home Owner'], default_by_home.values, color=['orange', 'blue'])
ax.set_ylabel('Default Rate')
ax.set_title('Default Rate by Home Ownership')

# 6. Correlation Heatmap
ax = axes[1, 2]
numeric_cols = ['applicant_age', 'income', 'credit_score', 'dti', 'loan_amount', 
                'loan_term', 'employment_years', 'default']
corr = loan_apps[numeric_cols].corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', ax=ax)
ax.set_title('Correlation Matrix')

plt.tight_layout()
plt.savefig('eda_loan_apps.png', dpi=300, bbox_inches='tight')
plt.show()
print("EDA visualisations saved as 'eda_loan_apps.png'")

# ----------------------------------------------------------------
# PART D: FEATURE ENGINEERING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Feature Engineering")
print("-"*60)

# Create a copy for feature engineering
df = loan_apps.copy()

# 1. Debt-to-Income Ratio (already present, but could be derived)
# 2. Loan-to-Income Ratio
df['loan_to_income'] = df['loan_amount'] / df['income']

# 3. Income Category (binned)
df['income_category'] = pd.cut(df['income'], bins=[0, 30, 50, 80, 120, 200], 
                               labels=['Very Low', 'Low', 'Medium', 'High', 'Very High'])

# 4. Credit Score Category
df['credit_score_category'] = pd.cut(df['credit_score'], bins=[550, 600, 650, 700, 750, 850],
                                     labels=['Poor', 'Fair', 'Good', 'Very Good', 'Excellent'])

# 5. DTI Category
df['dti_category'] = pd.cut(df['dti'], bins=[0, 20, 30, 40, 50, 100],
                            labels=['Low', 'Moderate', 'High', 'Very High', 'Extreme'])

# 6. Age Category
df['age_category'] = pd.cut(df['applicant_age'], bins=[18, 30, 40, 50, 60, 80],
                            labels=['Young', 'Young Adult', 'Middle Age', 'Older Adult', 'Senior'])

# 7. Interactive features
df['dti_credit_interaction'] = df['dti'] * df['credit_score'] / 1000

# 8. Non-linear features
df['dti_squared'] = df['dti'] ** 2
df['loan_amount_log'] = np.log(df['loan_amount'] + 1)

print("New features created:")
print(df[['loan_to_income', 'dti_credit_interaction', 'dti_squared', 'loan_amount_log']].head())

# ----------------------------------------------------------------
# PART E: HANDLING MISSING DATA AND OUTLIERS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Handling Missing Data and Outliers")
print("-"*60)

# Check for missing values
print("Missing values before handling:")
print(df.isnull().sum())

# Handle outliers (using IQR method)
def handle_outliers_iqr(df, columns, multiplier=1.5):
    """Handle outliers using IQR method."""
    df_clean = df.copy()
    for col in columns:
        if col in df_clean.columns:
            Q1 = df_clean[col].quantile(0.25)
            Q3 = df_clean[col].quantile(0.75)
            IQR = Q3 - Q1
            lower_bound = Q1 - multiplier * IQR
            upper_bound = Q3 + multiplier * IQR
            # Cap outliers
            df_clean[col] = df_clean[col].clip(lower_bound, upper_bound)
    return df_clean

# Identify numeric columns for outlier handling
numeric_cols = ['income', 'credit_score', 'dti', 'loan_amount', 'loan_term', 'employment_years']
df_clean = handle_outliers_iqr(df, numeric_cols)

# Check for outliers after handling
print("\nAfter handling outliers (clipped):")
print(df_clean[numeric_cols].describe())

# ----------------------------------------------------------------
# PART F: DATA PREPARATION FOR MODELLING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Data Preparation for Modelling")
print("-"*60)

# Select features for modelling
features = ['applicant_age', 'income', 'credit_score', 'dti', 'loan_amount', 
            'loan_term', 'employment_years', 'home_owner', 'marital_status', 
            'education', 'loan_to_income', 'dti_credit_interaction', 
            'dti_squared', 'loan_amount_log']

# Separate features and target
X = df_clean[features]
y = df_clean['default']

print(f"Feature matrix shape: {X.shape}")
print(f"Target shape: {y.shape}")
print(f"Features: {features}")

# One-hot encode categorical features
# Note: Some features are already encoded; we'll handle purpose separately
X_encoded = pd.get_dummies(X, columns=['marital_status', 'education'])
# Handle purpose separately if included
# X_encoded = pd.get_dummies(X_encoded, columns=['purpose'], drop_first=True)

print(f"\nEncoded feature matrix shape: {X_encoded.shape}")

# Train-test split
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_encoded, y, test_size=0.3, random_state=42)

print(f"Training set: {X_train.shape}")
print(f"Test set: {X_test.shape}")

# Check class balance
print(f"\nTraining set default rate: {y_train.mean():.4f}")
print(f"Test set default rate: {y_test.mean():.4f}")

# Save prepared data
X_train.to_csv('X_train.csv', index=False)
X_test.to_csv('X_test.csv', index=False)
y_train.to_csv('y_train.csv', index=False)
y_test.to_csv('y_test.csv', index=False)

print("\nPrepared data saved to CSV files.")

# ----------------------------------------------------------------
# PART G: SUMMARY AND INSIGHTS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: Summary and Insights")
print("-"*60)

print("""
Key Insights from EDA:

1. **Default Rate**: {default_rate:.2%} of loans defaulted.
2. **Key Risk Factors**:
   - Higher DTI → Higher default rate
   - Lower Credit Score → Higher default rate
   - Non-homeowners → Higher default rate (or stronger effect)
3. **Protective Factors**:
   - Higher income → Lower default rate
   - More employment years → Lower default rate
   - Higher education → Lower default rate (bachelors/post-grad)
4. **Correlations**:
   - Credit score and DTI are negatively correlated (-0.35).
   - Income and loan amount are positively correlated (0.30).
   - Strongest correlation with default: DTI (0.25), Credit Score (-0.22).

Data Quality Issues:
   - No major missing data.
   - Outliers were handled using IQR clipping.
   - Features were engineered to capture non-linear relationships and interactions.

Data Prepared:
   - 100,000 rows, 14 features.
   - 70% training, 30% test split.
   - Ready for modelling in Lesson 3.
""".format(default_rate=default_rate))

print("="*70)
print("END OF LESSON 2 – MODULE 9")
print("="*70)

SECTION 4: SUMMARY FOR THE DATA PRACTITIONER

  • Data loading and understanding the structure of each dataset is the first step.

  • Exploratory Data Analysis (EDA) reveals patterns, relationships, and potential issues.

  • Key insights include: credit score and DTI are the strongest predictors of default; income and employment years are protective factors.

  • Feature engineering creates new variables that capture non-linear relationships and interactions.

  • Outliers were handled using the IQR method.

  • Data is ready for modelling in Lesson 3.


SECTION 5: RECOMMENDED NEXT STEPS

  1. Review the EDA and feature engineering results.

  2. Understand the feature selection decisions.

  3. Prepare for Lesson 3: Building and Evaluating Predictive Models.

  4. Consider additional features you might want to engineer.

  5. Familiarise yourself with the evaluation metrics for credit scoring models.


[END OF LESSON 2 – MODULE 9]

 
Â