Lab 6 Solutions: Basic Hypothesis Testing & Simple Regression

PSTAT 5A - Summer Session A 2025

Author

Instructor: Narjes Mathlouthi

Published

July 29, 2025

โฑ๏ธ Total Lab Time: 50 minutes

Welcome to Lab 6 Solutions! This lab focuses on two fundamental areas of statistical analysis that youโ€™ll use throughout your data science journey: hypothesis testing and simple linear regression. These tools allow us to make data-driven decisions and understand relationships between variables.

What Youโ€™ll Learn Today

By the end of this lab, youโ€™ll be able to:

  • Conduct hypothesis tests to determine if sample data provides evidence against a claim
  • Model relationships between variables using simple linear regression
  • Make predictions based on data patterns
  • Interpret statistical results in plain English for real-world applications

Getting Started

โฑ๏ธ Estimated time: 5 minutes

Setup

Navigate to our class Jupyterhub Instance. Create a new notebook and rename it โ€œlab6โ€ (for detailed instructions view lab1).

First, letโ€™s load our tools! Copy the below code to get started! Weโ€™ll be using the following core libraries:

  • NumPy: Fundamental package for fast array-based numerical computing.
  • Matplotlib (pyplot): Primary library for creating static 2D plots and figures.
  • SciPy (stats): Collection of scientific algorithms, including probability distributions and statistical tests.
  • Pandas: High-performance data structures (DataFrame) and tools for data wrangling and analysis.
  • Statsmodels: Econometric and statistical modeling for regression analysis, time series, and more.
  • Seaborn:Seaborn is a Python data visualization library based on matplotlib. It provides a high-level interface for drawing attractive and informative statistical graphics.
# Install any missing packages (will skip those already installed)
#!%pip install --quiet numpy matplotlib scipy pandas statsmodels seaborn

# Load our tools (libraries)
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
import pandas as pd
import statsmodels.api as sm
import seaborn as sns

# Make our graphs look nice
plt.style.use('seaborn-v0_8-whitegrid')
sns.set_palette("husl")

# Set random seed for reproducible results
np.random.seed(42)

print("โœ… All tools loaded successfully!") 
โœ… All tools loaded successfully!

Task 1: One-Sample T-Test

โฑ๏ธ Estimated time: 20 minutes

What is a One-Sample T-Test?

A one-sample t-test helps us determine whether a sample mean is significantly different from a claimed or hypothesized population mean. Itโ€™s one of the most common statistical tests youโ€™ll encounter.

Real-world example: A coffee shop advertises that their espresso shots contain an average of 75mg of caffeine. As a health-conscious consumer (or maybe a caffeine researcher!), you want to test this claim. You collect a sample of espresso shots and measure their caffeine content.

The Question: Is the actual average caffeine content different from what the coffee shop claims?

Scenario

A coffee shop claims their average espresso shot contains 75 mg of caffeine. You suspect itโ€™s actually higher. You test 20 shots and want to test at \(\alpha = 0.05\) significance level.

Your Goal: Determine if thereโ€™s sufficient evidence that the actual caffeine content exceeds the coffee shopโ€™s claim.

Step 1: Explore the Data

# Generate caffeine data for our analysis
np.random.seed(123)
caffeine_data = np.random.normal(78, 8, 20)  # Sample data: n=20 espresso shots

print("โ˜• Coffee Shop Caffeine Analysis")
print("=" * 40)
print(f"๐Ÿ“Š Sample size: {len(caffeine_data)}")
print(f"๐Ÿ“ˆ Sample mean: {np.mean(caffeine_data):.2f} mg")
print(f"๐Ÿ“Š Sample std dev: {np.std(caffeine_data, ddof=1):.2f} mg")
print(f"๐Ÿช Coffee shop's claim: 75 mg")

# Let's look at our raw data
print(f"\n๐Ÿ” First 10 caffeine measurements:")
print([f"{x:.1f}" for x in caffeine_data[:10]])
โ˜• Coffee Shop Caffeine Analysis
========================================
๐Ÿ“Š Sample size: 20
๐Ÿ“ˆ Sample mean: 78.92 mg
๐Ÿ“Š Sample std dev: 10.06 mg
๐Ÿช Coffee shop's claim: 75 mg

๐Ÿ” First 10 caffeine measurements:
['69.3', '86.0', '80.3', '65.9', '73.4', '91.2', '58.6', '74.6', '88.1', '71.1']

Step 2: Set Up Your Hypotheses

Think about this carefully: - What does the coffee shop claim? (This becomes your null hypothesis) - What do you suspect? (This becomes your alternative hypothesis) - Are you testing if the caffeine content is different, higher, or lower?

print("๐Ÿ“ STEP 1: Setting Up Hypotheses")
print("=" * 35)

# SOLUTION: Complete these hypotheses
print("$H_0$ (Null Hypothesis): $\\mu$ = 75 mg")        # Coffee shop's claim
print("$H_1$ (Alternative Hypothesis): $\\mu$ > 75 mg")  # We suspect it's higher

# SOLUTION: What type of test is this?
print("Test type: RIGHT-tailed test")   # Testing if mean is greater than 75

print("\n๐Ÿ’ก Explanation:")
print("โ€ข $H_0$ represents the coffee shop's claim (status quo)")
print("โ€ข $H_1$ represents what we suspect is actually true")
print("โ€ข We use $\\alpha$ = 0.05 as our significance level")
๐Ÿ“ STEP 1: Setting Up Hypotheses
===================================
$H_0$ (Null Hypothesis): $\mu$ = 75 mg
$H_1$ (Alternative Hypothesis): $\mu$ > 75 mg
Test type: RIGHT-tailed test

๐Ÿ’ก Explanation:
โ€ข $H_0$ represents the coffee shop's claim (status quo)
โ€ข $H_1$ represents what we suspect is actually true
โ€ข We use $\alpha$ = 0.05 as our significance level

โœ… Answer Key: - \(H_0\): \(\mu\) = 75 mg (coffee shopโ€™s claim) - \(H_1\): \(\mu\) > 75 mg (we suspect itโ€™s higher) - Right-tailed test (testing if mean is greater than 75)

Step 3: Calculate the Test Statistic

The t-statistic formula is: \(t = \frac{\bar{x} - \mu_0}{s / \sqrt{n}}\)

print("๐Ÿ”ข STEP 2: Calculating Test Statistic")
print("=" * 38)

# Calculate the components
sample_mean = np.mean(caffeine_data)
sample_std = np.std(caffeine_data, ddof=1)  # ddof=1 for sample std dev
n = len(caffeine_data)
claimed_mean = 75

print(f"Sample mean ($\\bar{{x}}$): {sample_mean:.3f} mg")
print(f"Sample std dev (s): {sample_std:.3f} mg")
print(f"Sample size (n): {n}")
print(f"Claimed mean ($\\mu_0$): {claimed_mean} mg")

# SOLUTION: Calculate the t-statistic using the formula above
t_statistic = (sample_mean - claimed_mean) / (sample_std / np.sqrt(n))

degrees_freedom = n - 1

print(f"\n๐Ÿ“ Formula: $t = \\frac{{\\bar{{x}} - \\mu_0}}{{s / \\sqrt{{n}}}}$")
print(f"๐Ÿ“ Calculation: t = ({sample_mean:.3f} - {claimed_mean}) / ({sample_std:.3f} / โˆš{n})")
print(f"๐Ÿ“Š t-statistic: {t_statistic:.3f}")
print(f"๐Ÿ“Š Degrees of freedom: {degrees_freedom}")
๐Ÿ”ข STEP 2: Calculating Test Statistic
======================================
Sample mean ($\bar{x}$): 78.915 mg
Sample std dev (s): 10.060 mg
Sample size (n): 20
Claimed mean ($\mu_0$): 75 mg

๐Ÿ“ Formula: $t = \frac{\bar{x} - \mu_0}{s / \sqrt{n}}$
๐Ÿ“ Calculation: t = (78.915 - 75) / (10.060 / โˆš20)
๐Ÿ“Š t-statistic: 1.741
๐Ÿ“Š Degrees of freedom: 19

Step 4: Find the P-Value

For a right-tailed test, the p-value is the probability of getting a t-statistic as extreme or more extreme than what we observed.

What exactly is a pโ€‘value?

Loosely speaking, the pโ€‘value answers the question:

โ€œIf the null hypothesis were true, how surprising would my sample be?โ€

Formally, it is the probability, calculated under the assumption that the null hypothesis is correct; of obtaining a test statistic as extreme or more extreme than the one observed.

  • Small pโ€‘value (e.g., < 0.05) โ†’ data are rare under \(H_0\) โ†’ strong evidence against \(H_0\).
  • Large pโ€‘value โ†’ data are plausible under \(H_0\) โ†’ little or no evidence against \(H_0\).

Important: A pโ€‘value does not give the probability that the null hypothesis is true; it quantifies how incompatible your data are with \(H_0\).

print("๐Ÿ“ˆ STEP 3: Finding the P-Value")
print("=" * 32)

# SOLUTION: Calculate p-value for right-tailed test
# For right-tailed test, p-value = 1 - stats.t.cdf(t_statistic, df)
p_value = 1 - stats.t.cdf(t_statistic, degrees_freedom)

print(f"๐ŸŽฏ P-value calculation:")
print(f"   P(t > {t_statistic:.3f}) = {p_value:.4f}")
print(f"\n๐Ÿ’ญ Interpretation:")
print(f"   If the coffee shop's claim is true ($\\mu$ = 75),")
print(f"   there's a {p_value:.1%} chance of getting a sample")
print(f"   mean as high or higher than {sample_mean:.2f} mg")
๐Ÿ“ˆ STEP 3: Finding the P-Value
================================
๐ŸŽฏ P-value calculation:
   P(t > 1.741) = 0.0490

๐Ÿ’ญ Interpretation:
   If the coffee shop's claim is true ($\mu$ = 75),
   there's a 4.9% chance of getting a sample
   mean as high or higher than 78.92 mg

Step 5: Make Your Decision

Compare your p-value to \(\alpha = 0.05\) and make a statistical decision.

print("โš–๏ธ STEP 4: Making the Decision")
print("=" * 31)

alpha = 0.05
print(f"๐ŸŽฏ Significance level ($\\alpha$): {alpha}")
print(f"๐Ÿ“Š P-value: {p_value:.4f}")
print(f"๐Ÿ“‹ Decision rule: Reject $H_0$ if p-value < $\\alpha$")

print(f"\n๐Ÿ” Comparison:")
if p_value < alpha:
    print(f"   {p_value:.4f} < {alpha} โœ…")
    print(f"   Decision: **REJECT $H_0$**")
    print(f"   Conclusion: There IS sufficient evidence that")
    print(f"             the average caffeine content > 75 mg")
    print(f"   ๐Ÿช The coffee shop's claim appears to be FALSE")
else:
    print(f"   {p_value:.4f} โ‰ฅ {alpha} โŒ")
    print(f"   Decision: **FAIL TO REJECT $H_0$**")
    print(f"   Conclusion: There is NOT sufficient evidence that")
    print(f"             the average caffeine content > 75 mg")
    print(f"   ๐Ÿช We cannot conclude the coffee shop's claim is false")

# SOLUTION: Write conclusion in plain English
print(f"\n๐Ÿ“ Conclusion in plain English:")
print(f"   Based on our sample of 20 espresso shots, we found")
print(f"   strong statistical evidence that the coffee shop's")
print(f"   claim of 75mg caffeine is too low. The actual average")
print(f"   appears to be significantly higher than advertised.")
โš–๏ธ STEP 4: Making the Decision
===============================
๐ŸŽฏ Significance level ($\alpha$): 0.05
๐Ÿ“Š P-value: 0.0490
๐Ÿ“‹ Decision rule: Reject $H_0$ if p-value < $\alpha$

๐Ÿ” Comparison:
   0.0490 < 0.05 โœ…
   Decision: **REJECT $H_0$**
   Conclusion: There IS sufficient evidence that
             the average caffeine content > 75 mg
   ๐Ÿช The coffee shop's claim appears to be FALSE

๐Ÿ“ Conclusion in plain English:
   Based on our sample of 20 espresso shots, we found
   strong statistical evidence that the coffee shop's
   claim of 75mg caffeine is too low. The actual average
   appears to be significantly higher than advertised.

Step 6: Verify with Python

Letโ€™s double-check our work using Pythonโ€™s built-in statistical functions.

print("โœ… VERIFICATION using scipy.stats")
print("=" * 35)

# Use scipy's one-sample t-test function
t_stat_scipy, p_val_scipy = stats.ttest_1samp(caffeine_data, 75, alternative='greater')

print(f"๐Ÿ“Š Your calculations:")
print(f"   t-statistic: {t_statistic:.3f}")
print(f"   p-value: {p_value:.4f}")

print(f"\n๐Ÿ Python's calculations:")
print(f"   t-statistic: {t_stat_scipy:.3f}")
print(f"   p-value: {p_val_scipy:.4f}")

print(f"\n๐ŸŽฏ Match? {abs(t_statistic - t_stat_scipy) < 0.001 and abs(p_value - p_val_scipy) < 0.001}")
โœ… VERIFICATION using scipy.stats
===================================
๐Ÿ“Š Your calculations:
   t-statistic: 1.741
   p-value: 0.0490

๐Ÿ Python's calculations:
   t-statistic: 1.741
   p-value: 0.0490

๐ŸŽฏ Match? True

Step 7: Visualize Your Results

# Create visualizations to understand our test
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# Plot 1: Sample data histogram with means
ax1.hist(caffeine_data, bins=8, density=True, alpha=0.7, color='lightblue', 
         edgecolor='black', label='Sample Data')
ax1.axvline(sample_mean, color='red', linestyle='-', linewidth=3, 
           label=f'Sample Mean = {sample_mean:.1f}mg')
ax1.axvline(claimed_mean, color='orange', linestyle='--', linewidth=3, 
           label=f'Claimed Mean = {claimed_mean}mg')
ax1.set_xlabel('Caffeine Content (mg)', fontsize=12)
ax1.set_ylabel('Density', fontsize=12)
ax1.set_title('โ˜• Sample vs Claimed Caffeine Content', fontsize=14, fontweight='bold')
ax1.legend(fontsize=11)
ax1.grid(True, alpha=0.3)

# Plot 2: t-distribution with test statistic and p-value
x = np.linspace(-4, 4, 1000)
y = stats.t.pdf(x, degrees_freedom)
ax2.plot(x, y, 'b-', linewidth=2, label=f't-distribution (df={degrees_freedom})')
ax2.fill_between(x, y, alpha=0.3, color='lightblue')

# Shade the rejection region (right tail)
x_reject = x[x >= t_statistic]
y_reject = stats.t.pdf(x_reject, degrees_freedom)
ax2.fill_between(x_reject, y_reject, alpha=0.7, color='red', 
                label=f'p-value = {p_value:.4f}')

ax2.axvline(t_statistic, color='red', linestyle='-', linewidth=3, 
           label=f'Our t-statistic = {t_statistic:.3f}')
ax2.set_xlabel('t-value', fontsize=12)
ax2.set_ylabel('Density', fontsize=12)
ax2.set_title('๐Ÿ“Š T-Distribution with Test Statistic', fontsize=14, fontweight='bold')
ax2.legend(fontsize=11)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

๐Ÿค” Reflection Questions - SOLUTIONS

Answer these questions to check your understanding:

  1. Hypotheses: What were your null and alternative hypotheses? Why did you choose a right-tailed test?
    • Answer: \(H_0\): \(\mu\) = 75 mg, \(H_1\): \(\mu\) > 75 mg. We chose a right-tailed test because we specifically suspected the caffeine content was higher than claimed, not just different.
  2. Test Choice: Why did you use a t-test instead of a z-test for this problem?
    • Answer: We used a t-test because: (1) small sample size (n=20 < 30), (2) population standard deviation unknown, (3) assuming approximately normal distribution.
  3. Results: What was your t-statistic and p-value? What do these numbers mean?
    • Answer: t โ‰ˆ 1.84, p โ‰ˆ 0.041. The t-statistic tells us how many standard errors our sample mean is above the claimed mean. The p-value tells us thereโ€™s only a 4.1% chance of seeing this result if the true mean were 75mg.
  4. Decision: What was your final conclusion at \(\alpha = 0.05\)? Do you reject or fail to reject the null hypothesis?
    • Answer: We REJECT \(H_0\) because p-value (0.041) < ฮฑ (0.05). Thereโ€™s sufficient evidence that the actual caffeine content exceeds 75mg.
  5. Real-World Impact: If you were advising the coffee shop, what would you tell them based on your analysis?
    • Answer: โ€œYour espresso shots appear to contain significantly more caffeine than advertised. You should either update your labeling to reflect the actual content or adjust your brewing process to match your claim.โ€

Task 2: Simple Linear Regression

โฑ๏ธ Estimated time: 25 minutes

What is Simple Linear Regression?

Simple linear regression helps us understand and model the relationship between two continuous variables. Unlike hypothesis testing (which answers yes/no questions), regression helps us predict outcomes and quantify relationships.

Real-world example: As a student, youโ€™ve probably wondered: โ€œIf I study more hours, how much will my exam score improve?โ€ Linear regression can help answer this question by finding the relationship between study time and exam performance.

The Question: Can we predict exam scores based on hours studied? And if so, how much does each additional hour of studying improve your expected score?

  1. Explore & visualize the data
  2. Measure correlation (r) and \(R^2\)
  3. Fit the regression line \(\hat{y} = \beta_0 + \beta_1 x\)
  4. Test if the slope is significant
  5. Predict new values & quantify error
  6. Check model assumptions
  7. Visualize diagnostics
  8. Write a plainโ€‘English conclusion
  • Correlation: How strongly two variables move together (-1 to +1)
  • Slope: How much \(Y\) changes when X increases by \(1\) unit
  • Intercept: The predicted value of \(Y\) when \(X = 0\)
  • \(R^2\): What percentage of the variation in \(Y\) is explained by \(X\)
Important

Remember: Correlation does not imply causation! Just because two variables are related doesnโ€™t mean one causes the other.

Scenario

You want to investigate the relationship between study hours and exam performance. You collect data from \(50\) students about their weekly study hours and corresponding exam scores.

Your Goal: Create a statistical model to predict exam scores based on study hours and determine how much each additional hour of studying helps.

Step 1: Explore the Data

# Generate realistic study data
np.random.seed(101)
n_students = 50

# Study hours (predictor variable X)
study_hours = np.random.uniform(1, 20, n_students)

# Exam scores with linear relationship plus noise
# True relationship: score = 65 + 2*hours + noise
true_intercept = 65
true_slope = 2
noise = np.random.normal(0, 8, n_students)
exam_scores = true_intercept + true_slope * study_hours + noise

# Create DataFrame for easier handling
study_data = pd.DataFrame({
    'hours_studied': study_hours,
    'exam_score': exam_scores
})

print("๐Ÿ“š Study Hours vs Exam Scores Analysis")
print("=" * 45)
print(f"๐Ÿ‘ฅ Sample size: {len(study_data)} students")
print(f"โฐ Study hours range: {study_hours.min():.1f} to {study_hours.max():.1f} hours")
print(f"๐Ÿ“ Exam scores range: {exam_scores.min():.1f} to {exam_scores.max():.1f} points")

print(f"\n๐Ÿ” First 10 students:")
print(study_data.head(10).round(2))
๐Ÿ“š Study Hours vs Exam Scores Analysis
=============================================
๐Ÿ‘ฅ Sample size: 50 students
โฐ Study hours range: 1.5 to 19.9 hours
๐Ÿ“ Exam scores range: 60.1 to 111.6 points

๐Ÿ” First 10 students:
   hours_studied  exam_score
0          10.81       88.53
1          11.84      104.66
2           1.54       60.14
3           4.26       75.09
4          14.02       83.95
5          16.84       98.69
6           6.83       86.87
7          17.98       99.70
8          14.71       94.17
9           4.61       79.42

๐Ÿค” Quick Questions:

  • Do you see any obvious pattern in the data?
    • Answer: Yes! As study hours increase, exam scores tend to increase too.
  • Which variable is the predictor (X) and which is the response (Y)?
    • Answer: Study hours is the predictor (X), exam scores is the response (Y).

Step 2: Calculate and Interpret Correlation

Correlation measures how strongly two variables move together.

print("๐Ÿ“Š STEP 1: Measuring the Relationship")
print("=" * 40)

# SOLUTION: Calculate the correlation coefficient
correlation = np.corrcoef(study_hours, exam_scores)[0, 1]

print(f"๐Ÿ”— Correlation coefficient: r = {correlation:.3f}")

# SOLUTION: Interpret the correlation strength
print(f"\n๐Ÿ’ญ Interpretation:")
if abs(correlation) < 0.3:
    strength = "weak"
elif abs(correlation) < 0.7:
    strength = "moderate"
else:
    strength = "strong"

direction = "positive" if correlation > 0 else "negative"
print(f"   This indicates a {strength} {direction} relationship")
print(f"   between study hours and exam scores.")

print(f"\n๐Ÿ“‹ What this means:")
print(f"   โ€ข r = {correlation:.3f} means the variables are strongly related")
print(f"   โ€ข As study hours increase, exam scores tend to increase")
print(f"   โ€ข About {correlation**2:.1%} of the variation in scores")
print(f"     can be explained by study hours alone")
๐Ÿ“Š STEP 1: Measuring the Relationship
========================================
๐Ÿ”— Correlation coefficient: r = 0.753

๐Ÿ’ญ Interpretation:
   This indicates a strong positive relationship
   between study hours and exam scores.

๐Ÿ“‹ What this means:
   โ€ข r = 0.753 means the variables are strongly related
   โ€ข As study hours increase, exam scores tend to increase
   โ€ข About 56.8% of the variation in scores
     can be explained by study hours alone

โœ… Check Your Understanding:

  • What does r = 0.8 vs r = 0.3 tell you?
    • Answer: r = 0.8 indicates a strong relationship (variables move together closely), while r = 0.3 indicates a weak relationship (more scattered, less predictable).
  • If r = -0.9, what would that mean?
    • Answer: Very strong negative relationship - as one variable increases, the other decreases in a highly predictable way.

Step 3: Fit the Linear Regression Model

Now weโ€™ll find the โ€œline of best fitโ€ through our data points.

print("๐Ÿ“ STEP 2: Fitting the Regression Line")
print("=" * 42)

# Set up the regression (add constant for intercept)
X = sm.add_constant(study_hours)  # Add intercept term

# SOLUTION: Fit the OLS (Ordinary Least Squares) model
model = sm.OLS(exam_scores, X).fit()

print(f"๐ŸŽฏ Regression Equation:")
print(f"   Exam Score = $\\beta_0$ + $\\beta_1$ ร— Hours Studied")
print(f"   Exam Score = {model.params[0]:.2f} + {model.params[1]:.2f} ร— Hours")

print(f"\n๐Ÿ“Š Model Coefficients:")
print(f"   Intercept ($\\beta_0$): {model.params[0]:.3f}")
print(f"   Slope ($\\beta_1$): {model.params[1]:.3f}")
print(f"   R-squared ($R^2$): {model.rsquared:.3f}")

# SOLUTION: Complete these interpretations
print(f"\n๐Ÿ’ก What These Numbers Mean:")
print(f"   ๐Ÿ“Œ Intercept ({model.params[0]:.1f}): Expected score with 0 hours of study")
print(f"   ๐Ÿ“ˆ Slope ({model.params[1]:.2f}): Each additional hour increases score by {model.params[1]:.2f} points")
print(f"   ๐Ÿ“Š $R^2$ ({model.rsquared:.3f}): Study hours explain {model.rsquared:.1%} of score variation")
๐Ÿ“ STEP 2: Fitting the Regression Line
==========================================
๐ŸŽฏ Regression Equation:
   Exam Score = $\beta_0$ + $\beta_1$ ร— Hours Studied
   Exam Score = 69.94 + 1.67 ร— Hours

๐Ÿ“Š Model Coefficients:
   Intercept ($\beta_0$): 69.936
   Slope ($\beta_1$): 1.670
   R-squared ($R^2$): 0.568

๐Ÿ’ก What These Numbers Mean:
   ๐Ÿ“Œ Intercept (69.9): Expected score with 0 hours of study
   ๐Ÿ“ˆ Slope (1.67): Each additional hour increases score by 1.67 points
   ๐Ÿ“Š $R^2$ (0.568): Study hours explain 56.8% of score variation

Step 4: Test Statistical Significance

Is the relationship we found statistically significant, or could it be due to chance?

print("๐Ÿ”ฌ STEP 3: Testing Statistical Significance")
print("=" * 46)

# Check if the slope is significantly different from zero
slope_pvalue = model.pvalues[1]  # p-value for the slope
alpha = 0.05

print(f"๐Ÿงช Hypothesis Test for Slope:")
print(f"   $H_0$: $\\beta_1$ = 0 (no relationship)")
print(f"   $H_1$: $\\beta_1$ โ‰  0 (there is a relationship)")
print(f"   $\\alpha$ = {alpha}")

print(f"\n๐Ÿ“Š Test Results:")
print(f"   Slope p-value: {slope_pvalue:.6f}")

# SOLUTION: Make the decision
if slope_pvalue < alpha:
    print(f"   Decision: REJECT $H_0$")
    print(f"   Conclusion: The relationship IS statistically significant")
    significance = "IS"
else:
    print(f"   Decision: FAIL TO REJECT $H_0$")
    print(f"   Conclusion: The relationship is NOT statistically significant")
    significance = "IS NOT"

print(f"\nโœ… Bottom Line:")
print(f"   Study hours {significance} a significant predictor of exam scores")

# Show confidence intervals
conf_int = model.conf_int(alpha=0.05)
print(f"\n๐Ÿ“ 95% Confidence Intervals:")
print(f"   Intercept: [{conf_int[0,0]:.2f}, {conf_int[0,1]:.2f}]")
print(f"   Slope: [{conf_int[1,0]:.2f}, {conf_int[1,1]:.2f}]")
๐Ÿ”ฌ STEP 3: Testing Statistical Significance
==============================================
๐Ÿงช Hypothesis Test for Slope:
   $H_0$: $\beta_1$ = 0 (no relationship)
   $H_1$: $\beta_1$ โ‰  0 (there is a relationship)
   $\alpha$ = 0.05

๐Ÿ“Š Test Results:
   Slope p-value: 0.000000
   Decision: REJECT $H_0$
   Conclusion: The relationship IS statistically significant

โœ… Bottom Line:
   Study hours IS a significant predictor of exam scores

๐Ÿ“ 95% Confidence Intervals:
   Intercept: [64.81, 75.06]
   Slope: [1.25, 2.09]

Step 5: Make Predictions

Now letโ€™s use our model to predict exam scores for different study scenarios.

print("๐Ÿ”ฎ STEP 4: Making Predictions")
print("=" * 32)

# SOLUTION: Calculate predictions for different study hours
example_hours = [5, 10, 15, 20]

print(f"๐ŸŽฏ Prediction Examples:")
for hours in example_hours:
    # SOLUTION: Calculate predicted score
    pred_score = model.params[0] + model.params[1] * hours
    print(f"   ๐Ÿ“š {hours:2d} hours โ†’ Predicted score: {pred_score:.1f} points")

print(f"\n๐Ÿค” Your Turn:")
# SOLUTION: Pick your own study hours and make a prediction
your_hours = 12  # Enter a number between 1-20
your_prediction = model.params[0] + model.params[1] * your_hours
print(f"   ๐Ÿ“š {your_hours} hours โ†’ Predicted score: {your_prediction:.1f} points")

# Calculate residuals for analysis
y_predicted = model.predict(X)
residuals = exam_scores - y_predicted
residual_std = np.std(residuals, ddof=2)

print(f"\n๐Ÿ“Š Prediction Accuracy:")
print(f"   Average prediction error: ยฑ{residual_std:.1f} points")
print(f"   This means most predictions are within ยฑ{residual_std:.1f} points of actual scores")
๐Ÿ”ฎ STEP 4: Making Predictions
================================
๐ŸŽฏ Prediction Examples:
   ๐Ÿ“š  5 hours โ†’ Predicted score: 78.3 points
   ๐Ÿ“š 10 hours โ†’ Predicted score: 86.6 points
   ๐Ÿ“š 15 hours โ†’ Predicted score: 95.0 points
   ๐Ÿ“š 20 hours โ†’ Predicted score: 103.3 points

๐Ÿค” Your Turn:
   ๐Ÿ“š 12 hours โ†’ Predicted score: 90.0 points

๐Ÿ“Š Prediction Accuracy:
   Average prediction error: ยฑ8.2 points
   This means most predictions are within ยฑ8.2 points of actual scores

Step 6: Check Model Assumptions

Before trusting our model, we need to verify it meets the assumptions of linear regression.

print("โœ… STEP 5: Checking Model Assumptions")
print("=" * 42)

print("๐Ÿ” Linear Regression Assumptions:")
print("   1๏ธโƒฃ Linear relationship between X and Y")
print("   2๏ธโƒฃ Residuals are normally distributed") 
print("   3๏ธโƒฃ Residuals have constant variance (homoscedasticity)")
print("   4๏ธโƒฃ Residuals are independent")

# Calculate residuals
y_predicted = model.predict(X)
residuals = exam_scores - y_predicted

print(f"\n๐Ÿ“Š Residual Analysis:")
print(f"   Mean residual: {np.mean(residuals):.6f} (should be โ‰ˆ 0)")
print(f"   Std of residuals: {np.std(residuals, ddof=2):.3f}")

# SOLUTION: Check normality of residuals using Shapiro-Wilk test
from scipy.stats import shapiro
shapiro_stat, shapiro_p = shapiro(residuals)
print(f"\n๐Ÿงช Normality Test (Shapiro-Wilk):")
print(f"   p-value: {shapiro_p:.4f}")
if shapiro_p > 0.05:
    print("   โœ… Residuals appear normally distributed")
else:
    print("   โš ๏ธ Residuals may not be normally distributed")
โœ… STEP 5: Checking Model Assumptions
==========================================
๐Ÿ” Linear Regression Assumptions:
   1๏ธโƒฃ Linear relationship between X and Y
   2๏ธโƒฃ Residuals are normally distributed
   3๏ธโƒฃ Residuals have constant variance (homoscedasticity)
   4๏ธโƒฃ Residuals are independent

๐Ÿ“Š Residual Analysis:
   Mean residual: -0.000000 (should be โ‰ˆ 0)
   Std of residuals: 8.167

๐Ÿงช Normality Test (Shapiro-Wilk):
   p-value: 0.4928
   โœ… Residuals appear normally distributed

Step 7: Visualize Your Results

# Create comprehensive visualization
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))

# Plot 1: Scatter plot with regression line
ax1.scatter(study_hours, exam_scores, alpha=0.6, color='blue', s=60, 
           label='Student Data')
sorted_hours = np.sort(study_hours)
sorted_predictions = model.params[0] + model.params[1] * sorted_hours
ax1.plot(sorted_hours, sorted_predictions, color='red', linewidth=3, 
         label=f'y = {model.params[0]:.1f} + {model.params[1]:.2f}x')

ax1.set_xlabel('Study Hours', fontsize=12)
ax1.set_ylabel('Exam Score', fontsize=12)
ax1.set_title(f'๐Ÿ“š Study Hours vs Exam Scores\n$R^2$ = {model.rsquared:.3f}', 
              fontsize=14, fontweight='bold')
ax1.legend(fontsize=11)
ax1.grid(True, alpha=0.3)

# Plot 2: Residuals vs Fitted values
ax2.scatter(y_predicted, residuals, alpha=0.6, color='purple', s=50)
ax2.axhline(y=0, color='red', linestyle='--', linewidth=2)
ax2.set_xlabel('Fitted Values', fontsize=12)
ax2.set_ylabel('Residuals', fontsize=12)
ax2.set_title('๐Ÿ” Residuals vs Fitted\n(Should show no pattern)', 
              fontsize=14, fontweight='bold')
ax2.grid(True, alpha=0.3)

# Plot 3: Q-Q plot for normality of residuals
stats.probplot(residuals, dist="norm", plot=ax3)
ax3.set_title('๐Ÿ“Š Q-Q Plot of Residuals\n(Should be roughly linear)', 
              fontsize=14, fontweight='bold')
ax3.grid(True, alpha=0.3)

# Plot 4: Histogram of residuals
ax4.hist(residuals, bins=12, density=True, alpha=0.7, color='lightgreen', 
         edgecolor='black')
ax4.set_xlabel('Residuals', fontsize=12)
ax4.set_ylabel('Density', fontsize=12)
ax4.set_title('๐Ÿ“ˆ Distribution of Residuals\n(Should look normal)', 
              fontsize=14, fontweight='bold')
ax4.grid(True, alpha=0.3)

# Overlay normal curve
x_norm = np.linspace(residuals.min(), residuals.max(), 100)
y_norm = stats.norm.pdf(x_norm, np.mean(residuals), np.std(residuals))
ax4.plot(x_norm, y_norm, 'r-', linewidth=2, label='Normal curve')
ax4.legend()

plt.tight_layout()
plt.show()

Step 8: Interpret Your Model

print("๐Ÿ“‹ FINAL INTERPRETATION")
print("=" * 25)

print(f"๐ŸŽฏ Our Model: Exam Score = {model.params[0]:.1f} + {model.params[1]:.2f} ร— Study Hours")
print(f"\nโœ… Key Findings:")
print(f"   ๐Ÿ“ˆ Strong positive relationship (r = {correlation:.3f})")
print(f"   ๐Ÿ“Š Study hours explain {model.rsquared:.1%} of score variation")
print(f"   ๐ŸŽฏ Each extra hour โ†’ {model.params[1]:.1f} point increase")
print(f"   ๐Ÿ”ฌ Relationship is statistically significant (p < 0.001)")

print(f"\n๐Ÿ’ก Practical Insights:")
print(f"   ๐Ÿƒโ€โ™‚๏ธ Going from 5 to 10 hours of study:")
pred_5 = model.params[0] + model.params[1] * 5
pred_10 = model.params[0] + model.params[1] * 10
improvement = pred_10 - pred_5
print(f"      Expected score improvement: {improvement:.1f} points")

print(f"\nโš ๏ธ Important Limitations:")
print(f"   โ€ข Correlation โ‰  Causation")
print(f"   โ€ข Model only explains {model.rsquared:.1%} of variation")
print(f"   โ€ข Other factors matter too (sleep, prior knowledge, etc.)")
print(f"   โ€ข Predictions have uncertainty: ยฑ{residual_std:.1f} points")

# Show full model summary
print(f"\n๐Ÿ“Š Full Statistical Summary:")
print("=" * 30)
print(model.summary())
๐Ÿ“‹ FINAL INTERPRETATION
=========================
๐ŸŽฏ Our Model: Exam Score = 69.9 + 1.67 ร— Study Hours

โœ… Key Findings:
   ๐Ÿ“ˆ Strong positive relationship (r = 0.753)
   ๐Ÿ“Š Study hours explain 56.8% of score variation
   ๐ŸŽฏ Each extra hour โ†’ 1.7 point increase
   ๐Ÿ”ฌ Relationship is statistically significant (p < 0.001)

๐Ÿ’ก Practical Insights:
   ๐Ÿƒโ€โ™‚๏ธ Going from 5 to 10 hours of study:
      Expected score improvement: 8.4 points

โš ๏ธ Important Limitations:
   โ€ข Correlation โ‰  Causation
   โ€ข Model only explains 56.8% of variation
   โ€ข Other factors matter too (sleep, prior knowledge, etc.)
   โ€ข Predictions have uncertainty: ยฑ8.2 points

๐Ÿ“Š Full Statistical Summary:
==============================
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      y   R-squared:                       0.568
Model:                            OLS   Adj. R-squared:                  0.559
Method:                 Least Squares   F-statistic:                     63.01
Date:                Tue, 29 Jul 2025   Prob (F-statistic):           2.73e-10
Time:                        20:58:50   Log-Likelihood:                -174.93
No. Observations:                  50   AIC:                             353.9
Df Residuals:                      48   BIC:                             357.7
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         69.9358      2.551     27.415      0.000      64.807      75.065
x1             1.6702      0.210      7.938      0.000       1.247       2.093
==============================================================================
Omnibus:                        2.245   Durbin-Watson:                   2.594
Prob(Omnibus):                  0.325   Jarque-Bera (JB):                1.440
Skew:                           0.139   Prob(JB):                        0.487
Kurtosis:                       2.217   Cond. No.                         26.9
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

๐Ÿค” Reflection Questions - SOLUTIONS

Test your understanding by answering these questions:

  1. Correlation vs Causation:
    • What was your correlation coefficient?
      • Answer: r โ‰ˆ 0.89 (strong positive correlation)
    • Does this prove that studying more causes higher exam scores? Why or why not?
      • Answer: No! Correlation โ‰  causation. While thereโ€™s a strong relationship, other factors could explain both variables (intelligence, motivation, time management skills) or the relationship could be reverse (students who are doing well might be motivated to study more).
  2. Model Interpretation:
    • What does the slope coefficient mean in practical terms?
      • Answer: Each additional hour of study is associated with about 2.1 point increase in exam score on average.
    • What does the intercept represent, and does it make sense?
      • Answer: The intercept (~65) represents the predicted exam score for 0 hours of study. This might not be realistic (students likely have some baseline knowledge), but itโ€™s a mathematical extrapolation.
  3. Prediction Quality:
    • What percentage of exam score variation is explained by study hours?
      • Answer: About 79% (Rยฒ โ‰ˆ 0.79)
    • How accurate are your predictions (whatโ€™s the typical error)?
      • Answer: Typical prediction error is about ยฑ8 points.
  4. Statistical Significance:
    • Is the relationship statistically significant?
      • Answer: Yes, the p-value for the slope is much less than 0.05.
    • What would it mean if the p-value for the slope was 0.20?
      • Answer: We would fail to reject Hโ‚€ and conclude thereโ€™s insufficient evidence of a relationship between study hours and exam scores.
  5. Assumptions:
    • Based on your diagnostic plots, are the regression assumptions satisfied?
      • Answer: Generally yes - residuals appear roughly normal and randomly scattered around zero with fairly constant variance.
    • What would you do if the assumptions were violated?
      • Answer: Consider data transformations, use different modeling approaches, or collect more data.
  6. Practical Application:
    • If you were advising a student, what would you tell them based on this analysis?
      • Answer: โ€œStudy time appears to have a strong positive relationship with exam performance. Each extra hour might improve your score by about 2 points on average. However, remember that other factors also matter, and everyone is different.โ€
    • What other variables might improve your prediction model?
      • Answer: Sleep quality, prior GPA, attendance, quality of study methods, stress levels, nutrition, etc.

๐ŸŽฏ Lab Summary

Congratulations! Youโ€™ve successfully completed Lab 6 and learned fundamental statistical analysis techniques:

What You Accomplished

โœ… One-Sample T-Test: Tested a coffee shopโ€™s caffeine claims using hypothesis testing

โœ… Simple Linear Regression: Modeled the relationship between study hours and exam performance

โœ… Statistical Interpretation: Translated statistical results into practical insights

โœ… Critical Thinking: Distinguished between correlation and causation

Key Skills Developed

  • Setting up and testing hypotheses
  • Calculating and interpreting p-values
  • Fitting regression models and making predictions
  • Checking model assumptions with diagnostic plots
  • Communicating statistical findings clearly