AIで当番表を作るときは、「参加できない」と「できればこの枠がよい」を別の条件にします。希望が多くかなった表でも、参加できない人を割り当てていれば、そのまま使える案にはなりません。
この記事では、架空のイベント受付3枠と参加者3人で、必須条件を満たす案と希望を多く満たす案を分けます。人数を埋めた表だけで終わらせず、どの条件を確認したかと、作れない場合の理由を残す方法です。実際の参加者への連絡や勤務計画の作成は行っていません。
目次
先に枠と必須条件を固定する
例は2026年11月4日、5日、6日の各10~11時、日本時間のイベント受付です。枠IDをS1・S2・S3とし、各枠に1人を置きます。参加者A・B・Cは各枠の受付作業を担当できるという架空の設定です。
今回の必須条件は、各枠をちょうど1人で担当すること、参加不可の枠へ入れないこと、3枠全体を通じて1人の担当を最大1回にすることです。3人で3枠を埋めるので、条件を満たせば結果として各人1回になります。「1日最大1回」ではなく「今回の3枠全体で最大1回」という条件に注意してください。
GoogleのOR-Tools公式例も、各シフトを1人へ割り当てる条件などと、希望の充足を増やす目的を分けています。この記事の3枠は独自例で、公式例の勤務条件を流用したものではありません。OR-Toolsや外部APIも実行していません。Google:Employee Scheduling(新しいタブで開く)
参加可否と希望を別の表現で渡す
表は横にスクロールできます
| 参加者 | S1:11月4日 | S2:11月5日 | S3:11月6日 | 今回の希望 |
|---|---|---|---|---|
| A | 可 | 可 | 不可 | S2 |
| B | 可 | 不可 | 可 | S1 |
| C | 不可 | 可 | 可 | S3 |
「可」は希望の強さを意味しません。AはS1に参加できますが、希望はS2です。希望は、必須条件を満たす案が複数あるときの比較に使います。希望と異なる割当を、参加不可と同じエラーへしません。
未回答は、この表の「不可」とも「可」とも別です。返事のない人をAIが参加可能と補完して人数を埋めないようにします。参加理由や個人事情を集める必要がない運用なら、可否だけを扱い、実名と対応する管理情報は許可された場所へ分けます。
成立する案を確認してから、希望を比べる
S1・S2・S3の順で担当者を並べると、この例の成立案は2つです。
表は横にスクロールできます
| 案 | S1 | S2 | S3 | 希望を満たす人数 | 必須条件 |
|---|---|---|---|---|---|
| X | A | C | B | 0人 | 満たす |
| Y | B | A | C | 3人 | 満たす |
案Xも参加不可への割当はなく、同じ人の重複もないため、成立する案です。希望を同じ重みで1人につき1点と数える今回の比較では、案Yが3点で案Xが0点なので、案Yを候補として選べます。
これは、一般に最も公平な表だという意味ではありません。過去の担当回数、作業負担、移動、担当経験、本人間の調整は入力にありません。今回は3人とも1回・各1時間という条件と、指定した希望の数だけを比べています。点数に含めていない観点を、評価済みと書かないでください。
小さな例を全列挙で検算する
次のPythonは、A・B・Cを3枠へ入れる27通りから、同じ人の重複と参加不可を除きます。希望の点数は、成立する案に対して計算します。
from itertools import product
slots = ("S1", "S2", "S3")
people = ("A", "B", "C")
allowed = {"S1": {"A", "B"}, "S2": {"A", "C"}, "S3": {"B", "C"}}
wishes = {"A": "S2", "B": "S1", "C": "S3"}
def feasible(availability):
return [
assignment
for assignment in product(people, repeat=len(slots))
if len(set(assignment)) == len(slots)
and all(person in availability[slot]
for slot, person in zip(slots, assignment))
]
def score(assignment):
return sum(wishes[person] == slot
for slot, person in zip(slots, assignment))
plans = feasible(allowed)
assert set(plans) == {("A", "C", "B"), ("B", "A", "C")}
assert {p: score(p) for p in plans} == {
("A", "C", "B"): 0,
("B", "A", "C"): 3,
}
assert max(plans, key=score) == ("B", "A", "C")
blocked = {**allowed, "S1": {"A"}, "S2": {"A"}}
assert feasible(blocked) == []
print(plans, [score(p) for p in plans])
27通りはこの固定例の探索数です。人数と枠が増えると全列挙は急に大きくなるため、そのまま大規模な当番表へ広げる実装ではありません。入力IDの重複、未回答、日付や時間の重なりを検査する汎用処理も含めていません。固定した3枠の計算例として使います。
作れない場合は、条件を勝手に緩めない
コード末尾のblockedは、S1とS2の両方がAしか参加できない別ケースです。各枠1人、Aは今回最大1回という条件を同時に満たせません。BやCが空いていることだけでは解消できず、この条件で成立する案は0件です。
このとき、「なるべく均等に」と読み替えてAを2回へ増やす、Bを参加不可のS2へ入れる、S2を空欄にしたまま完成扱いにする、といった処理は避けます。いずれも入力した必須条件の変更や未充足です。
返す内容は、「S1とS2はAのみ参加可だが、Aの担当上限は1回のため両枠を埋められない。参加可否、担当上限、枠の設定のどれを見直せるか、担当者へ確認が必要」とします。担当者が条件変更を決めたら、新しい版として再計算し、元の条件で成立したとは書きません。
AIへの依頼に、成立と希望の判定を分けて書く
架空の受付枠S1~S3と参加者A~Cから、当番表の候補を整理してください。
各枠ちょうど1人、参加可の人のみ、3枠全体で各人最大1回が必須です。
希望は必須ではなく、成立案どうしで満たす人数を比較するために使います。
入力にない参加可能日や条件変更を追加しないでください。
出力:枠IDと担当者、必須条件の確認、希望を満たす人数、未確認事項。
成立案がない場合は表を埋めず、両立できない条件と確認質問を返してください。
今回は過去回数や移動負担を評価していません。公平性全般を保証しないでください。
この依頼では参加者への連絡、カレンダー登録、確定通知は行いません。
この依頼文と表は独自の手製例です。AIで生成して正答率を測った結果ではありません。AIが別の案を出した場合も、文面の自然さより先に、枠の欠け、同じ枠の複数担当、参加不可、担当上限を確認します。希望点だけが合っていても、必須条件の違反を相殺できません。
候補表と、合意した当番表を分けて残す
候補には入力版、作成時点、必須条件、希望の数え方を添えます。案Yを選んだとしても、参加者へ確定連絡したことにはなりません。必要な確認を終えた版と、調整中の候補を区別して管理します。
変更が入ったら、変更された可否や枠を元の版と比べ、全枠の条件を再確認します。1人だけを入れ替えると、入替先の担当上限や希望が変わる場合があります。今回の3枠では成立と希望を検算できましたが、実運用で必要な資格、勤務・休憩、組織の規則などを確認した結果ではありません。対象に必要な条件を責任者が定義した上で、候補を合意へ進めてください。
参考資料
公式資料の確認日:2026年10月20日。枠、参加者、参加可否、希望、当番案は独自の架空例です。掲載Pythonと小規模例の計算をローカルで検査しました。OR-Tools、外部API、AI生成、実勤務条件、参加者との合意、通知、画面表示は未検証です。
