AnyLearn
All lessons

Conditioning and Stability: Whose Fault Is the Error

Two different things can make a numerical answer inaccurate: the problem may amplify any input perturbation, or the algorithm may introduce its own. They are separate quantities, one belongs to the problem and one to the code, and only the second is fixable. This lesson separates them and shows what follows.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

Two separate causes

When a numerical result is inaccurate, there are exactly two candidates, and they demand opposite responses.

The algorithm may be at fault: a sequence of operations that loses precision unnecessarily, like the textbook quadratic formula from the second lesson. This is a bug, and rewriting the computation fixes it.

The problem may be at fault: the true mathematical answer may be so sensitive to its inputs that no algorithm could do better, because the inputs themselves are only known to sixteen digits. This is not a bug, and no rewriting fixes it.

Conflating the two wastes effort in both directions. People rewrite stable code chasing an error that was inherent to the problem, and they accept inherent-looking errors that were actually a fixable algorithm.

Numerical analysis separates them with two named quantities: conditioning describes the problem, stability describes the algorithm. Getting these apart is the single most useful idea in the subject.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. Two separate causes

When a numerical result is inaccurate, there are exactly two candidates, and they demand opposite responses.

The algorithm may be at fault: a sequence of operations that loses precision unnecessarily, like the textbook quadratic formula from the second lesson. This is a bug, and rewriting the computation fixes it.

The problem may be at fault: the true mathematical answer may be so sensitive to its inputs that no algorithm could do better, because the inputs themselves are only known to sixteen digits. This is not a bug, and no rewriting fixes it.

Conflating the two wastes effort in both directions. People rewrite stable code chasing an error that was inherent to the problem, and they accept inherent-looking errors that were actually a fixable algorithm.

Numerical analysis separates them with two named quantities: conditioning describes the problem, stability describes the algorithm. Getting these apart is the single most useful idea in the subject.

2. Conditioning: how much the problem amplifies

The condition number measures how much a small relative change in the input changes the output, relatively. It is a property of the mathematical problem and the specific input, and it exists whether or not a computer is involved.

A condition number of 1 means input and output perturbations are the same size. A condition number of 10 to the 8 means a relative input perturbation of 10 to the minus 16 becomes a relative output perturbation of 10 to the minus 8: eight of your sixteen digits are gone before any algorithm runs.

The rule of thumb follows directly. Digits lost is roughly the base-10 logarithm of the condition number. Sixteen available digits, a condition number of 10 to the 12, and four digits survive.

What makes this a hard limit is that your inputs are already inexact. They were measured, or they were rounded on entry, so a perturbation of at least machine epsilon is present before the computation begins. The problem then amplifies it by its condition number, and no algorithm can un-amplify it.

An ill-conditioned problem is one where that factor is large. Its answer is genuinely uncertain given the precision of its inputs.

3. Seeing it happen

A two by two system makes the effect visible without any linear algebra machinery.

def solve2(a, b, c, d, e, f):
    """Solve [a b; c d] x = [e f] by Cramer's rule."""
    det = a*d - b*c
    return ((e*d - b*f) / det, (a*f - e*c) / det)

# two nearly parallel lines
solve2(1.0, 1.0, 1.0, 1.0000001, 2.0, 2.0000001)
# (1.000000002220446, 0.999999997779554)     true answer: (1, 1)

# nudge one right-hand side entry by 1e-7
solve2(1.0, 1.0, 1.0, 1.0000001, 2.0, 2.0000002)
# (0.0, 2.0)

A change of 1e-7 in one input moved the answer from (1, 1) to (0, 2). The output moved by order 1 in response to an input change of order 1e-7, so the amplification factor is about 10 to the 7.

Geometrically the two equations are nearly parallel lines, and their intersection point slides enormously when either line tilts slightly. That is what ill-conditioning is, and it is a fact about the geometry rather than about the code.

Nothing here is fixable by a better solver. The question "where do these two nearly parallel lines cross" does not have a well-determined answer when the lines are known to only seven digits.

4. Stability: what the algorithm adds

Stability is the complementary property, and it belongs to the algorithm rather than the problem.

The useful formulation is backward error analysis, developed by James Wilkinson. Rather than asking how far the computed answer is from the true one, it asks a different question: for what nearby problem is this computed answer the exact solution?

An algorithm is backward stable when the answer it produces is the exact answer to a problem within rounding distance of the one you asked. That is the strongest guarantee a finite-precision algorithm can offer, and it is achievable.

The reframing is powerful because it separates the two concerns cleanly. A backward stable algorithm has done everything possible: it solved a problem indistinguishable from yours, given your input precision. If the answer is still inaccurate, the amplification came from conditioning, and the algorithm is exonerated.

An unstable algorithm produces an answer that is not the exact solution to any nearby problem. It manufactured error. The textbook quadratic formula does this: its bad root is not the exact root of any quadratic close to the one supplied.

5. Diagnosing an inaccurate result

Reading the diagram in order prevents the two most common wasted efforts.

The left path is a genuine bug with a genuine fix. The quadratic formula, naive summation, and 1 - cos(x) all live here, and rewriting recovers the accuracy.

The box at the bottom right is the one people resist. If the problem is ill-conditioned and the algorithm is backward stable, the inaccuracy is real, unavoidable, and correctly reported. Switching libraries will not help, and higher precision only buys digits proportional to the extra precision, which is a delaying tactic rather than a fix.

The response there is to change the question. Reformulate to a better-conditioned equivalent, add regularisation to trade a little bias for far more stability, or improve the inputs.

That last option is the one most often overlooked. If the condition number is 10 to the 8 and the inputs are known to six digits, the answer is meaningless regardless of the arithmetic, and the fix is measurement rather than code.

flowchart TD
A["Answer is inaccurate"] --> B["Is the problem ill-conditioned?"]
B --> C["No: the algorithm is unstable"]
C --> D["Rewrite the computation"]
B --> E["Yes: is the algorithm backward stable?"]
E --> F["Yes: this is the best achievable"]
F --> G["Get better inputs or reformulate the problem"]
E --> H["No: fix the algorithm, then re-evaluate"]

6. The four combinations

Crossing the two properties gives four cases, and each has a different correct response.

Stable algorithmUnstable algorithm
Well-conditioned problemAccurate. Nothing to do.Inaccurate, and entirely your fault. Fix the code.
Ill-conditioned problemInaccurate, and nobody's fault. Change the problem or the inputs.Inaccurate for two independent reasons. Fix the code first.

The bottom right cell is where debugging goes wrong. With both problems present, rewriting the algorithm improves matters and does not make the result accurate, which reads as evidence the rewrite failed. It did not; the remaining error is the conditioning.

The correct sequence is to fix stability first, then measure what is left and compare it against what the condition number predicts. If they agree, you are done, and the residual inaccuracy is a property of the question rather than a defect.

The top right cell is the encouraging one, and it is more common than expected: an ordinary, well-behaved problem being computed badly. Every example in the second lesson was this case, and every one was fixed by rearranging the arithmetic without any change to precision.

7. Estimating both in practice

Neither quantity requires theory to approximate, and both are worth measuring before optimising anything.

Conditioning by perturbation. Nudge each input by a relative amount around 10 to the minus 8, rerun, and observe the relative change in the output. The ratio estimates the condition number directly, and the two-by-two example above was exactly this experiment. It costs a handful of extra runs and answers the question that determines everything else.

Stability by the residual. Substitute the computed answer back into the original problem. A backward stable algorithm produces a small residual even on an ill-conditioned problem, because it solved a nearby problem exactly. A large residual indicates instability rather than conditioning.

That pairing is diagnostic. Small residual plus inaccurate answer means ill-conditioned and stable, so stop optimising the code. Large residual means the algorithm is at fault, whatever the conditioning.

For library routines, the work is usually already done. Standard implementations of matrix factorisations, linear solvers and eigenvalue problems are backward stable and documented as such, and many expose a condition number estimate alongside the answer. Reading it is the cheapest possible version of this analysis.

8. What the path establishes

Four lessons, from representation to diagnosis.

A float is a finite value from a logarithmically spaced set, and every operation is correctly rounded to about sixteen relative digits. That guarantee is per-operation and relative, which leaves the loophole: when a result is much smaller than its inputs, inherited error is promoted from insignificant to dominant. Comparison then needs a tolerance, and no universal one exists because "close enough" is a claim about the problem. And accuracy itself has two independent causes, only one of which is in your code.

The transferable habit is a single question asked in the right order. Is this problem well conditioned? If yes, any inaccuracy is a bug and worth hunting. If no, find out how many digits the conditioning permits, and stop when you have them.

That inverts the usual instinct, which is to reach for higher precision when results look wrong. Higher precision helps only in the well-conditioned case, where it was rarely needed, and merely delays the problem in the ill-conditioned one, where the answer was never determined by the inputs to begin with.

The standard reference for going further is Nicholas Higham's Accuracy and Stability of Numerical Algorithms, which works through the analysis for essentially every algorithm in common use.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What does the condition number describe?
    • How much the mathematical problem amplifies a relative input perturbation into a relative output change
    • How much rounding error a particular algorithm introduces
    • The number of operations a computation performs
    • The precision of the floating point format in use
  2. A 2x2 system's solution moves from (1,1) to (0,2) when one input changes by 1e-7. What does that show?
    • The solver is unstable and should be replaced
    • The problem is ill-conditioned: the equations are nearly parallel and the intersection slides enormously
    • Cramer's rule is never appropriate for 2x2 systems
    • The inputs underflowed during the determinant calculation
  3. What does backward error analysis ask?
    • How far the computed answer is from the true answer
    • How many operations were performed before the error appeared
    • For what nearby problem is the computed answer the exact solution
    • Whether the error grows or shrinks as precision increases
  4. A computation is inaccurate but substituting the answer back gives a tiny residual. What follows?
    • The algorithm is unstable and should be rewritten
    • The inputs contain NaN or infinity
    • Higher precision will fix the result
    • The algorithm is backward stable, so the inaccuracy comes from conditioning and optimising the code will not help
  5. Why is reaching for higher precision usually the wrong first response to an inaccurate result?
    • It helps only when the problem is well conditioned, where it was rarely needed, and merely delays the ill-conditioned case
    • It changes the rounding mode in ways that break IEEE 754
    • It cannot be applied to library routines
    • It always introduces new cancellation

Related lessons

Computer Science
advanced

Cancellation: Where the Digits Actually Go

Every operation is correctly rounded, yet results can be wrong in the first digit. The reason is that subtracting nearly equal numbers is exact and still catastrophic: it does not create error, it promotes error that was already there. This lesson builds that mechanism and the summation techniques that recover the lost accuracy.

8 steps·~12 min
Computer Science
advanced

Comparing Floats, and Testing Code That Uses Them

Equality fails on floats, and both standard replacements fail too: absolute tolerance breaks at scale, relative tolerance breaks near zero. This lesson works through why each fails, what the combined form actually does, and how to choose a tolerance from the problem rather than copying a magic constant.

8 steps·~12 min
Computer Science
intermediate

How a Float Is Stored, and What That Rules Out

Floating point is not a slightly inaccurate version of real numbers. It is a finite set of values, spaced logarithmically, with exact rules. This lesson builds the IEEE 754 layout, shows why 0.1 cannot be represented, and derives the one error bound that every later result depends on.

8 steps·~12 min
Programming
advanced

Refs: A Branch Is a File Containing a Hash

Hashes are unusable by hand, so Git names them. A branch is not a copy of anything, not a directory, and not a container for commits: it is a forty-character file. Once that is clear, checkout, reset, detached HEAD and the reflog stop being separate concepts and become one operation on one kind of file.

8 steps·~12 min