Examples · Internet of Things
Cleaning sensor data with pandas
Two sensors log at different times, with duplicate rows and one of them in local time. A model writes the pandas code that puts both on a 5-minute grid in UTC; you run it, and a checker compares the result with values you worked out by hand.
Intermediate · 35 min
The idea
In a lab at school, two sensors log the same room:
- an ESP32 logs the temperature in UTC, roughly every two and a half minutes;
- a Raspberry Pi logs the humidity in Italian local time, written the Italian way (day first), roughly every four minutes.
Their messages travel over MQTT with QoS 1, as in MQTT topics for a smart classroom, which means “at least once”: now and then a reading arrives twice. The Pi also lost its Wi-Fi for about ten minutes, and when it came back it sent its last reading again.
To compare the two you need one table: one row every 5 minutes, in UTC, with the mean of each sensor in that slot. A model can write that pandas code in seconds. The code runs, prints a nice table, and can still be wrong by a few hundredths. So:
- the model writes
merge_sensors.pyfrom a written request; - you read it, then run it;
- a checker compares the result with numbers you worked out by hand.
What you need
A key and the openai library, as in Your first API call in Python, and pandas: pip install pandas. We used pandas 3.0.6, and the version matters, as you will see.
The two files
Save them as sensor_a.csv and sensor_b.csv:
time,temperature
2026-10-06T08:00:37Z,20.8
2026-10-06T08:03:36Z,20.9
2026-10-06T08:06:00Z,20.9
2026-10-06T08:09:05Z,21.1
2026-10-06T08:11:28Z,21.0
2026-10-06T08:14:06Z,21.1
2026-10-06T08:17:14Z,21.2
2026-10-06T08:19:19Z,21.2
2026-10-06T08:22:09Z,21.3
2026-10-06T08:24:29Z,21.3
2026-10-06T08:27:36Z,21.4
2026-10-06T08:29:40Z,21.3
2026-10-06T08:32:10Z,21.4
2026-10-06T08:35:09Z,21.5
2026-10-06T08:37:34Z,21.5
2026-10-06T08:40:11Z,21.4
2026-10-06T08:42:21Z,21.6
2026-10-06T08:45:30Z,21.4
2026-10-06T08:47:58Z,21.6
2026-10-06T08:47:58Z,21.6
2026-10-06T08:50:16Z,21.7
2026-10-06T08:52:56Z,21.7
2026-10-06T08:56:01Z,21.8
2026-10-06T08:58:09Z,21.9
time,humidity
06/10/2026 10:01:52,46.0
06/10/2026 10:05:49,45.9
06/10/2026 10:09:11,46.5
06/10/2026 10:09:11,46.5
06/10/2026 10:12:31,46.2
06/10/2026 10:15:57,46.4
06/10/2026 10:20:07,46.6
06/10/2026 10:24:39,46.5
06/10/2026 10:28:33,46.8
06/10/2026 10:30:41,47.0
06/10/2026 10:34:02,47.3
06/10/2026 10:46:15,47.9
06/10/2026 10:34:02,47.3
06/10/2026 10:49:30,48.1
06/10/2026 10:53:08,48.0
06/10/2026 10:56:44,47.6
06/10/2026 10:59:20,47.4
On 6 October Italy is on summer time, UTC+2: the Pi's 10:01:52 is 08:01:52 UTC. Both files cover the same hour, 08:00 to 09:00 UTC.
1. The request
Save it as request.txt. Every trap of the two files is written down: the model knows only what you tell it.
Write a Python function merge(a_path, b_path) with pandas.
- sensor_a.csv has the columns time,temperature. time is UTC in ISO 8601, like 2026-10-06T08:00:37Z.
- sensor_b.csv has the columns time,humidity. time is the local time in Busto Arsizio (Italy), written dd/mm/yyyy hh:mm:ss, with no offset.
- Both files may contain the same row twice: count it once. The rows of sensor_b.csv are not always in time order.
Return one DataFrame on a 5-minute grid in UTC:
- the index is a timezone-aware DatetimeIndex in UTC, named time, from the first to the last 5-minute slot that has a reading;
- each slot holds the mean of the readings from its start (included) to 5 minutes later (excluded);
- the columns are temperature and humidity; a slot without readings stays NaN.
Only the function and its imports, in one ```python block.
2. The model writes the code
Save it as write_merge.py:
import os
import re
import sys
from pathlib import Path
from openai import OpenAI
client = OpenAI(
base_url="https://api.ai.itsincom.org/v1",
api_key=os.environ["API_KEY"],
)
model = sys.argv[1] if len(sys.argv) > 1 else "qwen3-coder-30b"
answer = client.chat.completions.create(
model=model,
temperature=0,
messages=[{"role": "user", "content": Path("request.txt").read_text()}],
)
if answer.choices[0].finish_reason != "stop":
sys.exit(f"The answer was cut off ({answer.choices[0].finish_reason}): run it again.")
text = answer.choices[0].message.content
code = re.search(r"```(?:python)?\n(.*?)```", text, re.S)
Path("merge_sensors.py").write_text(code.group(1) if code else text)
print(f"merge_sensors.py, by {model}: read it before you run it.\n")
print(Path("merge_sensors.py").read_text())
3. The checker
The numbers in KNOWN come from the two files, with a pencil: for example, the 08:05 slot of sensor B holds 45.9 and 46.5, so the mean is 46.2. If the second copy of 46.5 is counted too, it becomes 46.3. Save it as check_merge.py:
import math
import sys
import pandas as pd
from merge_sensors import merge
# Worked out by hand from the two files: the mean of each 5-minute slot, in UTC.
KNOWN = {
"08:05": (21.0, 46.2), # sensor B sent 10:09:11 twice: with both copies, 46.3
"08:30": (21.4, 47.15), # 10:34:02 came back after the gap: with both copies, 47.2
"08:40": (21.5, math.nan), # sensor B was offline: no humidity, and none made up
"08:45": (21.5, 48.0), # sensor A sent 08:47:58 twice: with both copies, 21.53
"08:55": (21.85, 47.5), # the last slot
}
try:
df = merge("sensor_a.csv", "sensor_b.csv")
except Exception as error: # a crash is a failed check too
sys.exit(f"FAIL merge() stopped with {type(error).__name__}: {error}")
print(df, "\n")
def same(got, expected):
return math.isnan(got) if math.isnan(expected) else abs(got - expected) < 0.01
def slot_is(hhmm, temperature, humidity):
try:
row = df.loc[pd.Timestamp(f"2026-10-06 {hhmm}", tz="UTC")]
return same(float(row["temperature"]), temperature) and same(float(row["humidity"]), humidity)
except (KeyError, TypeError, ValueError): # the slot is missing, twice, or in another time zone
return False
checks = {
"12 rows, 08:00 to 08:55": len(df) == 12,
"no time twice": df.index.is_unique,
"index in UTC": str(getattr(df.index, "tz", None)) == "UTC",
"columns temperature, humidity": list(df.columns) == ["temperature", "humidity"],
}
for hhmm, (temperature, humidity) in KNOWN.items():
label = f"{hhmm}: {temperature} °C, " + ("no humidity" if math.isnan(humidity) else f"{humidity} %")
checks[label] = slot_is(hhmm, temperature, humidity)
for name, ok in checks.items():
print("ok " if ok else "FAIL", name)
print(f"{sum(checks.values())} of {len(checks)} checks passed")
check_merge.py imports and runs code that a model wrote. Read merge_sensors.py before you run it, as you would read code from a stranger.
What you should see
First the model for code, qwen3-coder-30b:
python3 write_merge.py
python3 check_merge.py
merge_sensors.py, by qwen3-coder-30b: read it before you run it.
import pandas as pd
…
df_combined['time_bin'] = df_combined['time'].dt.floor('5T')
…
FAIL merge() stopped with ValueError: Invalid frequency: 5T. Failed to parse with error message: ValueError("Invalid frequency: T. Failed to parse with error message: KeyError('T'). Did you mean min?")
Then qwen3.8-27b:
python3 write_merge.py qwen3.8-27b
python3 check_merge.py
temperature humidity
time
2026-10-06 08:00:00+00:00 20.85 46.00
2026-10-06 08:05:00+00:00 21.00 46.20
…
2026-10-06 08:30:00+00:00 21.40 47.15
2026-10-06 08:35:00+00:00 21.50 NaN
2026-10-06 08:40:00+00:00 21.50 NaN
2026-10-06 08:45:00+00:00 21.50 48.00
2026-10-06 08:50:00+00:00 21.70 48.00
2026-10-06 08:55:00+00:00 21.85 47.50
ok 12 rows, 08:00 to 08:55
ok no time twice
ok index in UTC
ok columns temperature, humidity
ok 08:05: 21.0 °C, 46.2 %
ok 08:30: 21.4 °C, 47.15 %
ok 08:40: 21.5 °C, no humidity
ok 08:45: 21.5 °C, 48.0 %
ok 08:55: 21.85 °C, 47.5 %
9 of 9 checks passed
Its code reads sensor B with format="%d/%m/%Y %H:%M:%S", then .dt.tz_localize("Europe/Rome").dt.tz_convert("UTC"); it removes duplicates in each file, puts each reading in its slot with .dt.floor("5min"), and averages each slot.
What the generated code got wrong
qwen3-coder-30b never passed. With this request, its three runs all stopped with an error: twice on "5T", once because it put times with and without a time zone in the same column. "5T" is an old name for “5 minutes”: older pandas accepted it, pandas 3 does not. The model used it in every one of its seven runs, with this request and an earlier one. It also tried, in different runs, to subtract a fixed two hours from Italian time (“assuming summer”), and to read the Italian times as if they were UTC. Twice we sent its error back to it, with “I use pandas 3.0”: both new versions stopped again, one of them with the same error.
qwen3.8-27b passed three runs out of three with this request. Our first request said only “Both files may contain the same row twice”, without “count it once”. Six runs, and every one of them ran without errors and printed a table that looks right. In three of the six, the checker said 6 of 9: the model had kept both copies. One of them even wrote it in a comment:
# Mean per slot (duplicates are naturally included in the mean)
All three tables had 46.3 at 08:05, 47.2 at 08:30 and 21.533 at 08:45: wrong by a few hundredths, exactly where the comments of KNOWN say. Nobody would see that by looking at the table. A known value at a known time does.
A crash is the easy case: you see it. The dangerous one is code that runs and gives numbers that are almost right.
Good to know
- Pandas guesses dates month first.
pd.to_datetime("06/10/2026 10:01:52")gives 10 June, not 6 October. Always give aformatfor dates written the Italian way. - Local time without an offset is ambiguous for one hour every year. On Sunday 25 October 2026 the clocks go back, and 02:00 to 03:00 happens twice.
tz_localize("Europe/Rome")stops withValueError: Cannot infer dst time from 2026-10-25 02:30:00unless you tell it what to do withambiguous=. The code that passed today will stop on a file that contains that night, and our checker cannot know: there is no 25 October in the files. The real fix is on the device: log in UTC, like the ESP32. - Why
count it oncematters. QoS 1 delivers at least once, so duplicates are normal, not a bug of your sensor. When you write a request, say what to do with them: a fact alone (“may contain”) leaves the decision to the model. - The checks are only as good as the slots you chose. Each known slot tests one trap: a duplicate in A, one in B, the resent reading, the gap, the last slot. Before you trust a checker, break the code on purpose and see it fail.
Exercise
- Remove
.drop_duplicates(...)from amerge_sensors.pythat passes. Which checks fail? Then replace"Europe/Rome"with"UTC": what happens to the row count? - Add a known slot to
KNOWN, worked out by hand: 08:20, for example. - Write in
request.txtthat you use pandas 3.0, and runqwen3-coder-30bagain. Does it still write"5T"? - Add the line
25/10/2026 02:30:00,55.0tosensor_b.csvand run the checker. Then ask the model to handle the night when the clocks go back.
