AIで当番表を作る方法|参加可否・担当上限と希望を分ける

架空の受付3枠と3人から、成立する当番案と希望の充足を分けて検算します。成立しない反例の理由と確認質問も残します。

AIものさし編集部

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つです。

表は横にスクロールできます

案S1S2S3希望を満たす人数必須条件
XACB0人満たす
YBAC3人満たす

案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生成、実勤務条件、参加者との合意、通知、画面表示は未検証です。

質問から自分に合うAIを探す