Correlation & Regression Analysis
The Pearson Correlation Coefficient
The Pearson correlation coefficient, written r, measures the strength and direction of the linear relationship between two numeric variables.
import numpy as np import pandas as pd from scipy import stats # Hours studied vs. exam score, for 10 students hours_studied = np.array([1, 2, 2, 3, 4, 4, 5, 6, 7, 8]) exam_score = np.array([52, 58, 55, 64, 68, 70, 75, 80, 85, 92]) # NumPy: full correlation MATRIX (r between every pair of variables) corr_matrix = np.corrcoef(hours_studied, exam_score) print("Correlation matrix:\n", corr_matrix) print("r (hours vs score):", corr_matrix[0, 1].round(3)) # scipy.stats: r AND a p-value testing whether r is significantly different from 0 r, p_value = stats.pearsonr(hours_studied, exam_score) print(f"r = {r:.3f}, p-value = {p_value:.5f}")
| r (hours vs. score) | ≈ 0.99 — a very strong positive linear relationship |
|---|---|
| p-value | well below 0.05 — the correlation is statistically significant, not likely due to chance in this sample |
np.corrcoef(a, b) returns a 2×2 matrix: the diagonal is always 1 (a variable perfectly correlates with itself), and the off-diagonal values, [0,1] and [1,0], both hold the r between a and b — the matrix is symmetric. scipy.stats.pearsonr() is often more convenient for two variables since it directly returns just (r, p_value).Correlation Is Not Causation
This is the single most important idea in this lesson. A high r tells you two variables move together — it tells you nothing about why. There are several ways two variables can be strongly correlated without one causing the other.
df.corr() and Visualizing with a Heatmap
For a whole DataFrame with several numeric columns, pandas computes every pairwise correlation at once with df.corr() — and a Seaborn heatmap from Lesson 15 turns that grid of numbers into something you can read at a glance.
import seaborn as sns import matplotlib.pyplot as plt df = pd.DataFrame({ "hours_studied": [1, 2, 2, 3, 4, 4, 5, 6, 7, 8], "exam_score": [52, 58, 55, 64, 68, 70, 75, 80, 85, 92], "sleep_hours": [8, 7, 8, 6, 7, 6, 6, 5, 6, 5], }) corr = df.corr() print(corr) fig, ax = plt.subplots(figsize=(6, 5)) sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1, ax=ax) ax.set_title("Correlation Matrix") plt.show()
| hours_studied | exam_score | sleep_hours | |
|---|---|---|---|
| hours_studied | 1.00 | 0.99 | -0.85 |
| exam_score | 0.99 | 1.00 | -0.81 |
| sleep_hours | -0.85 | -0.81 | 1.00 |
cmap="coolwarm" maps values near +1 to warm red/orange, values near −1 to cool blue, and values near 0 to a neutral color in between. Setting vmin=-1, vmax=1 explicitly is important — without it, Seaborn scales the color range to the data's own min/max, which can make a moderate 0.5 correlation look artificially "maxed out" red.Simple Linear Regression — Fitting a Line
Where correlation measures how strongly two variables relate, regression goes a step further: it fits an actual straight line through the data, which you can then use to predict y from a new x.
# scipy.stats.linregress — slope, intercept, r, p-value, and std error, all at once result = stats.linregress(hours_studied, exam_score) print(f"slope (m): {result.slope:.3f}") print(f"intercept (b): {result.intercept:.3f}") print(f"r-value: {result.rvalue:.3f}") print(f"r-squared: {result.rvalue**2:.3f}") print(f"p-value: {result.pvalue:.5f}") # Predict the exam score for a student who studies 5.5 hours predicted = result.slope * 5.5 + result.intercept print(f"Predicted score at 5.5 hours: {predicted:.1f}") # np.polyfit(x, y, degree) is an equivalent, more general alternative m, b = np.polyfit(hours_studied, exam_score, deg=1) print(f"np.polyfit slope/intercept: {m:.3f}, {b:.3f}") # Plot the data with the fitted line on top fig, ax = plt.subplots(figsize=(7, 5)) ax.scatter(hours_studied, exam_score, color="#4f9eff", label="Actual scores") x_line = np.linspace(hours_studied.min(), hours_studied.max(), 100) ax.plot(x_line, result.slope * x_line + result.intercept, color="#2de8c0", linewidth=2.5, label="Line of best fit") ax.set_xlabel("Hours Studied") ax.set_ylabel("Exam Score") ax.legend() plt.show()
scipy.stats.linregress() is specialized for exactly one x and one y, and bundles in r-value, p-value, and standard error for free — ideal for this kind of statistical analysis. np.polyfit(x, y, deg=1) is more general — pass deg=2 or higher for a curved polynomial fit instead of a straight line — but it only returns the fitted coefficients, none of the statistical extras.Use this illustrative dataset relating advertising spend (in thousands of PKR) to weekly units sold: ad_spend = [10, 15, 20, 25, 30, 35, 40, 45], units_sold = [102, 118, 135, 149, 168, 180, 195, 210].
Use
stats.pearsonr(ad_spend, units_sold) to get the correlation coefficient and its p-value. How strong is the relationship?
Use
stats.linregress(ad_spend, units_sold) to get the slope and intercept. In plain English, what does the slope mean in terms of "units sold per extra 1,000 PKR spent on ads"?
Use the fitted line to predict units sold at an ad spend of 50 (thousand PKR) — a value beyond the original data range. Then write one sentence on why extrapolating a line beyond the range it was fit on is riskier than predicting within that range.
💡 Show hints if you're stuck
- Task 1:
r, p = stats.pearsonr(ad_spend, units_sold)— expect r to be very close to 1 for this clean, illustrative data. - Task 2:
result = stats.linregress(ad_spend, units_sold), thenresult.slopeis the predicted increase in units_sold per 1 unit increase in ad_spend (i.e. per 1,000 PKR). - Task 3:
result.slope * 50 + result.intercept— the caveat: the line was only fit on spend values from 10 to 45, so a prediction at 50 assumes the same linear trend continues beyond what was actually observed, which may not hold.