A fictional sensor reports numbers around 102 to 503, while a reference instrument reports temperatures from 0 to 40 °C. How can we convert a new raw reading into an estimated temperature?
We will fit a straight line with NumPy, inspect every residual, and plot the relationship with Matplotlib. The data is fictional and the script is self-contained. This is a numerical learning example, not a calibration procedure for medical, industrial, or other safety-critical equipment.
1. Write the model before calling the solver
Our assumption is temperature = slope × raw reading + intercept. Each observation contributes one equation. A row of the design matrix therefore contains [raw reading, 1], and the coefficient vector contains [slope, intercept] in that order.
Five observations give us five equations for two unknowns. Real measurements rarely sit on exactly the same straight line, so trying to satisfy every equation exactly would be the wrong goal. Least squares chooses coefficients that minimize the sum of squared temperature residuals.
np.linalg.lstsq handles this overdetermined system directly. We also inspect its returned rank. Rank below two means the data cannot uniquely identify both coefficients. For example, five identical raw readings would not establish a slope.
2. Keep errors in understandable units
A @ coef computes fitted temperatures for the original observations. This script defines residual as reference minus fitted, so a positive residual means the model predicted too low. State the sign convention: reversing it is valid too, but silently mixing conventions makes debugging confusing.
The root mean squared error, RMSE, squares the residuals, averages them, then takes the square root. The result is back in degrees Celsius. Squaring gives large errors more influence than small ones, which is one reason unusual observations deserve investigation.
Run the complete example
Save this as sensor_calibration.py in PythonIDE. The script uses NumPy and Matplotlib and makes no network requests.
import numpy as np import matplotlib.pyplot as plt # Fictional sensor readings and reference temperatures. raw = np.array([102, 203, 301, 402, 503], dtype=float) reference = np.array([0, 10, 20, 30, 40], dtype=float) # Model: temperature = slope * raw_reading + intercept. # Each row is [reading, 1]; lstsq fits both coefficients. A = np.column_stack([raw, np.ones(raw.size)]) coef, _, rank, _ = np.linalg.lstsq(A, reference, rcond=None) if rank < 2: raise ValueError('Need at least two distinct readings') slope, intercept = coef fitted = A @ coef residuals = reference - fitted rmse = np.sqrt(np.mean(residuals ** 2)) # Predict inside the reference range, not far beyond it. new_reading = 350 estimate = slope * new_reading + intercept print('SENSOR CALIBRATION: FICTIONAL DATA') print(f'Temperature = {slope:.6f} * raw {intercept:+.4f}') print(f'Training RMSE: {rmse:.4f} C') print(f'Raw {new_reading} -> {estimate:.2f} C') print('\nReference / fitted / residual (C):') for actual, guess, error in zip(reference, fitted, residuals): print(f'{actual:5.1f} / {guess:7.3f} / {error:+.3f}') print('\nSmall training error is not a validation test.') # Display the observations and the fitted relationship. x = np.linspace(raw.min(), raw.max(), 100) fig, ax = plt.subplots(figsize=(7, 4)) ax.scatter(raw, reference, label='Reference observations') ax.plot(x, slope*x + intercept, color='orange', label='Least-squares fit') ax.scatter([new_reading], [estimate], marker='x', s=90, color='red', label='New reading') ax.set(title='Sensor calibration (fictional data)', xlabel='Raw sensor reading', ylabel='Temperature (C)') ax.legend() ax.grid(alpha=0.2) plt.tight_layout() plt.show() Results from the PythonIDE run
The actual run produced temperature = 0.099897 × raw − 10.1890, a training RMSE of 0.0734 °C, and a prediction of 24.78 °C for raw reading 350. Rounded residuals were −0.001, −0.090, +0.120, +0.030, and −0.059 °C.
These are training-fit results for fictional observations, not an independent accuracy claim.
3. Read the chart alongside the numbers
The scatter points are reference observations. The orange line is the fitted relationship over the observed raw-reading range. The red cross is the model's estimate for a new raw reading of 350.
That cross is a prediction, not an independently measured temperature. The raw value lies between the observed endpoints, so this is interpolation. It still depends on the linear model and on measurements being representative of the conditions where the model will be used.
The printed table lets you inspect whether errors have a pattern. A curve-shaped residual pattern could suggest that a straight line is inadequate, even when the chart initially looks convincing.
4. A small training error is not validation
The RMSE uses the same five observations that determined the coefficients. It measures fit to those observations. It does not prove that predictions on new measurements will be accurate.
For an independent check, collect additional raw/reference pairs that were not used in fitting. Compare their prediction errors separately. Measurements far beyond the observed range, changing sensor conditions, and errors in the raw readings can all break the simple model. Ordinary least squares here does not model uncertainty in both axes.
Try one change
Change one reference temperature by 5 degrees. Inspect the coefficient changes, each residual, the RMSE, and the plot. Does the fitted line move even at the observations you did not edit?
Then replace every raw reading with 200. Explain why the rank check fails before trying to bypass it. More repeated observations are not the same as more independent information.
I build PythonIDE, a Python coding app for iPhone and iPad. The numerical ideas here work in other Python environments with NumPy and Matplotlib too.
AI assisted with drafting and translation. The example was run and its output checked in PythonIDE.






