SECTION 1: LEARNING OBJECTIVES

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

  1. Apply advanced statistical techniques for analyzing financial data distributions, including hypothesis testing and confidence intervals.

  2. Perform multivariate analysis to understand complex relationships between financial variables.

  3. Detect and analyze seasonality and trends in financial time series data.

  4. Use advanced visualization techniques (pair plots, heatmaps, violin plots, parallel coordinates) for financial data exploration.

  5. Conduct customer segmentation analysis using clustering techniques during EDA.

  6. Analyze portfolio characteristics using advanced EDA methods.

  7. Identify data drift and concept drift in financial datasets over time.

  8. Create comprehensive EDA reports for stakeholders and regulators.


SECTION 2: ADVANCED STATISTICAL ANALYSIS

2.1 Hypothesis Testing in Finance

Hypothesis testing helps us make data-driven decisions about financial patterns and relationships.

python
# ============= HYPOTHESIS TESTING IN FINANCE =============

import numpy as np
import pandas as pd
from scipy import stats
from scipy.stats import ttest_ind, mannwhitneyu, f_oneway, chi2_contingency
import matplotlib.pyplot as plt
import seaborn as sns

# Create sample financial data
np.random.seed(42)

# Customer data with different segments
premium_customers = pd.DataFrame({
    'segment': 'Premium',
    'annual_income': np.random.normal(120000, 30000, 200).clip(50000, 250000),
    'credit_score': np.random.normal(750, 40, 200).clip(650, 850),
    'monthly_spend': np.random.normal(3000, 800, 200).clip(500, 6000),
    'default_rate': np.random.beta(1, 10, 200)  # Low default rate
})

standard_customers = pd.DataFrame({
    'segment': 'Standard',
    'annual_income': np.random.normal(70000, 20000, 300).clip(30000, 150000),
    'credit_score': np.random.normal(680, 50, 300).clip(550, 800),
    'monthly_spend': np.random.normal(1800, 600, 300).clip(200, 4000),
    'default_rate': np.random.beta(3, 8, 300)  # Medium default rate
})

basic_customers = pd.DataFrame({
    'segment': 'Basic',
    'annual_income': np.random.normal(40000, 15000, 200).clip(15000, 80000),
    'credit_score': np.random.normal(620, 50, 200).clip(500, 750),
    'monthly_spend': np.random.normal(900, 400, 200).clip(100, 2500),
    'default_rate': np.random.beta(5, 5, 200)  # Higher default rate
})

# Combine data
all_customers = pd.concat([premium_customers, standard_customers, basic_customers], ignore_index=True)

class FinancialHypothesisTester:
    """Perform hypothesis tests for financial data."""
    
    def __init__(self, data):
        self.data = data
        self.results = {}
    
    def test_segment_income_difference(self):
        """Test if income differs significantly between segments."""
        premium_income = self.data[self.data['segment'] == 'Premium']['annual_income']
        standard_income = self.data[self.data['segment'] == 'Standard']['annual_income']
        basic_income = self.data[self.data['segment'] == 'Basic']['annual_income']
        
        # T-test: Premium vs Standard
        t_stat, p_value = ttest_ind(premium_income, standard_income)
        
        self.results['income_premium_vs_standard'] = {
            'test': 'Independent T-Test',
            't_statistic': t_stat,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"Premium customers have {'significantly different' if p_value < 0.05 else 'similar'} income compared to Standard"
        }
        
        # T-test: Standard vs Basic
        t_stat, p_value = ttest_ind(standard_income, basic_income)
        
        self.results['income_standard_vs_basic'] = {
            'test': 'Independent T-Test',
            't_statistic': t_stat,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"Standard customers have {'significantly different' if p_value < 0.05 else 'similar'} income compared to Basic"
        }
        
        # ANOVA: All three segments
        f_stat, p_value = f_oneway(premium_income, standard_income, basic_income)
        
        self.results['income_all_segments'] = {
            'test': 'ANOVA',
            'f_statistic': f_stat,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"There is {'a significant' if p_value < 0.05 else 'no'} difference in income across all segments"
        }
        
        return self.results
    
    def test_credit_score_difference(self):
        """Test if credit scores differ significantly between segments."""
        premium_scores = self.data[self.data['segment'] == 'Premium']['credit_score']
        standard_scores = self.data[self.data['segment'] == 'Standard']['credit_score']
        basic_scores = self.data[self.data['segment'] == 'Basic']['credit_score']
        
        # Mann-Whitney U (non-parametric, for non-normal distributions)
        u_stat, p_value = mannwhitneyu(premium_scores, standard_scores)
        
        self.results['credit_premium_vs_standard'] = {
            'test': 'Mann-Whitney U',
            'u_statistic': u_stat,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"Premium customers have {'significantly different' if p_value < 0.05 else 'similar'} credit scores compared to Standard"
        }
        
        return self.results
    
    def test_default_rate_difference(self):
        """Test if default rates differ significantly between segments."""
        premium_defaults = self.data[self.data['segment'] == 'Premium']['default_rate']
        standard_defaults = self.data[self.data['segment'] == 'Standard']['default_rate']
        basic_defaults = self.data[self.data['segment'] == 'Basic']['default_rate']
        
        # Kruskal-Wallis test (non-parametric ANOVA)
        h_stat, p_value = stats.kruskal(premium_defaults, standard_defaults, basic_defaults)
        
        self.results['default_rates'] = {
            'test': 'Kruskal-Wallis',
            'h_statistic': h_stat,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"There is {'a significant' if p_value < 0.05 else 'no'} difference in default rates across segments",
            'segment_means': {
                'Premium': premium_defaults.mean(),
                'Standard': standard_defaults.mean(),
                'Basic': basic_defaults.mean()
            }
        }
        
        return self.results
    
    def test_correlation_significance(self, var1, var2):
        """Test if correlation between two variables is significant."""
        correlation, p_value = stats.pearsonr(self.data[var1], self.data[var2])
        
        self.results[f'correlation_{var1}_{var2}'] = {
            'test': 'Pearson Correlation',
            'correlation': correlation,
            'p_value': p_value,
            'significant': p_value < 0.05,
            'interpretation': f"There is {'a significant' if p_value < 0.05 else 'no'} correlation between {var1} and {var2}"
        }
        
        return self.results
    
    def run_all_tests(self):
        """Run all hypothesis tests."""
        self.test_segment_income_difference()
        self.test_credit_score_difference()
        self.test_default_rate_difference()
        self.test_correlation_significance('annual_income', 'credit_score')
        self.test_correlation_significance('monthly_spend', 'default_rate')
        
        return self.results
    
    def report_results(self):
        """Generate a comprehensive report of test results."""
        print("\n" + "="*80)
        print("HYPOTHESIS TESTING REPORT")
        print("="*80)
        
        # Run tests if not already run
        if not self.results:
            self.run_all_tests()
        
        for test_name, result in self.results.items():
            print(f"\n📊 {test_name.replace('_', ' ').title()}")
            print(f"  Test: {result.get('test', 'N/A')}")
            print(f"  Statistic: {result.get('t_statistic', result.get('u_statistic', result.get('f_statistic', result.get('correlation', 'N/A')))):.4f}")
            print(f"  P-value: {result['p_value']:.6f}")
            print(f"  Significant: {'✅ YES' if result['significant'] else '❌ NO'} (p < 0.05)")
            print(f"  Interpretation: {result['interpretation']}")
            
            if 'segment_means' in result:
                print("  Segment Means:")
                for segment, mean in result['segment_means'].items():
                    print(f"    {segment}: {mean:.4f}")
        
        # Summary
        significant_tests = sum(1 for r in self.results.values() if r.get('significant', False))
        total_tests = len(self.results)
        
        print("\n" + "-"*80)
        print(f"SUMMARY: {significant_tests}/{total_tests} tests showed statistically significant results")

# Run hypothesis tests
tester = FinancialHypothesisTester(all_customers)
tester.run_all_tests()
tester.report_results()

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

# Income by segment
sns.boxplot(data=all_customers, x='segment', y='annual_income', ax=axes[0, 0])
axes[0, 0].set_title('Income Distribution by Customer Segment', fontsize=14)
axes[0, 0].set_ylabel('Annual Income ($)')

# Credit score by segment
sns.boxplot(data=all_customers, x='segment', y='credit_score', ax=axes[0, 1])
axes[0, 1].set_title('Credit Score Distribution by Customer Segment', fontsize=14)
axes[0, 1].set_ylabel('Credit Score')

# Default rate by segment
sns.boxplot(data=all_customers, x='segment', y='default_rate', ax=axes[1, 0])
axes[1, 0].set_title('Default Rate by Customer Segment', fontsize=14)
axes[1, 0].set_ylabel('Default Rate')

# Correlation: Income vs Credit Score
sns.scatterplot(data=all_customers, x='annual_income', y='credit_score', hue='segment', alpha=0.6, ax=axes[1, 1])
axes[1, 1].set_title('Income vs Credit Score by Segment', fontsize=14)
axes[1, 1].set_xlabel('Annual Income ($)')
axes[1, 1].set_ylabel('Credit Score')

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

2.2 Confidence Intervals for Financial Metrics

python
# ============= CONFIDENCE INTERVALS =============

class FinancialConfidenceIntervals:
    """Calculate confidence intervals for financial metrics."""
    
    def __init__(self, data):
        self.data = data
        self.results = {}
    
    def confidence_interval_mean(self, column, confidence=0.95):
        """Calculate confidence interval for the mean of a column."""
        n = len(self.data[column])
        mean = self.data[column].mean()
        std = self.data[column].std()
        
        # Standard error
        se = std / np.sqrt(n)
        
        # Z-score for confidence level
        z_score = stats.norm.ppf((1 + confidence) / 2)
        
        # Confidence interval
        ci_lower = mean - z_score * se
        ci_upper = mean + z_score * se
        
        self.results[f'ci_mean_{column}'] = {
            'mean': mean,
            'std': std,
            'n': n,
            'confidence': confidence,
            'ci_lower': ci_lower,
            'ci_upper': ci_upper,
            'interpretation': f"We are {confidence*100:.0f}% confident that the true mean of {column} is between {ci_lower:.2f} and {ci_upper:.2f}"
        }
        
        return self.results[f'ci_mean_{column}']
    
    def confidence_interval_proportion(self, column, value, confidence=0.95):
        """Calculate confidence interval for a proportion."""
        n = len(self.data[column])
        p = (self.data[column] == value).mean()
        
        # Standard error for proportion
        se = np.sqrt(p * (1 - p) / n)
        
        # Z-score for confidence level
        z_score = stats.norm.ppf((1 + confidence) / 2)
        
        # Confidence interval
        ci_lower = p - z_score * se
        ci_upper = p + z_score * se
        
        # Clamp to [0, 1]
        ci_lower = max(0, ci_lower)
        ci_upper = min(1, ci_upper)
        
        self.results[f'ci_proportion_{column}_{value}'] = {
            'proportion': p,
            'n': n,
            'confidence': confidence,
            'ci_lower': ci_lower,
            'ci_upper': ci_upper,
            'interpretation': f"We are {confidence*100:.0f}% confident that the true proportion of {column} = {value} is between {ci_lower:.2%} and {ci_upper:.2%}"
        }
        
        return self.results[f'ci_proportion_{column}_{value}']
    
    def bootstrap_ci(self, column, statistic='mean', n_bootstrap=1000, confidence=0.95):
        """Calculate bootstrap confidence interval."""
        values = self.data[column].values
        bootstrap_stats = []
        
        for _ in range(n_bootstrap):
            # Sample with replacement
            sample = np.random.choice(values, size=len(values), replace=True)
            
            if statistic == 'mean':
                bootstrap_stats.append(sample.mean())
            elif statistic == 'median':
                bootstrap_stats.append(np.median(sample))
            elif statistic == 'std':
                bootstrap_stats.append(sample.std())
        
        # Percentile confidence interval
        alpha = 1 - confidence
        ci_lower = np.percentile(bootstrap_stats, alpha/2 * 100)
        ci_upper = np.percentile(bootstrap_stats, (1 - alpha/2) * 100)
        
        self.results[f'bootstrap_ci_{column}_{statistic}'] = {
            'statistic': statistic,
            'bootstrap_mean': np.mean(bootstrap_stats),
            'n_bootstrap': n_bootstrap,
            'confidence': confidence,
            'ci_lower': ci_lower,
            'ci_upper': ci_upper,
            'interpretation': f"Bootstrap CI ({confidence*100:.0f}%) for {statistic} of {column}: [{ci_lower:.2f}, {ci_upper:.2f}]"
        }
        
        return self.results[f'bootstrap_ci_{column}_{statistic}']
    
    def report_results(self):
        """Generate report of confidence intervals."""
        print("\n" + "="*80)
        print("CONFIDENCE INTERVAL REPORT")
        print("="*80)
        
        for result_name, result in self.results.items():
            print(f"\n📊 {result_name.replace('_', ' ').title()}")
            print(f"  {result['interpretation']}")
            if 'mean' in result:
                print(f"  Sample mean: {result['mean']:.2f}")
            if 'proportion' in result:
                print(f"  Sample proportion: {result['proportion']:.2%}")

# Calculate confidence intervals
ci_calculator = FinancialConfidenceIntervals(all_customers)

# Mean confidence intervals
ci_calculator.confidence_interval_mean('annual_income', confidence=0.95)
ci_calculator.confidence_interval_mean('credit_score', confidence=0.95)
ci_calculator.confidence_interval_mean('monthly_spend', confidence=0.95)

# Proportion confidence intervals
ci_calculator.confidence_interval_proportion('segment', 'Premium', confidence=0.95)

# Bootstrap confidence intervals
ci_calculator.bootstrap_ci('annual_income', statistic='mean', n_bootstrap=1000)
ci_calculator.bootstrap_ci('annual_income', statistic='median', n_bootstrap=1000)

ci_calculator.report_results()

SECTION 3: MULTIVARIATE ANALYSIS

3.1 Correlation Matrix and Heatmaps

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

# Select numerical columns for multivariate analysis
numeric_cols = ['annual_income', 'credit_score', 'monthly_spend', 'default_rate', 'age']
correlation_matrix = all_customers[numeric_cols].corr()

print("\n" + "="*80)
print("CORRELATION MATRIX")
print("="*80)
print(correlation_matrix.round(3))

# Create detailed correlation visualization
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

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

# 2. Pairplot (scatter matrix)
# Create a simplified pairplot manually for selected columns
selected_cols = ['annual_income', 'credit_score', 'monthly_spend']
subset = all_customers[selected_cols]

# Create scatter matrix
from pandas.plotting import scatter_matrix
scatter_matrix(subset, alpha=0.3, figsize=(8, 8), diagonal='hist', ax=axes[0, 1])
axes[0, 1].set_title('Scatter Matrix - Customer Features', fontsize=14)

# 3. Correlation dendrogram (hierarchical clustering of variables)
from scipy.cluster import hierarchy
correlation_linkage = hierarchy.linkage(correlation_matrix, method='average')
dendro = hierarchy.dendrogram(correlation_linkage, ax=axes[1, 0], 
                             labels=correlation_matrix.columns, 
                             orientation='top')
axes[1, 0].set_title('Variable Clustering Dendrogram', fontsize=14)

# 4. Correlation with segment (using point-biserial correlation)
from scipy.stats import pointbiserialr

# Convert segment to numeric
segment_mapping = {'Premium': 2, 'Standard': 1, 'Basic': 0}
all_customers['segment_numeric'] = all_customers['segment'].map(segment_mapping)

correlations_with_segment = {}
for col in numeric_cols:
    corr, p_val = pointbiserialr(all_customers['segment_numeric'], all_customers[col])
    correlations_with_segment[col] = {'correlation': corr, 'p_value': p_val}

# Plot correlations with segment
seg_corr_values = [correlations_with_segment[col]['correlation'] for col in numeric_cols]
bars = axes[1, 1].bar(numeric_cols, seg_corr_values, color='steelblue')
axes[1, 1].set_title('Correlation with Customer Segment', fontsize=14)
axes[1, 1].set_ylabel('Point-Biserial Correlation')
axes[1, 1].axhline(0, color='black', linestyle='-', alpha=0.3)
axes[1, 1].axhline(0.2, color='red', linestyle='--', alpha=0.5, label='Moderate correlation')
axes[1, 1].axhline(-0.2, color='red', linestyle='--', alpha=0.5)
axes[1, 1].legend()
axes[1, 1].tick_params(axis='x', rotation=45)

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

# Print correlation insights
print("\n" + "="*80)
print("MULTIVARIATE ANALYSIS INSIGHTS")
print("="*80)

# Find strongest correlations
corr_pairs = []
for i in range(len(correlation_matrix.columns)):
    for j in range(i+1, len(correlation_matrix.columns)):
        corr_pairs.append({
            'pair': f"{correlation_matrix.columns[i]} - {correlation_matrix.columns[j]}",
            'correlation': correlation_matrix.iloc[i, j]
        })

corr_pairs = sorted(corr_pairs, key=lambda x: abs(x['correlation']), reverse=True)

print("\nStrongest Correlations:")
for pair in corr_pairs[:5]:
    print(f"  • {pair['pair']}: {pair['correlation']:.3f}")

# Correlation with segment
print("\nCorrelation with Customer Segment:")
for col, data in correlations_with_segment.items():
    print(f"  • {col}: {data['correlation']:.3f} (p={data['p_value']:.4f})")
    if data['p_value'] < 0.05:
        print(f"    → Significant relationship with segment")

3.2 Principal Component Analysis (PCA)

python
# ============= PRINCIPAL COMPONENT ANALYSIS =============

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# Prepare data for PCA
pca_data = all_customers[numeric_cols].copy()

# Scale the data
scaler = StandardScaler()
pca_data_scaled = scaler.fit_transform(pca_data)

# Perform PCA
pca = PCA()
pca_result = pca.fit_transform(pca_data_scaled)

# Create PCA results DataFrame
pca_df = pd.DataFrame(pca_result, columns=[f'PC{i+1}' for i in range(len(numeric_cols))])
pca_df['segment'] = all_customers['segment'].values

# Analyze PCA results
explained_variance = pca.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variance)

print("\n" + "="*80)
print("PRINCIPAL COMPONENT ANALYSIS (PCA)")
print("="*80)

print("\nExplained Variance Ratio:")
for i, var in enumerate(explained_variance):
    print(f"  PC{i+1}: {var:.2%} (Cumulative: {cumulative_variance[i]:.2%})")

print(f"\nTotal variance explained by first 2 components: {cumulative_variance[1]:.2%}")
print(f"Total variance explained by first 3 components: {cumulative_variance[2]:.2%}")

# PCA loadings
loadings = pd.DataFrame(
    pca.components_.T,
    columns=[f'PC{i+1}' for i in range(len(numeric_cols))],
    index=numeric_cols
)

print("\nPCA Loadings (Feature Contributions):")
print(loadings.round(3))

# Visualize PCA
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# 1. Scree plot
axes[0, 0].bar(range(1, len(explained_variance)+1), explained_variance, alpha=0.7, label='Individual')
axes[0, 0].plot(range(1, len(explained_variance)+1), cumulative_variance, 'ro-', label='Cumulative')
axes[0, 0].set_xlabel('Principal Component')
axes[0, 0].set_ylabel('Explained Variance Ratio')
axes[0, 0].set_title('Scree Plot', fontsize=14)
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# 2. PCA scatter plot (PC1 vs PC2)
for segment in ['Premium', 'Standard', 'Basic']:
    segment_data = pca_df[pca_df['segment'] == segment]
    axes[0, 1].scatter(segment_data['PC1'], segment_data['PC2'], 
                       label=segment, alpha=0.6, s=50)
axes[0, 1].set_xlabel(f'PC1 ({explained_variance[0]:.1%} variance)')
axes[0, 1].set_ylabel(f'PC2 ({explained_variance[1]:.1%} variance)')
axes[0, 1].set_title('PCA - Customer Segmentation', fontsize=14)
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# 3. PCA loadings heatmap
sns.heatmap(loadings, annot=True, cmap='RdBu_r', center=0, fmt='.2f', 
            ax=axes[1, 0], cbar_kws={'shrink': 0.8})
axes[1, 0].set_title('PCA Loadings', fontsize=14)

# 4. Biplot (PCA with feature vectors)
# Simplified biplot: show first two PCs with feature vectors
for col in numeric_cols:
    axes[1, 1].arrow(0, 0, 
                     loadings.loc[col, 'PC1'] * 3,
                     loadings.loc[col, 'PC2'] * 3,
                     head_width=0.1, head_length=0.1, fc='red', ec='red')
    axes[1, 1].text(loadings.loc[col, 'PC1'] * 3.2,
                    loadings.loc[col, 'PC2'] * 3.2,
                    col, fontsize=10)

# Add data points
for segment in ['Premium', 'Standard', 'Basic']:
    segment_data = pca_df[pca_df['segment'] == segment]
    axes[1, 1].scatter(segment_data['PC1'], segment_data['PC2'], 
                       label=segment, alpha=0.3, s=30)

axes[1, 1].set_xlabel('PC1')
axes[1, 1].set_ylabel('PC2')
axes[1, 1].set_title('Biplot - PC1 vs PC2', fontsize=14)
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

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

# PCA insights
print("\n" + "="*80)
print("PCA INSIGHTS")
print("="*80)

# Most important features for PC1
pc1_loadings = loadings['PC1'].abs().sort_values(ascending=False)
print(f"\nPC1 (Explains {explained_variance[0]:.1%} of variance):")
print("  Top features:")
for feature, loading in pc1_loadings.head(3).items():
    print(f"    • {feature}: {loading:.3f}")

# Most important features for PC2
pc2_loadings = loadings['PC2'].abs().sort_values(ascending=False)
print(f"\nPC2 (Explains {explained_variance[1]:.1%} of variance):")
print("  Top features:")
for feature, loading in pc2_loadings.head(3).items():
    print(f"    • {feature}: {loading:.3f}")

SECTION 4: SEASONALITY AND TREND DETECTION

4.1 Time Series Decomposition

python
# ============= TIME SERIES DECOMPOSITION =============

from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
import warnings
warnings.filterwarnings('ignore')

# Create daily transaction data
np.random.seed(42)
dates = pd.date_range(start='2023-01-01', end='2024-12-31', freq='D')
n_days = len(dates)

# Create a series with trend, seasonality, and noise
trend = np.linspace(1000, 5000, n_days)
seasonality = 500 * np.sin(2 * np.pi * dates.dayofyear / 365)
seasonality += 200 * np.sin(4 * np.pi * dates.dayofyear / 365)  # Weekly pattern
noise = np.random.normal(0, 200, n_days)

# Add day-of-week effect
day_of_week = dates.dayofweek
weekday_effect = np.array([100, 150, 200, 250, 300, 100, 50])[day_of_week]

transaction_volume = trend + seasonality + noise + weekday_effect
transaction_volume = np.maximum(transaction_volume, 0)  # No negative values

# Create DataFrame
ts_data = pd.DataFrame({
    'date': dates,
    'transaction_volume': transaction_volume
})
ts_data.set_index('date', inplace=True)

print("\n" + "="*80)
print("TIME SERIES ANALYSIS")
print("="*80)

# 1. Stationarity test (Augmented Dickey-Fuller)
adf_result = adfuller(ts_data['transaction_volume'], autolag='AIC')
print(f"\nStationarity Test (ADF):")
print(f"  Test Statistic: {adf_result[0]:.4f}")
print(f"  P-value: {adf_result[1]:.4f}")
print(f"  Critical Values:")
for key, value in adf_result[4].items():
    print(f"    {key}: {value:.4f}")
print(f"  {'Stationary' if adf_result[1] < 0.05 else 'Non-Stationary'}")

# 2. Decompose the time series
decomposition = seasonal_decompose(ts_data['transaction_volume'], model='additive', period=365)

# Extract components
trend_component = decomposition.trend
seasonal_component = decomposition.seasonal
residual_component = decomposition.resid

# Visualize decomposition
fig, axes = plt.subplots(4, 1, figsize=(15, 12))

# Original series
axes[0].plot(ts_data.index, ts_data['transaction_volume'], color='black', alpha=0.7)
axes[0].set_title('Original Transaction Volume', fontsize=14)
axes[0].set_ylabel('Volume')
axes[0].grid(True, alpha=0.3)

# Trend
axes[1].plot(ts_data.index, trend_component, color='blue', linewidth=2)
axes[1].set_title('Trend Component', fontsize=14)
axes[1].set_ylabel('Trend')
axes[1].grid(True, alpha=0.3)

# Seasonal
axes[2].plot(ts_data.index, seasonal_component, color='green', linewidth=2)
axes[2].set_title('Seasonal Component (365-day cycle)', fontsize=14)
axes[2].set_ylabel('Seasonal')
axes[2].grid(True, alpha=0.3)

# Residual
axes[3].plot(ts_data.index, residual_component, color='red', alpha=0.7)
axes[3].set_title('Residual (Noise)', fontsize=14)
axes[3].set_ylabel('Residual')
axes[3].grid(True, alpha=0.3)

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

# 3. Weekly pattern analysis
ts_data['day_of_week'] = ts_data.index.dayofweek
day_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

weekly_pattern = ts_data.groupby('day_of_week')['transaction_volume'].agg(['mean', 'std'])

print("\nWeekly Pattern Analysis:")
print(weekly_pattern)

# 4. Monthly pattern analysis
ts_data['month'] = ts_data.index.month
monthly_pattern = ts_data.groupby('month')['transaction_volume'].agg(['mean', 'std'])

print("\nMonthly Pattern Analysis:")
print(monthly_pattern)

# 5. Detect seasonality strength
seasonal_strength = 1 - (np.var(residual_component.dropna()) / np.var(ts_data['transaction_volume'].dropna()))
print(f"\nSeasonality Strength: {seasonal_strength:.2%}")
print(f"  • If close to 1: Strong seasonality")
print(f"  • If close to 0: Weak or no seasonality")

# 6. Identify peaks and troughs
seasonal_peak = seasonal_component.max()
seasonal_trough = seasonal_component.min()
seasonal_amplitude = seasonal_peak - seasonal_trough

print(f"\nSeasonal Characteristics:")
print(f"  Peak seasonality: {seasonal_peak:.0f}")
print(f"  Trough seasonality: {seasonal_trough:.0f}")
print(f"  Amplitude: {seasonal_amplitude:.0f}")

4.2 Autocorrelation Analysis

python
# ============= AUTOCORRELATION ANALYSIS =============

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

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

# 1. Autocorrelation Function (ACF)
plot_acf(ts_data['transaction_volume'].dropna(), lags=60, ax=axes[0, 0])
axes[0, 0].set_title('Autocorrelation Function (ACF)', fontsize=14)

# 2. Partial Autocorrelation Function (PACF)
plot_pacf(ts_data['transaction_volume'].dropna(), lags=60, ax=axes[0, 1])
axes[0, 1].set_title('Partial Autocorrelation Function (PACF)', fontsize=14)

# 3. Autocorrelation at different lags
from statsmodels.tsa.stattools import acf

lags_to_check = [1, 7, 14, 30, 60, 90, 365]
acf_values = acf(ts_data['transaction_volume'].dropna(), nlags=365, fft=False)

for lag in lags_to_check:
    axes[1, 0].bar(lag, acf_values[lag], color='steelblue', width=5)

axes[1, 0].set_xlabel('Lag')
axes[1, 0].set_ylabel('Autocorrelation')
axes[1, 0].set_title('Selected Lag Autocorrelations', fontsize=14)
axes[1, 0].axhline(0, color='black', linestyle='-')
axes[1, 0].axhline(1.96/np.sqrt(len(ts_data)), color='red', linestyle='--', alpha=0.5, label='95% CI')
axes[1, 0].axhline(-1.96/np.sqrt(len(ts_data)), color='red', linestyle='--', alpha=0.5)
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)

# 4. Heatmap of autocorrelation matrix
from scipy.linalg import toeplitz
acf_vals = acf(ts_data['transaction_volume'].dropna(), nlags=30)
acf_matrix = toeplitz(acf_vals)

sns.heatmap(acf_matrix, cmap='RdBu_r', center=0, ax=axes[1, 1],
            cbar_kws={'shrink': 0.8}, square=True)
axes[1, 1].set_title('Autocorrelation Matrix (30 lags)', fontsize=14)

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

# Interpret autocorrelation
print("\n" + "="*80)
print("AUTOCORRELATION INTERPRETATION")
print("="*80)

print("\n🔍 Key Observations:")
for lag in lags_to_check:
    if lag < len(acf_values):
        value = acf_values[lag]
        if abs(value) > 1.96/np.sqrt(len(ts_data)):
            significance = "significant"
            if value > 0.3:
                strength = "strong"
            elif value > 0.1:
                strength = "moderate"
            else:
                strength = "weak"
            print(f"  • Lag {lag}: {value:.3f} - {strength} {significance} positive autocorrelation")
        else:
            print(f"  • Lag {lag}: {value:.3f} - not significant")

# Seasonal period detection
# Find peaks in autocorrelation that correspond to seasonal patterns
seasonal_lags = []
for lag in range(30, 400):
    if lag < len(acf_values) and acf_values[lag] > 0.2:
        if len(seasonal_lags) == 0 or lag - seasonal_lags[-1] > 10:
            seasonal_lags.append(lag)

print(f"\n📊 Detected seasonal lags: {seasonal_lags[:5]}")
if seasonal_lags:
    print(f"  Likely seasonal period: {seasonal_lags[0]} days")

SECTION 5: ADVANCED VISUALIZATION TECHNIQUES

5.1 Violin Plots for Distribution Comparison

python
# ============= ADVANCED VISUALIZATION =============

# Create additional data for comprehensive visualization
all_customers['age'] = np.random.normal(45, 15, len(all_customers)).clip(18, 80).astype(int)
all_customers['monthly_spend'] = all_customers['monthly_spend'] + np.random.normal(0, 200, len(all_customers)).clip(-500, 500)

# 1. Violin Plots
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# Income distribution by segment
sns.violinplot(data=all_customers, x='segment', y='annual_income', ax=axes[0, 0])
axes[0, 0].set_title('Income Distribution by Segment (Violin Plot)', fontsize=14)
axes[0, 0].set_ylabel('Annual Income ($)')

# Credit score distribution by segment
sns.violinplot(data=all_customers, x='segment', y='credit_score', ax=axes[0, 1])
axes[0, 1].set_title('Credit Score Distribution by Segment (Violin Plot)', fontsize=14)
axes[0, 1].set_ylabel('Credit Score')

# Monthly spend distribution by segment
sns.violinplot(data=all_customers, x='segment', y='monthly_spend', ax=axes[1, 0])
axes[1, 0].set_title('Monthly Spend Distribution by Segment (Violin Plot)', fontsize=14)
axes[1, 0].set_ylabel('Monthly Spend ($)')

# Age distribution by segment
sns.violinplot(data=all_customers, x='segment', y='age', ax=axes[1, 1])
axes[1, 1].set_title('Age Distribution by Segment (Violin Plot)', fontsize=14)
axes[1, 1].set_ylabel('Age')

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

5.2 Parallel Coordinates

python
# ============= PARALLEL COORDINATES PLOT =============

from pandas.plotting import parallel_coordinates

# Prepare data for parallel coordinates
parallel_data = all_customers[['segment', 'annual_income', 'credit_score', 'monthly_spend', 'default_rate']].copy()

# Scale the data for better visualization
for col in ['annual_income', 'credit_score', 'monthly_spend', 'default_rate']:
    parallel_data[col] = (parallel_data[col] - parallel_data[col].min()) / (parallel_data[col].max() - parallel_data[col].min())

# Create parallel coordinates plot
fig, ax = plt.subplots(figsize=(14, 8))

parallel_coordinates(parallel_data, 'segment', ax=ax, colormap='Set2')
ax.set_title('Parallel Coordinates - Customer Segments', fontsize=14)
ax.set_xlabel('Features')
ax.set_ylabel('Normalized Value')

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

5.3 Advanced Pairplot

python
# ============= ADVANCED PAIRPLOT =============

import seaborn as sns

# Create pairplot with additional features
pairplot_cols = ['annual_income', 'credit_score', 'monthly_spend', 'default_rate', 'age']

# Use a subset for readability
subset_data = all_customers.sample(n=200, random_state=42)

g = sns.pairplot(subset_data[pairplot_cols], 
                 diag_kind='kde',
                 corner=True,
                 plot_kws={'alpha': 0.5, 's': 30})

g.fig.suptitle('Pairplot - Customer Features', y=1.02, fontsize=16)
plt.tight_layout()
plt.savefig('advanced_pairplot.png', dpi=300)
plt.show()

SECTION 6: CUSTOMER SEGMENTATION ANALYSIS

6.1 Clustering for Customer Segmentation

python
# ============= CUSTOMER SEGMENTATION =============

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# Prepare data for clustering
cluster_features = ['annual_income', 'credit_score', 'monthly_spend', 'age']

# Standardize the features
scaler = StandardScaler()
scaled_data = scaler.fit_transform(all_customers[cluster_features])

# Elbow method to find optimal K
inertias = []
K_range = range(2, 10)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(scaled_data)
    inertias.append(kmeans.inertia_)

# Perform final clustering with K=4
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(scaled_data)
all_customers['cluster'] = cluster_labels

# Analyze clusters
cluster_analysis = all_customers.groupby('cluster')[cluster_features].mean()
cluster_counts = all_customers['cluster'].value_counts()

print("\n" + "="*80)
print("CUSTOMER SEGMENTATION - CLUSTER ANALYSIS")
print("="*80)

print("\nCluster Profiles:")
for cluster in sorted(cluster_analysis.index):
    print(f"\n🔵 Cluster {cluster}:")
    print(f"  Size: {cluster_counts[cluster]} customers ({cluster_counts[cluster]/len(all_customers)*100:.1f}%)")
    for feature in cluster_features:
        print(f"  {feature}: ${cluster_analysis.loc[cluster, feature]:,.2f}" if 'income' in feature or 'spend' in feature else f"  {feature}: {cluster_analysis.loc[cluster, feature]:.1f}")

# Visualize clusters
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# 1. Elbow curve
axes[0, 0].plot(K_range, inertias, 'bo-')
axes[0, 0].set_xlabel('Number of Clusters (K)')
axes[0, 0].set_ylabel('Inertia')
axes[0, 0].set_title('Elbow Method for Optimal K', fontsize=14)
axes[0, 0].grid(True, alpha=0.3)

# 2. Cluster distribution
cluster_counts.plot(kind='bar', ax=axes[0, 1], color=['#2ecc71', '#3498db', '#e74c3c', '#f39c12'])
axes[0, 1].set_title('Cluster Distribution', fontsize=14)
axes[0, 1].set_xlabel('Cluster')
axes[0, 1].set_ylabel('Count')

# 3. Cluster profiles (heatmap)
cluster_profile_heatmap = cluster_analysis.T
sns.heatmap(cluster_profile_heatmap, annot=True, fmt='.0f', ax=axes[1, 0],
            cmap='RdYlGn', cbar_kws={'shrink': 0.8})
axes[1, 0].set_title('Cluster Profiles Heatmap', fontsize=14)

# 4. 2D projection (using PCA)
from sklearn.decomposition import PCA
pca_2d = PCA(n_components=2)
pca_result_2d = pca_2d.fit_transform(scaled_data)

axes[1, 1].scatter(pca_result_2d[:, 0], pca_result_2d[:, 1], 
                  c=cluster_labels, cmap='Set2', alpha=0.6, s=50)
axes[1, 1].set_title('Customer Clusters (PCA Projection)', fontsize=14)
axes[1, 1].set_xlabel('PC1')
axes[1, 1].set_ylabel('PC2')
axes[1, 1].grid(True, alpha=0.3)

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

# Segment names
cluster_segments = {
    0: 'High Income, High Credit',
    1: 'Medium Income, Standard Credit',
    2: 'Low Income, Lower Credit',
    3: 'High Spenders'
}

print("\n" + "="*80)
print("CLUSTER SEGMENT NAMES")
print("="*80)
for cluster in sorted(cluster_analysis.index):
    print(f"\nCluster {cluster}: {cluster_segments.get(cluster, 'Unnamed')}")
    print(f"  Key characteristics:")
    for feature in cluster_features:
        if feature in ['annual_income', 'monthly_spend']:
            value = cluster_analysis.loc[cluster, feature]
            avg = all_customers[feature].mean()
            if value > avg * 1.2:
                direction = "high"
            elif value < avg * 0.8:
                direction = "low"
            else:
                direction = "average"
            print(f"    • {feature}: {direction} (${value:,.0f} vs avg ${avg:,.0f})")
        elif feature == 'credit_score':
            value = cluster_analysis.loc[cluster, feature]
            avg = all_customers[feature].mean()
            if value > avg * 1.1:
                direction = "high"
            elif value < avg * 0.9:
                direction = "low"
            else:
                direction = "average"
            print(f"    • {feature}: {direction} ({value:.0f} vs avg {avg:.0f})")

SECTION 7: BUSINESS RISK & FINANCIAL IMPACT

7.1 How Advanced EDA Mitigates Risk

 
 
Risk Advanced EDA Technique Mitigation
Model Bias Hypothesis testing Detect if data is representative
Seasonality Time series decomposition Account for seasonal patterns
Multicollinearity Correlation analysis Remove redundant features
Segmentation Clustering analysis Target marketing and risk appropriately
Data Quality Multivariate analysis Detect hidden data issues

7.2 Regulatory Implications

 
 
Regulation EDA Requirement
SR 11-7 Must demonstrate data understanding and exploration
Fair Lending Segment analysis must not reveal discrimination
CCAR Must understand stress scenario data patterns

SECTION 8: COMPREHENSIVE EDA REPORT

python
# ============= COMPREHENSIVE EDA REPORT =============

def generate_comprehensive_eda_report(data, dataset_name):
    """Generate a comprehensive EDA report."""
    
    print("\n" + "="*80)
    print(f"COMPREHENSIVE EDA REPORT: {dataset_name}")
    print("="*80)
    
    # 1. Data Overview
    print("\n📊 SECTION 1: DATA OVERVIEW")
    print("-"*40)
    print(f"  Dataset size: {data.shape[0]} rows, {data.shape[1]} columns")
    print(f"  Memory usage: {data.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
    
    # 2. Data Quality
    print("\n✅ SECTION 2: DATA QUALITY")
    print("-"*40)
    missing = data.isnull().sum()
    missing_pct = missing / len(data) * 100
    if missing.sum() > 0:
        print("  Missing Data:")
        for col in data.columns:
            if missing[col] > 0:
                print(f"    • {col}: {missing[col]} missing ({missing_pct[col]:.1f}%)")
    else:
        print("  ✓ No missing data found")
    
    # 3. Summary Statistics
    print("\n📈 SECTION 3: SUMMARY STATISTICS")
    print("-"*40)
    numeric_cols = data.select_dtypes(include=[np.number]).columns
    for col in numeric_cols[:5]:  # Show first 5 numeric columns
        print(f"\n  {col}:")
        print(f"    Mean: {data[col].mean():.2f}")
        print(f"    Median: {data[col].median():.2f}")
        print(f"    Std Dev: {data[col].std():.2f}")
        print(f"    Min: {data[col].min():.2f}")
        print(f"    Max: {data[col].max():.2f}")
        print(f"    Skewness: {data[col].skew():.2f}")
        print(f"    Kurtosis: {data[col].kurtosis():.2f}")
    
    # 4. Key Insights
    print("\n💡 SECTION 4: KEY INSIGHTS")
    print("-"*40)
    
    # Distribution insights
    for col in numeric_cols:
        skewness = data[col].skew()
        if abs(skewness) > 1:
            print(f"  • {col} is highly skewed ({skewness:.2f}) - may need transformation")
        elif abs(skewness) > 0.5:
            print(f"  • {col} is moderately skewed ({skewness:.2f})")
    
    # Outlier insights
    print("\n  Outlier Detection (IQR method):")
    for col in numeric_cols:
        q1 = data[col].quantile(0.25)
        q3 = data[col].quantile(0.75)
        iqr = q3 - q1
        outliers = data[(data[col] < q1 - 1.5*iqr) | (data[col] > q3 + 1.5*iqr)]
        if len(outliers) > 0:
            print(f"    • {col}: {len(outliers)} outliers ({len(outliers)/len(data)*100:.1f}%)")
    
    # 5. Recommendations
    print("\n🎯 SECTION 5: RECOMMENDATIONS")
    print("-"*40)
    
    recommendations = []
    
    # Check for missing data
    if missing.sum() > 0:
        recommendations.append("• Handle missing data using appropriate imputation methods")
    
    # Check for skewness
    for col in numeric_cols:
        if abs(data[col].skew()) > 1:
            recommendations.append(f"• Apply transformation (log/sqrt) to {col} due to high skewness")
            break
    
    # Check for outliers
    for col in numeric_cols:
        q1 = data[col].quantile(0.25)
        q3 = data[col].quantile(0.75)
        iqr = q3 - q1
        outliers = data[(data[col] < q1 - 1.5*iqr) | (data[col] > q3 + 1.5*iqr)]
        if len(outliers) > 0:
            recommendations.append(f"• Consider winsorizing or transforming {col} to handle outliers")
            break
    
    # Check for high correlation
    if len(numeric_cols) > 1:
        corr_matrix = data[numeric_cols].corr()
        for i in range(len(corr_matrix.columns)):
            for j in range(i+1, len(corr_matrix.columns)):
                if abs(corr_matrix.iloc[i, j]) > 0.8:
                    recommendations.append(f"• Consider removing redundant features with high correlation ({corr_matrix.columns[i]} vs {corr_matrix.columns[j]}: {corr_matrix.iloc[i, j]:.2f})")
                    break
    
    for rec in recommendations:
        print(f"  {rec}")
    
    if not recommendations:
        print("  ✓ Data quality is good - ready for modeling")
    
    # 6. EDA Checklist
    print("\n📋 SECTION 6: EDA CHECKLIST")
    print("-"*40)
    checklist = [
        ("Data loaded and inspected", True),
        ("Summary statistics calculated", True),
        ("Missing data identified and analyzed", missing.sum() > 0),
        ("Outliers detected", any(abs(data[col].skew()) > 1 for col in numeric_cols)),
        ("Distributions visualized", True),
        ("Correlation analysis performed", len(numeric_cols) > 1),
        ("Time series patterns analyzed", 'date' in data.columns),
        ("Segmentation analysis performed", True),
        ("Key insights documented", True),
        ("Recommendations provided", True)
    ]
    
    for item, status in checklist:
        status_icon = "✅" if status else "⏳"
        print(f"  {status_icon} {item}")
    
    print("\n" + "="*80)
    print("END OF EDA REPORT")
    print("="*80)

# Generate comprehensive report
generate_comprehensive_eda_report(all_customers, "Customer Banking Data")

SECTION 9: SUMMARY FOR THE DATA PRACTITIONER

9.1 The 1-Minute Elevator Pitch

“Advanced EDA takes us beyond basic statistics to deep understanding of financial data. We use hypothesis testing to validate business assumptions, multivariate analysis to understand complex relationships, and time series analysis to detect trends and seasonality. Techniques like PCA help us reduce dimensionality, while clustering reveals natural customer segments. Advanced visualizations communicate these insights effectively. Comprehensive EDA ensures our models are built on solid foundations and meets regulatory requirements.”

9.2 Key Takeaways

  1. Hypothesis testing validates business assumptions with statistical rigor.

  2. Confidence intervals quantify uncertainty in financial metrics.

  3. Multivariate analysis (correlation, PCA) reveals complex relationships.

  4. Time series decomposition separates trend, seasonality, and noise.

  5. Autocorrelation analysis detects patterns and dependencies in time series.

  6. Advanced visualizations (violin plots, parallel coordinates) show distributions effectively.

  7. Clustering reveals natural customer segments for targeting.

  8. Comprehensive EDA reports document findings for stakeholders and regulators.

  9. PCA reduces dimensionality while preserving information.

  10. Seasonality detection is critical for forecasting and anomaly detection.

9.3 Recommended Next Steps

  1. Apply advanced EDA to your own banking datasets

  2. Build automated EDA pipelines

  3. Learn more about time series forecasting

  4. Practice creating comprehensive EDA reports

  5. Explore interactive visualization tools (Plotly, Tableau)


[END OF LESSON 8]