Least-Squares Curve Fitting
Learning Objectives
- Set up a curve fit as for any model linear in its parameters, with the rank hypothesis it needs.
- Show that , and measure how many digits the normal equations lose against QR on an ill-conditioned fit.
- Solve a least-squares problem by QR, by hand and in code, and check it against
numpy.linalg.lstsq. - Explain how the size of the residual changes the accuracy of every method.
- Choose a basis and a scaling so that the fit is well conditioned, and recognize when no basis can save it.
Context & Motivation
least-squares-via-linear-algebra derived the normal equations twice, by projection and by gradient, and linear-regression-as-least-squares wrote the closed form for a linear model. Those lessons settle what the answer is. This one asks how to compute it in float64, and the formula turns out to be a poor algorithm. It is the fitting counterpart of the Hilbert matrix story in the-matrix-condition-number.
Fitting also finishes what interpolation started. polynomial-interpolation-lagrange-and-newton and splines force the curve through every point, which is the wrong goal when the data has noise. With fewer parameters than points, the curve smooths instead.
Core Theory
The problem
Take a model , linear in the coefficients and possibly not in , and data for with . With and , find minimizing . Hypothesis: has full column rank. Then the minimizer is unique. Without it there are infinitely many (Example 6).
Why the normal equations lose digits
From the SVD we get , so for full column rank
Forming and solving by Cholesky takes about flops. It is cheap, but the problem handed to the solver is conditioned like the square of the original, and the error scales accordingly. With , is at the edge of what float64 can tell from singular.
QR
Write in reduced form: is with orthonormal columns and is upper triangular. Full column rank makes invertible. Substituting into the normal equations and cancelling leaves
Nothing gets squared in this step: , because multiplying by preserves 2-norms. Householder QR costs about flops, roughly twice the normal equations when . numpy.linalg.lstsq goes further and works from the singular values, which also reveal numerical rank.
What the residual does
The sensitivity of least squares itself is not simply . It has a term near and a term in , where is the angle between and the range of . The second term takes over once is about , which is for , so a residual that looks tiny can already trigger it. So QR is not always immune to : it matches the true sensitivity of the problem, while the normal equations pay for regardless (Example 3).
The basis
Polynomials of degree at most form one space, but each basis gives a different . A monomial basis on has columns that nearly coincide, and the Gram matrix tends to the Hilbert matrix as grows. Chebyshev polynomials on the data's interval keep the columns far from parallel, though they are not orthogonal on equispaced data: at on 100 equispaced points the largest cosine between two columns is , against for monomials on . Changing basis is a change in the problem's conditioning, while changing solver is not.
Worked Examples
Example 1: QR by hand on a familiar fit
Use the four points of least-squares-via-linear-algebra, , which gave . The columns of are and . Gram-Schmidt gives , , . Then , so . With , , and back substitution on
gives , , the same line. numpy.linalg.qr returns with both diagonal entries negative and the same solution. Here and .
Example 2: normal equations against QR on a real fit
Fit degree monomials to equispaced points on , with plus Normal noise of standard deviation (seed 42). The reference is the exact minimizer of the stored and , computed with 250 digits, so the table measures solver error only.
plaintextQ, R = np.linalg.qr(A) x_qr = scipy.linalg.solve_triangular(R, Q.T @ y) c = scipy.linalg.cho_factor(A.T @ A, lower=True) x_ne = scipy.linalg.cho_solve(c, A.T @ y)
Relative errors in the coefficients and in the fitted values :
textd kappa(A) kappa(A^T A) coef NE coef QR curve NE curve QR 4 6.65e+02 4.42e+05 3.90e-12 4.90e-15 1.87e-13 1.07e-15 6 2.07e+04 4.27e+08 2.91e-10 5.47e-14 5.78e-13 9.33e-16 8 6.56e+05 4.31e+11 6.48e-06 5.38e-12 8.03e-09 2.31e-14 10 2.12e+07 4.47e+14 4.35e-03 4.08e-10 7.70e-06 1.36e-12 12 6.90e+08 4.76e+17 2.64e-01 8.68e-08 7.63e-05 2.46e-11 14 2.27e+10 5.16e+20 fails 3.26e-07 fails 9.13e-10
At QR keeps about 9 coefficient digits and the normal equations about 2. At , Cholesky reports the matrix is not positive definite. Falling back to LU there gives coefficient error . The computed residual norm gives the failure away: at that fallback reports , higher than the of , which is impossible for a true minimizer over a larger space. QR gives .
numpy.linalg.lstsq agrees with the QR code: at the coefficients differ by in relative terms. Agreement is not accuracy: both sit from the exact minimizer, and only the high-precision reference shows that. The normal-equation digits are noise too: with the default lower=False, the error at is instead of . Notice that curves are more accurate than coefficients. Errors go mostly into combinations of nearly dependent columns, which barely change .
The link to the Hilbert matrix is measurable. For and midpoints, differs from by at most per entry, and their condition numbers are and .
Example 3: the residual changes the picture
Keep and build . Here is numpy.random.default_rng(0).normal(size=100) with its projection on the range of removed in high precision, then normalized, repeated for 40 successive draws. The residual norm is and . Coefficient errors:
textresidual t NE median QR median QR min QR max QR worse than NE 0 7.17e-03 5.01e-10 (no r, one case) 0 1e-03 6.33e-03 6.56e-09 1.37e-10 2.93e-08 0 of 40 1e-01 6.15e-03 6.44e-07 2.54e-08 2.95e-06 0 of 40 1 5.71e-03 6.44e-06 2.59e-07 2.95e-05 0 of 40 1e+02 4.82e-03 6.44e-04 2.60e-05 2.95e-03 3 of 40 1e+04 6.19e-02 6.44e-02 2.60e-03 2.95e-01 18 of 40
The QR error grows linearly with , the term at work, and lstsq has the same medians from on. Already at the median is 13 times the value at . The slope depends on the direction of , by a factor of about 100 across these draws. The normal equations stay near until the residual dwarfs the fit. The advantage of QR shrinks from about seven orders of magnitude to nothing: at it loses in 18 of the 40 draws.
Example 4: the basis is the real fix
for the same 100 points and the same polynomial space:
textd monomial on [0,1] monomial on [-1,1] Chebyshev on [-1,1] 4 6.65e+02 1.85e+01 2.33 8 6.56e+05 5.31e+02 2.81 12 6.90e+08 1.63e+04 2.92 16 7.56e+11 5.24e+05 2.94
With the Chebyshev basis at , even the normal equations reach coefficient error , against for QR, and numpy.polynomial.Chebyshev.fit matches the QR coefficients to . So the normal equations are not always wrong. On a Chebyshev matrix (), they took s against s for lstsq and s for qr on one machine, with answers differing by . That gap is larger than the flop ratio of 2, partly because qr also forms explicitly. can also be accumulated row by row in memory, which suits streams too long to store.
A good basis has limits. With equispaced points, the Chebyshev is at , at , at and at . At the fitted curve reaches about between samples of data bounded by about 1. Runge's phenomenon is back, in a problem where no basis helps.
Keeping well under avoids it. Max errors for Runge's sampled at equispaced points:
textm d=10 d=20 d=30 d=40 d=60 41 1.03e-01 6.39e-02 4.75e+00 1.05e+05 101 1.02e-01 1.39e-02 5.60e-03 1.53e-02 5.51e+00 401 1.01e-01 1.39e-02 1.91e-03 2.61e-04 1.70e-05
The case , is interpolation, and it reproduces the blow-up of the earlier lesson. The best degree rises with , staying far below it.
Example 5: fitting a trend on Unix timestamps
Take 720 hourly latency samples starting at s, an illustrative series with mean near 135 ms and a quadratic trend. Fit a parabola to raw .
plaintextt = 1.7e9 + 3600 * np.arange(720.0) days = (t - 1.7e9) / 86400 y = 120 + 0.6 * days + 0.02 * days**2 + np.random.default_rng(7).normal(0, 1.5, 720) c, _, rank, _ = np.linalg.lstsq(np.vander(t, 3), y, rcond=None) # rank == 2 p = np.polynomial.Polynomial.fit(t, y, 2) # maps t to [-1, 1]
The raw matrix has . The default cutoff, , removes the smallest singular value with no warning, and the returned rank is 2. The curvature comes back as ms per day$^2$ instead of , the residual norm is instead of , and the forecast one week past the data is ms instead of . Mapping to gives . Polynomial.fit matches the exact fit to ms. numpy.polyfit scales columns internally and gets ms.
Example 6: when the hypotheses fail
Rank deficiency. Take and with a cubic: four coefficients, three distinct abscissas, rank 3. Infinitely many cubics share the best fitted values. Cholesky fails, unpivoted QR gives and coefficients up to , and lstsq reports rank 3 and returns the minimum-norm solution, in ascending powers, . numpy.polyfit warns that the fit may be poorly conditioned.
A model not linear in its parameters. is not of the form above. Taking logarithms makes it linear, but it needs and minimizes a different sum. For x = np.linspace(0, 4, 30) and y = 2 * np.exp(0.8 * x) + np.random.default_rng(11).normal(0, 1, 30), the log fit gives , with squared error . A direct fit with scipy.optimize.curve_fit gives , with squared error .
Common Misconceptions & Pitfalls
- " is how to compute the fit." It defines the answer. Example 2 shows it losing about twice the digits of QR once is large.
- "Normal equations are always bad." With a well-conditioned they match QR and cost less (Example 4).
- "QR always gives error ." Once passes about , the term dominates, even for a residual that looks tiny (Example 3).
- "Any QR will do." At the loss of orthogonality is for classical Gram-Schmidt, for modified Gram-Schmidt and for Householder. Solving through gives coefficient errors of , and .
- "
lstsqwarns when something is wrong." It returnsrankand raises no warning. Example 5 lost a whole degree of freedom silently, so checkrankagainst the number of columns. - "A smaller residual means a better model." The minimum residual can only fall as rises, while the fit at in Example 4 swings to about between samples.
- "Book vs today." The NumPy documentation marks
numpy.polyfitas part of the old polynomial API and prefersnumpy.polynomial, whosefitmethods map the data to a standard window.
Summary
Minimizing has a unique solution when has full column rank, and the normal equations characterize it without being the right way to compute it, because . On a monomial fit with , the normal equations left coefficient error and QR left . QR solves at about twice the flops, lstsq adds rank detection through singular values, and a residual with above about degrades both through . The strongest tool is the basis: mapping the data to and using Chebyshev polynomials brought from to at , after which even the normal equations work. Keep the degree well below , and check rank.
Documentation Links
- Driscoll and Braun: Fundamentals of Numerical Computation, 3.2 The normal equations
- Driscoll and Braun: Fundamentals of Numerical Computation, 3.3 The QR factorization
- Driscoll and Braun: Fundamentals of Numerical Computation, 3.4 Computing QR factorizations
- MIT OpenCourseWare 18.335J (2019): Week 4
- NumPy documentation: numpy.linalg.lstsq
- NumPy documentation: numpy.linalg.qr
- NumPy documentation: numpy.polyfit
- NumPy documentation: Polynomial.fit
- NumPy documentation: Chebyshev.fit
- SciPy documentation: scipy.optimize.curve_fit