Linear Regression

Learn how linear regression predicts continuous values by finding the best-fitting line through data points. Master the fundamentals of this essential supervised learning algorithm.

Understanding Linear Regression: From Theory to Practice

Linear regression is one of the most fundamental and widely used statistical techniques in data science and machine learning. Despite its simplicity, it forms the backbone of many advanced algorithms and provides crucial insights into the relationships between variables. This article explores its mathematical underpinnings, key assumptions, practical implementation, and common extensions.


What is Linear Regression?

Linear regression is a statistical method that models the relationship between a dependent variable (target) and one or more independent variables (features) by fitting a linear equation to observed data. The goal is to find the "line of best fit" that minimizes the distance between the observed data points and the line's predictions.

The case of one explanatory variable is called simple linear regression. When we use multiple explanatory variables, it's known as multiple linear regression.

linear regression

This technique serves two main purposes:

  1. Inference: Understanding and quantifying the strength and direction of the relationship between variables (e.g., "How much does a $10,000 increase in ad spend affect sales?").
  2. Prediction: Forecasting new values of the dependent variable based on new values of the independent variables (e.g., "What will sales be next quarter if we spend $50,000 on ads?").

"All models are wrong, but some are useful." - George E.P. Box

This famous quote perfectly encapsulates the philosophy behind linear regression. It makes strong, often unrealistic, assumptions about the data, but its simplicity, interpretability, and effectiveness make it an invaluable tool.


The Mathematical Foundation

The Model Equation

At its core, the model assumes a linear relationship.

Simple Linear Regression: y=β0+β1x+ϵy = \beta_0 + \beta_1 x + \epsilon

Where:

  • yy is the dependent variable (what we want to predict).
  • xx is the independent variable (our feature).
  • β0\beta_0 is the y-intercept (the value of yy when x=0x=0).
  • β1\beta_1 is the slope coefficient (the change in yy for a one-unit change in xx).
  • ϵ\epsilon (epsilon) is the error term, representing random noise and unobserved factors.

Multiple Linear Regression: The equation simply extends to include multiple features: y=β0+β1x1+β2x2+...+βnxn+ϵy = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_n x_n + \epsilon

In matrix form, this is written more concisely: y=Xβ+ϵ\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\epsilon}

Where y\mathbf{y} is a vector of outcomes, X\mathbf{X} is the matrix of features (with a leading column of ones for the β0\beta_0 intercept), β\boldsymbol{\beta} is the vector of coefficients, and ϵ\boldsymbol{\epsilon} is the vector of errors.

<div id="VZ-linear-equation" data-placeholder="Interactive Linear Equation Visualization"></div>

How Do We "Fit" the Line?

"Fitting" the model means finding the optimal coefficients (β\boldsymbol{\beta}) that make the model's predictions (y^\hat{y}) as close as possible to the actual data (yy). We quantify this "closeness" using a cost function. The most common one is the Mean Squared Error (MSE) or Residual Sum of Squares (RSS).

Cost(β)=MSE=1mi=1m(y^iyi)2=1mi=1m((β0+β1x1+...)yi)2\text{Cost}( \boldsymbol{\beta} ) = \text{MSE} = \frac{1}{m} \sum_{i=1}^{m} (\hat{y}_i - y_i)^2 = \frac{1}{m} \sum_{i=1}^{m} ((\beta_0 + \beta_1 x_1 + ...) - y_i)^2

Our goal is to find the β\boldsymbol{\beta} that minimizes this cost. There are two primary methods to do this:

1. The Normal Equation (Analytical Solution)

This is a direct, closed-form solution that calculates the optimal β\boldsymbol{\beta} analytically. It's derived by taking the derivative of the cost function with respect to β\boldsymbol{\beta}, setting it to zero, and solving.

The solution is expressed in a single matrix operation: β^=(XTX)1XTy\boldsymbol{\hat{\beta}} = (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{y}

  • Pros: No iterations, no need to choose a learning rate, provides an exact solution.
  • Cons: Computationally expensive. Calculating the inverse of XTX\mathbf{X}^T \mathbf{X} is typically an O(n3)O(n^3) operation (where nn is the number of features). This becomes unfeasible for datasets with tens of thousands of features. It also fails if XTX\mathbf{X}^T \mathbf{X} is non-invertible (which can happen with perfect multicollinearity).

2. Gradient Descent (Iterative Solution)

Gradient Descent is an iterative optimization algorithm. Imagine standing on a hillside and taking small steps in the steepest downhill direction until you reach the bottom (the minimum cost).

  1. Initialize β\boldsymbol{\beta} with random values.
  2. Calculate the gradient (the partial derivative) of the cost function with respect to each βj\beta_j. This tells us the direction of steepest ascent. J(β)βj=2mi=1m(y^(i)y(i))xj(i)\frac{\partial J(\boldsymbol{\beta})}{\partial \beta_j} = \frac{2}{m} \sum_{i=1}^{m} (\hat{y}^{(i)} - y^{(i)}) x_j^{(i)}
  3. Update each βj\beta_j by taking a small step in the opposite (downhill) direction. βj:=βjαJ(β)βj\beta_j := \beta_j - \alpha \frac{\partial J(\boldsymbol{\beta})}{\partial \beta_j} Here, α\alpha (alpha) is the learning rate, a hyperparameter that controls the size of each step.
  4. Repeat steps 2 and 3 for a fixed number of iterations or until the cost stops decreasing significantly.
  • Pros: Scales much better to large datasets (many features and/or samples).
  • Cons: Requires manually tuning the learning rate α\alpha. (If α\alpha is too small, convergence is slow; if too large, it may overshoot the minimum and diverge). It's an iterative approximation, not an exact solution (though it gets arbitrarily close).

Key Assumptions of Linear Regression

The validity of a linear regression model's results, especially for inference, depends heavily on several key assumptions.

  1. Linearity: The relationship between the independent variables and the mean of the dependent variable is linear.
  2. Independence: Observations are independent of each other (e.g., no autocorrelation in time series data).
  3. Homoscedasticity: The variance of the error terms (ϵ\epsilon) is constant across all levels of the independent variables. (In plain English: the "spread" of the errors is consistent).
  4. Normality: The error terms (ϵ\epsilon) are normally distributed. This is most important for constructing confidence intervals and p-values.
  5. No (perfect) multicollinearity: The independent variables are not highly correlated with each other.

Testing Assumptions with Diagnostic Plots

We don't test assumptions on the raw variables, but on the model's residuals (the errors: e=yy^e = y - \hat{y}).

  • Residuals vs. Fitted Plot: This is the most important plot.
    • For Linearity: Look for no clear pattern. The points should be randomly scattered around the horizontal line at 0. A curved pattern (like a "U" shape) indicates the relationship is non-linear.
    • For Homoscedasticity: Look for a constant spread of points. A "funnel" or "megaphone" shape (where the spread increases or decreases with the fitted values) indicates heteroscedasticity.
  • Normal Q-Q Plot:
    • For Normality: The points (standardized residuals) should fall closely along the 45-degree dashed line. Systematic deviations, especially S-curves or "banana" shapes, indicate the residuals are not normally distributed.
  • Variance Inflation Factor (VIF):
    • For Multicollinearity: This is a numerical test, not a plot. VIF measures how much the variance of a coefficient is "inflated" due to its correlation with other features.
    • Rule of thumb: A VIF > 5 is often a cause for concern, and a VIF > 10 strongly indicates problematic multicollinearity.

Loading visualization...


Implementation in Python

Let's implement linear regression using both the Normal Equation and Gradient Descent, and then compare it to the optimized scikit-learn library.

From Scratch (Normal Equation)

This implementation directly uses the np.linalg.inv function to solve the Normal Equation.

1import numpy as np
2import matplotlib.pyplot as plt
3from sklearn.datasets import make_regression
4from sklearn.model_selection import train_test_split
5
6class LinearRegressionNormalEq:
7    def __init__(self):
8        self.beta = None
9    
10    def fit(self, X, y):
11        # Add bias term (column of ones) to X for the intercept
12        X_with_bias = np.c_[np.ones(X.shape[0]), X]
13        
14        # Calculate weights using normal equation
15        # β = (X^T X)^(-1) X^T y
16        try:
17            self.beta = np.linalg.inv(X_with_bias.T @ X_with_bias) @ X_with_bias.T @ y
18        except np.linalg.LinAlgError:
19            print("Error: Singular matrix. Cannot compute inverse.")
20            self.beta = None
21    
22    def predict(self, X):
23        if self.beta is None:
24            raise ValueError("Model has not been fitted yet.")
25        
26        # Add bias term
27        X_with_bias = np.c_[np.ones(X.shape[0]), X]
28        
29        # Make predictions
30        return X_with_bias @ self.beta

From Scratch (Gradient Descent)

This implementation uses the iterative approach.

1class LinearRegressionGradientDescent:
2    def __init__(self, learning_rate=0.01, n_iterations=1000):
3        self.lr = learning_rate
4        self.n_iters = n_iterations
5        self.weights = None
6        self.bias = None
7        self.cost_history = []
8
9    def fit(self, X, y):
10        n_samples, n_features = X.shape
11        
12        # Initialize parameters
13        self.weights = np.zeros(n_features)
14        self.bias = 0
15        self.cost_history = []
16        
17        # Gradient descent loop
18        for _ in range(self.n_iters):
19            # Calculate predictions: y_pred = Xw + b
20            y_pred = X @ self.weights + self.bias
21            
22            # Calculate cost (MSE)
23            cost = (1 / n_samples) * np.sum((y_pred - y) ** 2)
24            self.cost_history.append(cost)
25            
26            # Calculate gradients
27            # dJ/dw = (2/m) * X^T * (y_pred - y)
28            # dJ/db = (2/m) * sum(y_pred - y)
29            dw = (2 / n_samples) * X.T @ (y_pred - y)
30            db = (2 / n_samples) * np.sum(y_pred - y)
31            
32            # Update parameters
33            self.weights -= self.lr * dw
34            self.bias -= self.lr * db
35            
36    def predict(self, X):
37        return X @ self.weights + self.bias

Using Scikit-learn (The Easy Way)

In practice, you'll almost always use a well-optimized library like scikit-learn.

1from sklearn.linear_model import LinearRegression
2from sklearn.metrics import mean_squared_error, r2_score
3
4# Generate sample data
5X, y = make_regression(n_samples=100, n_features=1, noise=10, random_state=42)
6X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
7
8# --- Scikit-learn Model ---
9model = LinearRegression()
10model.fit(X_train, y_train)
11
12# Make predictions
13y_pred_sklearn = model.predict(X_test)
14
15# Evaluate the model
16mse = mean_squared_error(y_test, y_pred_sklearn)
17r2 = r2_score(y_test, y_pred_sklearn)
18
19print("--- Scikit-learn Results ---")
20print(f"Mean Squared Error: {mse:.2f}")
21print(f"R-squared Score: {r2:.2f}")
22print(f"Coefficient (β1): {model.coef_[0]:.2f}")
23print(f"Intercept (β0): {model.intercept_:.2f}")
24
25# --- Plotting the results ---
26plt.figure(figsize=(10, 6))
27plt.scatter(X_test, y_test, color='#003f5c', label='Actual Data')
28plt.plot(X_test, y_pred_sklearn, color='#ffa600', linewidth=3, label='Predicted Line (sklearn)')
29plt.title('Linear Regression Fit')
30plt.xlabel('Feature (X)')
31plt.ylabel('Target (y)')
32plt.legend()
33plt.grid(True)
34plt.show()

<div id="VZ-regression-comparison" data-placeholder="Comparison of From-Scratch vs Scikit-learn Implementation"></div>


Model Evaluation Metrics

How do we know if our model is any good? We use evaluation metrics.

MetricFormulaInterpretation
MSE1ni=1n(yiyi^)2\frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2Average squared difference. Good for optimization, but units are squared (e.g., "dollars squared").
RMSE1ni=1n(yiyi^)2\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2}Square root of MSE. Interpretable, as it's in the same units as the target (e.g., "dollars").
MAE$\frac{1}{n}\sum_{i=1}^{n}y_i - \hat{y_i}
1SSresSStot=1(yiyi^)2(yiyˉ)21 - \frac{SS_{res}}{SS_{tot}} = 1 - \frac{\sum(y_i - \hat{y_i})^2}{\sum(y_i - \bar{y})^2}Proportion of the variance in yy that is predictable from XX. Ranges from 0 to 1.
Adj. R²1(1R2)n1np11 - (1-R^2) \frac{n-1}{n-p-1}R² adjusted for the number of predictors (pp). It penalizes adding useless features, making it better for multiple regression.

R-squared (R2R^2), or the coefficient of determination, is one of the most common. It tells you what percentage of the variance in your target variable is explained by your model. An R2R^2 of 0.75 means your model explains 75% of the variability in the target.

<div id="VZ-model-evaluation" data-placeholder="Model Performance Metrics Dashboard"></div>


Common Extensions and Variations

Linear regression's simplicity is a feature, not a bug, and it serves as a foundation for more complex models.

  • Polynomial Regression: Used to model non-linear relationships. You create new features by taking existing features to a power (e.g., x2,x3x^2, x^3). The model is still "linear" because it's linear in the coefficients β\beta. y=β0+β1x+β2x2+ϵy = \beta_0 + \beta_1 x + \beta_2 x^2 + \epsilon
  • Regularization (Ridge & Lasso): These techniques are used to prevent overfitting (when a model learns the training data "too well" and fails to generalize) and handle multicollinearity. They add a penalty term to the cost function to keep the coefficients (β\beta) small.
    • Ridge (L2L2): Adds a penalty proportional to the square of the coefficients (αβj2\alpha \sum \beta_j^2). It shrinks coefficients but rarely to zero.
    • Lasso (L1L1): Adds a penalty proportional to the absolute value of the coefficients (αβj\alpha \sum |\beta_j|). It can shrink "unimportant" feature coefficients all the way to zero, effectively performing automatic feature selection.

<div id="VZ-interactive-demo" data-placeholder="Live Markdown Editor Demo"></div>


Conclusion

Linear regression remains a cornerstone of statistical modeling and machine learning. Its power lies not in its complexity, but in its simplicity and interpretability. It provides a clear, understandable baseline for any regression problem.

When to Use Linear Regression

Use when:

  • You need a model that is easy to interpret and explain (e.g., for business stakeholders).
  • The relationship between variables appears to be linear.
  • You need a fast, low-computation baseline to compare against more complex models.

Avoid when:

  • The underlying relationships are clearly and complexly non-linear.
  • The key assumptions (especially independence and homoscedasticity) are severely violated.
  • You have high multicollinearity (though Ridge or Lasso can help).

References and Further Reading

  1. James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An Introduction to Statistical Learning: with Applications in R. Springer, New York, NY.
  2. Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning: Data Mining, Inference, and Prediction. Springer, New York, NY.
  3. Montgomery, D. C., Peck, E. A., & Vining, G. G. (2012). Introduction to Linear Regression Analysis. John Wiley & Sons.

Contributors

Yahia Alqurnawi
ash1706_
Linear Regression