Skip to content

Polynomial Regression

What you’ll learn

  • why adding x2x^2 to the feature matrix lets a linear model fit a curve
  • how to solve a quadratic fit by hand on five points
  • how PolynomialFeaturesPolynomialFeatures explodes combinatorially, and how fast
  • how to choose the degree with cross-validation and the one-standard-error rule
  • how to read a learning curve to tell underfitting from overfitting
  • why polynomial pipelines must scale their features

Intuition

A straight line through curved data is wrong in a specific, visible way: it misses low, then high, then low again. The residual plot shows a smile or a frown instead of a shapeless band.

The fix is disarmingly cheap. Do not change the algorithm — change the columns. Hand the model x2x^2 alongside xx, and least squares will find the best parabola exactly the way it found the best line, because the model is still a weighted sum of whatever columns you supply. Linear regression is linear in the parameters, and never had an opinion about the features.

diagram Diagram mermaid

The math

The model

A degree-dd polynomial in one feature:

y^=θ0+θ1x+θ2x2++θdxd\hat{y} = \theta_0 + \theta_1 x + \theta_2 x^2 + \cdots + \theta_d x^d

Rename z1=x,  z2=x2,  ,  zd=xdz_1 = x,\; z_2 = x^2,\; \ldots,\; z_d = x^d and it reads y^=θ0+θ1z1++θdzd\hat{y} = \theta_0 + \theta_1 z_1 + \cdots + \theta_d z_d — an ordinary multiple linear regression. The Normal Equation applies unchanged:

θ=(XX)1Xy,X=[1x1x12x1d1x2x22x2d1xmxm2xmd]\boldsymbol{\theta} = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}, \qquad \mathbf{X} = \begin{bmatrix} 1 & x_1 & x_1^2 & \cdots & x_1^d \\ 1 & x_2 & x_2^2 & \cdots & x_2^d \\ \vdots & \vdots & \vdots & & \vdots \\ 1 & x_m & x_m^2 & \cdots & x_m^d \end{bmatrix}

That matrix has a name — the Vandermonde matrix — and a reputation: it becomes badly conditioned fast as dd grows, which is one reason high-degree fits misbehave numerically as well as statistically.

How many terms?

With nn input features and degree dd, PolynomialFeaturesPolynomialFeatures generates every product of total degree at most dd:

number of terms=(n+dd)\text{number of terms} = \binom{n + d}{d}
features nndegree 2degree 3degree 5
1346
3102056
10662863,003
1005,151176,851≈ 96 million

Two features at degree 3 already give you interaction terms like x02x1x_0^2 x_1 — which is genuinely useful, and also how a tidy 10-column dataset becomes 286 columns without anyone deciding to do that.

Worked example by hand

Five points, xx chosen symmetric about zero so that xi=0\sum x_i = 0 and xi3=0\sum x_i^3 = 0. That kills half the terms in XX\mathbf{X}^\top\mathbf{X} and makes the algebra tractable.

iixix_iyiy_ixi2x_i^2xiyix_i y_ixi2yix_i^2 y_i
1−294−1836
2−141−44
301000
412122
52941836
02510−278

Step 1 — assemble the normal equations. With x=0\sum x = 0, x2=10\sum x^2 = 10, x3=0\sum x^3 = 0 and x4=34\sum x^4 = 34:

XX=[5010010010034],Xy=[25278]\mathbf{X}^\top\mathbf{X} = \begin{bmatrix} 5 & 0 & 10 \\ 0 & 10 & 0 \\ 10 & 0 & 34 \end{bmatrix}, \qquad \mathbf{X}^\top\mathbf{y} = \begin{bmatrix} 25 \\ -2 \\ 78 \end{bmatrix}

Step 2 — the middle row is already solved. 10θ1=210\theta_1 = -2, so θ1=0.2\theta_1 = -0.2.

Step 3 — solve the remaining 2×2 system.

5θ0+10θ2=25    θ0=52θ25\theta_0 + 10\theta_2 = 25 \;\Longrightarrow\; \theta_0 = 5 - 2\theta_2

Substitute into 10θ0+34θ2=7810\theta_0 + 34\theta_2 = 78:

10(52θ2)+34θ2=78    14θ2=28    θ2=2,θ0=110(5 - 2\theta_2) + 34\theta_2 = 78 \;\Longrightarrow\; 14\theta_2 = 28 \;\Longrightarrow\; \theta_2 = 2,\quad \theta_0 = 1

The fitted curve is y^=10.2x+2x2\hat{y} = 1 - 0.2x + 2x^2.

Step 4 — check.

xix_iyiy_iy^i\hat{y}_iresidualsquared
−299.4−0.40.16
−143.2+0.80.64
011.00.00.00
122.8−0.80.64
298.6+0.40.16
0.01.60
SST=58,R2=11.6580.972\text{SST} = 58, \qquad R^2 = 1 - \frac{1.6}{58} \approx 0.972

Nothing here is new machinery. It is the Normal Equation from the previous page, applied to a design matrix that happens to contain a squared column.

See it move

Three capacities cycling over the same curved dataset — watch the degree-12 curve chase individual points instead of the trend.

sketch Under and over-fitting a curved dataset p5.js
A degree-1 line underfits, a degree-2 curve fits well, and a high-degree polynomial overfits by wiggling through the noise. Click for a fresh sample.
figureThe same 40 points, three model capacitiesmatplotlib
Three panels showing the same 40 scattered points fitted with a degree-1 line, a degree-2 curve, and a wildly oscillating degree-15 curve.Three panels showing the same 40 scattered points fitted with a degree-1 line, a degree-2 curve, and a wildly oscillating degree-15 curve.
Training MSE falls from 2.68 to 0.83 to 0.37 — and the model with the lowest training error is the one you would least want to deploy.

From scratch

Build the Vandermonde matrix explicitly and reuse the Normal Equation:

poly_from_scratch.py
import numpy as np
 
 
def vandermonde(x, degree):
    """Columns [1, x, x^2, ..., x^degree]."""
    x = np.asarray(x, dtype=float)
    return np.vstack([x**k for k in range(degree + 1)]).T
 
 
x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
y = np.array([9.0, 4.0, 1.0, 2.0, 9.0])
 
X = vandermonde(x, 2)
print(X)
# [[ 1. -2.  4.]
#  [ 1. -1.  1.]
#  [ 1.  0.  0.]
#  [ 1.  1.  1.]
#  [ 1.  2.  4.]]
 
theta = np.linalg.solve(X.T @ X, X.T @ y)
print(theta.round(4))              # [ 1.  -0.2  2. ]
 
resid = y - X @ theta
print(resid.round(4))              # [-0.4  0.8  0.  -0.8  0.4]
print(round((resid ** 2).sum(), 4))  # 1.6
poly_from_scratch.py
import numpy as np
 
 
def vandermonde(x, degree):
    """Columns [1, x, x^2, ..., x^degree]."""
    x = np.asarray(x, dtype=float)
    return np.vstack([x**k for k in range(degree + 1)]).T
 
 
x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
y = np.array([9.0, 4.0, 1.0, 2.0, 9.0])
 
X = vandermonde(x, 2)
print(X)
# [[ 1. -2.  4.]
#  [ 1. -1.  1.]
#  [ 1.  0.  0.]
#  [ 1.  1.  1.]
#  [ 1.  2.  4.]]
 
theta = np.linalg.solve(X.T @ X, X.T @ y)
print(theta.round(4))              # [ 1.  -0.2  2. ]
 
resid = y - X @ theta
print(resid.round(4))              # [-0.4  0.8  0.  -0.8  0.4]
print(round((resid ** 2).sum(), 4))  # 1.6

Same numbers as the hand calculation, including the residuals.

With scikit-learn

Never fit PolynomialFeaturesPolynomialFeatures outside a pipeline — the expansion has to be re-applied identically at prediction time, and the scaler after it has to be fitted on training data only:

poly_pipeline.py
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
 
rng = np.random.default_rng(7)
x = np.sort(rng.uniform(-3, 3, 120)).reshape(-1, 1)
y = 0.5 * x.ravel() ** 2 + x.ravel() + 2 + rng.normal(0, 1.1, 120)
 
model = make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),
    StandardScaler(),        # essential: x^5 dwarfs x, and scale wrecks conditioning
    LinearRegression(),
)
model.fit(x, y)
 
print(model.score(x, y).round(4))                   # 0.8196
print(model[0].get_feature_names_out())             # ['x0' 'x0^2']
print(model[-1].coef_.round(3))                     # [1.78  1.248]
poly_pipeline.py
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
 
rng = np.random.default_rng(7)
x = np.sort(rng.uniform(-3, 3, 120)).reshape(-1, 1)
y = 0.5 * x.ravel() ** 2 + x.ravel() + 2 + rng.normal(0, 1.1, 120)
 
model = make_pipeline(
    PolynomialFeatures(degree=2, include_bias=False),
    StandardScaler(),        # essential: x^5 dwarfs x, and scale wrecks conditioning
    LinearRegression(),
)
model.fit(x, y)
 
print(model.score(x, y).round(4))                   # 0.8196
print(model[0].get_feature_names_out())             # ['x0' 'x0^2']
print(model[-1].coef_.round(3))                     # [1.78  1.248]

include_bias=Falseinclude_bias=False matters: LinearRegressionLinearRegression already fits an intercept, so leaving the constant column in gives you two intercepts and a rank-deficient design.

Choosing the degree

Training error falls forever as you add degrees. It has to — each extra term is a strictly larger family of curves. The only honest signal comes from data the model has not seen:

figureTraining error versus cross-validated error by degreematplotlib
Training MSE and 5-fold cross-validated MSE plotted against polynomial degree from 1 to 18, on a log scale. Training error falls steadily; CV error drops sharply at degree 2, stays flat, then rises after degree 10.Training MSE and 5-fold cross-validated MSE plotted against polynomial degree from 1 to 18, on a log scale. Training error falls steadily; CV error drops sharply at degree 2, stays flat, then rises after degree 10.
The CV curve is flat from degree 2 to about 10 — those models are statistically indistinguishable. The one-standard-error rule breaks the tie in favour of the simplest, degree 2.

Reading the plot

  1. The cliff at degree 2 is the real signal. CV error drops from 2.79 to 1.06 and never improves meaningfully again. The data was generated by a quadratic, and the curve says so.
  2. Degrees 2 through 10 are a tie. The shaded band is one standard error across folds; every point in that range sits inside it. Picking the exact minimum here would be reading noise.
  3. The one-standard-error rule. Find the best CV score, add one standard error, and take the simplest model still under that threshold. Here it selects degree 2 — the right answer, and the one that generalises.
  4. The gap after degree 10 is overfitting made visible. Training error keeps falling while CV error climbs.

Learning curves

The degree curve varies capacity at fixed data. A learning curve does the opposite: fixed capacity, growing data. The two failure modes look completely different.

figureLearning curves for an underfitting and an overfitting modelmatplotlib
Two panels of learning curves. Left, degree 1: training and validation error both plateau at a high value and nearly meet. Right, degree 15: training error is very low while validation error stays far above it.Two panels of learning curves. Left, degree 1: training and validation error both plateau at a high value and nearly meet. Right, degree 15: training error is very low while validation error stays far above it.
Curves that meet at a high error mean high bias — more data will not help. A wide persistent gap means high variance — more data will.
SymptomDiagnosisWhat actually helps
Both curves plateau high, close togetherUnderfitting (high bias)More capacity: higher degree, more features
Training low, validation high, gap persistsOverfitting (high variance)More data, lower degree, regularisation
Both low and convergedGood fitShip it
algorithmPolynomial RegressionSupervised · Regression

APIsklearn.preprocessing.PolynomialFeatures + sklearn.linear_model.LinearRegression

Assumes

  • The relationship is smooth and can be approximated by a polynomial on the observed range
  • Observations are independent
  • Residual variance is constant

Cost

train
O(m·p² + p³)
predict
O(p)
memory
O(p)

m = samples, p = expanded term count = binomial(n + d, d)

Hyperparameters that matter

  • degreedefault 2The one that matters. Choose by cross-validation, then take the simplest degree within one standard error of the best.
  • include_biasdefault TrueSet False inside a pipeline whose final estimator already fits an intercept.
  • interaction_onlydefault FalseKeep cross terms like x0·x1 but drop pure powers — useful when you want interactions without curvature.

Reach for it when

  • A scatter plot or residual plot shows smooth curvature
  • You have one to a handful of features and want to keep interpretability
  • A physical relationship is genuinely quadratic or cubic

Look elsewhere when

  • You have many features — the term count explodes combinatorially
  • You need to predict outside the training range; polynomials diverge violently
  • The curve has sharp kinks or steps — use splines or trees instead

Pitfalls

Compare

ApproachShape it fitsExtrapolatesCost with many featuresInterpretability
Polynomial regressionSmooth global curveVery badlyExplodesGood at low degree
SplinesSmooth, piecewise, localPoorlyLinearModerate
Decision treeStepsFlat (constant)LinearModerate
Random forestSmooth stepsFlatLinearPoor
Kernel ridge (RBF)Smooth, localReverts to meanQuadratic in samplesPoor
Log/sqrt transformOne fixed curve shapeReasonablyFreeVery good

If the curvature is monotone, try transforming yy or xx with a log before reaching for a polynomial. One column, no degree to tune, and it extrapolates sensibly.

quizCheck yourself
  1. Why is polynomial regression still called a linear model?

    Show answer

    B — Because it is linear in the parameters — x squared is just another column — Linearity refers to how the parameters enter the model. Treat x squared as a new feature and ordinary least squares applies with no modification.

  2. Training MSE keeps falling as you raise the degree while CV MSE starts rising after degree 10. What is happening?

    Show answer

    B — Overfitting: the extra capacity is fitting noise that does not generalise — Training error must fall with capacity. When held-out error turns upward, the extra flexibility is being spent memorising noise.

  3. You have 10 features and set degree=3. Roughly how many columns does PolynomialFeatures produce?

    Show answer

    C — 286 — The count is binomial(n + d, d) = binomial(13, 3) = 286, including the bias term. The growth is combinatorial, not linear.

  4. Your CV curve is flat between degrees 2 and 10. Which degree should you ship?

    Show answer

    B — Degree 2 — the simplest model within one standard error of the best — Differences inside one standard error are noise. The one-standard-error rule takes the simplest model that is statistically indistinguishable from the best, which is more likely to hold up on new data.

🧪 Try It Yourself

Exercise 1 – Expand features by hand

Exercise 2 – Solve the quadratic fit

Exercise 3 – Build a polynomial pipeline

Exercise 4 – Watch overfitting appear

Exercise 5 – Extrapolate and watch it explode

Recap

  • Adding x2,x3,x^2, x^3, \ldots as columns lets ordinary least squares fit curves — the model stays linear in its parameters.
  • The hand-worked quadratic on five points gives y^=10.2x+2x2\hat{y} = 1 - 0.2x + 2x^2 with R2=0.972R^2 = 0.972.
  • PolynomialFeaturesPolynomialFeatures produces (n+dd)\binom{n+d}{d} terms; 10 features at degree 3 is 286 columns.
  • Choose the degree with cross-validation, then apply the one-standard-error rule to prefer the simplest model in the tie.
  • Learning curves distinguish the two failure modes: converged-and-high is bias, wide-gap is variance.
  • Scale after expanding, and never extrapolate.

Exercise 6 – Apply the one-standard-error rule

Next

Continue to Cost Functions - Mean Squared Error (MSE) — a closer look at the loss every fit on this page was silently minimising, and what changes when you pick a different one.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did