SECTION 1: LEARNING OBJECTIVES

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

  1. Understand the EDA process and why it is critical before building any financial model.

  2. Load and inspect financial datasets using Python’s pandas library.

  3. Calculate and interpret summary statistics for financial variables (mean, median, standard deviation, skewness, kurtosis).

  4. Create visualizations to understand data distributions, relationships, and patterns.

  5. Identify data quality issues including missing values, outliers, and inconsistencies.

  6. Understand the concept of data profiling and how it applies to banking data.

  7. Document EDA findings effectively for stakeholders and regulators.

  8. Recognize common patterns in financial data (heavy tails, volatility clustering, seasonality).


SECTION 2: INTRODUCTION TO EXPLORATORY DATA ANALYSIS

2.1 What Is EDA and Why Does It Matter in Banking?

Exploratory Data Analysis (EDA) is the process of analyzing datasets to summarize their main characteristics, often using visual methods. In banking, EDA is the critical first step before any modeling or reporting.

Why EDA Is Essential in Banking:

 
 
Reason Example
Understand Data Quality Find missing credit scores before building a risk model
Identify Patterns Detect seasonality in transaction volumes
Detect Anomalies Find suspicious transactions that might indicate fraud
Validate Assumptions Check if loan amounts follow expected distributions
Guide Feature Engineering Discover that log transformation improves model performance
Communicate Insights Present data characteristics to stakeholders

2.2 The EDA Process

text
EDA WORKFLOW:

Step 1: Data Collection & Loading
    ↓
Step 2: Data Profiling (Understanding Structure)
    ↓
Step 3: Summary Statistics & Distributions
    ↓
Step 4: Visual Exploration
    ↓
Step 5: Missing Data Analysis
    ↓
Step 6: Outlier Detection
    ↓
Step 7: Correlation Analysis
    ↓
Step 8: Insights & Documentation

2.3 Setting Up Your EDA Environment

python
# Import required libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')

# Set visualization style
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("Set2")
plt.rcParams['figure.figsize'] = (12, 8)

# Create sample banking dataset for EDA
def generate_banking_data(n_customers=1000, n_transactions=5000):
    """Generate realistic banking data for EDA."""
    np.random.seed(42)
    
    # Customers
    customers = pd.DataFrame({
        'customer_id': range(1, n_customers + 1),
        'age': np.random.normal(45, 15, n_customers).astype(int).clip(18, 80),
        'annual_income': np.random.lognormal(10.5, 0.6, n_customers).astype(int).clip(20000, 250000),
        'credit_score': np.random.normal(700, 50, n_customers).astype(int).clip(500, 850),
        'state': np.random.choice(['CA', 'NY', 'TX', 'FL', 'IL', 'PA', 'OH', 'GA', 'NC', 'MI'], n_customers),
        'segment': np.random.choice(['Premium', 'Standard', 'Basic'], n_customers, p=[0.15, 0.55, 0.30]),
        'tenure_months': np.random.exponential(36, n_customers).astype(int).clip(1, 240)
    })
    
    # Introduce some missing values
    customers.loc[np.random.choice(customers.index, size=50, replace=False), 'credit_score'] = np.nan
    customers.loc[np.random.choice(customers.index, size=30, replace=False), 'annual_income'] = np.nan
    
    # Transactions (last 12 months)
    transactions = []
    for i in range(n_transactions):
        cust_id = np.random.randint(1, n_customers + 1)
        days_ago = np.random.randint(0, 365)
        amount = np.random.lognormal(4, 1.2)
        trans_type = np.random.choice(['Deposit', 'Withdrawal', 'Purchase', 'Transfer'], 
                                     p=[0.25, 0.30, 0.35, 0.10])
        status = np.random.choice(['Completed', 'Pending', 'Failed'], p=[0.85, 0.08, 0.07])
        
        transactions.append({
            'transaction_id': i + 1,
            'customer_id': cust_id,
            'date': datetime.now() - timedelta(days=days_ago),
            'amount': round(amount, 2),
            'transaction_type': trans_type,
            'status': status,
            'merchant': np.random.choice(['Amazon', 'Walmart', 'Target', 'Starbucks', 'Whole Foods', 
                                         'Shell Gas', 'CVS', 'Home Depot', 'Apple Store', 'Netflix'], size=1)[0],
            'category': np.random.choice(['Shopping', 'Groceries', 'Dining', 'Transportation', 
                                          'Entertainment', 'Health', 'Home Improvement'], size=1)[0]
        })
    
    transactions = pd.DataFrame(transactions)
    
    # Convert date
    transactions['date'] = pd.to_datetime(transactions['date'])
    transactions['year'] = transactions['date'].dt.year
    transactions['month'] = transactions['date'].dt.month
    transactions['day'] = transactions['date'].dt.day
    transactions['day_of_week'] = transactions['date'].dt.day_name()
    
    return customers, transactions

# Generate data
customers, transactions = generate_banking_data()
print(f"Customers: {len(customers)}")
print(f"Transactions: {len(transactions)}")
print("\nCustomers Sample:")
print(customers.head())
print("\nTransactions Sample:")
print(transactions.head())

SECTION 3: DATA PROFILING – UNDERSTANDING YOUR DATA

3.1 Basic Data Inspection

python
# ============= DATA PROFILING =============

# 1. Data Overview
print("="*60)
print("DATA PROFILING REPORT")
print("="*60)

print(f"\n📊 Data Shape:")
print(f"  Customers: {customers.shape[0]} rows, {customers.shape[1]} columns")
print(f"  Transactions: {transactions.shape[0]} rows, {transactions.shape[1]} columns")

# 2. Data Types
print("\n📋 Data Types:")
print(f"Customers:\n{customers.dtypes}")
print(f"\nTransactions:\n{transactions.dtypes}")

# 3. Memory Usage
print("\n💾 Memory Usage:")
print(f"  Customers: {customers.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
print(f"  Transactions: {transactions.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

# 4. Column Info
print("\n📝 Column Info:")
print("Customers:")
print(customers.info())
print("\nTransactions:")
print(transactions.info())

# 5. Unique Values
print("\n🔑 Unique Values:")
print(f"  Customer states: {customers['state'].unique()}")
print(f"  Customer segments: {customers['segment'].unique()}")
print(f"  Transaction types: {transactions['transaction_type'].unique()}")
print(f"  Transaction statuses: {transactions['status'].unique()}")

3.2 Summary Statistics

python
# ============= SUMMARY STATISTICS =============

# Numerical columns
print("\n" + "="*60)
print("SUMMARY STATISTICS - NUMERICAL COLUMNS")
print("="*60)

print("\nCustomers - Numerical:")
print(customers.describe())

print("\nTransactions - Numerical:")
print(transactions[['amount']].describe())

# Categorical columns
print("\n" + "="*60)
print("SUMMARY STATISTICS - CATEGORICAL COLUMNS")
print("="*60)

print("\nCustomers - Categorical:")
for col in ['state', 'segment']:
    print(f"\n{col.upper()}:")
    print(customers[col].value_counts())
    print(f"  Unique values: {customers[col].nunique()}")
    print(f"  Most common: {customers[col].mode().values[0]} ({customers[col].value_counts().iloc[0]} records)")

print("\nTransactions - Categorical:")
for col in ['transaction_type', 'status', 'category', 'merchant']:
    print(f"\n{col.upper()}:")
    print(transactions[col].value_counts().head(5))
    print(f"  Unique values: {transactions[col].nunique()}")

3.3 Understanding Distribution Statistics

python
# ============= DISTRIBUTION ANALYSIS =============

def calculate_distribution_stats(data):
    """Calculate detailed distribution statistics."""
    stats = {
        'mean': data.mean(),
        'median': data.median(),
        'mode': data.mode().iloc[0] if not data.mode().empty else np.nan,
        'std': data.std(),
        'variance': data.var(),
        'skewness': data.skew(),
        'kurtosis': data.kurtosis(),
        'range': data.max() - data.min(),
        'iqr': data.quantile(0.75) - data.quantile(0.25),
        'q1': data.quantile(0.25),
        'q3': data.quantile(0.75),
        'min': data.min(),
        'max': data.max()
    }
    return stats

print("\n" + "="*60)
print("DISTRIBUTION STATISTICS")
print("="*60)

# Customer age distribution
print("\n📊 Customer Age Distribution:")
age_stats = calculate_distribution_stats(customers['age'].dropna())
for key, value in age_stats.items():
    print(f"  {key}: {value:.2f}")

# Customer income distribution
print("\n📊 Customer Income Distribution:")
income_stats = calculate_distribution_stats(customers['annual_income'].dropna())
for key, value in income_stats.items():
    print(f"  {key}: ${value:,.2f}" if key in ['mean', 'median'] else f"  {key}: {value:.2f}")

# Credit score distribution
print("\n📊 Credit Score Distribution:")
credit_stats = calculate_distribution_stats(customers['credit_score'].dropna())
for key, value in credit_stats.items():
    print(f"  {key}: {value:.2f}")

# Transaction amount distribution
print("\n📊 Transaction Amount Distribution:")
txn_stats = calculate_distribution_stats(transactions['amount'].dropna())
for key, value in txn_stats.items():
    print(f"  {key}: ${value:,.2f}" if key in ['mean', 'median'] else f"  {key}: {value:.2f}")

SECTION 4: VISUAL EXPLORATION

4.1 Visualizing Distributions

python
# ============= VISUAL EXPLORATION =============

# 1. Histogram: Customer Age Distribution
fig, axes = plt.subplots(2, 3, figsize=(18, 10))

# Age distribution
customers['age'].hist(bins=30, ax=axes[0, 0], edgecolor='black', alpha=0.7)
axes[0, 0].set_title('Customer Age Distribution', fontsize=14)
axes[0, 0].set_xlabel('Age')
axes[0, 0].set_ylabel('Count')
axes[0, 0].axvline(customers['age'].mean(), color='red', linestyle='--', label=f"Mean: {customers['age'].mean():.1f}")
axes[0, 0].axvline(customers['age'].median(), color='blue', linestyle='--', label=f"Median: {customers['age'].median():.1f}")
axes[0, 0].legend()

# Income distribution (with log transform)
customers['annual_income'].hist(bins=50, ax=axes[0, 1], edgecolor='black', alpha=0.7)
axes[0, 1].set_title('Annual Income Distribution', fontsize=14)
axes[0, 1].set_xlabel('Annual Income ($)')
axes[0, 1].set_ylabel('Count')
axes[0, 1].axvline(customers['annual_income'].mean(), color='red', linestyle='--', label=f"Mean: ${customers['annual_income'].mean():,.0f}")
axes[0, 1].axvline(customers['annual_income'].median(), color='blue', linestyle='--', label=f"Median: ${customers['annual_income'].median():,.0f}")
axes[0, 1].legend()

# Log income distribution (better visualization)
np.log(customers['annual_income'].dropna()).hist(bins=30, ax=axes[0, 2], edgecolor='black', alpha=0.7)
axes[0, 2].set_title('Log Annual Income Distribution', fontsize=14)
axes[0, 2].set_xlabel('Log(Annual Income)')
axes[0, 2].set_ylabel('Count')

# Credit score distribution
customers['credit_score'].dropna().hist(bins=30, ax=axes[1, 0], edgecolor='black', alpha=0.7)
axes[1, 0].set_title('Credit Score Distribution', fontsize=14)
axes[1, 0].set_xlabel('Credit Score')
axes[1, 0].set_ylabel('Count')
axes[1, 0].axvline(customers['credit_score'].mean(), color='red', linestyle='--', label=f"Mean: {customers['credit_score'].mean():.1f}")
axes[1, 0].axvline(customers['credit_score'].median(), color='blue', linestyle='--', label=f"Median: {customers['credit_score'].median():.1f}")
axes[1, 0].legend()

# Tenure distribution
customers['tenure_months'].hist(bins=30, ax=axes[1, 1], edgecolor='black', alpha=0.7)
axes[1, 1].set_title('Customer Tenure Distribution', fontsize=14)
axes[1, 1].set_xlabel('Tenure (Months)')
axes[1, 1].set_ylabel('Count')

# Transaction amount distribution
transactions['amount'].hist(bins=50, ax=axes[1, 2], edgecolor='black', alpha=0.7)
axes[1, 2].set_title('Transaction Amount Distribution', fontsize=14)
axes[1, 2].set_xlabel('Amount ($)')
axes[1, 2].set_ylabel('Count')
axes[1, 2].axvline(transactions['amount'].mean(), color='red', linestyle='--', label=f"Mean: ${transactions['amount'].mean():.2f}")
axes[1, 2].axvline(transactions['amount'].median(), color='blue', linestyle='--', label=f"Median: ${transactions['amount'].median():.2f}")
axes[1, 2].legend()

plt.tight_layout()
plt.savefig('distribution_plots.png', dpi=300)
plt.show()

4.2 Box Plots for Outlier Detection

python
# ============= BOX PLOTS =============

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

# Box plot by segment
customers.boxplot(column='annual_income', by='segment', ax=axes[0, 0])
axes[0, 0].set_title('Income by Customer Segment', fontsize=14)
axes[0, 0].set_xlabel('Segment')
axes[0, 0].set_ylabel('Annual Income ($)')

# Box plot by state
customers.boxplot(column='credit_score', by='state', ax=axes[0, 1])
axes[0, 1].set_title('Credit Score by State', fontsize=14)
axes[0, 1].set_xlabel('State')
axes[0, 1].set_ylabel('Credit Score')
axes[0, 1].tick_params(axis='x', rotation=45)

# Box plot for transaction amounts by type
transactions.boxplot(column='amount', by='transaction_type', ax=axes[1, 0])
axes[1, 0].set_title('Transaction Amount by Type', fontsize=14)
axes[1, 0].set_xlabel('Transaction Type')
axes[1, 0].set_ylabel('Amount ($)')

# Box plot for transaction amounts by status
transactions.boxplot(column='amount', by='status', ax=axes[1, 1])
axes[1, 1].set_title('Transaction Amount by Status', fontsize=14)
axes[1, 1].set_xlabel('Status')
axes[1, 1].set_ylabel('Amount ($)')

plt.tight_layout()
plt.savefig('box_plots.png', dpi=300)
plt.show()

4.3 Categorical Data Visualization

python
# ============= CATEGORICAL VISUALIZATION =============

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

# Customer segment distribution
segment_counts = customers['segment'].value_counts()
segment_counts.plot(kind='bar', ax=axes[0, 0], color=['#2ecc71', '#f1c40f', '#e74c3c'])
axes[0, 0].set_title('Customer Segment Distribution', fontsize=14)
axes[0, 0].set_xlabel('Segment')
axes[0, 0].set_ylabel('Count')
axes[0, 0].set_ylim(0, max(segment_counts) * 1.1)
for i, v in enumerate(segment_counts.values):
    axes[0, 0].text(i, v + 10, str(v), ha='center', va='bottom')

# Transaction type distribution
txn_type_counts = transactions['transaction_type'].value_counts()
txn_type_counts.plot(kind='bar', ax=axes[0, 1], color=['#3498db', '#e67e22', '#2ecc71', '#9b59b6'])
axes[0, 1].set_title('Transaction Type Distribution', fontsize=14)
axes[0, 1].set_xlabel('Transaction Type')
axes[0, 1].set_ylabel('Count')

# Transaction status distribution
txn_status_counts = transactions['status'].value_counts()
txn_status_counts.plot(kind='bar', ax=axes[1, 0], color=['#2ecc71', '#f1c40f', '#e74c3c'])
axes[1, 0].set_title('Transaction Status Distribution', fontsize=14)
axes[1, 0].set_xlabel('Status')
axes[1, 0].set_ylabel('Count')

# Top merchants
top_merchants = transactions['merchant'].value_counts().head(10)
top_merchants.plot(kind='bar', ax=axes[1, 1], color='#3498db')
axes[1, 1].set_title('Top 10 Merchants by Transaction Volume', fontsize=14)
axes[1, 1].set_xlabel('Merchant')
axes[1, 1].set_ylabel('Count')
axes[1, 1].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.savefig('categorical_plots.png', dpi=300)
plt.show()

4.4 Time Series Visualization

python
# ============= TIME SERIES VISUALIZATION =============

# Aggregate transactions by date
daily_volume = transactions.groupby('date')['amount'].agg(['sum', 'count'])
daily_volume.columns = ['total_amount', 'transaction_count']

# Monthly aggregation
monthly_volume = transactions.groupby(['year', 'month'])['amount'].agg(['sum', 'count'])
monthly_volume.columns = ['total_amount', 'transaction_count']
monthly_volume['date'] = pd.to_datetime(monthly_volume.index.map(lambda x: f"{x[0]}-{x[1]:02d}-01"))

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

# Daily transaction volume
daily_volume['total_amount'].plot(ax=axes[0, 0], color='#2c3e50', alpha=0.8)
axes[0, 0].set_title('Daily Transaction Volume (Last 365 Days)', fontsize=14)
axes[0, 0].set_xlabel('Date')
axes[0, 0].set_ylabel('Total Amount ($)')
axes[0, 0].axhline(daily_volume['total_amount'].mean(), color='red', linestyle='--', label=f"Mean: ${daily_volume['total_amount'].mean():,.0f}")
axes[0, 0].legend()

# Daily transaction count
daily_volume['transaction_count'].plot(ax=axes[0, 1], color='#e67e22', alpha=0.8)
axes[0, 1].set_title('Daily Transaction Count (Last 365 Days)', fontsize=14)
axes[0, 1].set_xlabel('Date')
axes[0, 1].set_ylabel('Transaction Count')
axes[0, 1].axhline(daily_volume['transaction_count'].mean(), color='red', linestyle='--', label=f"Mean: {daily_volume['transaction_count'].mean():.0f}")
axes[0, 1].legend()

# Monthly total amount
monthly_volume.sort_values('date').plot(x='date', y='total_amount', ax=axes[1, 0], kind='bar', color='#2c3e50')
axes[1, 0].set_title('Monthly Transaction Volume', fontsize=14)
axes[1, 0].set_xlabel('Month')
axes[1, 0].set_ylabel('Total Amount ($)')
axes[1, 0].tick_params(axis='x', rotation=45)

# Monthly transaction count
monthly_volume.sort_values('date').plot(x='date', y='transaction_count', ax=axes[1, 1], kind='bar', color='#e67e22')
axes[1, 1].set_title('Monthly Transaction Count', fontsize=14)
axes[1, 1].set_xlabel('Month')
axes[1, 1].set_ylabel('Transaction Count')
axes[1, 1].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.savefig('time_series_plots.png', dpi=300)
plt.show()

4.5 Correlation Analysis

python
# ============= CORRELATION ANALYSIS =============

# Select numerical columns for correlation
numerical_cols = ['age', 'annual_income', 'credit_score', 'tenure_months']
correlation_matrix = customers[numerical_cols].corr()

# Heatmap
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Correlation heatmap
sns.heatmap(correlation_matrix, annot=True, cmap='RdBu_r', center=0, ax=axes[0], 
            fmt='.2f', square=True, cbar_kws={'shrink': 0.8})
axes[0].set_title('Correlation Matrix - Customer Features', fontsize=14)

# Scatter plot with regression
sns.regplot(x='age', y='credit_score', data=customers, ax=axes[1], 
            scatter_kws={'alpha': 0.5}, line_kws={'color': 'red'})
axes[1].set_title('Age vs Credit Score', fontsize=14)
axes[1].set_xlabel('Age')
axes[1].set_ylabel('Credit Score')

plt.tight_layout()
plt.savefig('correlation_plots.png', dpi=300)
plt.show()

# Print correlation insights
print("\n" + "="*60)
print("CORRELATION INSIGHTS")
print("="*60)
print("\nCorrelation Matrix:")
print(correlation_matrix.round(3))
print("\n🔍 Key Observations:")
print(f"  • Age vs Income: {correlation_matrix.loc['age', 'annual_income']:.3f} (weak relationship)")
print(f"  • Age vs Credit Score: {correlation_matrix.loc['age', 'credit_score']:.3f} (weak relationship)")
print(f"  • Income vs Credit Score: {correlation_matrix.loc['annual_income', 'credit_score']:.3f} (moderate relationship)")
print(f"  • Tenure vs Income: {correlation_matrix.loc['tenure_months', 'annual_income']:.3f} (weak relationship)")

SECTION 5: MISSING DATA ANALYSIS

5.1 Identifying Missing Data

python
# ============= MISSING DATA ANALYSIS =============

def analyze_missing_data(df, dataset_name):
    """Analyze and report missing data."""
    missing_data = df.isnull().sum()
    missing_percent = (missing_data / len(df)) * 100
    
    missing_df = pd.DataFrame({
        'Column': missing_data.index,
        'Missing Count': missing_data.values,
        'Missing %': missing_percent.values
    }).sort_values('Missing %', ascending=False)
    
    missing_df = missing_df[missing_df['Missing Count'] > 0]
    
    print(f"\n{'='*60}")
    print(f"MISSING DATA ANALYSIS: {dataset_name}")
    print(f"{'='*60}")
    print(f"Total records: {len(df)}")
    
    if missing_df.empty:
        print("✅ No missing data found!")
    else:
        print(f"\n📊 Columns with Missing Data:")
        print(missing_df.to_string(index=False))
        print(f"\nTotal missing values: {missing_df['Missing Count'].sum()}")
        print(f"Overall completeness: {(1 - missing_df['Missing Count'].sum() / (len(df) * len(df.columns))) * 100:.1f}%")
    
    return missing_df

# Analyze missing data
missing_customers = analyze_missing_data(customers, "Customers")
missing_transactions = analyze_missing_data(transactions, "Transactions")

# Visualize missing data
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Customers missing data
missing_customers_vis = customers.isnull().sum()
missing_customers_vis = missing_customers_vis[missing_customers_vis > 0]
if not missing_customers_vis.empty:
    missing_customers_vis.sort_values().plot(kind='barh', ax=axes[0], color='#e74c3c')
    axes[0].set_title('Missing Data - Customers', fontsize=14)
    axes[0].set_xlabel('Missing Count')
    axes[0].set_ylabel('Column')

# Transactions missing data
missing_transactions_vis = transactions.isnull().sum()
missing_transactions_vis = missing_transactions_vis[missing_transactions_vis > 0]
if not missing_transactions_vis.empty:
    missing_transactions_vis.sort_values().plot(kind='barh', ax=axes[1], color='#e74c3c')
    axes[1].set_title('Missing Data - Transactions', fontsize=14)
    axes[1].set_xlabel('Missing Count')
    axes[1].set_ylabel('Column')

plt.tight_layout()
plt.savefig('missing_data_plots.png', dpi=300)
plt.show()

5.2 Patterns in Missing Data

python
# ============= PATTERNS IN MISSING DATA =============

def analyze_missing_patterns(df):
    """Analyze patterns in missing data."""
    # Check if missing data is related to other columns
    missing_cols = df.columns[df.isnull().any()].tolist()
    
    patterns = {}
    for col in missing_cols:
        # Check if missingness is related to another column
        pattern = []
        for other_col in df.columns:
            if other_col != col:
                # Compare distributions when missing vs not missing
                missing_grp = df[df[col].isnull()][other_col]
                not_missing_grp = df[df[col].notnull()][other_col]
                
                if len(missing_grp) > 0 and len(not_missing_grp) > 0:
                    if pd.api.types.is_numeric_dtype(missing_grp):
                        # Compare means
                        diff = missing_grp.mean() - not_missing_grp.mean()
                        if abs(diff) > 0.1 * not_missing_grp.std():
                            pattern.append((other_col, 'numeric', diff))
                    else:
                        # Compare value counts
                        if len(missing_grp.unique()) > 0:
                            pattern.append((other_col, 'categorical', len(missing_grp.unique())))
        
        if pattern:
            patterns[col] = pattern
    
    return patterns

# Analyze patterns
patterns = analyze_missing_patterns(customers)
if patterns:
    print("\n" + "="*60)
    print("PATTERNS IN MISSING DATA")
    print("="*60)
    for col, pattern in patterns.items():
        print(f"\n📊 {col}:")
        for other_col, type_info, value in pattern:
            if type_info == 'numeric':
                print(f"  • Missingness related to {other_col} (difference: {value:.2f})")
            else:
                print(f"  • Missingness related to {other_col}")

SECTION 6: OUTLIER DETECTION

6.1 Statistical Methods for Outlier Detection

python
# ============= OUTLIER DETECTION =============

def detect_outliers_iqr(data, column, multiplier=1.5):
    """Detect outliers using IQR method."""
    q1 = data[column].quantile(0.25)
    q3 = data[column].quantile(0.75)
    iqr = q3 - q1
    
    lower_bound = q1 - multiplier * iqr
    upper_bound = q3 + multiplier * iqr
    
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    
    return outliers, lower_bound, upper_bound

def detect_outliers_zscore(data, column, threshold=3):
    """Detect outliers using Z-score method."""
    mean = data[column].mean()
    std = data[column].std()
    
    z_scores = (data[column] - mean) / std
    outliers = data[abs(z_scores) > threshold]
    
    return outliers

# Analyze outliers for key columns
outlier_columns = ['age', 'annual_income', 'credit_score', 'tenure_months']

print("\n" + "="*60)
print("OUTLIER DETECTION - IQR METHOD")
print("="*60)

for col in outlier_columns:
    outliers, lb, ub = detect_outliers_iqr(customers, col)
    print(f"\n📊 {col}:")
    print(f"  Normal range: [{lb:.2f}, {ub:.2f}]")
    print(f"  Outliers found: {len(outliers)} ({len(outliers)/len(customers)*100:.1f}%)")
    if len(outliers) > 0:
        print(f"  Outlier values: {outliers[col].min():.2f} to {outliers[col].max():.2f}")

# Visualize outliers
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for idx, col in enumerate(outlier_columns):
    row = idx // 2
    col_idx = idx % 2
    
    # IQR method
    outliers, lb, ub = detect_outliers_iqr(customers, col)
    
    # Histogram with outlier boundaries
    customers[col].hist(bins=30, ax=axes[row, col_idx], edgecolor='black', alpha=0.7)
    axes[row, col_idx].axvline(lb, color='red', linestyle='--', label=f'Lower Bound: {lb:.2f}')
    axes[row, col_idx].axvline(ub, color='red', linestyle='--', label=f'Upper Bound: {ub:.2f}')
    axes[row, col_idx].set_title(f'{col} - Outlier Detection', fontsize=12)
    axes[row, col_idx].set_xlabel(col)
    axes[row, col_idx].set_ylabel('Count')
    axes[row, col_idx].legend()

plt.tight_layout()
plt.savefig('outlier_plots.png', dpi=300)
plt.show()

6.2 Handling Outliers

python
# ============= HANDLING OUTLIERS =============

def handle_outliers(data, column, method='winsorize', threshold=1.5):
    """Handle outliers using different methods."""
    
    if method == 'winsorize':
        # Winsorization: cap extreme values
        q1 = data[column].quantile(0.25)
        q3 = data[column].quantile(0.75)
        iqr = q3 - q1
        
        lower_bound = q1 - threshold * iqr
        upper_bound = q3 + threshold * iqr
        
        data_processed = data[column].copy()
        data_processed = data_processed.clip(lower=lower_bound, upper=upper_bound)
        
        return data_processed, f"Winsorized at {lower_bound:.2f} and {upper_bound:.2f}"
    
    elif method == 'remove':
        # Remove outliers
        q1 = data[column].quantile(0.25)
        q3 = data[column].quantile(0.75)
        iqr = q3 - q1
        
        lower_bound = q1 - threshold * iqr
        upper_bound = q3 + threshold * iqr
        
        mask = (data[column] >= lower_bound) & (data[column] <= upper_bound)
        data_processed = data.loc[mask, column]
        
        return data_processed, f"Removed {len(data) - len(data_processed)} records"
    
    elif method == 'log_transform':
        # Log transform (works well for right-skewed data)
        data_processed = np.log(data[column] + 1)
        
        return data_processed, "Applied log transformation"
    
    else:
        return data[column], "No transformation applied"

# Test outlier handling methods
test_col = 'annual_income'
print("\n" + "="*60)
print(f"OUTLIER HANDLING - {test_col}")
print("="*60)

# Original
print(f"\nOriginal Data:")
print(f"  Mean: ${customers[test_col].mean():,.2f}")
print(f"  Median: ${customers[test_col].median():,.2f}")
print(f"  Min: ${customers[test_col].min():,.2f}")
print(f"  Max: ${customers[test_col].max():,.2f}")
print(f"  Std Dev: ${customers[test_col].std():,.2f}")

# Winsorize
winsorized, info = handle_outliers(customers, test_col, method='winsorize')
print(f"\nWinsorized ({info}):")
print(f"  Mean: ${winsorized.mean():,.2f}")
print(f"  Median: ${winsorized.median():,.2f}")
print(f"  Min: ${winsorized.min():,.2f}")
print(f"  Max: ${winsorized.max():,.2f}")
print(f"  Std Dev: ${winsorized.std():,.2f}")

# Log transform
log_transformed, info = handle_outliers(customers, test_col, method='log_transform')
print(f"\nLog Transformed ({info}):")
print(f"  Mean: {log_transformed.mean():,.2f}")
print(f"  Median: {log_transformed.median():,.2f}")
print(f"  Std Dev: {log_transformed.std():,.2f}")

# Visualize methods
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Original
customers[test_col].hist(bins=50, ax=axes[0], edgecolor='black', alpha=0.7)
axes[0].set_title('Original Distribution', fontsize=14)
axes[0].set_xlabel('Annual Income ($)')
axes[0].set_ylabel('Count')

# Winsorized
winsorized.hist(bins=50, ax=axes[1], edgecolor='black', alpha=0.7)
axes[1].set_title('Winsorized Distribution', fontsize=14)
axes[1].set_xlabel('Annual Income ($)')
axes[1].set_ylabel('Count')

# Log transformed
log_transformed.hist(bins=50, ax=axes[2], edgecolor='black', alpha=0.7)
axes[2].set_title('Log Transformed Distribution', fontsize=14)
axes[2].set_xlabel('Log(Annual Income)')
axes[2].set_ylabel('Count')

plt.tight_layout()
plt.savefig('outlier_handling.png', dpi=300)
plt.show()

SECTION 7: BUSINESS RISK & FINANCIAL IMPACT

7.1 Why EDA Matters for Risk Management

 
 
Risk How EDA Helps Example
Credit Risk Identifies patterns in default behavior Finding that customers with DTI > 40% have 5x default rate
Market Risk Detects volatility patterns Identifying volatility clustering in returns
Operational Risk Finds unusual transaction patterns Detecting fraud rings through transaction networks
Compliance Risk Ensures data quality for reporting Finding missing data in regulatory reports

7.2 Regulatory Requirements

 
 
Regulation EDA Requirement
SR 11-7 Must understand data distribution before modeling
BASEL III Must validate data quality and completeness
CCAR Must identify outliers and anomalies in stress scenarios
GDPR Must understand what data is collected and stored

7.3 EDA Best Practices for Banking

  1. Start with Business Questions: Know what you’re looking for before you start.

  2. Document Everything: Regulators will want to see your EDA process.

  3. Visualize Before Modeling: Charts reveal patterns that numbers hide.

  4. Check Data Quality Early: Bad data leads to bad models.

  5. Understand Distributions: Financial data is rarely normal.

  6. Look for Seasonality: Many banking metrics have monthly/quarterly patterns.

  7. Identify Outliers Carefully: Some outliers are fraud, others are just unusual.

  8. Correlation ≠ Causation: Just because two things move together doesn’t mean one causes the other.


SECTION 8: SUMMARY FOR THE DATA PRACTITIONER

8.1 The 1-Minute Elevator Pitch

“Exploratory Data Analysis is the critical first step in any banking analytics project. We inspect data structure, calculate summary statistics, visualize distributions, identify missing values and outliers, and understand relationships between variables. EDA ensures we understand our data before building models or making decisions. In banking, EDA helps us detect fraud, manage risk, and ensure regulatory compliance. A thorough EDA saves time, prevents costly mistakes, and builds confidence in our analytics.”

8.2 Key Takeaways

  1. EDA is the first step in any analytics project—understand before you model.

  2. Data Profiling reveals the structure, types, and quality of your data.

  3. Summary Statistics (mean, median, std, skewness, kurtosis) describe distributions.

  4. Visualizations reveal patterns that numbers alone can’t show.

  5. Missing Data must be analyzed and handled appropriately.

  6. Outliers can be detected using IQR or Z-score methods.

  7. Correlation Analysis reveals relationships between variables.

  8. Time Series Plots show trends, seasonality, and volatility.

  9. Documentation is essential for regulatory compliance.

  10. EDA saves time by identifying issues before modeling.

8.3 Recommended Next Steps

  1. Practice EDA on real banking data (try Kaggle datasets)

  2. Build an EDA template for your work

  3. Learn advanced visualization libraries (Plotly, Bokeh)

  4. Document your EDA process for regulatory purposes


[END OF LESSON 6]