ロットトレーサビリティで回収の対象範囲を洗い出す — Claude Codeで出荷記録を結合
原料ロットから製造ロット、出荷先までをCSVで結合し、回収の対象範囲を洗い出すスクリプトの組み方。結合キーの表記ゆれと空欄で対象が漏れる落とし穴も、実行結果つきで示します。
回収の対象範囲は、原料ロットを起点に「どの製造ロットに使ったか」「その製造ロットをどこへ出荷したか」と二段で結合して決まります。この二段の結合をClaude Codeにスクリプトとして書かせ、手元のCSVで実行すれば、Excelの目視突合より再現性のある一覧が取れます。落とし穴は結合の技術ではなく、キーの表記ゆれと空欄です。
ここでは製造記録・出荷記録のCSVを使い、原料ロットから出荷先までを洗い出す手順を、実行結果つきで示します。回収するかどうかの判断や行政への届出は別の作業です。届出書の書き方は食品リコール届出書の書き方にあります。
三つの記録をつなぐと範囲が決まる
トレーサビリティの記録は、多くの現場で三つの表に分かれています。
| 表 | 主なキー | 持っている情報 |
|---|---|---|
| 入荷記録 | 主なキー原料ロット | 持っている情報仕入先、入荷日 |
| 製造記録 | 主なキー製造ロット、原料ロット | 持っている情報製造日、ライン |
| 出荷記録 | 主なキー製造ロット | 持っている情報出荷先、数量、出荷日 |
回収の起点が「仕入先から不具合の連絡があった原料ロットM-100」だとすると、辿る道筋は一本です。
回収対象を絞り込む順序
- 1
製造記録から原料ロットを使った製造ロットを選ぶ
M-100を原料にした製造ロットがすべて候補になります。
- 2
製造ロットを出荷記録に結合する
候補の製造ロットごとに、出荷先と数量が引けます。
- 3
結合できなかった行を別に取り出す
どちらの表にも相手が見つからない行が、漏れの原因になります。
三つ目の手順が、この記事の中心です。一致した行だけを見ていると、一致しなかった行が視界から消えます。
手元で動かす: Claude Codeに結合スクリプトを書かせる
実行環境が手元なら、Claude Codeにはスクリプトを書かせて、実行までさせるのが早い方法です。CSVの列名と、起点にする原料ロットだけを伝えます。
claude -p "production.csv と shipments.csv を結合し、
原料ロット M-100 を使った製造ロットの出荷先を一覧にして。
結合できない行は別に出力して" \
--allowedTools "Read,Write,Bash"-p(--print)は対話なしで実行するフラグで、--allowedToolsは確認なしで使わせるツールを指定します。書き込みや実行まで許すので、顧客データの置き場所とは別の作業用ディレクトリで実行すると事故を防げます。権限モードで固める方法もあります。dontAskは確認が要る操作をすべて拒否するモードで、許可ルールに入れた操作だけが動きます。
CSVの中身がモデルに送られる点は、先に決めておく事項です。データの扱いはClaude Codeのデータは学習に使われるかに整理があります。出荷先が取引先の実名なら、卸Xのような符号に置き換えたCSVで試し、スクリプトだけ本番データに当てる運用も選べます。
例示用のCSVを用意する
以下は動作確認用に作った架空のデータです。製造記録には、後で触れる三種類の罠を仕込んであります。
production.csv
product_lot,made_on,line,material_lot
P-0901,2026-09-01,A,M-100
P-0902,2026-09-02,A,M-100
P-0903,2026-09-03,B,M-101
P-0904,2026-09-04,A,m-100
P-0905,2026-09-05,B,
P-0906,2026-09-06,A,M-102
P-0907,2026-09-07,B,M-100shipments.csv
product_lot,customer,qty,shipped_on
P-0901,卸X,200,2026-09-03
P-0902,卸X,150,2026-09-04
P-0902,卸Y,100,2026-09-05
P-0903,卸Z,300,2026-09-06
P-0904,卸Y,120,2026-09-07
P-0905,卸Z,80,2026-09-08
P-0907,卸X,90,2026-09-09
P-0908,卸Y,60,2026-09-10P-0904の原料ロットは小文字で末尾に空白が付いたm-100 、P-0905は空欄です。出荷記録のP-0908は、製造記録にそもそも存在しません。
結合スクリプト
Claude Codeに書かせて出てくる形の一例です。要点は三つで、キーの正規化、indicator付きの結合、未一致行の別出力です。
import sys
import pandas as pd
TARGET = sys.argv[1] # 起点の原料ロット(例: M-100)
def norm(s):
return (s.astype("string").str.normalize("NFKC")
.str.strip().str.upper())
prod = pd.read_csv("production.csv", dtype=str)
ship = pd.read_csv("shipments.csv", dtype=str)
for df, col in ((prod, "material_lot"), (prod, "product_lot"),
(ship, "product_lot")):
df[col] = norm(df[col])
hit = prod[prod["material_lot"] == TARGET]
blank = prod[prod["material_lot"].isna()]
def join(p):
return p.merge(ship, on="product_lot", how="left", indicator=True)
cand, unknown = join(hit), join(blank)
print("== 確定対象 ==")
print(cand[["product_lot", "customer", "qty"]].to_string(index=False))
print("== 原料ロット未記録(要確認) ==")
print(unknown[["product_lot", "customer", "qty"]].to_string(index=False))
orphan = ship[~ship["product_lot"].isin(prod["product_lot"])]
print("== 製造記録に無い出荷ロット ==")
print(orphan.to_string(index=False))
print("確定対象の出荷数量:", cand["qty"].astype(float).sum())dtype=strは、ロット番号の先頭ゼロが数値変換で落ちるのを防ぐための指定です。0901が901になると、別のロットに結合してしまいます。
実行結果で見る: 素朴な結合は一ロット漏らす
手元でpandasを入れてpython trace.py M-100を実行した出力が次です。
== 確定対象 ==
product_lot customer qty
P-0901 卸X 200
P-0902 卸X 150
P-0902 卸Y 100
P-0904 卸Y 120
P-0907 卸X 90
== 原料ロット未記録(要確認) ==
product_lot customer qty
P-0905 卸Z 80
== 製造記録に無い出荷ロット ==
product_lot customer qty shipped_on
P-0908 卸Y 60 2026-09-10
確定対象の出荷数量: 660.0比較のために、正規化なしでmaterial_lot == "M-100"の完全一致だけを使うと、ヒットする製造ロットは3つ(P-0901、P-0902、P-0907)で、出荷数量の合計は540です。正規化すれば、m-100 と書かれたP-0904が加わり、出荷先は卸Yへの120が増えて660になります。
差の120は、小文字と末尾空白という見た目のずれだけで生じています。完全一致の結果は「それらしい一覧」に見えるので、人も気づきません。
対象が漏れる三つの落とし穴
表記ゆれ: 大文字小文字・全角半角・空白
手入力のロット番号は、M-100、m-100、M‐100(ハイフンの種類違い)のように揺れます。NFKC正規化とstripと大文字化で、前の二つは吸収できます。ハイフンの種類は、置換のルールを書き足さないと残ります。正規化の前後で、ユニークなロット数がどう変わったかを出力させると、見落としに気づきやすくなります。
空欄: 「使っていない」と「記録がない」は別物
P-0905のように原料ロットが空欄の製造ロットは、M-100を使ったかどうかがデータから判断できません。確定対象にも除外にも入れず、「要確認」の枠で出すのがこのスクリプトの方針です。この枠の出荷先は、製造日報や現場の確認で人が決めます。Claude Codeに空欄を推測で埋めさせない指示を、プロンプトに明記しておく必要があります。
孤児ロット: 製造記録にない出荷ロット
出荷記録のP-0908は、どの製造ロットにも結合できません。入力ミスの可能性も、別工場の製品の可能性も、製造記録側の欠落の可能性もあります。範囲の確定前に原因を調べる対象で、結合のinnerではこの行が黙って消えます。
複数の原料ロットにまたがる場合
実際の製品は原料を複数使います。製造記録が「製造ロット×原料ロット」の一行一組み合わせの縦持ちなら、上のスクリプトは変更なしで動きます。一行に原料が横に並ぶ形式(material_lot_1、material_lot_2)なら、先に縦持ちに変換してから結合します。
long = prod.melt(id_vars=["product_lot", "made_on", "line"],
value_name="material_lot").dropna(subset=["material_lot"])変換後は、同じ製造ロットが複数行になるので、出荷先の結合で行が増えます。数量を合計するときは、meltで増えた原料ロット列を外したうえで、出荷記録の行そのもの(product_lot・customer・qty・shipped_onの組。伝票番号があればそれ)で重複を除いてから足します。そうしないと、同じ出荷を原料の数だけ二重に数えます。product_lotとcustomerだけで除くと、同じ製造ロットを同じ出荷先へ別日に2回出荷した行まで1行に潰れ、数量を少なく数えます。
cand.drop(columns=["material_lot"]).drop_duplicates(
subset=["product_lot", "customer", "qty", "shipped_on"])製造日の範囲で絞る使い方もあります。入荷日以前の製造ロットが候補に入っていたら、入荷記録との照合を疑う材料です。入荷記録を加える場合は、同じ要領でreceiving.csvを原料ロットで左結合し、indicatorで未一致を確認します。
ファイルが手元にないとき: API経由のコード実行ツール
Claude Codeを使えない環境や、アプリケーションに組み込みたい場合は、APIのコード実行ツールで同じ処理ができます。CSVはFiles APIでアップロードし、container_uploadブロックで参照します。サンドボックスにはpandasが入っているので、先のスクリプトがそのまま動かせます。
押さえておく条件は次のとおりです。
- コンテナにはインターネット接続がなく、使えるのは事前インストール済みのライブラリだけです
- メモリは5GiB、ディスクは5GiB、CPUは1つです
- ファイルを含むリクエストは、ツールが呼ばれなくても実行時間が課金対象になります
- Files APIの1ファイルの上限は500MBです
生成した結果のCSVを取り出すには、$OUTPUT_DIRに書き出す必要があります。ここを忘れると成果物が回収されません。症状と対処はコード実行ツールでファイルが消える理由にまとめてあります。料金面はコード実行ツールが無料になる条件を見てください。
アップロードしたCSV自体は、Files APIからダウンロードできません。ダウンロードできるのは、コード実行ツールやスキルが作ったファイルだけで、アップロード済みのファイルを取得しようとすると400エラーが返ります。元データを手元に残すのは、運用側の責任です。
結果を回収の判断につなぐ
出力は三つの区画に分かれています。回収判断の場に持ち込むのは、確定対象の出荷先別数量と、要確認の区画です。
出力の使い分け
確定対象
出荷先ごとの数量を、連絡先リストと照合して通知に回します。
原料ロット未記録
製造日報や現場の記録で、M-100を使ったかを確定させます。
製造記録にない出荷ロット
入力ミスか別系統の製品かを判別します。範囲を確定する前に潰す項目です。
出荷先への連絡と並行して、原料ロットを取り扱った他の製品がないかも別途確認が要ります。連絡リストの抜けは、確定対象の出荷先を一覧にして連絡済みの欄と突き合わせると見つかります。一覧で未一致を見る考え方は、RoHS・REACH書類の回収漏れを確認する手順でも使っています。
逆向きの検算で漏れを確かめる
順方向の結果は、出荷先から逆に辿っても同じになるはずです。確定対象に入った出荷先の製造ロットを、製造記録から引き直します。原料ロットが想定どおりM-100になっていれば一致で、別の値や空欄が出れば、結合かキーの扱いに見落としがあります。
もう一つの検算は、行数の収支です。製造記録の全行は、確定対象・要確認・除外(別の原料ロット)のどれかに必ず入るはずです。今回の例なら7行が、確定4、要確認1、除外2に分かれます。合計が合わなければ、正規化で落ちた行か、重複した行があります。この収支の確認もスクリプトの出力に含めるよう、プロンプトに書いておけます。
よくあるつまずき
- 先頭ゼロの消失:
read_csvにdtype=strを付けないと、0901が数値の901になる - 日付の型: 出荷日で期間を絞るなら
parse_datesで型を揃えてから比べる - 結合方向: 製造記録を左に置いて
how="left"にすると、出荷実績のない製造ロットも一覧に残る。在庫として残っている可能性があるので、数量が空欄の行は消さずに確認する - 件数の検算: 結合の前後で行数を出力させる。左結合で行が増える場合は、出荷先が複数ある製造ロットが原因であることが多い
まとめ
ロットの結合そのものは数行で書けます。範囲が決まるかどうかは、正規化後に一致しなかった行をどう扱うかで変わります。indicatorで未一致を残し、空欄と孤児ロットを別の区画で出す。この二点を最初のプロンプトで指示に入れておくと、返ってくるスクリプトにも未一致の出力が含まれます。
最終的な回収範囲の確定は、スクリプトの出力に現場の記録を重ねて人が行う作業です。