Claude Codeで温度ログの逸脱区間を洗い出す — データロガーのCSVから出荷確認表まで
データロガーの温度CSVから、許容範囲を外れた区間と継続時間を出す手順です。欠測とエラー値の扱い、継続時間を2通りで出す理由、出荷確認表の作り方を示します。
冷蔵・冷凍品の輸送では、データロガーが数分おきに庫内温度を記録します。荷受け側が知りたいのは「許容範囲を外れた区間があったか、あったなら何時から何分間か」です。Claude Codeには、この判定を行う短いスクリプトを書かせて実行させ、結果を出荷確認表の形にまとめさせられます。
判定そのものはスクリプトが行い、Claudeが書くのはスクリプトと説明です。判定ルールを先に決めておけば、同じ手順を別のロットや別のロガーにそのまま使えます。例のCSVは手順の確認用に作った合成データで、実際の輸送のものではありません。
逸脱の判定に先立って決める4つのルール
温度ログは、閾値より高いか低いかを見るだけでは正しく読めません。データロガーのCSVには、欠測とエラー値が混ざります。ここを曖昧にしたままClaudeに頼むと、実行のたびに結論が変わります。依頼の前に、次の4点を文章で決めておきます。
| 決めること | 例 | 決めないと起きること |
|---|---|---|
| 許容範囲 | 例2.0〜8.0℃ | 決めないと起きること境界値(ちょうど8.0℃)の扱いが実行ごとにぶれる |
| 記録間隔 | 例5分 | 決めないと起きること継続時間を何分と数えるかが決まらない |
| 欠測の扱い | 例間隔の2倍を超える空きは欠測 | 決めないと起きること欠測をまたいだ区間が連続として数えられる |
| エラー値 | 例-999は除外して件数を報告 | 決めないと起きること-999が低温逸脱として出る |
許容範囲は製品ごとの規格や取引先との取り決めで決まります。例の2〜8℃は説明用の値なので、実際の数値に置き換えてください。出荷可否を決めるのは品質責任者で、この手順が用意するのは判断材料までです。
判定ルールをCLAUDE.mdに書いておく
CLAUDE.mdは、プロジェクトの直下に置くmarkdownファイルです。Claude Codeは毎回のセッションの開始時に読みます。温度ログ用のフォルダに次の内容を置けば、毎回同じルールで判定させられます。
# 温度ログ判定のルール
- 許容範囲は 2.0〜8.0℃。境界値(2.0 と 8.0)は範囲内として扱う
- 記録間隔は5分。間隔の2倍を超える空きは欠測として別に報告する
- -999 はセンサーのエラー値。判定から除き、件数を報告する
- 継続時間は2通りで出す
- 確認済み: 最初の逸脱記録から最後の逸脱記録まで
- 上限: 最初の逸脱記録から、範囲内に戻った最初の記録まで
- 出荷可否は判定しない。判定欄は空欄のまま出力する
- 元のCSVは編集しない。結果は別ファイルに書く最後の2行は、念のための決まりです。CSVの原本を書き換えさせないことと、出荷の可否をClaudeに判定させないことを、ファイルに明記しておきます。
実行できるスクリプトを書かせる
次の依頼文をClaude Codeに渡します。
lg07.csv は温度データロガーの記録です。CLAUDE.md のルールで、許容範囲を
外れた区間を洗い出すスクリプト excursions.py を書いてください。
結果は check_sheet.csv に出力し、書いたら実際に実行して出力を見せてください。最後の一文が大事です。公式のベストプラクティスは、Claudeに実行できる確認手段を与えることを勧めています。確認手段がないと、Claudeは「できたように見える」ところで止まるためです。スクリプトを実行させて出力を見せれば、結果を読むのは人間の側になります。
書かせるスクリプトは、たとえば次の形になります。外部ライブラリは使わず、標準のPythonだけで動きます。
import csv
import sys
from datetime import datetime, timedelta
LO, HI = 2.0, 8.0 # 許容範囲(規格に合わせる)
STEP = timedelta(minutes=5) # ロガーの記録間隔
GAP = STEP * 2 # これを超える空きは欠測とみなす
ERR = {-999.0} # センサーのエラー値
FMT = "%Y-%m-%d %H:%M"
def load(path):
with open(path, newline="") as f:
rows = [(datetime.strptime(r["timestamp"], FMT), float(r["temp_c"]))
for r in csv.DictReader(f)]
return sorted(rows)
def minutes(td):
return int(td.total_seconds() // 60)
def main(path):
rows = load(path)
errors = [r for r in rows if r[1] in ERR]
data = [r for r in rows if r[1] not in ERR]
gaps = [(a[0], b[0]) for a, b in zip(data, data[1:])
if b[0] - a[0] > GAP]
found, cur = [], None
for t, v in data:
if v < LO or v > HI:
if cur is None:
cur = {"start": t, "last": t, "vals": []}
cur["last"] = t
cur["vals"].append(v)
elif cur:
cur["back"] = t
found.append(cur)
cur = None
if cur:
cur["back"] = None
found.append(cur)
print(f"records={len(data)} errors={len(errors)} gaps={len(gaps)}")
out = csv.writer(open("check_sheet.csv", "w", newline=""))
out.writerow(["開始", "終了(最後の逸脱)", "確認済み(分)",
"上限(分)", "最高/最低", "判定"])
for e in found:
mid = (LO + HI) / 2
peak = max(e["vals"], key=lambda x: abs(x - mid))
sure = minutes(e["last"] - e["start"])
top = minutes(e["back"] - e["start"]) if e["back"] else "継続中"
out.writerow([e["start"].strftime(FMT), e["last"].strftime(FMT),
sure, top, peak, ""])
print(e["start"].strftime("%H:%M"), e["last"].strftime("%H:%M"),
sure, top, peak)
main(sys.argv[1])読むときの要点は3つです。
- 判定は
v < LO or v > HIの1行で、境界値は範囲内になる - 逸脱が始まると
curに記録し、範囲内の記録が来た時点で区間を閉じる - ファイルの最後まで逸脱が続いたときは、
backをNoneにして「継続中」と出す
実行結果の読み方
1日分(12時間・5分間隔)の合成データに、高温の逸脱、短い低温の逸脱、65分の欠測、エラー値1件、末尾まで続く高温を仕込んで、Python 3.12.2で実行しました。標準出力は次のとおりです。
records=131 errors=1 gaps=1
11:20 12:00 40 45 10.0
14:40 14:50 10 15 1.5
18:50 19:55 65 継続中 8.71行目は、エラー値を除いた131件を判定し、エラー値が1件、欠測が1か所あったという意味です。-999が低温逸脱として出ていないことが分かります。2行目以降が逸脱区間です。
合成データの逸脱区間
11:20 の高温
40〜45分
最高 10.0℃
14:40 の低温
10〜15分
最低 1.5℃
18:50 の高温
65分以上
末尾まで継続
同じ区間でも、継続時間が2つ出ている点に注目してください。11:20から12:00まで逸脱の記録が並び、次の12:05に範囲内へ戻っています。確認済みは40分、上限は45分です。ロガーは5分おきにしか測らないので、12:00〜12:05の間にいつ戻ったかは記録から分かりません。
ここで2通りを出す理由は、片方だけだと基準が変わったときに答えが変わるからです。確認済みを「最短」、上限を「最長」として併記しておけば、取引先の取り決めがどちらの数え方でも、同じ表から読めます。
出荷確認表の形にまとめる
スクリプトは同時にcheck_sheet.csvを書きます。中身は次のとおりです。
開始,終了(最後の逸脱),確認済み(分),上限(分),最高/最低,判定
2026-10-01 11:20,2026-10-01 12:00,40,45,10.0,
2026-10-01 14:40,2026-10-01 14:50,10,15,1.5,
2026-10-01 18:50,2026-10-01 19:55,65,継続中,8.7,最後の「判定」列は空のままです。担当者が確認し、品質責任者が出荷の可否を書き込みます。Excelで開いて、備考欄や承認欄を足せばそのまま確認表になります。
「継続中」の行は特に注意が必要です。ログの終端で逸脱が続いているのは、ロガーの回収時点でまだ戻っていなかったという意味で、いつ戻ったかを示す記録がありません。上限を数字で書けないので、この行は別途、輸送記録など他の情報で確かめます。
落とし穴
欠測をまたぐ逸脱を連続とみなす
例では欠測の65分(16:15〜17:20)の前後は範囲内でした。しかし欠測の前後がどちらも逸脱だった場合、この単純なスクリプトは1つの長い区間として数えます。欠測中の温度は分からないので、欠測をまたぐ区間は「欠測を含む」と注記します。CLAUDE.mdに「欠測をまたぐ区間は分割する」と足し、スクリプトを直させます。
欠測をまたぐ区間を分割する
直し方も実際に試しました。合成データの16:10・16:15と17:20・17:25を9℃台に書き換え、欠測の前後がどちらも逸脱になるCSV(lg07_gap.csv)を作ります。元のスクリプトで実行すると、欠測の65分を含む1行になります。
16:10 17:25 75 80 9.2直したスクリプトでは、直前の記録との間隔がGAPを超えた時点で、進行中の区間を閉じます。変更は次の数行です。
found, cur, prev = [], None, None
for t, v in data:
if cur and prev and t - prev > GAP:
cur["back"] = "欠測" # 欠測で区間を打ち切る
found.append(cur)
cur = None
prev = t
# 以降の判定は元のまま出力側では、backが"欠測"の行は上限を数字にせず「欠測で中断」と書きます。同じCSVを再実行した結果は次のとおりです。
16:10 16:15 5 欠測で中断 9.0
17:20 17:25 5 10 9.2欠測の前後が別の区間になり、欠測中の65分は逸脱にも正常にも数えられません。元のCSV(lg07.csv)では出力は変わらないので、既存の結果を壊さない変更です。
ロットごとに繰り返し回す
2回目以降は、スクリプトとルールがあるので、依頼文は短くて済みます。claude -pなら非対話で実行でき、標準入力でデータを渡せます。ただし生のCSVをそのままパイプせず、スクリプトにパスを渡して、その出力だけを流します。標準入力の上限は10MBで、これを超えると非ゼロの終了コードでエラーになるためです。
python3 excursions.py lg07.csv | claude -p \
"この出力を要約し、継続中の区間と欠測があれば先頭に書いて"Claudeに渡るのは、スクリプトが出した数行の標準出力だけです。数千行の生データは読ませません。大容量のログを扱うときの考え方は、PLCのログ解析の記事にもあります。
複数のロガーを毎日回すなら、スケジュール実行の仕組みも使えます。公式の概要では、クラウドで動くRoutinesと、手元で動くデスクトップのスケジュールタスクが紹介されています。ログを社外のクラウドに置きたくなければ、手元で動く側を選びます。
結果が正しいかを確かめる
スクリプトが出した数字は、そのまま信じず、次の3点で確認します。
- 逸脱区間のうち1つを選び、CSVの該当行を直接開いて開始・終了の時刻を目で照合する
- 記録件数(131)と、元のCSVの行数からヘッダーとエラー値の行を引いた数を照合する
- 許容範囲をわざと狭めて再実行し、逸脱区間が増えることを確かめる
3つ目は、スクリプトが閾値を正しく使っているかの簡易な試験です。ここで増えなければ、判定が閾値に連動していません。荷待ち時間の集計のように、物流の記録CSVを扱う場面でも、手で数えた1件と照合するやり方が基本になります。
依頼文の書き方で結果が変わる
曖昧な依頼
温度ログを見て、問題がないか教えて。
ルールと確認を添えた依頼
CLAUDE.mdのルールで逸脱区間を出して。欠測とエラー値の件数も報告し、実行した出力を見せて。
別のロガーのCSVを読めるように直す
機種が変わると、文字コード・列名・時刻の書式が変わります。直すのはload()とFMTだけで、判定の部分は触りません。たとえばShift_JISで、列名が「ロガー」「日時」「温度(℃)」、時刻が2026/10/01 8:00の形式なら、次のようになります。
FMT = "%Y/%m/%d %H:%M"
def load(path):
with open(path, newline="", encoding="shift_jis") as f:
rows = [(r["ロガー"], datetime.strptime(r["日時"], FMT),
float(r["温度(℃)"])) for r in csv.DictReader(f)]
by_logger = {}
for logger, t, v in rows:
by_logger.setdefault(logger, []).append((t, v))
return {k: sorted(v) for k, v in by_logger.items()}合成の3行(LG-07が2件、LG-08が1件)で試すと、8:00のように時が1桁の時刻も読め、ロガーごとに分かれて返ります。main()ではこの辞書をロガーごとに回し、出力のCSVにはロガーIDの列を足します。庫内の個体差を混ぜないためです。
つまずきやすい点
データロガーのCSVで起きがちなこと
時刻の書式が違う
ロガーの機種によって
2026/10/01 8:00や01-10-2026のような書式になります。スクリプトのFMTを実際の列に合わせます。タイムゾーンの記載がなければ、どの時刻系かを先に確かめます。列名に単位が付いている
列名が「温度(℃)」のように単位付きだったり、文字コードがShift_JISだったりします。列名と文字コードをCLAUDE.mdに書いておくと、毎回の指定が要りません。
複数のロガーが1ファイル
ロガーごとに判定を分けます。混ぜると、庫内の個体差が逸脱に見えます。
まとめ
温度ログの逸脱判定は、許容範囲・記録間隔・欠測・エラー値の4つを先に決めれば、Claude Codeに任せられる作業になります。スクリプトに判定を担わせ、継続時間を確認済みと上限の2通りで出し、判定欄は人が埋める。この分担にすると、Claudeの出力を信じるかどうかを迫られずに済みます。まずは手元の1本のCSVで、逸脱区間を1つ選んで元の行と照合するところから始めてください。