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.
10 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.
Keep the expected result
We want a converted reading to match its reference closely.
Reference
- 20
Calculated
- 20.0001
What we can now calculate
The largest error represents the worst entry. Putting tolerance first in the threshold comparison includes equality at the boundary.
Explore the full example
Choose an example, then change a value. Follow the calculation below to see which result changes and why. These are small teaching examples; the construction later asks you to choose the operations yourself.
BOTIE
Result from these values
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. Editing this example does not change the workshop tests.
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.
Check yourself
Questions on Parts 1 to 4
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