SECTION 1: LEARNING OBJECTIVES
By the end of this lesson, you will be able to:
-
Understand the fundamentals of statistical inference and its application in financial decision-making.
-
Construct and interpret confidence intervals for financial metrics (means, proportions, differences).
-
Perform hypothesis tests for comparing financial metrics across groups.
-
Apply t-tests for comparing means in financial data.
-
Use ANOVAÂ for comparing multiple groups in financial analysis.
-
Perform chi-square tests for categorical financial data.
-
Apply non-parametric tests for non-normal financial data.
-
Interpret p-values and confidence intervals for regulatory and business decision-making.
SECTION 2: SAMPLING AND SAMPLING DISTRIBUTIONS
2.1 The Central Limit Theorem
The Central Limit Theorem (CLT) states that the distribution of sample means approaches a normal distribution as sample size increases, regardless of the population distribution.
# ============= CENTRAL LIMIT THEOREM ============= import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from scipy import stats print("="*60) print("CENTRAL LIMIT THEOREM DEMONSTRATION") print("="*60) # Create a highly skewed population (lognormal) np.random.seed(42) population = np.random.lognormal(2, 1, 10000) print(f"\n📊 Population Statistics:") print(f" Mean: {population.mean():.2f}") print(f" Median: {np.median(population):.2f}") print(f" Skewness: {pd.Series(population).skew():.3f}") # Draw samples of different sizes sample_sizes = [5, 10, 30, 100] n_samples = 1000 fig, axes = plt.subplots(2, 2, figsize=(14, 10)) for i, sample_size in enumerate(sample_sizes): row = i // 2 col = i % 2 # Draw multiple samples and calculate means sample_means = [] for _ in range(n_samples): sample = np.random.choice(population, size=sample_size, replace=True) sample_means.append(sample.mean()) # Plot distribution of sample means ax = axes[row, col] ax.hist(sample_means, bins=30, edgecolor='black', alpha=0.7, density=True) # Overlay normal distribution mu = np.mean(sample_means) sigma = np.std(sample_means) x = np.linspace(mu - 4*sigma, mu + 4*sigma, 100) y = stats.norm.pdf(x, mu, sigma) ax.plot(x, y, 'r-', linewidth=2, label='Normal Fit') # Add vertical line for population mean ax.axvline(population.mean(), color='black', linestyle='--', label=f'Population Mean: {population.mean():.2f}') ax.set_title(f'Sample Size: {sample_size}', fontsize=12) ax.set_xlabel('Sample Mean') ax.set_ylabel('Density') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('central_limit_theorem.png', dpi=300) plt.show() print("\n💡 Key Insight:") print(" • As sample size increases, distribution of sample means becomes more normal") print(" • Mean of sample means approaches population mean") print(" • Standard deviation of sample means decreases (Standard Error)")
2.2 Standard Error and Confidence Intervals
# ============= STANDARD ERROR AND CONFIDENCE INTERVALS ============= def calculate_confidence_interval(data, confidence=0.95): """Calculate confidence interval for the mean.""" n = len(data) mean = np.mean(data) std = np.std(data, ddof=1) se = std / np.sqrt(n) # t-value for confidence level t_value = stats.t.ppf((1 + confidence) / 2, n - 1) margin = t_value * se ci_lower = mean - margin ci_upper = mean + margin return { 'mean': mean, 'std': std, 'se': se, 'ci_lower': ci_lower, 'ci_upper': ci_upper, 'margin': margin } # Apply to banking data print("\n" + "="*60) print("CONFIDENCE INTERVALS FOR BANKING METRICS") print("="*60) # Create sample data np.random.seed(42) n = 100 # Monthly transaction amounts for different customer segments premium_transactions = np.random.normal(350, 80, n) standard_transactions = np.random.normal(200, 60, n) basic_transactions = np.random.normal(120, 40, n) print("\n📊 Monthly Transaction Amounts:") print(f" Premium: Mean={premium_transactions.mean():.2f}, Std={premium_transactions.std():.2f}") print(f" Standard: Mean={standard_transactions.mean():.2f}, Std={standard_transactions.std():.2f}") print(f" Basic: Mean={basic_transactions.mean():.2f}, Std={basic_transactions.std():.2f}") # Calculate confidence intervals for segment, data in [('Premium', premium_transactions), ('Standard', standard_transactions), ('Basic', basic_transactions)]: ci = calculate_confidence_interval(data, confidence=0.95) print(f"\n{segment} Segment (95% CI):") print(f" Mean: ${ci['mean']:.2f}") print(f" Standard Error: ${ci['se']:.2f}") print(f" CI: [${ci['ci_lower']:.2f}, ${ci['ci_upper']:.2f}]") print(f" Margin of Error: ±${ci['margin']:.2f}") # Visualize confidence intervals fig, ax = plt.subplots(figsize=(10, 6)) segments = ['Premium', 'Standard', 'Basic'] means = [np.mean(premium_transactions), np.mean(standard_transactions), np.mean(basic_transactions)] cis = [calculate_confidence_interval(data, 0.95) for data in [premium_transactions, standard_transactions, basic_transactions]] for i, (segment, mean, ci) in enumerate(zip(segments, means, cis)): ax.errorbar(i, mean, yerr=ci['margin'], fmt='o', capsize=5, capthick=2, markersize=10, color='blue', ecolor='red', elinewidth=2) ax.text(i, mean + ci['margin'] + 5, f'${mean:.0f}', ha='center', va='bottom') ax.set_xticks(range(len(segments))) ax.set_xticklabels(segments) ax.set_ylabel('Monthly Transaction Amount ($)') ax.set_title('95% Confidence Intervals by Customer Segment', fontsize=14) ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('confidence_intervals.png', dpi=300) plt.show()
SECTION 3: HYPOTHESIS TESTING
3.1 One-Sample t-Test
A one-sample t-test compares the mean of a sample to a known value.
# ============= ONE-SAMPLE T-TEST ============= print("\n" + "="*60) print("ONE-SAMPLE T-TEST") print("="*60) # Example: Is the average credit score different from 700? credit_scores = np.random.normal(685, 50, 100) known_mean = 700 # Perform one-sample t-test t_stat, p_value = stats.ttest_1samp(credit_scores, known_mean) print(f"\n📊 Credit Score Analysis:") print(f" Sample Mean: {credit_scores.mean():.2f}") print(f" Hypothesized Mean: {known_mean}") print(f" Sample Std: {credit_scores.std():.2f}") print(f" Sample Size: {len(credit_scores)}") print(f" T-Statistic: {t_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: REJECT null hypothesis (p < 0.05)") print(f" Interpretation: The average credit score IS significantly different from {known_mean}") else: print(f" Result: FAIL TO REJECT null hypothesis (p >= 0.05)") print(f" Interpretation: No significant difference from {known_mean}") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Histogram with hypothesized mean ax = axes[0] credit_scores.hist(bins=20, ax=ax, edgecolor='black', alpha=0.7) ax.axvline(credit_scores.mean(), color='red', linestyle='--', linewidth=2, label=f'Sample Mean: {credit_scores.mean():.2f}') ax.axvline(known_mean, color='blue', linestyle='--', linewidth=2, label=f'Hypothesized Mean: {known_mean}') ax.set_title('Credit Score Distribution', fontsize=12) ax.set_xlabel('Credit Score') ax.set_ylabel('Frequency') ax.legend() ax.grid(True, alpha=0.3) # T-distribution ax = axes[1] x = np.linspace(-4, 4, 1000) y = stats.t.pdf(x, len(credit_scores) - 1) ax.plot(x, y, 'b-', linewidth=2) ax.axvline(t_stat, color='red', linestyle='--', linewidth=2, label=f'T-statistic: {t_stat:.3f}') ax.axvline(-t_stat, color='red', linestyle='--', linewidth=2) # Shade critical regions alpha = 0.05 t_critical = stats.t.ppf(1 - alpha/2, len(credit_scores) - 1) ax.fill_between(x, 0, y, where=(x > t_critical), color='red', alpha=0.3, label='Rejection Region') ax.fill_between(x, 0, y, where=(x < -t_critical), color='red', alpha=0.3) ax.set_title('T-Distribution', fontsize=12) ax.set_xlabel('T-Statistic') ax.set_ylabel('Density') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('one_sample_ttest.png', dpi=300) plt.show()
3.2 Two-Sample t-Test
A two-sample t-test compares the means of two groups.
# ============= TWO-SAMPLE T-TEST ============= print("\n" + "="*60) print("TWO-SAMPLE T-TEST") print("="*60) # Example: Compare transaction amounts between Premium and Standard customers premium_transactions = np.random.normal(350, 80, 100) standard_transactions = np.random.normal(200, 60, 100) # Perform two-sample t-test t_stat, p_value = stats.ttest_ind(premium_transactions, standard_transactions) print(f"\n📊 Transaction Amount Comparison:") print(f" Premium: Mean={premium_transactions.mean():.2f}, Std={premium_transactions.std():.2f}") print(f" Standard: Mean={standard_transactions.mean():.2f}, Std={standard_transactions.std():.2f}") print(f" Difference: {premium_transactions.mean() - standard_transactions.mean():.2f}") print(f" T-Statistic: {t_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: REJECT null hypothesis") print(f" Interpretation: Premium customers have significantly different transaction amounts") else: print(f" Result: FAIL TO REJECT null hypothesis") print(f" Interpretation: No significant difference in transaction amounts") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Box plots ax = axes[0] data = [premium_transactions, standard_transactions] ax.boxplot(data, labels=['Premium', 'Standard']) ax.set_title('Transaction Amounts by Segment', fontsize=12) ax.set_ylabel('Amount ($)') ax.grid(True, alpha=0.3) # Histograms ax = axes[1] ax.hist(premium_transactions, bins=20, alpha=0.5, label='Premium', edgecolor='black') ax.hist(standard_transactions, bins=20, alpha=0.5, label='Standard', edgecolor='black') ax.axvline(premium_transactions.mean(), color='blue', linestyle='--', label=f'Premium Mean: {premium_transactions.mean():.2f}') ax.axvline(standard_transactions.mean(), color='orange', linestyle='--', label=f'Standard Mean: {standard_transactions.mean():.2f}') ax.set_title('Transaction Amount Distributions', fontsize=12) ax.set_xlabel('Amount ($)') ax.set_ylabel('Frequency') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('two_sample_ttest.png', dpi=300) plt.show()
3.3 Paired t-Test
A paired t-test compares two related samples (e.g., before and after).
# ============= PAIRED T-TEST ============= print("\n" + "="*60) print("PAIRED T-TEST") print("="*60) # Example: Impact of financial education on credit scores np.random.seed(42) n_customers = 50 # Before education before_scores = np.random.normal(640, 50, n_customers) # After education (some improvement) improvement = np.random.normal(20, 15, n_customers) after_scores = before_scores + improvement # Perform paired t-test t_stat, p_value = stats.ttest_rel(before_scores, after_scores) print(f"\n📊 Financial Education Impact:") print(f" Before: Mean={before_scores.mean():.2f}, Std={before_scores.std():.2f}") print(f" After: Mean={after_scores.mean():.2f}, Std={after_scores.std():.2f}") print(f" Average Improvement: {after_scores.mean() - before_scores.mean():.2f}") print(f" T-Statistic: {t_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: REJECT null hypothesis") print(f" Interpretation: Financial education significantly improved credit scores") else: print(f" Result: FAIL TO REJECT null hypothesis") print(f" Interpretation: No significant improvement detected") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Before and after distributions ax = axes[0] ax.hist(before_scores, bins=15, alpha=0.5, label='Before', edgecolor='black') ax.hist(after_scores, bins=15, alpha=0.5, label='After', edgecolor='black') ax.axvline(before_scores.mean(), color='blue', linestyle='--', label=f'Before Mean: {before_scores.mean():.2f}') ax.axvline(after_scores.mean(), color='orange', linestyle='--', label=f'After Mean: {after_scores.mean():.2f}') ax.set_title('Credit Score Distribution - Before vs After', fontsize=12) ax.set_xlabel('Credit Score') ax.set_ylabel('Frequency') ax.legend() ax.grid(True, alpha=0.3) # Paired differences ax = axes[1] differences = after_scores - before_scores ax.hist(differences, bins=15, edgecolor='black', alpha=0.7) ax.axvline(0, color='red', linestyle='--', linewidth=2, label='No Change') ax.axvline(differences.mean(), color='blue', linestyle='--', linewidth=2, label=f'Mean Improvement: {differences.mean():.2f}') ax.set_title('Distribution of Improvements', fontsize=12) ax.set_xlabel('Score Improvement') ax.set_ylabel('Frequency') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('paired_ttest.png', dpi=300) plt.show()
SECTION 4: ANALYSIS OF VARIANCE (ANOVA)
ANOVA compares means across three or more groups.
# ============= ANALYSIS OF VARIANCE (ANOVA) ============= print("\n" + "="*60) print("ANALYSIS OF VARIANCE (ANOVA)") print("="*60) # Example: Compare transaction amounts across all three segments np.random.seed(42) premium = np.random.normal(350, 80, 100) standard = np.random.normal(200, 60, 100) basic = np.random.normal(120, 40, 100) # Perform one-way ANOVA f_stat, p_value = stats.f_oneway(premium, standard, basic) print(f"\n📊 Transaction Amount by Segment:") print(f" Premium: Mean={premium.mean():.2f}, Std={premium.std():.2f}") print(f" Standard: Mean={standard.mean():.2f}, Std={standard.std():.2f}") print(f" Basic: Mean={basic.mean():.2f}, Std={basic.std():.2f}") print(f"\n F-Statistic: {f_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: REJECT null hypothesis") print(f" Interpretation: There is a significant difference between at least two segments") # Post-hoc analysis (Tukey's HSD) from statsmodels.stats.multicomp import pairwise_tukeyhsd # Prepare data for post-hoc all_data = np.concatenate([premium, standard, basic]) all_groups = np.concatenate([['Premium']*100, ['Standard']*100, ['Basic']*100]) tukey = pairwise_tukeyhsd(all_data, all_groups, alpha=0.05) print("\n Post-hoc Analysis (Tukey's HSD):") print(tukey) else: print(f" Result: FAIL TO REJECT null hypothesis") print(f" Interpretation: No significant difference between segments") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Box plots ax = axes[0] data = [premium, standard, basic] ax.boxplot(data, labels=['Premium', 'Standard', 'Basic']) ax.set_title('Transaction Amounts by Segment', fontsize=12) ax.set_ylabel('Amount ($)') ax.grid(True, alpha=0.3) # Violin plots ax = axes[1] sns.violinplot(data=[premium, standard, basic], ax=ax) ax.set_xticklabels(['Premium', 'Standard', 'Basic']) ax.set_title('Transaction Amount Distributions', fontsize=12) ax.set_ylabel('Amount ($)') ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('anova_analysis.png', dpi=300) plt.show()
SECTION 5: CHI-SQUARE TESTS
Chi-square tests are used for categorical data.
# ============= CHI-SQUARE TESTS ============= print("\n" + "="*60) print("CHI-SQUARE TESTS") print("="*60) # Example: Is there an association between customer segment and loan default? np.random.seed(42) # Create contingency table n_customers = 500 segments = np.random.choice(['Premium', 'Standard', 'Basic'], n_customers, p=[0.15, 0.55, 0.30]) # Simulate defaults based on segment default_prob = {'Premium': 0.02, 'Standard': 0.05, 'Basic': 0.10} defaults = np.array([1 if np.random.random() < default_prob[seg] else 0 for seg in segments]) # Create contingency table contingency = pd.crosstab(segments, defaults, margins=False) print("\n📊 Contingency Table - Segment vs Default:") print(contingency) # Perform chi-square test from scipy.stats import chi2_contingency chi2, p_value, dof, expected = chi2_contingency(contingency) print(f"\n Chi-Square Statistic: {chi2:.3f}") print(f" P-Value: {p_value:.4f}") print(f" Degrees of Freedom: {dof}") if p_value < 0.05: print(f" Result: REJECT null hypothesis") print(f" Interpretation: There is a significant association between segment and default") else: print(f" Result: FAIL TO REJECT null hypothesis") print(f" Interpretation: No significant association between segment and default") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Stacked bar chart ax = axes[0] contingency_pct = contingency.div(contingency.sum(axis=1), axis=0) contingency_pct.plot(kind='bar', stacked=True, ax=ax) ax.set_title('Default Rate by Segment', fontsize=12) ax.set_xlabel('Segment') ax.set_ylabel('Proportion') ax.legend(['No Default', 'Default'], title='Status') ax.grid(True, alpha=0.3) # Heatmap of expected vs observed ax = axes[1] sns.heatmap(contingency, annot=True, fmt='d', cmap='Blues', ax=ax) ax.set_title('Observed Frequencies', fontsize=12) ax.set_xlabel('Default') ax.set_ylabel('Segment') plt.tight_layout() plt.savefig('chi_square_analysis.png', dpi=300) plt.show()
SECTION 6: NON-PARAMETRIC TESTS
Non-parametric tests are used when data is not normally distributed.
# ============= NON-PARAMETRIC TESTS ============= print("\n" + "="*60) print("NON-PARAMETRIC TESTS") print("="*60) # Create skewed data (lognormal) np.random.seed(42) group_a = np.random.lognormal(3, 0.5, 100) group_b = np.random.lognormal(3.5, 0.5, 100) print("\n📊 Data with Non-Normal Distribution:") print(f" Group A: Mean={group_a.mean():.2f}, Median={np.median(group_a):.2f}, Skew={pd.Series(group_a).skew():.3f}") print(f" Group B: Mean={group_b.mean():.2f}, Median={np.median(group_b):.2f}, Skew={pd.Series(group_b).skew():.3f}") # Mann-Whitney U test (non-parametric alternative to t-test) u_stat, p_value = stats.mannwhitneyu(group_a, group_b, alternative='two-sided') print(f"\n📊 Mann-Whitney U Test:") print(f" U-Statistic: {u_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: Significant difference between groups") else: print(f" Result: No significant difference between groups") # Kruskal-Wallis test (non-parametric alternative to ANOVA) # Add a third group group_c = np.random.lognormal(4, 0.5, 100) h_stat, p_value = stats.kruskal(group_a, group_b, group_c) print(f"\n📊 Kruskal-Wallis Test:") print(f" H-Statistic: {h_stat:.3f}") print(f" P-Value: {p_value:.4f}") if p_value < 0.05: print(f" Result: Significant difference between at least two groups") else: print(f" Result: No significant difference between groups") # Visualize fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # Box plots ax = axes[0] data = [group_a, group_b, group_c] ax.boxplot(data, labels=['Group A', 'Group B', 'Group C']) ax.set_title('Non-Normal Data Comparison', fontsize=12) ax.set_ylabel('Value') ax.grid(True, alpha=0.3) # QQ plots for normality check ax = axes[1] stats.probplot(group_a, dist="norm", plot=ax) ax.set_title('QQ Plot - Group A', fontsize=12) plt.tight_layout() plt.savefig('nonparametric_tests.png', dpi=300) plt.show()
SECTION 7: BUSINESS RISK & FINANCIAL IMPACT
7.1 Statistical Inference in Banking Decision-Making
# ============= BUSINESS APPLICATIONS ============= print("\n" + "="*60) print("BUSINESS APPLICATIONS OF STATISTICAL INFERENCE") print("="*60) applications = [ { 'application': 'Credit Scoring Model Validation', 'test': 't-test', 'use_case': 'Compare model predictions vs actual outcomes', 'impact': 'Ensure model accuracy, comply with SR 11-7' }, { 'application': 'Marketing Campaign Effectiveness', 'test': 'paired t-test', 'use_case': 'Compare customer spending before and after campaign', 'impact': 'Optimize marketing spend, increase ROI' }, { 'application': 'Customer Segment Analysis', 'test': 'ANOVA', 'use_case': 'Compare metrics across customer segments', 'impact': 'Targeted product offerings, improve profitability' }, { 'application': 'Fraud Detection', 'test': 'chi-square', 'use_case': 'Test association between transaction characteristics and fraud', 'impact': 'Improve fraud detection, reduce losses' }, { 'application': 'A/B Testing', 'test': 'two-sample t-test', 'use_case': 'Compare performance of different strategies', 'impact': 'Data-driven decision making, optimize outcomes' } ] for app in applications: print(f"\n📊 {app['application']}:") print(f" Test: {app['test']}") print(f" Use Case: {app['use_case']}") print(f" Business Impact: {app['impact']}")
7.2 Regulatory Considerations
# ============= REGULATORY CONSIDERATIONS ============= print("\n" + "="*60) print("REGULATORY CONSIDERATIONS") print("="*60) regulatory = { 'SR 11-7': { 'requirement': 'Model validation must include statistical testing', 'implication': 'Document all hypothesis tests and their results' }, 'BASEL III': { 'requirement': 'Risk models must be statistically sound', 'implication': 'Use appropriate tests for model calibration' }, 'Fair Lending': { 'requirement': 'No discrimination in lending decisions', 'implication': 'Use statistical tests to check for bias' }, 'GDPR': { 'requirement': 'Right to explanation of automated decisions', 'implication': 'Statistical results must be interpretable and explainable' } } for reg, details in regulatory.items(): print(f"\n📋 {reg}:") print(f" Requirement: {details['requirement']}") print(f" Implication: {details['implication']}")
SECTION 8: SUMMARY FOR THE DATA PRACTITIONER
8.1 The 1-Minute Elevator Pitch
“Statistical inference helps us make data-driven decisions with quantified uncertainty. We use confidence intervals to estimate population parameters, t-tests to compare group means, ANOVA to analyze multiple groups, and chi-square tests for categorical relationships. Non-parametric tests work when data isn’t normal. These techniques are essential for validating credit models, testing marketing effectiveness, and ensuring regulatory compliance. Proper statistical inference prevents costly business mistakes.”
8.2 Key Takeaways
-
Central Limit Theorem enables inference even with non-normal data (with sufficient sample size).
-
Confidence intervals quantify uncertainty in estimates.
-
t-tests compare means (one-sample, two-sample, paired).
-
ANOVAÂ compares means across three or more groups.
-
Chi-square tests analyze categorical data relationships.
-
Non-parametric tests work when data isn’t normal.
-
P-values indicate the strength of evidence against the null hypothesis.
-
Sample size affects statistical power and precision.
-
Business context is crucial for interpreting statistical results.
-
Regulatory compliance requires documentation of statistical testing.
8.3 Recommended Next Steps
-
Apply hypothesis tests to your banking data
-
Build confidence intervals for key metrics
-
Validate models using statistical tests
-
Document statistical procedures for compliance
-
Learn about power analysis for experiment design
[END OF LESSON 2]