Data Science Fundamentals Course
Hypothesis testing is one of the most important statistical tools in data science. It provides a formal framework for making decisions about populations based on sample data. This week you'll learn how to:
Hypothesis testing is used everywhere in data science and business:
By the end of Week 7, you will be able to:
Week 7 is divided into three 2-hour sessions:
import numpy as np from scipy import stats import pandas as pd import matplotlib.pyplot as plt print("="*50) print("HYPOTHESIS TESTING FRAMEWORK") print("="*50) print(""" THE BASIC IDEA: We make a claim about a population and test it using sample data. STEPS: 1. State hypotheses 2. Choose significance level (α) 3. Calculate test statistic 4. Find p-value 5. Make decision and interpret KEY CONCEPTS: """) print(""" NULL HYPOTHESIS (H₀): - Status quo or no effect - What we assume is true initially - We try to find evidence AGAINST it ALTERNATIVE HYPOTHESIS (H₁ or Hₐ): - What we're testing for - Different from null hypothesis SIGNIFICANCE LEVEL (α): - Probability of rejecting H₀ when it's actually true - Common values: 0.05, 0.01, 0.10 - 0.05 means 5% chance of Type I error P-VALUE: - Probability of observing data as extreme (or more extreme) if H₀ is true - Small p-value: strong evidence against H₀ - NOT the probability that H₀ is true! DECISION RULE: - If p-value < α: Reject H₀ (significant result) - If p-value ≥ α: Fail to reject H₀ (not significant) """) # Type I and Type II errors print("\n" + "="*50) print("TYPE I AND TYPE II ERRORS") print("="*50) print(""" TYPE I ERROR (α - False Positive): - Reject H₀ when H₀ is actually true - "Crying wolf" - finding effect that doesn't exist - Probability = α (significance level) TYPE II ERROR (β - False Negative): - Fail to reject H₀ when H₀ is actually false - Missing real effect - Probability = β STATISTICAL POWER: - Power = 1 - β - Probability of correctly rejecting false H₀ - Typical target: 0.80 (80% power) TRUTH TABLE: H₀ True H₀ False Reject H₀ Type I Error Correct ✓ Fail to Reject Correct ✓ Type II Error """) # Example visualization fig, ax = plt.subplots(figsize=(10, 6)) # Two normal distributions x = np.linspace(-4, 4, 1000) null_dist = stats.norm.pdf(x, 0, 1) alt_dist = stats.norm.pdf(x, 2, 1) ax.plot(x, null_dist, 'b-', linewidth=2, label='H₀ Distribution (μ=0)') ax.plot(x, alt_dist, 'r-', linewidth=2, label='H₁ Distribution (μ=2)') # Critical value at α=0.05 critical_value = stats.norm.ppf(0.95) ax.axvline(critical_value, color='black', linestyle='--', linewidth=2, label=f'Critical value (α=0.05)') # Shade Type I error (α) x_alpha = x[x >= critical_value] alpha_area = stats.norm.pdf(x_alpha, 0, 1) ax.fill_between(x_alpha, alpha_area, alpha=0.3, color='blue', label='Type I Error (α)') # Shade Type II error (β) x_beta = x[x < critical_value] beta_area = stats.norm.pdf(x_beta, 2, 1) ax.fill_between(x_beta, beta_area, alpha=0.3, color='red', label='Type II Error (β)') ax.set_xlabel('Test Statistic', fontsize=12) ax.set_ylabel('Probability Density', fontsize=12) ax.set_title('Type I and Type II Errors', fontsize=14, fontweight='bold') ax.legend(fontsize=10) ax.grid(True, alpha=0.3) plt.tight_layout() plt.show() print(f"Critical value: {critical_value:.3f}") print(f"Type I error (α): 0.05 (5%)") print(f"Type II error (β): {stats.norm.cdf(critical_value, 2, 1):.3f} (~27%)") print(f"Power (1-β): {1 - stats.norm.cdf(critical_value, 2, 1):.3f} (~73%)")
import numpy as np from scipy import stats import pandas as pd print("="*50) print("ONE-SAMPLE T-TEST") print("="*50) print(""" WHEN TO USE: - Testing if population mean equals specific value - Sample size is small (n < 30) or population SD unknown - Data approximately normally distributed HYPOTHESES: - H₀: μ = μ₀ (mean equals specified value) - H₁: μ ≠ μ₀ (two-tailed, most common) - Or: μ > μ₀ (one-tailed, right) - Or: μ < μ₀ (one-tailed, left) """) # Example: Factory production print("\nEXAMPLE: FACTORY PRODUCTION") print("-"*50) # Production line target: 500g per package target_weight = 500 # Sample weights from production weights = np.array([498, 502, 499, 501, 497, 503, 498, 500, 499, 502, 501, 498, 497, 503, 499, 500, 502, 498, 501, 499]) print(f"Target weight: {target_weight}g") print(f"Sample: {weights}") print(f"Sample size: {len(weights)}") print(f"Sample mean: {weights.mean():.2f}g") print(f"Sample std dev: {weights.std(ddof=1):.2f}g") # Conduct one-sample t-test t_stat, p_value = stats.ttest_1samp(weights, target_weight) print(f"\nT-Test Results:") print(f"T-statistic: {t_stat:.4f}") print(f"P-value: {p_value:.4f}") # Interpretation alpha = 0.05 print(f"\nSignificance level (α): {alpha}") if p_value < alpha: print(f"Result: REJECT H₀ (p={p_value:.4f} < {alpha})") print("Conclusion: The mean weight is significantly different from 500g") else: print(f"Result: FAIL TO REJECT H₀ (p={p_value:.4f} ≥ {alpha})") print("Conclusion: No significant difference from target weight") # Calculate 95% confidence interval n = len(weights) se = weights.std(ddof=1) / np.sqrt(n) t_crit = stats.t.ppf(0.975, df=n-1) ci_lower = weights.mean() - t_crit * se ci_upper = weights.mean() + t_crit * se print(f"\n95% Confidence Interval: [{ci_lower:.2f}, {ci_upper:.2f}]") print(f"Target {target_weight}g is {'INSIDE' if ci_lower <= target_weight <= ci_upper else 'OUTSIDE'} the CI") # Example 2: Test score hypothesis print("\n" + "="*50) print("EXAMPLE 2: TEST SCORE ANALYSIS") print("-"*50) # Hypothesis: Class average is 75 hypothesized_mean = 75 test_scores = np.array([78, 82, 76, 74, 88, 91, 73, 71, 85, 79, 81, 75, 84, 77, 80, 72, 86, 79, 83, 75]) t_stat, p_value = stats.ttest_1samp(test_scores, hypothesized_mean) print(f"Null hypothesis: μ = {hypothesized_mean}") print(f"Sample mean: {test_scores.mean():.2f}") print(f"Sample size: {len(test_scores)}") print(f"\nT-statistic: {t_stat:.4f}") print(f"P-value: {p_value:.4f}") if p_value < 0.05: print(f"Result: Class average is SIGNIFICANTLY DIFFERENT from {hypothesized_mean}") else: print(f"Result: Class average is NOT significantly different from {hypothesized_mean}") # Effect size (Cohen's d) cohens_d = (test_scores.mean() - hypothesized_mean) / test_scores.std(ddof=1) print(f"\nEffect size (Cohen's d): {cohens_d:.3f}") print(f"Interpretation: {'Small' if abs(cohens_d) < 0.5 else 'Medium' if abs(cohens_d) < 0.8 else 'Large'} effect")
import numpy as np from scipy import stats import pandas as pd print("="*50) print("TWO-SAMPLE T-TEST") print("="*50) print(""" WHEN TO USE: - Comparing means of two independent groups - Testing if two population means are equal - Small sample sizes or population SD unknown HYPOTHESES: - H₀: μ₁ = μ₂ (means are equal) - H₁: μ₁ ≠ μ₂ (means are different - two-tailed) ASSUMPTIONS: - Both samples approximately normal - Variances equal (can test with Levene's test) - Samples are independent """) # Example: Treatment effectiveness print("\nEXAMPLE: DRUG TREATMENT EFFECTIVENESS") print("-"*50) # Control group (no treatment) control = np.array([5.2, 4.8, 5.1, 4.9, 5.0, 4.7, 5.3, 4.9, 5.1, 4.8]) # Treatment group (received drug) treatment = np.array([3.2, 3.5, 3.1, 3.4, 3.0, 3.6, 3.3, 3.2, 3.4, 3.5]) print(f"Control group (n={len(control)}): {control}") print(f"Treatment group (n={len(treatment)}): {treatment}") print(f"\nDescriptive Statistics:") print(f"Control mean: {control.mean():.2f}, SD: {control.std(ddof=1):.2f}") print(f"Treatment mean: {treatment.mean():.2f}, SD: {treatment.std(ddof=1):.2f}") print(f"Difference in means: {control.mean() - treatment.mean():.2f}") # Test for equal variances (Levene's test) stat_levene, p_levene = stats.levene(control, treatment) print(f"\nLevene's Test for Equal Variances:") print(f"P-value: {p_levene:.4f}") equal_var = p_levene > 0.05 print(f"Assumption: Variances are {'EQUAL' if equal_var else 'NOT EQUAL'}") # Conduct two-sample t-test t_stat, p_value = stats.ttest_ind(control, treatment, equal_var=equal_var) print(f"\nTwo-Sample T-Test:") print(f"T-statistic: {t_stat:.4f}") print(f"P-value: {p_value:.6f}") alpha = 0.05 if p_value < alpha: print(f"Result: REJECT H₀ (p={p_value:.6f} < {alpha})") print("Conclusion: Treatment significantly reduces the outcome") else: print(f"Result: FAIL TO REJECT H₀ (p={p_value:.6f} ≥ {alpha})") print("Conclusion: No significant difference between groups") # Cohen's d pooled_std = np.sqrt(((len(control)-1)*control.std(ddof=1)**2 + (len(treatment)-1)*treatment.std(ddof=1)**2) / (len(control) + len(treatment) - 2)) cohens_d = (control.mean() - treatment.mean()) / pooled_std print(f"\nEffect size (Cohen's d): {abs(cohens_d):.3f}") print(f"Interpretation: {'Small' if abs(cohens_d) < 0.5 else 'Medium' if abs(cohens_d) < 0.8 else 'Large'} effect") # Paired t-test example print("\n" + "="*50) print("PAIRED T-TEST (Within-subject design)") print("-"*50) print(""" WHEN TO USE: - Same subjects measured twice (before/after) - Dependent/correlated samples - Examples: weight before/after diet, test scores before/after training """) # Before and after training before = np.array([65, 72, 68, 71, 70, 73, 69, 74, 66, 68]) after = np.array([72, 78, 74, 76, 77, 79, 75, 81, 72, 75]) print(f"\nBefore training: {before}") print(f"After training: {after}") print(f"Improvements: {after - before}") # Paired t-test t_stat_paired, p_value_paired = stats.ttest_rel(before, after) print(f"\nPaired T-Test Results:") print(f"Mean before: {before.mean():.2f}") print(f"Mean after: {after.mean():.2f}") print(f"Mean difference: {(after-before).mean():.2f}") print(f"T-statistic: {t_stat_paired:.4f}") print(f"P-value: {p_value_paired:.6f}") if p_value_paired < 0.05: print("Result: Training SIGNIFICANTLY improved performance") else: print("Result: Training did NOT significantly improve performance")
import numpy as np from scipy import stats import pandas as pd import matplotlib.pyplot as plt print("="*50) print("ONE-WAY ANOVA") print("="*50) print(""" WHEN TO USE: - Comparing means across 3+ groups - Extension of two-sample t-test - Testing if at least one group mean differs from others HYPOTHESES: - H₀: μ₁ = μ₂ = μ₃ = ... (all means equal) - H₁: At least one mean differs ASSUMPTIONS: - Each group approximately normal - Variances equal across groups (test with Levene's) - Independent observations """) # Example: Store performance print("\nEXAMPLE: STORE PERFORMANCE") print("-"*50) # Sales from three store locations store_A = np.array([45, 48, 42, 46, 44, 47, 43, 45, 46, 44]) store_B = np.array([52, 55, 53, 54, 51, 56, 52, 54, 53, 55]) store_C = np.array([40, 42, 39, 41, 38, 43, 40, 39, 42, 41]) print(f"Store A sales: {store_A}") print(f"Store B sales: {store_B}") print(f"Store C sales: {store_C}") print(f"\nDescriptive Statistics:") print(f"Store A: mean={store_A.mean():.2f}, sd={store_A.std(ddof=1):.2f}") print(f"Store B: mean={store_B.mean():.2f}, sd={store_B.std(ddof=1):.2f}") print(f"Store C: mean={store_C.mean():.2f}, sd={store_C.std(ddof=1):.2f}") # Test homogeneity of variance stat_levene, p_levene = stats.levene(store_A, store_B, store_C) print(f"\nLevene's Test for Equal Variances: p={p_levene:.4f}") print(f"Variances are {'EQUAL' if p_levene > 0.05 else 'NOT EQUAL'}") # Conduct one-way ANOVA f_stat, p_value = stats.f_oneway(store_A, store_B, store_C) print(f"\nOne-Way ANOVA Results:") print(f"F-statistic: {f_stat:.4f}") print(f"P-value: {p_value:.6f}") if p_value < 0.05: print(f"Result: REJECT H₀") print("Conclusion: Store performance differs significantly") else: print(f"Result: FAIL TO REJECT H₀") print("Conclusion: No significant difference in store performance") # Post-hoc test (pairwise comparisons) if p_value < 0.05: print("\n" + "-"*50) print("POST-HOC: PAIRWISE COMPARISONS (T-tests)") print("-"*50) # Bonferroni correction alpha_corrected = 0.05 / 3 # 3 pairwise comparisons comparisons = [ ("A vs B", store_A, store_B), ("A vs C", store_A, store_C), ("B vs C", store_B, store_C) ] for name, group1, group2 in comparisons: t_stat_post, p_post = stats.ttest_ind(group1, group2) sig = "***" if p_post < alpha_corrected else "" print(f"{name}: t={t_stat_post:.3f}, p={p_post:.4f} {sig}") print(f"Note: Bonferroni corrected α = {alpha_corrected:.4f}") # Chi-Square Test print("\n" + "="*50) print("CHI-SQUARE TEST") print("="*50) print(""" WHEN TO USE: - Testing relationships between categorical variables - Goodness of fit for categorical data - Independence of two categorical variables HYPOTHESES (Independence): - H₀: Variables are independent - H₁: Variables are associated """) # Example: Product preference by gender print("\nEXAMPLE: PRODUCT PREFERENCE BY GENDER") print("-"*50) # Contingency table observed = np.array([ [45, 30], # Male: Product A (45), Product B (30) [25, 60] # Female: Product A (25), Product B (60) ]) df_contingency = pd.DataFrame( observed, index=['Male', 'Female'], columns=['Product A', 'Product B'] ) print("Observed frequencies:") print(df_contingency) # Chi-square test chi2_stat, p_chi2, dof, expected = stats.chi2_contingency(observed) print(f"\nExpected frequencies:") print(expected) print(f"\nChi-Square Test Results:") print(f"Chi-square statistic: {chi2_stat:.4f}") print(f"P-value: {p_chi2:.6f}") print(f"Degrees of freedom: {dof}") if p_chi2 < 0.05: print(f"Result: REJECT H₀") print("Conclusion: Product preference is associated with gender") else: print(f"Result: FAIL TO REJECT H₀") print("Conclusion: No association between gender and product preference") # Cramér's V (effect size) n = observed.sum() min_dim = min(observed.shape) - 1 cramers_v = np.sqrt(chi2_stat / (n * min_dim)) print(f"\nEffect size (Cramér's V): {cramers_v:.3f}") print(f"Interpretation: {'Negligible' if cramers_v < 0.1 else 'Small' if cramers_v < 0.3 else 'Medium' if cramers_v < 0.5 else 'Large'} effect")
import numpy as np from scipy import stats import pandas as pd print("="*50) print("NON-PARAMETRIC TESTS") print("="*50) print(""" WHEN TO USE: - Data not normally distributed - Small sample sizes - Ordinal or ranked data - Violate parametric test assumptions NON-PARAMETRIC ALTERNATIVES: - t-test → Mann-Whitney U test (Wilcoxon rank-sum) - Paired t-test → Wilcoxon signed-rank test - ANOVA → Kruskal-Wallis test """) # Mann-Whitney U Test print("\nMANN-WHITNEY U TEST (Non-parametric alternative to two-sample t-test)") print("-"*50) print(""" WHEN TO USE: - Two independent groups - Data not normally distributed or unknown distribution - Ordinal data or small samples """) # Example: Customer satisfaction (non-normal data) old_method = np.array([3, 2, 4, 2, 3, 1, 3, 2, 4, 1]) # Ranks 1-5 new_method = np.array([5, 4, 5, 4, 4, 5, 3, 4, 5, 4]) print(f"\nOld method satisfaction: {old_method}") print(f"New method satisfaction: {new_method}") # Mann-Whitney U test u_stat, p_mann_whitney = stats.mannwhitneyu(old_method, new_method, alternative='two-sided') print(f"\nMann-Whitney U Test:") print(f"U-statistic: {u_stat:.4f}") print(f"P-value: {p_mann_whitney:.6f}") if p_mann_whitney < 0.05: print("Result: Methods have SIGNIFICANTLY different satisfaction ratings") else: print("Result: No significant difference in satisfaction ratings") # Wilcoxon Signed-Rank Test print("\n" + "="*50) print("WILCOXON SIGNED-RANK TEST (Non-parametric paired t-test)") print("-"*50) print(""" WHEN TO USE: - Paired/dependent samples - Data not normally distributed - Ordinal or ranked data """) # Before and after (ordinal data) before = np.array([2, 3, 2, 1, 3, 2, 1, 2, 3, 2]) after = np.array([4, 5, 4, 3, 5, 4, 3, 4, 5, 4]) print(f"\nBefore: {before}") print(f"After: {after}") print(f"Differences: {after - before}") # Wilcoxon signed-rank test w_stat, p_wilcoxon = stats.wilcoxon(before, after) print(f"\nWilcoxon Signed-Rank Test:") print(f"W-statistic: {w_stat:.4f}") print(f"P-value: {p_wilcoxon:.6f}") if p_wilcoxon < 0.05: print("Result: Significant improvement after intervention") else: print("Result: No significant improvement") # Kruskal-Wallis Test print("\n" + "="*50) print("KRUSKAL-WALLIS TEST (Non-parametric ANOVA)") print("-"*50) print(""" WHEN TO USE: - Three or more groups - Data not normally distributed - Ordinal data or unknown distribution """) # Example: Customer ratings for three products product_A = np.array([2, 3, 2, 1, 3, 2, 4, 2, 3, 1]) product_B = np.array([4, 5, 4, 3, 5, 4, 5, 4, 5, 3]) product_C = np.array([3, 4, 3, 2, 4, 3, 4, 3, 4, 2]) print(f"\nProduct A ratings: {product_A}") print(f"Product B ratings: {product_B}") print(f"Product C ratings: {product_C}") # Kruskal-Wallis test h_stat, p_kruskal = stats.kruskal(product_A, product_B, product_C) print(f"\nKruskal-Wallis Test:") print(f"H-statistic: {h_stat:.4f}") print(f"P-value: {p_kruskal:.6f}") if p_kruskal < 0.05: print("Result: Products have SIGNIFICANTLY different ratings") else: print("Result: No significant difference in product ratings")
import numpy as np from scipy import stats import pandas as pd print("="*50) print("EFFECT SIZES") print("="*50) print(""" WHY EFFECT SIZE MATTERS: - P-value tells if effect exists (significant) - Effect size tells how LARGE the effect is - Large sample can detect tiny (insignificant) effects - Small sample might miss large (important) effects PRACTICAL vs STATISTICAL SIGNIFICANCE: - Statistical significance: Effect exists (p < 0.05) - Practical significance: Effect is large enough to matter """) # Cohen's d print("\n" + "="*50) print("COHEN'S D (for comparing means)") print("="*50) print(""" INTERPRETATION: - d = 0.2: Small effect - d = 0.5: Medium effect - d = 0.8: Large effect CALCULATION: Cohen's d = (Mean₁ - Mean₂) / Pooled SD """) # Example group1 = np.array([85, 88, 82, 90, 86, 84, 87, 89, 83, 85]) group2 = np.array([78, 80, 75, 82, 79, 77, 81, 83, 76, 79]) mean1, mean2 = group1.mean(), group2.mean() sd1, sd2 = group1.std(ddof=1), group2.std(ddof=1) n1, n2 = len(group1), len(group2) pooled_sd = np.sqrt(((n1-1)*sd1**2 + (n2-1)*sd2**2) / (n1+n2-2)) cohens_d = (mean1 - mean2) / pooled_sd print(f"\nGroup 1: mean={mean1:.2f}, sd={sd1:.2f}") print(f"Group 2: mean={mean2:.2f}, sd={sd2:.2f}") print(f"\nCohen's d = ({mean1:.2f} - {mean2:.2f}) / {pooled_sd:.2f}") print(f"Cohen's d = {cohens_d:.3f}") effect_interpretation = "Small" if abs(cohens_d) < 0.5 else "Medium" if abs(cohens_d) < 0.8 else "Large" print(f"\nInterpretation: {effect_interpretation} effect") # Other effect sizes print("\n" + "="*50) print("OTHER EFFECT SIZES") print("="*50) print(""" R² (Coefficient of Determination): - Proportion of variance explained - 0 to 1 (0% to 100%) - Small: 0.01, Medium: 0.06, Large: 0.14 Cramér's V (for chi-square): - Small: 0.1, Medium: 0.3, Large: 0.5 Eta² (for ANOVA): - Proportion of variance between groups - Similar scale to R² Odds Ratio: - For categorical data - Ratio of odds in two groups """) # Example: R-squared print("\nEXAMPLE: R-SQUARED FOR CORRELATION") print("-"*50) x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) y = 2*x + np.random.normal(0, 2, 10) correlation = np.corrcoef(x, y)[0, 1] r_squared = correlation ** 2 print(f"Correlation coefficient (r): {correlation:.3f}") print(f"R-squared: {r_squared:.3f}") print(f"Interpretation: {r_squared*100:.1f}% of variance in y explained by x") # Reporting results print("\n" + "="*50) print("HOW TO REPORT RESULTS") print("="*50) print(""" COMPLETE RESULT REPORT SHOULD INCLUDE: 1. Test name and type (one-tailed vs two-tailed) 2. Sample descriptive statistics 3. Test statistic and value 4. Degrees of freedom (if applicable) 5. P-value (exact if p > 0.001, otherwise p < 0.001) 6. Effect size 7. Confidence interval 8. Conclusion EXAMPLE REPORT: "A two-sample t-test comparing treatment group (M=85.2, SD=4.3, n=10) with control group (M=79.8, SD=5.1, n=10) revealed a significant difference, t(18)=2.84, p=0.011, d=1.01. The 95% CI [3.12, 12.64] does not include zero, indicating treatment significantly improved outcomes with a large effect size." """) # Real-world reporting print("\n" + "-"*50) print("REAL-WORLD EXAMPLE") print("-"*50) data1 = np.array([45, 48, 42, 46, 44, 47, 43, 45]) data2 = np.array([52, 55, 53, 54, 51, 56, 52, 54]) t_stat, p_value = stats.ttest_ind(data1, data2) n1, n2 = len(data1), len(data2) mean1, mean2 = data1.mean(), data2.mean() sd1, sd2 = data1.std(ddof=1), data2.std(ddof=1) pooled_sd = np.sqrt(((n1-1)*sd1**2 + (n2-1)*sd2**2) / (n1+n2-2)) cohens_d = (mean1 - mean2) / pooled_sd se = pooled_sd * np.sqrt(1/n1 + 1/n2) df = n1 + n2 - 2 t_crit = stats.t.ppf(0.975, df) ci_lower = (mean1 - mean2) - t_crit * se ci_upper = (mean1 - mean2) + t_crit * se report = f""" PROFESSIONAL REPORT: A two-sample t-test comparing Method A (M={mean1:.1f}, SD={sd1:.2f}, n={n1}) with Method B (M={mean2:.1f}, SD={sd2:.2f}, n={n2}) revealed a significant difference, t({df})={t_stat:.2f}, p={p_value:.4f}, d={cohens_d:.2f}. The 95% CI [{ci_lower:.1f}, {ci_upper:.1f}] does not include zero. Method B shows significantly better performance with a large effect size. """ print(report)
By completing Week 7, you have learned:
Conduct hypothesis tests on provided scenarios:
Analyze an A/B test (real or simulated):
Perform complete statistical analysis on real dataset: