Skip to content
Fondazione ITS INCOM

Examples · Internet of Things

Spot a drifting sensor

Four weeks of daily means from two CO2 sensors side by side go in, and a maintenance note comes out. Python computes the difference, its trend and which sensor is wrong; a model writes the note, and your code checks the sensor and the drift it names. Qwen3.8 and Gemma compared.

Intermediate · 35 min

The idea

A CO2 sensor does not break with a bang. It can drift: a few ppm a week, every week, and after a few months it says the air is fine when it is not.

One sensor alone cannot tell you that it drifts. Two sensors side by side can: they should read the same, and if the gap between them grows day after day, one of them is drifting. Which one? The empty school at the weekend tells you. So the work is split into three parts:

  1. your code computes the daily difference, its trend in ppm a week, and which sensor is wrong;
  2. a model writes the maintenance note for the technician, starting with a fixed sentence: “Sensor X drifts by N ppm per week.”;
  3. your code checks that sentence: the right sensor, and the drift within rounding.

What you need

A key and the openai library, as in Your first API call in Python.

The readings

Two CO2 sensors, A and B, hang next to each other on the same wall of Aula 12. Every evening the school's server saves the mean of each one from 08:00 to 18:00. Save four weeks of them as daily.csv:

date,sensor_a,sensor_b
2026-09-07,878,880
2026-09-08,789,795
2026-09-09,826,830
2026-09-10,905,911
2026-09-11,789,794
2026-09-12,424,437
2026-09-13,417,424
2026-09-14,852,858
2026-09-15,766,775
2026-09-16,772,785
2026-09-17,773,786
2026-09-18,823,836
2026-09-19,413,430
2026-09-20,415,433
2026-09-21,889,908
2026-09-22,748,770
2026-09-23,983,1003
2026-09-24,829,852
2026-09-25,762,788
2026-09-26,406,430
2026-09-27,399,424
2026-09-28,860,889
2026-09-29,829,855
2026-09-30,865,899
2026-10-01,736,768
2026-10-02,883,919
2026-10-03,391,426
2026-10-04,400,438

On school days the two are close, and the gap grows slowly. Now look at the weekends, 12–13 September and 3–4 October: who is wrong?

The program

Save it as drift.py, next to the CSV:

import csv
import datetime
import json
import os
import re
from statistics import linear_regression, mean

from openai import OpenAI

client = OpenAI(
    base_url="https://api.ai.itsincom.org/v1",
    api_key=os.environ["API_KEY"],
)
MODEL = "qwen3.8-27b"
OUTDOOR = 420  # ppm: an empty building cannot be much below the air outside

with open("daily.csv", newline="") as f:
    days = list(csv.DictReader(f))
for d in days:
    d["a"], d["b"] = int(d["sensor_a"]), int(d["sensor_b"])
    d["weekend"] = datetime.date.fromisoformat(d["date"]).weekday() >= 5

# 1. Your code computes: the daily difference, its trend, and the empty-school weekends
difference = [d["a"] - d["b"] for d in days]
per_day, _ = linear_regression(range(len(days)), difference)
per_week = per_day * 7
weekends = [d for d in days if d["weekend"]]
last = weekends[-2:]
far_a = abs(mean(d["a"] for d in last) - OUTDOOR)
far_b = abs(mean(d["b"] for d in last) - OUTDOOR)
suspect = "A" if far_a > far_b else "B"  # the one that is farther from the outdoor air, last weekend

summary = {
    "room": "Aula 12, two CO2 sensors side by side",
    "values": "daily means from 08:00 to 18:00, in ppm",
    "outdoor_ppm": OUTDOOR,
    "days": [{"date": d["date"], "A": d["a"], "B": d["b"], "A_minus_B": d["a"] - d["b"]} for d in days],
    "trend_of_A_minus_B_ppm_per_week": round(per_week, 1),
    "mean_A_minus_B_first_week": round(mean(difference[:7]), 1),
    "mean_A_minus_B_last_week": round(mean(difference[-7:]), 1),
    "weekends_empty_school": [{"date": d["date"], "A": d["a"], "B": d["b"]} for d in weekends],
}
print(f"Trend of A - B: {per_week:+.2f} ppm a week. Suspect, from the weekends: sensor {suspect}.")

# 2. The model writes the maintenance note
answer = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "You write maintenance notes for the technician of a school. Two CO2 "
                                      "sensors hang side by side, so they should read the same; the data "
                                      "shows they do not. Decide which sensor is drifting, and say why. "
                                      "Start the note with exactly this sentence: «Sensor X drifts by N ppm "
                                      "per week.», with X the sensor and N the drift, as a positive number. "
                                      "Then 3 to 5 short sentences in plain English: the evidence, and what "
                                      "the technician should do."},
        {"role": "user", "content": json.dumps(summary)},
    ],
)
if answer.choices[0].finish_reason != "stop":
    raise SystemExit("The answer was cut off.")
note = answer.choices[0].message.content.strip()
print()
print(note)
print()

# 3. Your code finds the sentence, and checks it against its own numbers
found = re.search(r"Sensor ([AB]) drifts by (\d+(?:\.\d+)?) ppm per week", note)
if not found:
    print("Check: the sentence we asked for is not in the note.")
else:
    sensor, claim = found.group(1), found.group(2)
    decimals = len(claim.split(".")[1]) if "." in claim else 0
    print(f"Check, sensor: {sensor}, code says {suspect}:", "OK" if sensor == suspect else "WRONG")
    print(f"Check, drift: {claim}, code says {abs(per_week):.2f}:",
          "OK" if round(abs(per_week), decimals) == float(claim) else "WRONG")

Run it with python3 drift.py. With qwen3.8-27b it takes 20 to 40 seconds or so: the model thinks before it writes.

Within rounding means: round the code's number the way the model rounded its own. The code says 9.06. “9.1” is right to one decimal, “9” to none, “9.2” is wrong.

What you should see

Trend of A - B: -9.06 ppm a week. Suspect, from the weekends: sensor A.

Sensor A drifts by 9.1 ppm per week. On empty-school days the room should sit at the outdoor level of roughly 420 ppm; sensor B stays within 4–18 ppm of that value the entire four weeks, whereas sensor A slides from 424 down to 391. The A-minus-B gap widens steadily from about –6 ppm in week one to –35 ppm by week four, a rate of roughly 9 ppm per week. This pattern points to a slow downward drift in sensor A, not a fault in B. Recalibrate sensor A against a reference analyser, and if the drift reappears within a few days, replace the unit.

Check, sensor: A, code says A: OK
Check, drift: 9.1, code says 9.06: OK

Which sensor is wrong

The difference A − B goes from −2 to −38 ppm in four weeks. That says the two sensors disagree more and more. It does not say who is right: A could be drifting down, or B up.

The weekends decide. The school is empty: nobody breathes in Aula 12, and the air inside slowly becomes the air outside, about 420 ppm. Sensor B reads 424 to 438 on all eight weekend days. Sensor A goes from 424 down to 391: less CO2 than the air outside, in an empty room. That cannot be true, so sensor A is the one drifting.

Your code does the same with one line: the sensor that was farther from 420 ppm on the last weekend is the suspect.

Two models

We ran the program eight times with qwen3.8-27b, and six times with MODEL = "gemma-4-26b". Five of the Qwen runs and three of the Gemma runs were with the first version of the check (see “The format” below); the notes are counted here as they were written:

qwen3.8-27b gemma-4-26b
names sensor A, the right one 8 out of 8 0 out of 6
drift within rounding 8 out of 8 6 out of 6
time for one note 20 to 42 s 1.3 to 1.9 s

Both copied the drift right: the code had already computed it. The difference is in the reasoning. Gemma named sensor B every time, and its evidence was the opposite of the data:

Sensor B drifts by 9.1 ppm per week. The difference between the two sensors is increasing steadily over time. While Sensor A stays close to the outdoor levels during weekends, Sensor B shows consistently higher readings. Please recalibrate Sensor B or replace it.

Check, sensor: B, code says A: WRONG
Check, drift: 9.1, code says 9.06: OK

We tried to help Gemma, three runs each time: with the difference from the outdoor air written next to each weekend value; with a sentence in the system message, “At weekends the school is empty: a correct sensor then reads close to the outdoor air, and never much below it”; and with the fixed sentence at the end of the note instead of the start. It named sensor B in all nine runs. For a note in seconds, Gemma is fine; for a decision that needs reading numbers, use a model that reasons first, and let your code check its decision.

What the check cannot see

The check reads one sentence. The rest of the note is for a person to read:

Good to know

Exercise

All examples