Solving tips
- Prepend a column of ones to X for the bias, then build the penalty matrix so its first diagonal entry (the bias) is 0. Regularizing the intercept just shrinks predictions toward 0 for no reason.
- Never form the inverse explicitly. Build A = X^T X + lambda * I_reg and b = X^T y, then call np.linalg.solve(A, b) — it is faster and more numerically stable than np.linalg.inv.
- Sanity check the shapes: X is (n, d), the augmented matrix is (n, d+1), A is (d+1, d+1), and the returned weight vector is (d+1,) with w[0] the intercept.
Implement ridge regression from scratch with NumPy using the closed-form normal equations. Ridge is ordinary least squares plus an L2 penalty on the coefficients, which shrinks them toward zero, keeps the fit stable when features are correlated, and makes X^T X invertible even when it otherwise would not be. Interviewers use it to check that you know the normal equations, that you handle the bias term correctly, and that you solve a linear system instead of inverting a matrix.
Definition
Augment X with a leading column of ones so the first weight is the intercept: X_aug has shape (n, d + 1). Ridge minimizes the penalized squared error
J(w) = ||X_aug @ w - y||^2 + lam * ||w[1:]||^2
where the sum in the penalty runs over the feature coefficients only, not the bias. Setting the gradient to zero gives the normal equations
(X_aug^T X_aug + lam * R) w = X_aug^T y
with R = diag(0, 1, 1, ..., 1) a (d + 1, d + 1) matrix whose first diagonal entry is 0 so the bias is left unpenalized. The lam * R term also lifts the diagonal, which is why the system stays solvable even when X_aug^T X_aug is singular.
Task
Complete ridge_regression(X, y, lam) so it returns the weight vector w of shape (d + 1,), where w[0] is the intercept and w[1:] are the feature coefficients. Build the augmented matrix, form A = X_aug^T X_aug + lam * R and b = X_aug^T y, and solve A w = b with np.linalg.solve. Do not call np.linalg.inv, and do not use sklearn, scipy, or any built-in ridge/regression helper.
Example
X = np.array([[1.0], [2.0], [3.0], [4.0]])
y = np.array([2.0, 4.0, 6.0, 8.0]) # exactly y = 2*x, intercept 0
w = ridge_regression(X, y, lam=0.0)
w # -> array([0., 2.]) # lam=0 recovers ordinary least squares: bias 0, slope 2
w = ridge_regression(X, y, lam=1.0)
w # -> array([0.83333333, 1.66666667]) # penalty shrinks the slope; bias absorbs the offset
With lam = 0 ridge is exactly OLS and recovers the true slope of 2 with a zero intercept. Turning up lam shrinks the slope while the unpenalized intercept shifts to compensate, so predictions stay reasonable rather than collapsing to zero.
Constraints
1 <= n <= 10^5,1 <= d <= 500; use vectorized NumPy, not Python loops over rows or features.lam >= 0. Atlam = 0the result must equal the ordinary least squares solution (when it exists).- The bias term (weight index
0) must never be regularized. - Solve the linear system with
np.linalg.solve; do not form an explicit inverse. Values fit in float64.