W6
Intermediate 3 sessions • 6 hours Python

Week 6: Probability, Distributions and Statistical Inference

.ipynb
Follow along in JupyterDownload the complete Week 6 notebook — every code example ready to run.
Download Notebook

Data Science Fundamentals Course

Week 6 Overview

Probability and statistics form the theoretical foundation of data science. Understanding these concepts is essential for:

  • Interpreting data correctly
  • Building and evaluating machine learning models
  • Making data-driven decisions
  • Quantifying uncertainty
  • Conducting hypothesis tests

This week focuses on fundamental concepts that underpin all statistical analysis and machine learning. You'll learn how to work with probability distributions, understand the Central Limit Theorem, calculate confidence intervals, and use simulations to understand complex statistical concepts. By the end of Week 6, you will be able to:

  • Understand and calculate probabilities
  • Work with discrete and continuous probability distributions
  • Apply the Central Limit Theorem
  • Calculate and interpret confidence intervals
  • Understand sampling distributions
  • Use bootstrapping for statistical estimation
  • Understand the difference between Bayesian and Frequentist approaches
  • Use simulations to understand statistical concepts

Week 6 is divided into three 2-hour sessions:

  • Session 1: Probability Fundamentals and Distributions
  • Session 2: Sampling Distributions and the Central Limit Theorem
  • Session 3: Confidence Intervals, Bootstrapping, and Bayesian Thinking

SESSION 1: Probability Fundamentals and Distributions

Duration: 2 hours

1.1 Probability Fundamentals

Probability is the mathematical study of randomness and uncertainty. It quantifies how likely an event is to occur. Key concepts:

  • Sample Space: All possible outcomes of an experiment
  • Event: A subset of the sample space
  • Probability: A number between 0 and 1 indicating likelihood
  • Mutually Exclusive: Events that cannot occur together
  • Independent: Events where one doesn't affect the other
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt

# Basic probability
# Probability = Number of favorable outcomes / Total possible outcomes

print("="*50)
print("BASIC PROBABILITY EXAMPLES")
print("="*50)

# Example 1: Rolling a die
# Probability of rolling a 4
favorable_outcomes = 1
total_outcomes = 6
prob_four = favorable_outcomes / total_outcomes
print(f"\nP(rolling a 4) = {favorable_outcomes}/{total_outcomes} = {prob_four:.3f} or {prob_four*100:.1f}%")

# Example 2: Drawing from a deck
# Probability of drawing a red card
red_cards = 26
total_cards = 52
prob_red = red_cards / total_cards
print(f"P(red card) = {red_cards}/{total_cards} = {prob_red:.3f} or {prob_red*100:.1f}%")

# Example 3: Probability rules
print("\n" + "="*50)
print("PROBABILITY RULES")
print("="*50)

# Addition rule: P(A or B) = P(A) + P(B) - P(A and B)
P_A = 0.3 # Probability of event A
P_B = 0.4 # Probability of event B
P_A_and_B = 0.1 # Probability of both occurring

P_A_or_B = P_A + P_B - P_A_and_B
print(f"\nAddition Rule: P(A or B) = P(A) + P(B) - P(A and B)")
print(f"P(A or B) = {P_A} + {P_B} - {P_A_and_B} = {P_A_or_B}")

# Multiplication rule: P(A and B) = P(A) * P(B|A)
# For independent events: P(A and B) = P(A) * P(B)
P_heads = 0.5
P_tails = 0.5
P_two_heads = P_heads * P_heads
print(f"\nMultiplication Rule (independent): P(two heads) = 0.5 × 0.5 = {P_two_heads}")

# Complement rule: P(not A) = 1 - P(A)
P_rain = 0.3
P_no_rain = 1 - P_rain
print(f"\nComplement Rule: P(no rain) = 1 - P(rain) = 1 - {P_rain} = {P_no_rain}")

# Conditional probability: P(A|B) = P(A and B) / P(B)
print("\n" + "="*50)
print("CONDITIONAL PROBABILITY")
print("="*50)

# Example: Disease test accuracy
# P(Disease|Positive Test)
P_positive_given_disease = 0.95 # Sensitivity
P_negative_given_no_disease = 0.90 # Specificity
P_disease = 0.01 # Prior probability of disease

# Using Bayes' theorem
P_positive = (P_positive_given_disease * P_disease) + ((1 - P_negative_given_no_disease) * (1 - P_disease))
P_disease_given_positive = (P_positive_given_disease * P_disease) / P_positive

print(f"Sensitivity (P(+|disease)): {P_positive_given_disease}")
print(f"Specificity (P(-|no disease)): {P_negative_given_no_disease}")
print(f"P(disease) prior: {P_disease}")
print(f"P(+) overall: {P_positive:.4f}")
print(f"P(disease|+) posterior: {P_disease_given_positive:.4f}")
print(f"\nInterpretation: Even with positive test, only {P_disease_given_positive*100:.1f}% probability of having disease!")

# Simulation: Rolling dice
print("\n" + "="*50)
print("SIMULATION: ROLLING DICE")
print("="*50)

np.random.seed(42)
num_rolls = 100000

# Simulate rolling a die
rolls = np.random.randint(1, 7, num_rolls)

# Calculate probabilities
for face in range(1, 7):
prob = (rolls == face).sum() / num_rolls
print(f"P(rolling {face}) = {prob:.4f}")

print(f"\nExpected: {1/6:.4f} for each face")

# Simulate coin flips
print("\n" + "="*50)
print("SIMULATION: COIN FLIPS")
print("="*50)

flips = np.random.randint(0, 2, 100000)
prob_heads = (flips == 1).sum() / 100000
prob_tails = (flips == 0).sum() / 100000

print(f"P(Heads) = {prob_heads:.4f}")
print(f"P(Tails) = {prob_tails:.4f}")

1.2 Discrete Probability Distributions

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

print("="*50)
print("DISCRETE DISTRIBUTIONS")
print("="*50)

# 1. BERNOULLI DISTRIBUTION
# Single trial with two outcomes (success/failure)
print("\n1. BERNOULLI DISTRIBUTION")
print("Example: Single coin flip, single product working/failing")

p = 0.7 # Probability of success
bernoulli = stats.bernoulli(p)

print(f"P(success) = {p}")
print(f"P(failure) = {1-p}")
print(f"Mean = {bernoulli.mean()}")
print(f"Variance = {bernoulli.var()}")

# 2. BINOMIAL DISTRIBUTION
# Number of successes in n independent trials
print("\n" + "="*50)
print("2. BINOMIAL DISTRIBUTION")
print("Example: Number of heads in 10 coin flips, defects in 100 items")

n = 10 # Number of trials
p = 0.5 # Probability of success per trial
binomial = stats.binom(n, p)

print(f"\nParameters: n={n} trials, p={p}")
print(f"Mean = {binomial.mean()}")
print(f"Variance = {binomial.var()}")

# Probability of exactly 5 successes
prob_5 = binomial.pmf(5)
print(f"P(X=5) = {prob_5:.4f}")

# Probability of 5 or fewer successes
prob_le5 = binomial.cdf(5)
print(f"P(X≤5) = {prob_le5:.4f}")

# Simulate
simulated = np.random.binomial(n, p, 10000)
print(f"\nSimulated mean: {simulated.mean():.3f}")
print(f"Actual mean: {binomial.mean():.3f}")

# 3. POISSON DISTRIBUTION
# Number of events in fixed interval
print("\n" + "="*50)
print("3. POISSON DISTRIBUTION")
print("Example: Number of customer arrivals per hour, defects per batch")

lambda_param = 3 # Average number of events
poisson = stats.poisson(lambda_param)

print(f"\nParameter (λ) = {lambda_param}")
print(f"Mean = {poisson.mean()}")
print(f"Variance = {poisson.var()}")

# Probability of exactly 5 events
prob_5 = poisson.pmf(5)
print(f"P(X=5) = {prob_5:.4f}")

# Probability of 5 or fewer events
prob_le5 = poisson.cdf(5)
print(f"P(X≤5) = {prob_le5:.4f}")

# Simulate
simulated = np.random.poisson(lambda_param, 10000)
print(f"\nSimulated mean: {simulated.mean():.3f}")
print(f"Actual mean: {poisson.mean():.3f}")

# Visualization
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Binomial
x = range(0, n+1)
pmf = binomial.pmf(x)
axes[0].bar(x, pmf, alpha=0.7, color='blue')
axes[0].set_title(f'Binomial(n={n}, p={p})')
axes[0].set_xlabel('Number of successes')
axes[0].set_ylabel('Probability')
axes[0].grid(True, alpha=0.3)

# Poisson
x = range(0, 10)
pmf = poisson.pmf(x)
axes[1].bar(x, pmf, alpha=0.7, color='green')
axes[1].set_title(f'Poisson(λ={lambda_param})')
axes[1].set_xlabel('Number of events')
axes[1].set_ylabel('Probability')
axes[1].grid(True, alpha=0.3)

# Distribution comparison
axes[2].bar([0, 1], [1-p, p], alpha=0.7, color='red', label='Bernoulli')
axes[2].set_title(f'Bernoulli(p={p})')
axes[2].set_xlabel('Outcome')
axes[2].set_ylabel('Probability')
axes[2].set_xticks([0, 1])
axes[2].set_xticklabels(['Failure', 'Success'])
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Real-world example: Product quality
print("\n" + "="*50)
print("REAL-WORLD: PRODUCT QUALITY CONTROL")
print("="*50)

# Defect rate is 2%, inspecting 50 units
n_units = 50
defect_rate = 0.02
quality = stats.binom(n_units, defect_rate)

print(f"\nDefect rate: {defect_rate*100}%")
print(f"Inspecting: {n_units} units")
print(f"Expected defects: {quality.mean()}")
print(f"P(0 defects) = {quality.pmf(0):.4f}")
print(f"P(≥2 defects) = {1 - quality.cdf(1):.4f}")

1.3 Continuous Probability Distributions

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

print("="*50)
print("CONTINUOUS DISTRIBUTIONS")
print("="*50)

# 1. NORMAL (GAUSSIAN) DISTRIBUTION
print("\n1. NORMAL DISTRIBUTION")
print("Example: Heights, test scores, measurement errors")

mu = 100 # Mean
sigma = 15 # Standard deviation
normal = stats.norm(mu, sigma)

print(f"\nParameters: μ={mu}, σ={sigma}")
print(f"Mean = {normal.mean()}")
print(f"Variance = {normal.var()}")

# Probabilities
prob_less_100 = normal.cdf(100)
print(f"P(X < 100) = {prob_less_100:.4f}")

prob_100_110 = normal.cdf(110) - normal.cdf(100)
print(f"P(100 < X < 110) = {prob_100_110:.4f}")

# Percentiles
percentile_90 = normal.ppf(0.90)
print(f"90th percentile = {percentile_90:.2f}")

# Z-scores (standardized)
z_score = (110 - mu) / sigma
print(f"Z-score for X=110: {z_score:.3f}")

# Standard normal (μ=0, σ=1)
print(f"\nP(Z < 0) in standard normal = {stats.norm.cdf(0):.4f}")
print(f"P(-1 < Z < 1) = {stats.norm.cdf(1) - stats.norm.cdf(-1):.4f}")
print(f"P(-2 < Z < 2) = {stats.norm.cdf(2) - stats.norm.cdf(-2):.4f}")
print(f"P(-3 < Z < 3) = {stats.norm.cdf(3) - stats.norm.cdf(-3):.4f}")

# 2. EXPONENTIAL DISTRIBUTION
print("\n" + "="*50)
print("2. EXPONENTIAL DISTRIBUTION")
print("Example: Time until next event, equipment failure time")

lambda_param = 0.5 # Rate parameter
exponential = stats.expon(scale=1/lambda_param)

print(f"\nParameter (λ) = {lambda_param}")
print(f"Mean = {exponential.mean():.3f}")
print(f"Variance = {exponential.var():.3f}")

# Probability density
x = np.linspace(0, 10, 100)
pdf = exponential.pdf(x)

# Cumulative probability
prob_less_2 = exponential.cdf(2)
print(f"P(X < 2) = {prob_less_2:.4f}")

# 3. UNIFORM DISTRIBUTION
print("\n" + "="*50)
print("3. UNIFORM DISTRIBUTION")
print("Example: Random number generation, continuous random selection")

a, b = 0, 10 # Bounds
uniform = stats.uniform(a, b-a)

print(f"\nBounds: [{a}, {b}]")
print(f"Mean = {uniform.mean():.3f}")
print(f"Variance = {uniform.var():.3f}")

prob_3_7 = uniform.cdf(7) - uniform.cdf(3)
print(f"P(3 < X < 7) = {prob_3_7:.4f}")

# Visualization
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Normal distribution
x = np.linspace(mu - 4*sigma, mu + 4*sigma, 100)
pdf = normal.pdf(x)
axes[0, 0].plot(x, pdf, 'b-', linewidth=2, label='Normal')
axes[0, 0].fill_between(x, pdf, alpha=0.3)
axes[0, 0].axvline(mu, color='r', linestyle='--', label=f'Mean = {mu}')
axes[0, 0].set_title(f'Normal Distribution μ={mu}, σ={sigma}')
axes[0, 0].set_xlabel('Value')
axes[0, 0].set_ylabel('Probability Density')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Exponential distribution
x = np.linspace(0, 10, 100)
pdf = exponential.pdf(x)
axes[0, 1].plot(x, pdf, 'g-', linewidth=2)
axes[0, 1].fill_between(x, pdf, alpha=0.3, color='green')
axes[0, 1].set_title(f'Exponential Distribution λ={lambda_param}')
axes[0, 1].set_xlabel('Value')
axes[0, 1].set_ylabel('Probability Density')
axes[0, 1].grid(True, alpha=0.3)

# Uniform distribution
x = np.linspace(a-1, b+1, 100)
pdf = uniform.pdf(x)
axes[1, 0].plot(x, pdf, 'r-', linewidth=2)
axes[1, 0].fill_between(x, pdf, alpha=0.3, color='red')
axes[1, 0].set_title(f'Uniform Distribution [{a}, {b}]')
axes[1, 0].set_xlabel('Value')
axes[1, 0].set_ylabel('Probability Density')
axes[1, 0].grid(True, alpha=0.3)

# Multiple normal distributions for comparison
x = np.linspace(-50, 250, 100)
for mu_i in [100, 150, 200]:
pdf = stats.norm.pdf(x, mu_i, 15)
axes[1, 1].plot(x, pdf, linewidth=2, label=f'μ={mu_i}')
axes[1, 1].set_title('Normal Distributions with Different Means')
axes[1, 1].set_xlabel('Value')
axes[1, 1].set_ylabel('Probability Density')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Real-world example: Test scores
print("\n" + "="*50)
print("REAL-WORLD: TEST SCORE ANALYSIS")
print("="*50)

mean_score = 75
std_score = 8
test_dist = stats.norm(mean_score, std_score)

print(f"\nMean score: {mean_score}")
print(f"Standard deviation: {std_score}")
print(f"P(score < 70) = {test_dist.cdf(70):.4f} or {test_dist.cdf(70)*100:.2f}%")
print(f"P(score > 90) = {1 - test_dist.cdf(90):.4f} or {(1-test_dist.cdf(90))*100:.2f}%")
print(f"P(70 < score < 85) = {test_dist.cdf(85) - test_dist.cdf(70):.4f} or {(test_dist.cdf(85) - test_dist.cdf(70))*100:.2f}%")

SESSION 2: Sampling Distributions and Central Limit Theorem

Duration: 2 hours

2.1 Sampling Distributions

import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt

print("="*50)
print("SAMPLING DISTRIBUTIONS")
print("="*50)

print("""
KEY CONCEPTS:
- Population: All items of interest
- Sample: Subset of population
- Sampling distribution: Distribution of sample statistics
- Standard Error: Standard deviation of sampling distribution
""")

# Create a population
np.random.seed(42)
population = np.random.normal(100, 15, 100000)

print(f"\nPopulation statistics:")
print(f"Mean: {population.mean():.2f}")
print(f"Std Dev: {population.std():.2f}")

# Draw many samples and calculate means
sample_size = 30
num_samples = 10000
sample_means = []

for i in range(num_samples):
sample = np.random.choice(population, sample_size, replace=False)
sample_means.append(sample.mean())

sample_means = np.array(sample_means)

print(f"\n" + "="*50)
print("SAMPLING DISTRIBUTION OF MEANS")
print("="*50)
print(f"Sample size: {sample_size}")
print(f"Number of samples: {num_samples}")
print(f"\nMean of sample means: {sample_means.mean():.2f}")
print(f"Std Dev of sample means (Standard Error): {sample_means.std():.2f}")

# Compare to theoretical standard error
se_theoretical = population.std() / np.sqrt(sample_size)
print(f"Theoretical SE: {se_theoretical:.2f}")

# Effect of sample size
print("\n" + "="*50)
print("EFFECT OF SAMPLE SIZE")
print("="*50)

sample_sizes = [10, 30, 100, 300]
results = {}

for n in sample_sizes:
sample_means_n = []
for i in range(5000):
sample = np.random.choice(population, n, replace=False)
sample_means_n.append(sample.mean())

results[n] = np.array(sample_means_n)
se = population.std() / np.sqrt(n)
print(f"Sample size {n:3d}: SE = {results[n].std():.2f} (theoretical: {se:.2f})")

# Visualization
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

for idx, n in enumerate(sample_sizes):
row = idx // 2
col = idx % 2

axes[row, col].hist(results[n], bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[row, col].set_title(f'Sample Size = {n}')
axes[row, col].set_xlabel('Sample Mean')
axes[row, col].set_ylabel('Frequency')
axes[row, col].axvline(population.mean(), color='red', linestyle='--', label='Population Mean')
axes[row, col].legend()
axes[row, col].grid(True, alpha=0.3)

plt.suptitle('Sampling Distribution of Means (Different Sample Sizes)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

# Standard Error for different statistics
print("\n" + "="*50)
print("STANDARD ERROR FORMULAS")
print("="*50)

print("""
For sample mean: SE = σ / √n
For sample proportion: SE = √(p(1-p) / n)
For sample variance: More complex formula
""")

# Example: Proportion
p = 0.4 # True population proportion
n = 100
se_proportion = np.sqrt(p * (1-p) / n)
print(f"\nFor proportion: p={p}, n={n}")
print(f"SE = √({p}×{1-p}/{n}) = {se_proportion:.4f}")

# Simulate
sample_proportions = []
for i in range(10000):
sample = np.random.binomial(n, p) / n
sample_proportions.append(sample)

print(f"Simulated SE: {np.std(sample_proportions):.4f}")
print(f"Theoretical SE: {se_proportion:.4f}")

2.2 The Central Limit Theorem

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

print("="*50)
print("CENTRAL LIMIT THEOREM (CLT)")
print("="*50)

print("""
THE CENTRAL LIMIT THEOREM STATES:
If you take repeated samples from ANY distribution,
the DISTRIBUTION OF THE SAMPLE MEANS approaches
a NORMAL DISTRIBUTION as sample size increases.

This is true regardless of the underlying distribution!

Implications:
- We can make inferences about populations using sample means
- Normal distribution can approximate many distributions
- Sample size matters: larger samples → more normal
""")

# Demonstrate CLT with different distributions
fig, axes = plt.subplots(3, 3, figsize=(15, 12))

# 1. UNIFORM DISTRIBUTION
print("\n" + "="*50)
print("1. UNIFORM DISTRIBUTION")
print("="*50)

# Generate population (uniform)
population_uniform = np.random.uniform(0, 10, 50000)
axes[0, 0].hist(population_uniform, bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[0, 0].set_title('Population (Uniform)')
axes[0, 0].set_xlabel('Value')
axes[0, 0].set_ylabel('Frequency')
axes[0, 0].grid(True, alpha=0.3)

# Sample means with n=10
sample_means_10 = []
for i in range(5000):
sample = np.random.choice(population_uniform, 10)
sample_means_10.append(sample.mean())

axes[0, 1].hist(sample_means_10, bins=50, alpha=0.7, color='green', edgecolor='black')
axes[0, 1].set_title('Sample Means (n=10)')
axes[0, 1].set_xlabel('Sample Mean')
axes[0, 1].set_ylabel('Frequency')
axes[0, 1].grid(True, alpha=0.3)

# Sample means with n=100
sample_means_100 = []
for i in range(5000):
sample = np.random.choice(population_uniform, 100)
sample_means_100.append(sample.mean())

axes[0, 2].hist(sample_means_100, bins=50, alpha=0.7, color='red', edgecolor='black')
axes[0, 2].set_title('Sample Means (n=100)')
axes[0, 2].set_xlabel('Sample Mean')
axes[0, 2].set_ylabel('Frequency')
axes[0, 2].grid(True, alpha=0.3)

# 2. EXPONENTIAL DISTRIBUTION (Right-skewed)
print("\n2. EXPONENTIAL DISTRIBUTION")

population_exp = np.random.exponential(2, 50000)
axes[1, 0].hist(population_exp, bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[1, 0].set_title('Population (Exponential)')
axes[1, 0].set_xlabel('Value')
axes[1, 0].set_ylabel('Frequency')
axes[1, 0].grid(True, alpha=0.3)

sample_means_exp_10 = []
for i in range(5000):
sample = np.random.choice(population_exp, 10)
sample_means_exp_10.append(sample.mean())

axes[1, 1].hist(sample_means_exp_10, bins=50, alpha=0.7, color='green', edgecolor='black')
axes[1, 1].set_title('Sample Means (n=10)')
axes[1, 1].set_xlabel('Sample Mean')
axes[1, 1].set_ylabel('Frequency')
axes[1, 1].grid(True, alpha=0.3)

sample_means_exp_100 = []
for i in range(5000):
sample = np.random.choice(population_exp, 100)
sample_means_exp_100.append(sample.mean())

axes[1, 2].hist(sample_means_exp_100, bins=50, alpha=0.7, color='red', edgecolor='black')
axes[1, 2].set_title('Sample Means (n=100)')
axes[1, 2].set_xlabel('Sample Mean')
axes[1, 2].set_ylabel('Frequency')
axes[1, 2].grid(True, alpha=0.3)

# 3. BIMODAL DISTRIBUTION
print("\n3. BIMODAL DISTRIBUTION")

population_bimodal = np.concatenate([
np.random.normal(20, 2, 25000),
np.random.normal(80, 2, 25000)
])

axes[2, 0].hist(population_bimodal, bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[2, 0].set_title('Population (Bimodal)')
axes[2, 0].set_xlabel('Value')
axes[2, 0].set_ylabel('Frequency')
axes[2, 0].grid(True, alpha=0.3)

sample_means_bi_10 = []
for i in range(5000):
sample = np.random.choice(population_bimodal, 10)
sample_means_bi_10.append(sample.mean())

axes[2, 1].hist(sample_means_bi_10, bins=50, alpha=0.7, color='green', edgecolor='black')
axes[2, 1].set_title('Sample Means (n=10)')
axes[2, 1].set_xlabel('Sample Mean')
axes[2, 1].set_ylabel('Frequency')
axes[2, 1].grid(True, alpha=0.3)

sample_means_bi_100 = []
for i in range(5000):
sample = np.random.choice(population_bimodal, 100)
sample_means_bi_100.append(sample.mean())

axes[2, 2].hist(sample_means_bi_100, bins=50, alpha=0.7, color='red', edgecolor='black')
axes[2, 2].set_title('Sample Means (n=100)')
axes[2, 2].set_xlabel('Sample Mean')
axes[2, 2].set_ylabel('Frequency')
axes[2, 2].grid(True, alpha=0.3)

plt.suptitle('Central Limit Theorem: Sample Means Approach Normal Distribution',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

print("\n" + "="*50)
print("OBSERVATIONS")
print("="*50)
print("As sample size increases, sample means distribution:")
print("1. Becomes more bell-shaped (normal)")
print("2. Becomes more concentrated (smaller variance)")
print("3. Approaches normal regardless of population shape")

SESSION 3: Confidence Intervals, Bootstrapping, and Bayesian Thinking

Duration: 2 hours

3.1 Confidence Intervals and Point Estimation

import numpy as np
from scipy import stats
import pandas as pd
import matplotlib.pyplot as plt

print("="*50)
print("CONFIDENCE INTERVALS")
print("="*50)

print("""
WHAT IS A CONFIDENCE INTERVAL?
- A range of values estimated to contain the true population parameter
- Constructed from sample data
- Associated with a confidence level (e.g., 95%)

INTERPRETATION:
If we repeated our sampling 100 times and created CI each time,
95% of those intervals would contain the true population parameter.

Common confidence levels: 90%, 95%, 99%
""")

# Example 1: Confidence interval for mean
print("\nEXAMPLE 1: CI FOR POPULATION MEAN")
print("="*50)

# Sample data
sample_data = np.array([85, 92, 78, 88, 95, 82, 90, 87, 93, 80])
print(f"Sample: {sample_data}")
print(f"n = {len(sample_data)}")

# Calculate statistics
sample_mean = sample_data.mean()
sample_std = sample_data.std(ddof=1) # Sample std dev
se = sample_std / np.sqrt(len(sample_data))

print(f"Sample mean: {sample_mean:.2f}")
print(f"Sample std dev: {sample_std:.2f}")
print(f"Standard error: {se:.2f}")

# 95% Confidence interval using t-distribution
confidence_level = 0.95
alpha = 1 - confidence_level
t_critical = stats.t.ppf(1 - alpha/2, df=len(sample_data)-1)
margin_error = t_critical * se

ci_lower = sample_mean - margin_error
ci_upper = sample_mean + margin_error

print(f"\nt-critical (df={len(sample_data)-1}): {t_critical:.3f}")
print(f"Margin of error: {margin_error:.2f}")
print(f"95% CI: [{ci_lower:.2f}, {ci_upper:.2f}]")

# Using scipy
ci = stats.t.interval(0.95, len(sample_data)-1, loc=sample_mean, scale=se)
print(f"Using scipy: [{ci[0]:.2f}, {ci[1]:.2f}]")

# Example 2: Confidence interval for proportion
print("\n" + "="*50)
print("EXAMPLE 2: CI FOR PROPORTION")
print("="*50)

# Survey: 200 people, 120 prefer product A
n = 200
x = 120 # Number preferring A
p_hat = x / n

print(f"Sample size: {n}")
print(f"Number preferring: {x}")
print(f"Sample proportion: {p_hat:.3f}")

# 95% CI for proportion
se_prop = np.sqrt(p_hat * (1 - p_hat) / n)
z_critical = stats.norm.ppf(0.975) # For 95%
margin_error_prop = z_critical * se_prop

ci_lower_prop = p_hat - margin_error_prop
ci_upper_prop = p_hat + margin_error_prop

print(f"Standard error: {se_prop:.4f}")
print(f"z-critical: {z_critical:.3f}")
print(f"Margin of error: {margin_error_prop:.3f}")
print(f"95% CI: [{ci_lower_prop:.3f}, {ci_upper_prop:.3f}]")
print(f"95% CI (percentage): [{ci_lower_prop*100:.1f}%, {ci_upper_prop*100:.1f}%]")

# Effect of confidence level
print("\n" + "="*50)
print("EFFECT OF CONFIDENCE LEVEL")
print("="*50)

confidence_levels = [0.90, 0.95, 0.99]
for conf_level in confidence_levels:
alpha = 1 - conf_level
t_crit = stats.t.ppf(1 - alpha/2, df=len(sample_data)-1)
me = t_crit * se
ci_l = sample_mean - me
ci_u = sample_mean + me
print(f"{conf_level*100:.0f}% CI: [{ci_l:.2f}, {ci_u:.2f}], Width: {ci_u-ci_l:.2f}")

print("\nObservation: Higher confidence level → Wider interval")

# Visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Sample means and CI
np.random.seed(42)
sample_means = []
cis_lower = []
cis_upper = []

true_mean = 100
for i in range(50):
sample = np.random.normal(true_mean, 15, 30)
m = sample.mean()
s = sample.std(ddof=1)
se_i = s / np.sqrt(30)
t_crit = stats.t.ppf(0.975, df=29)

sample_means.append(m)
cis_lower.append(m - t_crit * se_i)
cis_upper.append(m + t_crit * se_i)

colors = ['red' if true_mean < cis_lower[i] or true_mean > cis_upper[i] else 'blue'
for i in range(50)]

for i in range(50):
axes[0].plot([cis_lower[i], cis_upper[i]], [i, i], color=colors[i], linewidth=2)
axes[0].scatter(sample_means[i], i, color=colors[i], s=50, zorder=5)

axes[0].axvline(true_mean, color='green', linestyle='--', linewidth=2, label='True Mean')
axes[0].set_xlabel('Value')
axes[0].set_ylabel('Sample Number')
axes[0].set_title('50 Confidence Intervals (95%)
(Blue include true mean, Red do not)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Plot 2: Effect of sample size
sample_sizes = [10, 30, 50, 100, 200]
margin_errors = []

for n in sample_sizes:
se_n = 15 / np.sqrt(n)
t_crit = stats.t.ppf(0.975, df=n-1)
me = t_crit * se_n
margin_errors.append(me)

axes[1].plot(sample_sizes, margin_errors, marker='o', linewidth=2, markersize=8, color='blue')
axes[1].set_xlabel('Sample Size')
axes[1].set_ylabel('Margin of Error')
axes[1].set_title('Effect of Sample Size on Margin of Error
(95% CI)')
axes[1].grid(True, alpha=0.3)
axes[1].set_xscale('log')

plt.tight_layout()
plt.show()

3.2 Bootstrapping for Statistical Estimation

import numpy as np
from scipy import stats
import pandas as pd
import matplotlib.pyplot as plt

print("="*50)
print("BOOTSTRAPPING")
print("="*50)

print("""
WHAT IS BOOTSTRAPPING?
- Resampling method: repeatedly sample from data with replacement
- Estimates distribution of a statistic without assumptions
- Very powerful for complex statistics where theoretical distributions unknown

STEPS:
1. Draw random sample WITH REPLACEMENT from original sample
2. Calculate statistic of interest on resampled data
3. Repeat steps 1-2 many times (1000-10000)
4. Use distribution of results to estimate CI and SE
""")

# Original data
original_data = np.array([85, 92, 78, 88, 95, 82, 90, 87, 93, 80, 86, 91])
print(f"\nOriginal data: {original_data}")
print(f"Original mean: {original_data.mean():.2f}")
print(f"Original median: {np.median(original_data):.2f}")

# Bootstrap for mean
print("\n" + "="*50)
print("BOOTSTRAP: MEAN")
print("="*50)

n_bootstrap = 10000
bootstrap_means = []

np.random.seed(42)
for i in range(n_bootstrap):
# Resample with replacement
bootstrap_sample = np.random.choice(original_data, len(original_data), replace=True)
bootstrap_means.append(bootstrap_sample.mean())

bootstrap_means = np.array(bootstrap_means)

print(f"\nBootstrap distribution statistics:")
print(f"Mean of bootstrap means: {bootstrap_means.mean():.2f}")
print(f"Std of bootstrap means (SE): {bootstrap_means.std():.2f}")

# 95% CI using percentile method
ci_lower = np.percentile(bootstrap_means, 2.5)
ci_upper = np.percentile(bootstrap_means, 97.5)
print(f"95% CI (percentile method): [{ci_lower:.2f}, {ci_upper:.2f}]")

# Bootstrap for median
print("\n" + "="*50)
print("BOOTSTRAP: MEDIAN")
print("="*50)

bootstrap_medians = []
for i in range(n_bootstrap):
bootstrap_sample = np.random.choice(original_data, len(original_data), replace=True)
bootstrap_medians.append(np.median(bootstrap_sample))

bootstrap_medians = np.array(bootstrap_medians)

print(f"\nBootstrap median statistics:")
print(f"Mean of bootstrap medians: {bootstrap_medians.mean():.2f}")
print(f"Std of bootstrap medians (SE): {bootstrap_medians.std():.2f}")

ci_lower_median = np.percentile(bootstrap_medians, 2.5)
ci_upper_median = np.percentile(bootstrap_medians, 97.5)
print(f"95% CI for median: [{ci_lower_median:.2f}, {ci_upper_median:.2f}]")

# Bootstrap for correlation
print("\n" + "="*50)
print("BOOTSTRAP: CORRELATION")
print("="*50)

# Create paired data
np.random.seed(42)
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
y = 2 * x + np.random.normal(0, 2, 10)

original_corr = np.corrcoef(x, y)[0, 1]
print(f"Original correlation: {original_corr:.3f}")

bootstrap_corrs = []
for i in range(n_bootstrap):
indices = np.random.choice(len(x), len(x), replace=True)
x_boot = x[indices]
y_boot = y[indices]
corr_boot = np.corrcoef(x_boot, y_boot)[0, 1]
bootstrap_corrs.append(corr_boot)

bootstrap_corrs = np.array(bootstrap_corrs)

print(f"Bootstrap correlation mean: {bootstrap_corrs.mean():.3f}")
print(f"Bootstrap correlation SE: {bootstrap_corrs.std():.3f}")

ci_lower_corr = np.percentile(bootstrap_corrs, 2.5)
ci_upper_corr = np.percentile(bootstrap_corrs, 97.5)
print(f"95% CI for correlation: [{ci_lower_corr:.3f}, {ci_upper_corr:.3f}]")

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

# Bootstrap means
axes[0].hist(bootstrap_means, bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[0].axvline(original_data.mean(), color='red', linestyle='--', linewidth=2, label='Original Mean')
axes[0].axvline(ci_lower, color='green', linestyle='--', linewidth=2, label='95% CI')
axes[0].axvline(ci_upper, color='green', linestyle='--', linewidth=2)
axes[0].set_title('Bootstrap Distribution of Mean')
axes[0].set_xlabel('Sample Mean')
axes[0].set_ylabel('Frequency')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Bootstrap medians
axes[1].hist(bootstrap_medians, bins=50, alpha=0.7, color='green', edgecolor='black')
axes[1].axvline(np.median(original_data), color='red', linestyle='--', linewidth=2, label='Original Median')
axes[1].axvline(ci_lower_median, color='orange', linestyle='--', linewidth=2, label='95% CI')
axes[1].axvline(ci_upper_median, color='orange', linestyle='--', linewidth=2)
axes[1].set_title('Bootstrap Distribution of Median')
axes[1].set_xlabel('Sample Median')
axes[1].set_ylabel('Frequency')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# Bootstrap correlations
axes[2].hist(bootstrap_corrs, bins=50, alpha=0.7, color='purple', edgecolor='black')
axes[2].axvline(original_corr, color='red', linestyle='--', linewidth=2, label='Original Correlation')
axes[2].axvline(ci_lower_corr, color='orange', linestyle='--', linewidth=2, label='95% CI')
axes[2].axvline(ci_upper_corr, color='orange', linestyle='--', linewidth=2)
axes[2].set_title('Bootstrap Distribution of Correlation')
axes[2].set_xlabel('Sample Correlation')
axes[2].set_ylabel('Frequency')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

3.3 Bayesian Thinking and Prior Knowledge

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

print("="*50)
print("BAYESIAN THINKING")
print("="*50)

print("""
BAYES' THEOREM:
P(A|B) = P(B|A) × P(A) / P(B)

COMPONENTS:
- P(A|B) = Posterior (what we want to know)
- P(B|A) = Likelihood (probability of evidence given hypothesis)
- P(A) = Prior (what we believed before data)
- P(B) = Marginal likelihood (normalizing factor)

KEY INSIGHT:
Bayesian approach updates beliefs as we get new data
""")

# Example 1: Disease testing revisited
print("\nEXAMPLE 1: MEDICAL TESTING")
print("="*50)

# Prior: 1% of population has disease
prior_disease = 0.01
prior_no_disease = 1 - prior_disease

# Test accuracy
sensitivity = 0.95 # P(+|disease)
specificity = 0.90 # P(-|no disease)

# What we observe: positive test
# P(+|disease) × P(disease) = likelihood × prior
prob_positive_given_disease = sensitivity * prior_disease
prob_positive_given_no_disease = (1 - specificity) * prior_no_disease

# P(+) = total probability of positive
prob_positive_total = prob_positive_given_disease + prob_positive_given_no_disease

# Posterior: P(disease|+)
posterior_disease = prob_positive_given_disease / prob_positive_total

print(f"Prior P(disease) = {prior_disease:.3f}")
print(f"Sensitivity P(+|disease) = {sensitivity:.3f}")
print(f"Specificity P(-|no disease) = {specificity:.3f}")
print(f"False positive rate = {1-specificity:.3f}")
print(f"\nAfter positive test:")
print(f"Posterior P(disease|+) = {posterior_disease:.3f} or {posterior_disease*100:.1f}%")
print(f"\nInterpretation: Even with positive test, only {posterior_disease*100:.1f}% chance of disease!")

# Example 2: Coin fairness
print("\n" + "="*50)
print("EXAMPLE 2: IS COIN FAIR?")
print("="*50)

# Prior: assume coin is fair (50-50)
# Data: flip 10 times, get 8 heads

print(f"Prior belief: coin is fair (P(heads)=0.5)")
print(f"Data: 10 flips, 8 heads")

# Calculate likelihood for different values of p
p_values = np.linspace(0, 1, 100)
data = 8 # heads
trials = 10

# Likelihood (binomial)
likelihood = stats.binom.pmf(data, trials, p_values)

# Uniform prior
prior = np.ones_like(p_values)

# Posterior (proportional to likelihood × prior)
posterior = likelihood * prior
posterior = posterior / posterior.sum() # Normalize

# Plot
plt.figure(figsize=(12, 6))
plt.plot(p_values, likelihood, label='Likelihood (Data: 8H in 10 flips)', linewidth=2)
plt.plot(p_values, prior, label='Prior (uniform)', linewidth=2)
plt.plot(p_values, posterior, label='Posterior', linewidth=3)

plt.axvline(0.5, color='red', linestyle='--', label='Fair coin (p=0.5)')
plt.xlabel('Probability of Heads (p)')
plt.ylabel('Probability Density')
plt.title('Bayesian Inference: Is the Coin Fair?')
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Calculate posterior mean
posterior_mean = np.average(p_values, weights=posterior)
print(f"\nPosterior mean (best estimate): {posterior_mean:.3f}")
print(f"Our estimate shifted from 0.5 (prior) to {posterior_mean:.3f} (posterior)")

# Example 3: Updating beliefs
print("\n" + "="*50)
print("EXAMPLE 3: SEQUENTIAL LEARNING")
print("="*50)

print(f"Starting belief: coin is fair (p=0.5)")
print(f"\nSequence of events:")

p = 0.8 # True probability
prior_mean = 0.5

observations = [1, 1, 0, 1, 1, 1, 0, 1, 1, 1] # 1=heads, 0=tails

for i, obs in enumerate(observations, 1):
# Count heads so far
heads_count = sum(observations[:i])

# Update using Beta-Binomial model
# Beta distribution for prior on p
alpha = 1 + heads_count
beta = 1 + (i - heads_count)

# Posterior mean
posterior_mean_beta = alpha / (alpha + beta)

outcome = "H" if obs == 1 else "T"
print(f"Flip {i:2d}: {outcome} → Heads: {heads_count}, Posterior mean: {posterior_mean_beta:.3f}")

print(f"\nFinal posterior mean: {posterior_mean_beta:.3f}")
print(f"(True value was p={p})")

# Compare Frequentist vs Bayesian
print("\n" + "="*50)
print("FREQUENTIST vs BAYESIAN")
print("="*50)

print(f"""
FREQUENTIST:
- Probability is long-run frequency
- Parameter is fixed, unknown
- Uses only the data
- P-values and confidence intervals

BAYESIAN:
- Probability is subjective degree of belief
- Parameter has a distribution
- Combines data with prior knowledge
- Posterior distributions and credible intervals

WHEN TO USE:
- Frequentist: When you have lots of data, want standard tests
- Bayesian: When you want to incorporate prior knowledge, small samples
""")

Week 6 Summary

By completing Week 6, you have learned:

  • Probability fundamentals: sample space, events, basic rules
  • Addition rule: P(A or B)
  • Multiplication rule: P(A and B)
  • Conditional probability and Bayes' theorem
  • Discrete distributions: Bernoulli, Binomial, Poisson
  • Continuous distributions: Normal, Exponential, Uniform
  • Probability mass function (PMF) and probability density function (PDF)
  • Working with scipy.stats for distributions
  • Sampling distributions and standard error
  • Central Limit Theorem: why sampling distributions approach normal
  • Confidence intervals for means and proportions
  • Margin of error and confidence levels
  • Bootstrapping: resampling with replacement
  • Bootstrapping for complex statistics
  • Bayesian thinking: priors, likelihoods, posteriors
  • Comparing Frequentist vs Bayesian approaches

Week 6 Assignments

Assignment 1: Probability and Distributions

Work with probability distributions:

  • Calculate probabilities for various discrete distributions
  • Solve conditional probability and Bayes theorem problems
  • Plot and compare probability distributions
  • Demonstrate Central Limit Theorem with simulations
  • Calculate probabilities for real-world scenarios
  • Create visualizations showing different distributions

Assignment 2: Confidence Intervals and Bootstrapping

Perform interval estimation:

  • Calculate confidence intervals for different statistics
  • Interpret confidence levels correctly
  • Perform bootstrap resampling to estimate CIs
  • Compare bootstrap CIs with theoretical CIs
  • Explore effect of sample size on margin of error
  • Create visualizations of confidence intervals

Assignment 3: Comprehensive Statistical Analysis

Apply all concepts to real data:

  • Load a dataset and perform comprehensive statistical analysis
  • Calculate point estimates and confidence intervals
  • Use bootstrap for complex statistics
  • Demonstrate understanding of Central Limit Theorem
  • Interpret results in business context
  • Create professional report with visualizations
  • Compare results using both frequentist and Bayesian approaches
  • Solve probability problems using Bayes theorem
  • Calculate probabilities for various distributions
  • Demonstrate CLT with different sample sizes
  • Compare theoretical and simulated distributions
  • Interpret confidence intervals correctly
  • Perform bootstrapping on different statistics
  • Solve problems combining multiple probability distributions
  • Create simulations to understand statistical concepts
  • Compare CI methods (theoretical vs bootstrap)

Additional Resources

Books

  • Chapter 6: Probability and Statistics - "Python for Data Analysis" by Wes McKinney
  • Statistical Rethinking by Richard McElreath - Bayesian approach
  • Think Stats by Allen Downey - Free online

Online Documentation

  • scipy.stats documentation: https://docs.scipy.org/doc/scipy/reference/stats.html
  • NumPy random: https://numpy.org/doc/stable/reference/random/index.html
  • Bayesian Inference tutorial: https://en.wikipedia.org/wiki/Bayesian_inference
  • Central Limit Theorem visualizations