Claude Codeでクラス替えの組み分けスクリプトを作る — 氏名を渡さない手順
クラス替えの配慮条件(分離・同組・男女比・支援の配置)を条件表にして、Claude Codeに組み分けスクリプトを書かせ、違反を一覧で検証する手順。氏名と対応表は渡しません。
組み分けは、氏名のない名簿と条件表から作れる
クラス替えの組み分けは、「この2人は別の組」「この2人は同じ組」「各組の男女比をそろえる」「支援が必要な児童を偏らせない」といった条件を、全員分いっぺんに満たす作業です。条件が10を超えると、手作業では1つ直すたびに別の条件が崩れます。
この作業は、条件を満たす配置を探すプログラムに任せられます。Claude Codeに書かせるのは、名簿と条件表を読んで配置を出すスクリプトと、その結果が条件を守っているかを数える検証スクリプトの2本です。
児童生徒の氏名は、どちらにも要りません。名簿はIDと最小限の属性だけにし、IDと氏名の対応表はClaude Codeから見えない場所に置きます。Claude Codeは任意のフォルダで動くため、学校の事務フォルダのような「コードではない場所」でも同じように使えます。
IDに置き換えても、名簿は個人情報のまま扱う
氏名をIDに換えれば安全になる、とは言えません。性別、支援の有無、「S03とS17は別の組」という条件が並ぶ表は、対応表を持つ人にとっては個人を指す行の集まりです。三者面談の準備で個票を渡さない型は、三者面談の準備をClaudeで進めるにまとめています。仮名や匿名への加工の考え方は、仮名加工情報と匿名加工情報をClaudeに渡す前の加工が詳しい記事です。
Claude Codeは手元のマシンで動きますが、モデルとやり取りするときにネットワーク経由でデータを送ります。送られるのは、すべてのプロンプトとモデルの出力です。ファイルを読ませれば、その中身は会話の一部になります。ローカルには会話の記録(トランスクリプト)が平文で残り、既定の保存期間は30日です。期間は cleanupPeriodDays で変えられます。
サーバー側の保持期間はプランで違います。Free・Pro・Maxでは、モデル改善への利用を許可していると5年、許可していないと30日です。Team・Enterprise・APIの標準は30日です。
つまり、ここで決めるのは「何を渡さないか」です。この記事の設計では、次の線を引きます。
| 渡すもの | 渡さないもの |
|---|---|
| 任意のID(S01〜)、性別、支援の有無(0か1) | 渡さないもの氏名、出席番号、生年月日 |
| 「S03とS17は別の組」という条件 | 渡さないもの別の組にする理由(いじめ、家庭事情など) |
| 組の数と許容する人数差 | 渡さないもの診断名、支援の内容 |
| スクリプトの実行結果(IDと数だけ) | 渡さないものIDと氏名の対応表 |
学校で生成AIを使ってよいかどうか、校務用の端末で何を入力してよいかは、学校と教育委員会の規程が先に決めます。文部科学省のガイドラインの位置づけは文科省の生成AIガイドラインver2.0を学校のClaude導入にどう活かすかで整理しています。規程が許していない作業は、この手順でも行えません。
手順1: 作業フォルダを分け、対応表を起動場所の外に置く
Claude Codeを起動するフォルダは、組み分け専用の空のフォルダにします。対応表(IDと氏名の対応)は、そのフォルダの外に置きます。
class-assign/ ← ここで claude を起動する
CLAUDE.md
roster.csv 名簿(ID・性別・支援の有無)
rules.csv 条件表
assign.py 組み分けスクリプト
verify.py 検証スクリプト
.claude/settings.json
~/校務/対応表.csv ← 起動フォルダの外。氏名はここだけ外に置いただけでは、Claude Codeが自分でパスを指定すれば読めてしまいます。読み取りを止めるには、Read の拒否ルールを書きます。パスは ~/ で始めるとホームフォルダからの指定になります。
{
"permissions": {
"deny": [
"Read(~/校務/**)"
]
}
}この拒否ルールは、組み込みのファイル読み取りツールのほか、cat・head・tail・sed のようにファイル名を書いて読むコマンドにも効きます。一方で、grep -r pattern . のようにファイル名を書かずに読むコマンドや、任意のサブプロセスには効きません。確実に止めたいときは、サンドボックスを有効にします。対応表を氏名つきの別ファイルに分けておくのは、ルールが漏れたときの被害を小さくするためでもあります。
Claude Codeにその場で守らせたい運用は、CLAUDE.md に具体的に書きます。「適切に扱う」ではなく、検証できる文にします。
# class-assign の作業ルール
- 児童生徒の氏名・出席番号・生年月日を、ファイルにも会話にも書かない
- ID は S01 形式。ID から氏名を推測する作業は行わない
- ~/校務/ 以下のファイルは読まない
- スクリプトの出力は ID と数だけにする
- 変更のたびに `python3 verify.py roster.csv rules.csv result.csv` を実行する手順2: 名簿と条件表をCSVにする
名簿は3列で足ります。IDは、元の名簿の順番と無関係につけます。出席番号順にS01、S02とつけると、番号から個人を割り出せてしまうからです。
id,sex,support
S01,F,0
S02,F,0
S03,M,0support は支援が必要かどうかの0と1だけにします。理由や内容は、先生の手元メモに残します。診断名を入れない実態メモの型は、特別支援学級の個別の指導計画をClaudeで下書きすると同じ考え方です。
条件表は1行1条件です。
type,a,b
separate,S03,S17
separate,S05,S22
together,S10,S11配慮条件の種類と、スクリプトでの扱いは次のとおりです。
| 条件 | 書き方 | スクリプトでの判定 |
|---|---|---|
| 分離 | 書き方separate,S03,S17 | スクリプトでの判定2人が別の組なら満たす |
| 同組 | 書き方together,S10,S11 | スクリプトでの判定2人が同じ組なら満たす |
| 人数 | 書き方組の数と許容差 | スクリプトでの判定最大と最小の差が1以内 |
| 男女比 | 書き方sex 列 | スクリプトでの判定女子の人数の組間差が1以内 |
| 支援 | 書き方support 列 | スクリプトでの判定支援ありの人数の組間差が1以内 |
人数、男女比、支援の3つは、条件表に書かず、スクリプトの中に決め打ちします。許容差を変えたいときは、数字を1か所直すだけで済みます。
手順3: Claude Codeに探索スクリプトを書かせる
実装の前に、計画だけを見たいときは、変更を加えずに案を出させる計画モードで起動します。
cd class-assign
claude --permission-mode plan最初のプロンプトは、入力の形、条件、完了の条件を先に書きます。
roster.csv(id, sex, support)と rules.csv(type, a, b)を読み、
4つの組に分ける assign.py を書いてください。標準ライブラリだけを使います。
- separate は別の組、together は同じ組にする
- 人数、女子の人数、support=1 の人数は、組どうしの差を1以内にする
- 条件を満たす配置が見つからないときは、最も違反の少ない配置を出す
- 出力は id,class の CSV。氏名は扱わない
次に、結果を数えて違反を一覧にする verify.py を別に書いてください。
verify.py が違反 0 件になるまで、assign.py を直してください。最後の1文が要点です。公式の手順集にも、テストを書かせたあと「新しいテストを実行して、失敗を直して」と頼む型があります。ここでは、テストの代わりに検証スクリプトを使います。Claude Codeはコマンドを実行して出力を読めるので、違反が残っている間は自分で直し続けられます。
次は、同じ条件で組んだ探索スクリプトの例です。ランダムな配置から、2人の入れ替えを繰り返して違反の点数を減らします。条件の違反には100点、人数・男女・支援の偏りには10点の重みをつけ、点数が0になれば終わります。
import csv, random, sys
from collections import Counter
N_CLASSES = 4
def penalty(assign, roster, rules):
p = 0
for r in rules:
same = assign[r["a"]] == assign[r["b"]]
if r["type"] == "separate" and same: p += 100
if r["type"] == "together" and not same: p += 100
def spread(counts):
v = [counts.get(c, 0) for c in range(N_CLASSES)]
return max(v) - min(v)
p += 10 * max(0, spread(Counter(assign.values())) - 1)
p += 10 * max(0, spread(Counter(
assign[i] for i in roster if roster[i]["sex"] == "F")) - 1)
p += 10 * max(0, spread(Counter(
assign[i] for i in roster if roster[i]["support"] == "1")) - 1)
return p
def solve(roster, rules, seed):
rng = random.Random(seed)
ids = sorted(roster)
rng.shuffle(ids)
assign = {i: k % N_CLASSES for k, i in enumerate(ids)}
best = penalty(assign, roster, rules)
for _ in range(20000):
if best == 0: break
a, b = rng.sample(ids, 2)
if assign[a] == assign[b]: continue
assign[a], assign[b] = assign[b], assign[a]
p = penalty(assign, roster, rules)
if p <= best: best = p
else: assign[a], assign[b] = assign[b], assign[a]
return best, assign入力の読み込みとCSVの書き出しは省いています。上の solve を seed を変えて最大200回呼び、点数0の配置が出た時点で結果を書き出します。
手順4: 検証スクリプトで違反を一覧にする
組み分けの結果は、探索したスクリプトとは別のスクリプトで数え直します。探索側の「点数0」を信じず、別の目で条件を読み直すためです。
import csv, sys
from collections import Counter
roster = {r["id"]: r for r in csv.DictReader(open(sys.argv[1], newline=""))}
rules = list(csv.DictReader(open(sys.argv[2], newline="")))
cls = {r["id"]: int(r["class"]) for r in csv.DictReader(open(sys.argv[3], newline=""))}
bad = 0
for r in rules:
same = cls[r["a"]] == cls[r["b"]]
ok = (not same) if r["type"] == "separate" else same
print(("OK " if ok else "NG ") + f'{r["type"]:8} {r["a"]} {r["b"]} -> 組 {cls[r["a"]]} / {cls[r["b"]]}')
bad += (not ok)
for label, pick in [("人数", lambda x: True),
("女子", lambda x: x["sex"] == "F"),
("支援", lambda x: x["support"] == "1")]:
c = Counter(cls[i] for i in roster if pick(roster[i]))
row = [c.get(k, 0) for k in range(1, max(cls.values()) + 1)]
ok = max(row) - min(row) <= 1
print(("OK " if ok else "NG ") + f"{label} {row}")
bad += (not ok)
print(f"違反 {bad} 件")
sys.exit(1 if bad else 0)40人・4組の架空の名簿(女子20人、支援ありの児童6人)と、分離4件・同組2件の条件表で動かすと、Python 3.12.2では最初の乱数の種で点数0に達し、次の出力になりました。
OK separate S03 S17 -> 組 2 / 1
OK separate S05 S22 -> 組 2 / 1
OK separate S08 S09 -> 組 4 / 2
OK separate S12 S30 -> 組 4 / 1
OK together S10 S11 -> 組 1 / 1
OK together S25 S26 -> 組 4 / 4
OK 人数 [10, 10, 10, 10]
OK 女子 [5, 5, 5, 5]
OK 支援 [2, 1, 2, 1]
違反 0 件出力に出るのはIDと数だけで、氏名は一度も出ません。Claude Codeはこの出力を読んで次の修正を決めるため、出力に氏名が混ざる設計にしないことが、手順1の運用と対になります。
終了コードを1にしているので、Claude Codeは「違反あり」を出力の文字ではなく、コマンドの失敗として受け取れます。
条件表が矛盾していると、解は出ない
ここまでの例は、条件がすべて両立する前提です。実際の条件表は、先生たちの要望を足していくうちに、気づかないまま矛盾します。
上の条件表に separate,S10,S11 を1行足してみました。S10とS11には同組の条件がすでにあるので、両立しません。同じ名簿で乱数の種を200通り試しても、点数は100から下がらず(約34秒)、検証スクリプトは次の1件を報告しました。
NG together S10 S11 -> 組 3 / 4
違反 1 件検証は「同組が守れていない」とだけ報告し、分離の条件は満たされたと表示します。矛盾の相手は、出力からは分かりません。そこで、条件表の矛盾を探索の前に調べる関数を、Claude Codeに足してもらいます。同組の条件でつながるIDをひとまとめにし、その中に分離の組が含まれていないか、同組のまとまりが1つの組の人数を超えていないかを調べる、という指示で足ります。
落とし穴
矛盾した条件表は、いちばん違反の少ない配置を黙って返す
プロンプトに「見つからないときは最も違反の少ない配置を出す」と書いたため、矛盾がある条件表でも結果のファイルは作られます。結果のファイルがあることと、条件を満たしていることは別です。必ず verify.py の終了コードで判断します。
結果を氏名に戻すのは、自分の端末で行う
result.csv のIDを氏名に戻す作業は、Claude Codeの外で、対応表を持つ自分が実行します。
python3 - <<'EOF'
import csv
names = {r["id"]: r["name"] for r in csv.DictReader(open("/Users/me/校務/対応表.csv"))}
for r in csv.DictReader(open("result.csv")):
print(r["class"], names[r["id"]])
EOFこの結果を、そのまま組み分け案として確定してはいけません。スクリプトが守れるのは、条件表に書いた条件だけです。保護者の要望、きょうだいの組、前年度の人間関係のように、IDの条件に落とせない配慮は、候補を見た先生が最後に判断します。出た案を見て条件を足し、探索し直す往復が、この手順の使い方です。
つまずきやすい点
- IDから氏名が推測できる: 出席番号順や五十音順にIDをつけると、名簿の並びを知る人には対応が分かります。対応表の作成時に、IDの順番をシャッフルします
- 会話の記録が残る: 氏名を渡していなくても、名簿と条件表は会話の記録に残ります。保存期間は
cleanupPeriodDaysで短くできます - 性別の列が2値では足りない: 例のスクリプトは
Fの人数だけを数えます。学校の名簿の区分に合わせて、数える対象を変えます - 人数が組の数で割り切れない: 人数差を1以内としているので、1人多い組ができます。許容差を0にすると、割り切れない名簿では解が出ません
- 転入・転出が後から入る: 名簿に行を足して、
assign.pyを再実行します。前回の配置は引き継がれないため、確定後の変更は手作業で行います
まとめ
組み分けをClaude Codeに任せる利点は、配置を考えさせることではなく、条件を満たす配置を探す道具と、満たしているかを数える道具を作らせることにあります。氏名と対応表を作業フォルダの外に置き、出力をIDと数に絞れば、Claude Codeに見せる情報は最小限で済みます。最後の判断は、条件表に載らなかった事情を知っている先生に残ります。