Linear Regression
Simple and multiple linear models: MSE, the Normal Equation, gradient descent, checking assumptions, evaluating with Rยฒ, and the polynomial extension.
Learning Objectives
- โBuild simple and multiple linear regression models
- โKnow when to use the Normal Equation and when to switch to Gradient Descent
- โCheck the four assumptions, detect multicollinearity and read a residual plot
- โRead Rยฒ and RSE alongside the MAE/MSE/RMSE metrics
- โExtend to polynomial regression while keeping overfitting under control
The Model & the MSE Loss
Regression predicts a continuous numeric value; the univariate model fits a straight line through the cloud of points. Least squares minimizes the sum of squared residuals โ squaring removes the sign, penalizes large errors heavily, and keeps the function differentiable everywhere. The trade-off is that MSE is very sensitive to outliers. Univariate regression has a closed-form solution for and .
Multiple Regression & the Normal Equation
With many features the model compresses to . The Normal Equation gives the optimal solution in a single computation: no iteration, no learning rate to choose. The limitation is inverting at cost, which becomes infeasible past roughly 10,000 features; on top of that, if two columns carry nearly the same information then is singular and the solution destabilizes.
Gradient Descent
Gradient Descent solves the same problem iteratively, moving the weights against the gradient with step size . Advantages: no matrix inversion, so it scales to very large feature counts, and it applies to models with no closed form such as logistic regression or neural networks. SGD updates on a single sample; Mini-batch uses 32โ256 and exploits the GPU. Standardize first, or the algorithm will oscillate.
import numpy as np
def linear_regression_gd(X, y, lr=0.01, epochs=1000):
m, n = X.shape
w = np.zeros(n)
b = 0.0
for _ in range(epochs):
y_pred = X @ w + b
error = y_pred - y
w -= lr * (2 / m) * (X.T @ error)
b -= lr * (2 / m) * np.sum(error)
return w, b
# X must be standardized first, or the loss will diverge to nanAssumptions & Model Diagnostics
Four assumptions to check: a linear relationship, independent residuals, constant residual variance, and residuals that are approximately normal. The cheapest tool is a plot of residuals against fitted values โ randomly scattered around 0 passes, a U shape means the relationship is nonlinear, a funnel shape means the variance is not constant. Multicollinearity makes coefficients unstable and can flip their sign; detect it with VIF, and above 10 consider dropping a column.
Model Evaluation
says how much of the variance in the model explains compared to a model that always predicts . Remember that never decreases when you add a feature, even a meaningless one, so comparing models with different column counts requires adjusted . Report MAE (easy to interpret) and RMSE (same units as ) alongside it โ an RMSE much larger than the MAE is a sign of a handful of very large errors.
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
model = LinearRegression().fit(X_train, y_train)
y_pred = model.predict(X_test)
print("R2 :", r2_score(y_test, y_pred))
print("MAE :", mean_absolute_error(y_test, y_pred))
print("RMSE:", mean_squared_error(y_test, y_pred, squared=False))Extension: Polynomial Regression
When the real relationship is a curve, add the powers to the feature table and run linear regression on the new set of columns. The model is still linear in the parameters โ that is what "linear" actually means โ and only nonlinear in . The higher the degree the more the curve bends to touch each point, so choose the degree by cross-validation rather than by on the training set, which always favors the highest degree.
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import cross_val_score
for d in [1, 2, 3, 5, 9]:
pipe = make_pipeline(PolynomialFeatures(degree=d), LinearRegression())
cv = cross_val_score(pipe, X, y, cv=5, scoring='r2')
print(f'degree {d}: CV R2 = {cv.mean():.3f}')
# Pick the degree with the highest CV R2, not the highest train-set R2