When Two Calculations Almost Agree
A tiny rounding difference, a missing observation, and a wrongly shaped result are different problems. We need checks that distinguish them before trusting a numerical answer.
12 sections · 4 questions · 2 coding challenges
The arrow keys move between sections too.
Representing Real Quantities With Finite Storage
BOTIE
NumPy exposes data types, finite-value checks, and approximate comparisons so a program can account for those limitations. The tolerance is part of our numerical contract, not a way to declare every discrepancy harmless.
Sources for this history
Did the Method Fail, or Did We Ask the Wrong Check?
Botie converts a report, then reverses the conversion to check the original readings. The recovered numbers may differ by a tiny rounding amount. An exact comparison can reject a perfectly useful calculation, but accepting every small-looking difference would also be careless.
We need to choose what agreement means for the task. That includes the arrangement of the result, the kind of numbers it stores, whether its entries are finite, and how much numerical error we allow.
Original and recovered temperatures are paired illustrative values.
Step 1 of 3
Keep the expected result
We want a converted reading to match its reference closely.
Reference: 20 Calculated: 20.0001 Absolute difference = |20.0001 − 20| = 0.0001
Step 2 of 3
State an allowed difference
An absolute tolerance is measured in the result's own units.
Absolute difference: 0.0001 Allowed difference: 0.001 0.0001 ≤ 0.001: passes
Step 3 of 3
Check the whole contract
Check the shape and whether every value is finite as well as comparing the printed result. A plausible number in the wrong layout can still break the next calculation.
- Checks
- shape
- finite values
- tolerance
Tolerance is a stated requirement for a comparison
The largest error represents the worst entry. Putting tolerance first in the threshold comparison includes equality at the boundary.
How close is close enough?
Two routes through a calculation may differ slightly. Change one recovered reading or the permitted error and find the point where the comparison stops passing.
BOTIE
Whether every reading meets the absolute tolerance
1
Follow the operations
1. Allowed absolute difference
0.001
2. Recovered readings
- 020.0001
- 118.0002
3. Reference readings
- 020
- 118
4. Subtract
- 00.0001
- 10.0002
5. Absolute value
- 00.0001
- 10.0002
6. Largest value
0.0002
7. Threshold gate
1
8. Result
1
Workshop inputs are finite, equally sized vectors. Python adds shape, dtype, missing-value, and relative-tolerance checks.
1. Choose Storage Before Losing Information
The dtype controls the representation used for every entry in an ordinary numerical array. Assigning a fraction into integer storage discards its fractional part. Converting the array afterwards cannot recover information that has already been lost.
Fixed-width integer types also have limits, unlike Python's arbitrary-precision integers. Choose a type that can represent the required range. Narrowing a type merely to save memory can change the calculation.
Convert before storing fractional corrections
astype creates a converted array by default. np.iinfo reports the range of an integer type; it does not change that type's limits.
import numpy as np
integer_report = np.array([12,18,24], dtype=int)
integer_report[0] = 12.5
float_report = np.array([12,18,24], dtype=float)
float_report[0] = 12.5
print(integer_report.tolist())
print(float_report.tolist())
print(np.array([12,18]).astype(float).tolist())
print(np.iinfo(np.int8).max)[12, 18, 24]
[12.5, 18.0, 24.0]
[12.0, 18.0]
127The integer assignment lost the half-degree reading. Choose floating-point storage before the correction. An eight-bit signed integer cannot represent a count above the printed limit.
2. Distinguish Absence From a Measured Zero
NaN means not a number. It can mark an absent numerical observation or result from invalid arithmetic. Infinity can result from an unbounded or invalid numerical calculation. Neither is an ordinary finite thermometer reading.
NaN does not compare equal to itself. Use isnan to identify it and isfinite to require an ordinary finite value. Replacing these values is a modelling decision, so detecting them must come before silently filling them.
Inspect the invalid positions explicitly
We keep a real zero beside a missing marker and an infinity. nanmean ignores NaNs, but it does not generally ignore infinities; we use a separate finite mask when that is the intended selection.
import numpy as np
report = np.array([0., np.nan, 18., np.inf])
print(np.isnan(report).tolist())
print(np.isfinite(report).tolist())
print(report[np.isfinite(report)].tolist())
print(np.nanmean(np.array([0., np.nan, 18.])))[False, True, False, False]
[True, False, True, False]
[0.0, 18.0]
9.0The measured zero stays in the selected report. Ignoring missing entries changes which observations contribute; it does not prove the remaining observations represent the missing ones. An all-missing mean is still undefined.
3. Choose an Absolute and a Relative Tolerance
An absolute tolerance permits a fixed error in the result's units. A relative tolerance permits an error proportional to the reference value. np.isclose compares the difference with their combined allowance, using the second argument as the reference.
Use an explicit absolute tolerance near zero, and choose it for the required precision. allclose asks whether every aligned comparison passes. array_equal instead checks exact shape and value equality.
Inspect rounding, then apply a stated tolerance
The first comparison exposes a familiar binary rounding difference. For the temperature checks we allow an absolute error of one thousandth of a degree and no relative allowance.
import numpy as np
print(0.1 + 0.2 == 0.3)
print(np.isclose(0.1 + 0.2, 0.3, rtol=0, atol=1e-12))
actual = np.array([20.0001, 18.01])
expected = np.array([20., 18.])
print(np.isclose(actual, expected, rtol=0, atol=0.001).tolist())
print(np.allclose(actual, expected, rtol=0, atol=0.001))False
True
[True, False]
FalseOne temperature passes and the other does not. Increasing the tolerance until both pass would change the requirement instead of diagnosing the discrepancy.
4. Check the Shape Before Comparing Values
Approximate comparisons can broadcast compatible inputs. That is useful for some numerical checks but dangerous if a wrong result shape is supposed to fail. Confirm shape and finiteness first, then compare values.
Python's assert raises an AssertionError when a condition is false. It is useful in these learning checks and tests. For validation that must run in a deployed program, use an explicit condition and raise an error, because Python can disable assertions when run with optimisation.
Make three independent expectations explicit
The wrong-shaped reference can broadcast and agree numerically, so agreement alone does not establish that our function returned the required vector. The assertions check the correctly shaped result separately.
import numpy as np
actual = np.array([20., 20.])
wrong_shape = np.array([[20.], [20.]])
print(np.allclose(actual, wrong_shape))
print(actual.shape == wrong_shape.shape)
expected = np.array([20., 20.])
assert actual.shape == expected.shape
assert np.isfinite(actual).all()
np.testing.assert_allclose(actual, expected, rtol=0, atol=0.001)
print("Shape, finite values, and tolerance checked.")True
False
Shape, finite values, and tolerance checked.The testing helper raises an error when its numerical check fails and returns normally when it passes. A separate shape check makes our intended contract clear regardless of a comparison helper's broadcasting rules.
5. A Small Dtype Can Change the Arithmetic
Image values often arrive as unsigned eight-bit integers. Those integers have a limited range and cannot represent a negative difference. If we subtract before changing the dtype, we may already have lost the answer we wanted. Converting the result afterwards cannot undo that overflow.
We need a signed or floating-point representation before the subtraction. When adding many values, we also need a type large enough for their total. Python finishing the calculation without an exception does not tell us whether the chosen representation was suitable.
6. Move Exponentials into a Safe Range
Later, probabilities will be calculated from exponentials of model scores. Large positive scores can overflow even when the final probabilities are ordinary numbers. Subtracting the largest score leaves every probability ratio unchanged while making the largest exponential one.
This is an example of changing the numerical route while keeping the mathematical quantity. A tolerance check belongs after such a calculation, together with checks for finiteness and the expected shape.
Normalise two large scores
The absolute level of the scores is unnecessary for this normalisation.
import numpy as np
scores = np.array([1000., 1001.])
weights = np.exp(scores - scores.max())
probabilities = weights / weights.sum()
print(np.round(probabilities, 4).tolist())
print(bool(np.isfinite(probabilities).all()))[0.2689, 0.7311]
TrueThe difference between the scores determines their relative probabilities. Computing exp(1001) directly would add a numerical problem without adding information.
Check yourself
Questions on Parts 1 to 6
Question 1 of 4
What happens to the fractional part?
Use this setup. The code is given; its result is for you to work out.
import numpy as np
readings = np.array([12,18,24], dtype=int)
readings[0] = 12.5Try it with Botie
Build with Botie
Write and run Python