Give a Reusable Calculation a Clear Contract
Copying a calculation into every exercise makes mistakes hard to repair. We will put the rule in a function, state what its inputs mean, and check it on more than one report.
12 sections · 4 questions · 3 coding challenges
The arrow keys move between sections too.
Making Numerical Routines Reusable Without Losing Their Assumptions
BOTIE
NumPy's array conversion and testing interfaces support those contracts. They let a Python function accept array-like input, validate its layout, and compare a result with a small independently checked example. The function remains responsible for its scientific assumptions.
Sources for this history
Can We Explain the Rule Once and Use It Again?
Botie keeps converting new greenhouse reports. If we copy the same calculation into each program, one copy may use a different offset or quietly change the original data. A named function gives the rule one place to live.
The name alone is not enough. The function needs a clear agreement about its inputs, output shape, units, and treatment of invalid data. We will build that agreement as part of the code.
The training and incoming reports are separate invented samples.
Step 1 of 3
Name the input meaning
The function receives Celsius readings in their original greenhouse order.
| Greenhouse | Input |
|---|---|
| North | 12 |
| Middle | 18 |
| South | 24 |
North
- Input
- 12
Middle
- Input
- 18
South
- Input
- 24
Step 2 of 3
Keep the rule in one place
Scale each entry, then shift the zero point.
Scale Celsius by 1.8 Then add 32 For 12 °C: (12 × 1.8) + 32 = 53.6 °F
Step 3 of 3
Check a second report
A useful function works on values beyond its first demonstration.
| Conversion check | Reference Celsius | Expected Fahrenheit |
|---|---|---|
| Freezing reference | 0 | 32 |
| Boiling reference | 100 | 212 |
Freezing reference
- Reference Celsius
- 0
- Expected Fahrenheit
- 32
Boiling reference
- Reference Celsius
- 100
- Expected Fahrenheit
- 212
A reusable function must make its inputs and outputs clear
The same fitted reference explains every output. Collecting both results mirrors a function returning its reference and transformed array.
Will the reference stay fixed for a new report?
The training average has already been learned. Change the incoming readings and check that they are still described relative to that same reference.
BOTIE
Stored reference and centred report
[
12,
[
0,
6
]
]Follow the operations
1. Training readings
- 010
- 114
2. Mean
12
3. New report
- 012
- 118
4. Subtract
- 00
- 16
5. Collect two results
[ 12, [ 0, 6 ] ]6. Result
[ 12, [ 0, 6 ] ]
Workshop cases satisfy the finite, nonempty vector contract. Python is where we define functions and handle invalid inputs.
1. Separate a Function Definition From a Call
def introduces a function. Its parameters name the inputs inside the function; the indented body describes the work. Defining it does not yet perform the calculation. A call supplies actual arguments. return sends a result back to the caller.
Printing and returning are different: print displays something; return makes the result available to the next calculation. A function that only prints cannot be used as a numerical result in the same way.
Define the conversion, then use its returned array
asarray accepts a list or array and requests floating-point values. The arithmetic creates the output without modifying the incoming readings.
import numpy as np
def to_fahrenheit(celsius):
values = np.asarray(celsius, dtype=float)
return values * 1.8 + 32
first = to_fahrenheit([12,18,24])
reference = to_fahrenheit([0,100])
print(np.round(first,1).tolist())
print(reference.tolist())[53.6, 64.4, 75.2]
[32.0, 212.0]The function has one local parameter, but two calls provide different data. Neither call needs a separate copy of the conversion rule.
2. Reject an Input That Does Not Match the Promise
Suppose the function promises one nonempty vector of finite readings. A matrix, an empty vector, or a NaN should produce a useful error. Check these conditions before performing the calculation.
A raise statement stops the function with an exception. ValueError is appropriate when an argument's value or arrangement is unsuitable. A caller can catch a particular exception with try and except when it has a meaningful response.
Make the failed assumption visible
The three checks are separate because their error messages describe different problems. len is not a substitute for ndim: a matrix also has a length.
import numpy as np
def checked_readings(values):
array = np.asarray(values, dtype=float)
if array.ndim != 1:
raise ValueError("Supply one vector of readings.")
if array.size == 0:
raise ValueError("Supply at least one reading.")
if not np.isfinite(array).all():
raise ValueError("Every reading must be finite.")
return array
print(checked_readings([0,18]).tolist())
try:
checked_readings([[12,18]])
except ValueError as error:
print(str(error))[0.0, 18.0]
Supply one vector of readings.The all reduction turns the finite mask into one Boolean, so Python's not is appropriate here. The error describes the input contract instead of letting a later shape failure obscure it.
3. Return the Reference Along With the Result
Some calculations first learn a reference, then apply it elsewhere. If Botie centres each new report on its own average, comparisons no longer share the same reference. Learn from the training readings once and pass that value to the next operation.
A function can return more than one result in a tuple. The caller can unpack that tuple into names that keep their meanings distinct.
Keep the fitted reference separate from the new report
This example accepts already validated, nonempty finite vectors. The training average is learned before the new report is transformed.
import numpy as np
def centre_from_training(training, report):
training = np.asarray(training, dtype=float)
report = np.asarray(report, dtype=float)
reference = training.mean()
return reference, report - reference
reference, centred = centre_from_training([10,14], [12,18])
print(reference)
print(centred.tolist())12.0
[0.0, 6.0]The reference is one scalar and the centred report is an array. Returning both makes it possible to inspect the fitted value and reuse it without recalculating it from later observations.
4. Test More Than the First Demonstration
A check should describe what the function must do, not merely repeat the same implementation. Use reference cases with independently known answers, then include a changed input and a boundary case. Check shapes and whether the input was preserved.
A loop is useful here because we want to run a collection of separate test cases. It is not the numerical operation we are trying to vectorise.
Check familiar reference temperatures and input preservation
A test case is a pair of input and expected-output lists. The loop unpacks each pair, calls the function, and checks its contract. The final report only prints if every assertion succeeds.
import numpy as np
def to_fahrenheit(celsius):
return np.asarray(celsius, dtype=float) * 1.8 + 32
cases = [([0,100],[32,212]), ([-40],[-40])]
for values, expected in cases:
original = np.array(values, dtype=float)
saved = original.copy()
result = to_fahrenheit(original)
assert result.shape == original.shape
np.testing.assert_allclose(result, expected, rtol=0, atol=1e-10)
assert np.array_equal(original, saved)
print("2 cases passed; inputs preserved.")2 cases passed; inputs preserved.The tests do not fit a model or prove the conversion for every conceivable input. They check concrete expectations that would catch common mistakes in the rule and its handling of arrays.
5. Does Calling the Function Change Its Input?
Botie centres a training array in a function, then gives the original array to a second model. If the function changed its input in place, that second model now receives centred data too. The first function can return the right result and still cause this surprise.
We need to decide whether changing the caller's array is part of the function's job. If it promises to leave the input alone, create the result separately. If an in-place change is deliberate, say so in the function's documentation so its caller can keep a copy when needed.
6. Use the Same Reference for Every Incoming Batch
A reusable transformation should receive the reference it needs rather than quietly learning a new one on every call. Separate fitting the mean from subtracting it. The same transformation can then handle a full test table or one row at a time.
Test that those two routes agree. This catches a common hidden dependency on the current batch, which would make one greenhouse's prediction change when an unrelated greenhouse is submitted beside it.
Transform together or separately
These means were learned previously and remain fixed.
import numpy as np
def centre(X, mean):
X = np.asarray(X, dtype=float)
mean = np.asarray(mean, dtype=float)
if X.ndim != 2 or mean.shape != (X.shape[1],):
raise ValueError('Expected rows and one mean per column')
return X - mean
X = np.array([[12., 20.], [18., 22.]])
mean = np.array([10., 18.])
batch = centre(X, mean)
separate = np.concatenate([centre(X[i:i+1], mean) for i in range(len(X))])
print(batch.tolist())
print(bool(np.array_equal(batch, separate)))[[2.0, 2.0], [8.0, 4.0]]
TrueThe function applies the supplied reference. Learning that reference belongs to an earlier step with its own training data.
Check yourself
Questions on Parts 1 to 6
Question 1 of 4
What does the caller receive?
Use this setup. The code is given; its result is for you to work out.
import numpy as np
def convert(values):
return np.asarray(values, dtype=float) * 1.8 + 32
result = convert([0,100])Try it with Botie
Build with Botie
Write and run Python