Simple Linear Regression
Simple Linear Regression models the relationship between one feature $x$ and a continuous target $y$ as a straight line. It's the foundation of everything in supervised learning.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
# Synthetic data: house size vs price
np.random.seed(42)
X_simple = np.random.uniform(500, 3000, 100).reshape(-1, 1) # sq ft
y = 150 * X_simple.squeeze() + 50000 + np.random.randn(100) * 30000
model = LinearRegression()
model.fit(X_simple, y)
print(f"Slope ($/sqft): {model.coef_[0]:.2f}")
print(f"Intercept: {model.intercept_:.2f}")
print(f"R² score: {r2_score(y, model.predict(X_simple)):.4f}")
plt.figure(figsize=(8, 4))
plt.scatter(X_simple, y, alpha=0.5, color="#4a8fa8", label="data")
x_line = np.linspace(500, 3000, 100).reshape(-1, 1)
plt.plot(x_line, model.predict(x_line), color="#b85c2a", lw=2, label="fit")
plt.xlabel("Size (sq ft)"); plt.ylabel("Price ($)")
plt.title("Simple Linear Regression"); plt.legend(); plt.tight_layout(); plt.show()
Multiple Linear Regression
Extend to $n$ features. Each feature gets its own weight. The prediction is a dot product between the weight vector and the feature vector:
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# Simulate a housing dataset
np.random.seed(0)
n = 500
df = pd.DataFrame({
"size": np.random.uniform(500, 3500, n),
"bedrooms": np.random.randint(1, 6, n),
"age": np.random.randint(0, 50, n),
"dist_center":np.random.uniform(1, 30, n),
})
df["price"] = (120*df["size"] + 15000*df["bedrooms"]
- 800*df["age"] - 3000*df["dist_center"]
+ 40000 + np.random.randn(n)*20000)
X = df.drop("price", axis=1)
y = df["price"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
model = LinearRegression()
model.fit(X_train_s, y_train)
y_pred = model.predict(X_test_s)
print(f"RMSE: ${np.sqrt(mean_squared_error(y_test, y_pred)):,.0f}")
print(f"R²: {r2_score(y_test, y_pred):.4f}")
# Feature importance via coefficients (after scaling)
coef_df = pd.Series(model.coef_, index=X.columns).sort_values(key=abs, ascending=False)
print("
Feature coefficients (scaled):")
print(coef_df)
Polynomial Regression
Linear regression can only fit straight lines (or hyperplanes). For curved relationships, polynomial regression adds powers of the original features as new columns — then fits a standard linear model on top.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
# Non-linear data
np.random.seed(7)
X = np.sort(np.random.uniform(-3, 3, 80)).reshape(-1, 1)
y = 0.5*X.squeeze()**3 - 2*X.squeeze() + np.random.randn(80) * 1.5
fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)
x_plot = np.linspace(-3, 3, 200).reshape(-1, 1)
for ax, degree, color in zip(axes, [1, 3, 10], ["#4a8fa8","#5c8a58","#b85c2a"]):
pipe = Pipeline([
("poly", PolynomialFeatures(degree=degree, include_bias=False)),
("scaler", __import__("sklearn.preprocessing", fromlist=["StandardScaler"]).StandardScaler()),
("model", LinearRegression()),
])
pipe.fit(X, y)
cv = cross_val_score(pipe, X, y, cv=5, scoring="neg_mean_squared_error")
rmse = np.sqrt(-cv.mean())
ax.scatter(X, y, alpha=0.5, color=color, s=20)
ax.plot(x_plot, pipe.predict(x_plot), color=color, lw=2)
ax.set_title(f"Degree {degree}
CV RMSE: {rmse:.2f}")
ax.set_xlabel("x")
axes[0].set_ylabel("y")
fig.suptitle("Polynomial Regression — Underfitting vs. Overfitting", fontsize=12)
plt.tight_layout(); plt.show()
# Degree 1 underfits, Degree 3 fits well, Degree 10 overfits
Ridge, Lasso and ElasticNet
Polynomial features increase model complexity fast — regularisation is essential to prevent overfitting.
from sklearn.linear_model import Ridge, Lasso, ElasticNet, RidgeCV
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
import numpy as np
# High-degree polynomial + Ridge regularisation
pipe_ridge = Pipeline([
("poly", PolynomialFeatures(degree=8, include_bias=False)),
("scaler", StandardScaler()),
("model", Ridge(alpha=1.0)), # alpha = lambda (regularisation strength)
])
pipe_lasso = Pipeline([
("poly", PolynomialFeatures(degree=8, include_bias=False)),
("scaler", StandardScaler()),
("model", Lasso(alpha=0.01)), # Lasso drives some coefficients to exactly 0
])
for name, pipe in [("Ridge", pipe_ridge), ("Lasso", pipe_lasso)]:
cv = cross_val_score(pipe, X, y, cv=5, scoring="neg_mean_squared_error")
print(f"{name}: CV RMSE = {np.sqrt(-cv.mean()):.4f}")
Residual Analysis
import matplotlib.pyplot as plt
import scipy.stats as stats
model.fit(X_train_s, y_train)
residuals = y_test.values - model.predict(X_test_s)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Residuals vs. Fitted (should look random — no pattern)
axes[0].scatter(model.predict(X_test_s), residuals, alpha=0.5, color="#4a8fa8")
axes[0].axhline(0, color="red", ls="--"); axes[0].set_title("Residuals vs. Fitted")
axes[0].set_xlabel("Fitted values"); axes[0].set_ylabel("Residuals")
# Histogram (should look Gaussian)
axes[1].hist(residuals, bins=25, color="#5c8a58", edgecolor="white")
axes[1].set_title("Residual Distribution"); axes[1].set_xlabel("Residual")
# Q-Q plot (points should lie on the diagonal line)
stats.probplot(residuals, plot=axes[2]); axes[2].set_title("Normal Q-Q Plot")
plt.tight_layout(); plt.show()
Summary
- Simple LR: $\hat{y}=wx+b$. Minimise MSE. Closed-form solution via Normal Equation.
- Multiple LR: extends to $n$ features. Check for multicollinearity and heteroscedasticity.
- Polynomial regression: add $x^2, x^3, \ldots$ as features then fit linear model. Always regularise.
- Ridge shrinks all weights; Lasso zeroes some out. Use Ridge as default, Lasso for feature selection.
- Residual plots are non-negotiable — always verify assumptions after fitting.