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:
-
loan_applications.csv – Historical loan applications with default labels. -
customer_data.csv – Customer demographics and behaviour. -
transactions.csv – Customer transaction history. -
macro_data.csv – Quarterly macroeconomic indicators. -
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.
# =================================================================== # 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)
# ---------------------------------------------------------------- # 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
-
Review the EDA and feature engineering results.
-
Understand the feature selection decisions.
-
Prepare for Lesson 3:Â Building and Evaluating Predictive Models.
-
Consider additional features you might want to engineer.
-
Familiarise yourself with the evaluation metrics for credit scoring models.
[END OF LESSON 2 – MODULE 9]