シフトと訪問スケジュールを数理最適化で作る|AIと条件を組み立てる方法

シフトと訪問スケジュールを数理最適化で作る|AIと条件を組み立てる方法

介護施設の勤務シフトや翌月の訪問診療スケジュールを、Excelの手作業で組んでいる管理者・事務担当の方向けの記事です。条件が増えるほど「1か所直すと別の条件が崩れる」状態になりがちです。条件を式にして計算で案を出す「数理最適化」(Google OR-Tools の CP-SAT)を、AI(Claude Code)と一緒に組み立てる手順を紹介します。

この記事でわかること

  • ハード制約(必ず守る条件)とソフト制約(できれば守る条件)の分け方
  • AIと一緒に条件を洗い出す手順とチェックリスト
  • 架空の3人×7日で動く、Python+CP-SATの最小サンプル
  • シフトの検証レポートと、訪問スケジュールの監査で見る項目
  • 計算に任せず、人が最後に調整・判断する部分

数理最適化でシフトを作るとは:ソルバーとAIの役割分担

数理最適化は、「毎日、早番と遅番が1人ずつ必要」「希望休は必ず休み」といった条件を数式で表し、条件を満たす組み合わせの中から評価がいちばん良いものを計算で探す方法です。Google の OR-Tools は組合せ最適化のためのオープンソースソフトウェアで、公式ドキュメントでは配車(ルーティング)やスケジューリングが対象に挙げられています。CP-SAT ソルバーは整数で問題を定義するため、移動時間や労働時間は「分」などの整数に直して扱います。

結果には「状態」が付きます。公式ドキュメントの説明を言い換えると、OPTIMALは条件を満たし最良と確かめられた案、FEASIBLEは条件を満たす案はあるが最良かは不明、INFEASIBLEは条件をすべて満たす案が存在しないと証明された状態、UNKNOWNは上限に達して案が見つからなかった状態です(MODEL_INVALID はモデルの不備)。

Claude Code は Anthropic のAIエージェントで、公式ドキュメントではファイルを読み、編集し、コマンドを実行できるツールと説明されています。言葉で伝えた条件をPythonのモデルに書き起こし、実行し、結果を点検するところまで任せられます。ただしどの条件を守り、どれを優先するかを決めるのは人です。AIは条件の翻訳係と点検係、ソルバーは計算係、と分けて考えます。

私の現場では、介護施設の勤務シフトを CP-SAT で作っています。希望休、夜勤の回数、連勤の上限、夜勤明け、複数名での体制といった条件を満たす案を出させ、条件を満たしているかを確かめる検証レポートも一緒に作らせています。

ハード制約とソフト制約を分ける

条件を集めたら、まず二つに分けます。ハード制約は「破った案は使えない」条件、ソフト制約は「できれば守りたいが、ほかとぶつかったら譲ってよい」条件です。ソフト制約は、守れなかったときの減点(ペナルティ)の合計を小さくする形でモデルに入れます。OR-Tools の公式ドキュメントの看護師のシフト例でも、勤務の希望は、満たした希望の数を最大にする目的関数として扱われています。

条件の例 区分の目安 根拠・決める人 注意点
日ごと・時間帯ごとの必要人数(夜勤帯を含む) ハード 人員基準、自施設の体制 夜勤の人数が告示で定められたサービスがある(後述)
1人1日1区分、夜勤の翌日は明け ハード 勤務パターン表・就業規則 前月末からの持ち越しを忘れない
法定休日(毎週1日、または4週間で4日以上) ハード 労働基準法第35条 運用を就業規則で確認
1か月単位の変形労働時間制の総枠 ハード(検証で確認) 労働基準法第32条の2 労働日ごとの労働時間をあらかじめ定める
深夜業の制限を請求している職員 ハード 育児・介護休業法、労働基準法第66条(妊産婦の請求) 午後10時~午前5時に勤務を入れない
確定した希望休・有給休暇・研修 ハード 申請の記録 「確定」と「できれば」を分けて受け付ける
連続勤務・夜勤回数の上限 ハード(院内ルール) 就業規則・院内の取り決め 上限の値を文書にする
勤務と勤務の間の休息 院内で決める 勤務間インターバル制度(努力義務) 空ける時間を数字で決める
夜勤・遅番・土日勤務の回数の公平さ ソフト 管理者 「最多と最少の差」を減点にする
「できればこの日は早番」などの希望 ソフト 職員の希望 重みが大きすぎると他が通らない

迷ったときは、「破ったら法令・基準・安全に関わるか」ならハード、「破っても理由を説明すれば職員が納得できるか」ならソフトと考えます。ハードを増やしすぎるとすぐ INFEASIBLE(案がない)になり、守るべきものをソフトに入れると、重み次第で破られた案が出ます。ソフト制約どうしの優先順位は整数の重み(点数)で表します。

介護施設の夜勤人数の基準

厚生労働省の「厚生労働大臣が定める夜勤を行う職員の勤務条件に関する基準」(平成12年厚生省告示第29号)は、短期入所生活介護、介護老人福祉施設、介護老人保健施設、認知症対応型共同生活介護、介護医療院などのサービスごとに、夜勤を行う職員の数の基準を定めています。自施設の区分を告示と関連通知で確認し、その人数を下回らないことをハード制約にします。

AIと一緒に条件を洗い出す手順とチェックリスト

条件は、作っている人の頭の中にあることがほとんどです。AIに質問させながら言葉にすると、漏れが減ります。

  1. 材料をそろえる。直近数か月のシフト表、勤務パターンの一覧、就業規則の該当部分、希望休の受付表を用意し、職員名は「S01」のようなIDに置き換えます。
  2. AIに質問させる。「このシフト表を作るときに守っている条件を、質問しながら洗い出してください」と頼みます。過去の表の規則性をAIに挙げさせ、「ルール」か「たまたま」かを人が答えます。
  3. 条件一覧を作る。1行1条件で、ID・内容・ハード/ソフト・根拠・例外を書きます。
  4. モデルと突き合わせる。条件IDをコードのコメントに書かせ、一覧の条件がすべてモデルに入っているか確認します。
  5. 小さく試す。1週間・少人数で動かし、違反がないことを確かめてから1か月に広げます。

洗い出しのときに確認する項目は次のとおりです。

  • □ 勤務区分(早番・遅番・夜勤・明け・休みなど)と、それぞれの時刻・休憩
  • □ 日ごと・時間帯ごとの必要人数、必要な職種・資格の人数
  • □ 夜勤の翌日(明け)と明けの翌日の扱い、連続勤務・夜勤回数の上限・下限
  • □ 法定休日の取り方と、変形労働時間制の有無・対象期間・起算日
  • □ 深夜業の制限、短時間勤務など、配慮が必要な職員
  • □ 確定した希望休・有休・研修と、「できれば」の希望の区別
  • □ 前月からの持ち越し(月初の明け、月末からの連勤)
  • □ 人が足りない日の例外を誰が判断するか

私の現場では、手順や作業中に気づいた落とし穴を「メモリ」としてファイルに残し、繰り返す作業は「スキル」(手順書とスクリプトのまとまり)にして名前で呼び出せるようにしています。Claude Code の公式ドキュメントでも、CLAUDE.md がセッション開始時に読み込まれること、SKILL.md に書いた手順を「/スキル名」で呼び出せ、スクリプトも含められることが説明されています。条件一覧をファイルにしておけば、翌月も同じ条件から始められます。

私の現場では、患者情報・職員情報は院内の端末とシステムの中で扱い、外部のWebサービスに貼り付けないことを、AIに任せるときの作法として決めています。シフトの計算に必要なのは職員ID・勤務区分・日付・時間数などで、氏名は要りません。

PythonとCP-SATの最小サンプル(架空の3人×7日)

OR-Tools の公式ドキュメントでは、Python 3.8以上があれば python -m pip install ortools で導入でき、仮想環境での導入が勧められています。サンプルは架空のAさん・Bさん・Cさんの3人、月曜~日曜の7日、区分は早番・遅番・休みです。ハード制約は「毎日、早番1人・遅番1人」「遅番の翌日に早番を入れない(前週最終日の遅番を含む)」「連続勤務は4日まで」「希望休(Aさん水曜、Bさん土曜)」、ソフト制約は「遅番回数の偏りを小さく(重み10)」「Cさんの月・火は早番がよい(重み3)」です。

from ortools.sat.python import cp_model

staff = ["Aさん", "Bさん", "Cさん"]                 # 架空の職員
days = ["月", "火", "水", "木", "金", "土", "日"]
shifts = ["早", "遅", "休"]
need = {"早": 1, "遅": 1}                          # 1日に必要な人数
day_off = {("Aさん", 2), ("Bさん", 5)}             # 希望休(Aさん水曜、Bさん土曜)
prefer_early = {("Cさん", 0), ("Cさん", 1)}        # できれば早番(Cさん月・火)
last_prev_week = {"Cさん": "遅"}                   # 前週の最終日の勤務
MAX_RUN = 4                                        # 連続勤務の上限(日)

model = cp_model.CpModel()
x = {(p, d, s): model.new_bool_var(f"{p}_{d}_{s}")
     for p in staff for d in range(7) for s in shifts}

# ハード制約1:1人1日に1つの区分
for p in staff:
    for d in range(7):
        model.add_exactly_one(x[p, d, s] for s in shifts)
# ハード制約2:早番・遅番は毎日、必要な人数ちょうど
for d in range(7):
    for s, n in need.items():
        model.add(sum(x[p, d, s] for p in staff) == n)
# ハード制約3:遅番の翌日に早番を入れない(前週からの持ち越しを含む)
for p in staff:
    for d in range(6):
        model.add(x[p, d, "遅"] + x[p, d + 1, "早"] <= 1)
    if last_prev_week.get(p) == "遅":
        model.add(x[p, 0, "早"] == 0)
# ハード制約4:連続勤務は4日まで(どの5日間にも休みが1日以上)
for p in staff:
    for d in range(7 - MAX_RUN):
        model.add(sum(x[p, d + k, "休"] for k in range(MAX_RUN + 1)) >= 1)
# ハード制約5:希望休は必ず休み
for p, d in day_off:
    model.add(x[p, d, "休"] == 1)

# ソフト制約:遅番回数の偏り(最多−最少)と、通らなかった早番希望の数
late = [sum(x[p, d, "遅"] for d in range(7)) for p in staff]
late_max = model.new_int_var(0, 7, "late_max")
late_min = model.new_int_var(0, 7, "late_min")
model.add_max_equality(late_max, late)
model.add_min_equality(late_min, late)
unmet = sum(1 - x[p, d, "早"] for p, d in prefer_early)
model.minimize(10 * (late_max - late_min) + 3 * unmet)

solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 10.0
status = solver.solve(model)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
    raise SystemExit("条件をすべて満たす案がありません: " + solver.status_name(status))

table = {p: [next(s for s in shifts if solver.value(x[p, d, s])) for d in range(7)]
         for p in staff}
print("状態:", solver.status_name(status))
print("      " + " ".join(days))
for p in staff:
    print(p, " ".join(table[p]), " 遅番", table[p].count("遅"), "回")
print("通らなかった早番希望:", [f"{p} {days[d]}" for p, d in sorted(prefer_early)
                              if table[p][d] != "早"])

Python 3.14、OR-Tools 9.15 での実行結果の一例です。

状態: OPTIMAL
      月 火 水 木 金 土 日
Aさん 早 遅 休 休 早 遅 遅  遅番 3 回
Bさん 遅 休 早 早 遅 休 休  遅番 2 回
Cさん 休 早 遅 遅 休 早 早  遅番 2 回
通らなかった早番希望: ['Cさん 月']

ポイントは三つです。

  • 「同時に入れない」は足し算で書く。「遅番の翌日に早番」は、二つの変数の和を1以下にするだけで禁止できます。連勤の上限は「どの5日間にも休みが1日以上」という窓で表しています。
  • ハードはソフトより優先される。Cさんの月曜は、前週最終日が遅番のためハード制約3で早番にできず、希望は通っていません。どの条件とぶつかったかを説明できることが大切です。
  • 時間の上限を付ける。公式ドキュメントでは、長く動く可能性のあるプログラムには max_time_in_seconds で上限を付けることが勧められています。評価が同じ案が複数あると、実行のたびに表が変わります。

希望休を「AさんとBさんがともに水曜」に変えて実行すると、「条件をすべて満たす案がありません: INFEASIBLE」と表示されます。水曜に働けるのがCさん1人になり、早番と遅番の2人をそろえられないからです。このサンプルは、前週からの連勤日数、夜勤・明け、労働時間数を扱っていません。実際には、チェックリストの項目を一つずつ条件として足します。

結果の検証レポートで見る項目

ソルバーは書かれた条件は守りますが、書き忘れた条件や書き間違えた条件には気づきません。そこで、出来上がった表をモデルとは別のコードで点検します。サンプルの末尾に次のコードを足すと、ハード制約の違反を一覧にできます。わざと壊した表で、違反を拾えることも確認します。

def verify(table):
    errors = []
    for d in range(7):
        for s, n in need.items():
            c = sum(table[p][d] == s for p in staff)
            if c != n:
                errors.append(f"{days[d]}曜 {s}番が{c}人")
    for p in staff:
        prev, run = last_prev_week.get(p), 0
        for d in range(7):
            if prev == "遅" and table[p][d] == "早":
                errors.append(f"{p} {days[d]}曜 遅番の翌日に早番")
            run = run + 1 if table[p][d] != "休" else 0
            if run > MAX_RUN:
                errors.append(f"{p} {days[d]}曜 連続勤務{run}日目")
            prev = table[p][d]
    for p, d in day_off:
        if table[p][d] != "休":
            errors.append(f"{p} {days[d]}曜 希望休が入っていない")
    return errors

print("ハード制約の違反:", verify(table) or "なし")

検証レポートには次の項目を並べます。

項目 見る内容
ソルバーの状態 OPTIMAL か FEASIBLE か(FEASIBLE なら打ち切りを記録)
ハード制約の違反 モデルとは別のコードで判定し、0件であること
日ごとの配置 日・時間帯ごとの人数、職種・資格者の人数、夜勤の人数
個人別の集計 勤務日数、夜勤・遅番の回数、休日数、最長の連勤、労働時間の合計(法定休日と変形労働時間制の総枠を含む)
持ち越し 月初の明けや月末からの連勤が、前月の実績と合っているか
ソフト制約の結果と差分 通らなかった希望とぶつかった条件。手直しの後はどこが変わったか

厚生労働省のリーフレット「1か月単位の変形労働時間制」では、対象期間の労働時間の上限を「週40時間×暦日数÷7」で計算する方法が示されています(30日の月は171.4時間、31日の月は177.1時間。特例措置対象事業場は週44時間)。個人ごとの総労働時間と比べ、超えていればシフト表を見直す例も載っているので、この比較を全職員について出します。

私の現場では、AIに「完了しました」と報告させる前に、元データと結果を突き合わせて件数が合っているかを確認させています。シフトなら、受け付けた希望休の件数と、表に入った休みの件数の照合です。

訪問スケジュールの監査項目(重複・移動時間・訪問回数・負荷平準化)

訪問診療の予定は「患者×日×医師×時刻」の組み合わせで、施設や家族の都合、移動の順番も絡みます。私の現場では、翌月の予定を負荷の平準化と重複・移動時間・訪問回数の監査をしながら組み、施設ごとの予定表に分けて送るところまでAIと一緒に行っています。監査では次の項目を見ます。

監査項目 確認する内容 見落としたときに起きること
重複 同じ患者が同じ日に2件、同じ医師が同じ時間帯に2か所、施設内の時間帯の重なり 訪問の取り消し、施設側の段取りの混乱
移動時間 前の訪問の終了時刻+移動時間+余裕 ≦ 次の訪問の開始時刻 遅れの連鎖
訪問回数 患者ごとの月の回数が計画どおりか。週の回数が上限を超えていないか。間隔の偏り 算定区分のずれ、返戻
負荷の平準化 医師ごと・日ごと・曜日ごとの件数と移動時間の偏り 特定の日や医師への集中
同じ建物の訪問 同じ建物の患者を同じ日に何人訪問するか 訪問診療料の区分の確認漏れ

訪問回数と同じ建物の訪問は、請求に直結します。令和8年度改定後の医科点数表(告示)では、在宅患者訪問診療料(Ⅰ)の「1」は、別に厚生労働大臣が定める疾病等の患者を除き、患者1人につき週3回に限り算定するとされ、同じ建物に居住する他の患者にも同じ日に訪問診療を行う場合、その患者は「同一建物居住者」として算定区分が変わります。算定ルールは「訪問診療料(Ⅰ)(Ⅱ)と往診料の違い」「在宅時医学総合管理料(在医総管)と施設総管の違いと算定の基本」で解説しています。

同じ建物の患者を同じ日にまとめるかどうかは、患者の状態や施設・家族の都合で決めるものです。監査は、決まった予定の算定区分を請求担当が前もって把握するために行います。

順番や時刻まで計算で決めたい場合は、OR-Tools のルーティング機能が参考になります。公式ドキュメントでは、決められた時間帯(タイムウィンドウ)の中で訪問する「時間枠付き配車計画問題(VRPTW)」として、地点間の移動時間の表を入力に、総移動時間を最小にする例が紹介されています。突合の考え方は「在宅医療事務の効率化:Excel・RPA・API連携でできる突合と期限管理」とも共通です。

人が最後に調整する部分

計算で出た案は「条件を満たす下書き」です。次の部分は人が判断します。

  • 条件の優先順位と重み。何をどこまで譲るかは管理者が決めます。重みを変えたら、案の差分を確認します。
  • INFEASIBLE のときに緩める条件。AIに条件を一つずつ外して原因の候補を出させることはできますが、どれを緩めるか、職員にどう説明するかは人が決めます。
  • 数字にしにくい事情。体調、家庭の事情、新人の教育計画、患者・家族の希望など、条件に書き切れないものを反映します。
  • 手直しの後の再検証。表を手で直したら、必ず検証レポートを作り直します。
  • 確定と公開。1か月単位の変形労働時間制では、労働日ごとの労働時間をシフト表などであらかじめ具体的に定める必要があり、特定した労働日や労働時間を任意に変更することはできないとされています。
  • 本番システムへの登録と外部への送信。私の現場では、本番のシステムに書き込む前に、何をどう変えるかを示させて確認してから実行させ、メールやチャットなど外に出すものは、下書きまでをAIに作らせて最終の送信を人が確認しています。

Claude Code は、標準のモード(画面表示は Manual)ではファイルの編集やコマンドの実行の前に許可を求め、Plan モードでは調べるだけで編集をしないと公式ドキュメントで説明されています。こうした仕組みも使い、「AIが提案し、人が確定する」流れを保ちます。

よくある質問

Q. プログラミングの経験がなくても始められますか。

A. コードを書く部分はAIに任せられますが、条件一覧と検証レポートを読んで「意図どおりか」を判断するのは人の役割です。まずは1週間・数人の小さな表で、条件一覧→モデル→検証を一度通してみてください。

Q. 「INFEASIBLE」と出たら、何が悪いのでしょうか。

A. 条件どうしがぶつかり、すべてを満たす案が存在しない状態です。ハード制約を一つずつ外して再実行すると、原因の組み合わせを絞り込めます。希望休の調整、応援の手配、条件のソフト化のどれで対応するかは管理者が決めます。FEASIBLE(最良かは不明な案)は、検証レポートで違反がないことを確認できれば使えます。

Q. 職員や患者さんの情報をAIに渡してもよいですか。

A. 院内の情報管理ルールに従うことが前提です。計算に必要なのは職員ID・勤務区分・日付・時間数などで氏名は不要なので、扱う場所と範囲を先に決めてください。

まとめ

数理最適化でシフトや訪問スケジュールを作るときの要点は、計算そのものより、条件の整理と結果の確認にあります。条件をハード制約とソフト制約に分け、AIに質問させながら書き出して CP-SAT のモデルにし、出てきた案は検証レポートと訪問スケジュールの監査で点検します。夜勤の人数基準、法定休日、変形労働時間制の総枠、深夜業の制限は一次資料で確認してハード制約にし、優先順位の判断、手直し後の再検証、確定と送信は人が担います。この分担を守れば、毎月の作業は「ゼロから組む」から「案を確かめて仕上げる」に変わります。

参考資料

本記事は2026年10月時点で公表されている資料をもとに作成しています。算定の可否は個別の状況で異なるため、最終的には告示・通知・疑義解釈や、管轄の地方厚生局・保険者にご確認ください。