1.はじめに
看護師シフト編成では「相性の悪いメンバーで夜勤に組みたくない」「男性だけの夜勤は患者から敬遠される」「できるだけ連休が多いほうがいい/少ないほうがいい」「5連勤を少なく」といった細かな要望が日常的に寄せられます。
これらをモデルに落とし込み、Excel設定から最適化コード、実行例までを解説します。
2.追加機能の概要
2.1 相性不一致ペアの夜勤回避機能
- “相性NGペア”としてExcelに登録したスタッフ同士を同一日の夜勤シフトに配置しない制約を追加
前回使用したエクセルに「NGペアリスト」シートを追加し、同一日の夜勤シフトに配置しない看護師のidを入力します。
ここでは看護師5(id:5)と看護師6(id:6)、看護師2(id:2)と看護師12(id:12)を対象とします。
ng_ws = XLSX.readxlsx("shift_kibou.xlsx")["NGペアリスト"]
ng_headers = DataFrame(ng_ws["A1:B1"], :auto)[1, :]
ng_data = coalesce.(DataFrame(ng_ws["A2:B10"], Symbol.([h for h in ng_headers])), "")
for staff in eachrow(ng_data)
if staff.スタッフ1 !== ""
for d in days_list
@constraint(model, sum(x[s, d, SHIFT_TYPE[3]] for s in values(staff)) <= 1)
end
end
end
2.2 夜勤に女性看護師を必ず配置する機能
- 夜勤シフトには必ず女性看護師を1名以上配置する
- 男性だけで夜勤が構成されないように制御
本モデルでは女性看護師23名、男性看護師2名という偏ったスタッフ構成を踏まえ、上記要件を適用しました。患者の中には男性看護師を希望される場合もありますが、現状の男女比では女性のみの夜勤回避は難しいため、最低1名の女性配置を必須としています。
余談ですが、私が看護師としてキャリアを始めた頃は病棟看護師30名に対し男性はわずか1名でした。2025年現在は男性看護師の増加で、病棟によっては全体の3割を占めるケースもありますが、依然として女性が大多数を占めるため、女性必須配置の要件はしばらく必要とされるでしょう。
filtered_staff = staff_data[staff_data.性別 .== "男", :id]
for d in days_list
@constraint(model, sum(x[s, d, SHIFT_TYPE[3]] for s in filtered_staff) <= 1)
end
2.3 連休推奨/非推奨機能
@variable(model, is_renkyu[s in staff_data.id, d in days_list], Bin)
renkyu_days = 3
renkyu_weight = 1
for s in staff_data.id
for d in days_list[1:end-(renkyu_days-1)]
no_work_count = @expression(model, sum(x[s, d + h, SHIFT_TYPE[1]] for h in 0:(renkyu_days-1)))
@constraint(model, no_work_count - (renkyu_days-1) <= is_renkyu[s, d])
@constraint(model, no_work_count - renkyu_days + 0.001 >= -renkyu_days * (1 - is_renkyu[s, d]))
end
end
renkyu = sum(is_renkyu[s,d] for s in staff_data.id for d in days_list[1:end-(renkyu_days-1)]) * renkyu_weight
- renkyu_days: 連休として検出する連続休暇の日数(ここでは3日)
- renkyu_weight: 連休非推奨の場合は正の値、推奨の場合は負の値
2.4 5連勤検出・最小化機能
@variable(model, is_renkin[s in staff_data.id, d in days_list[1:end-4]], Bin)
for s in staff_data.id
for d in days_list[1:end-4]
work_count = @expression(model, sum(x[s, d+h, k] for h in 0:4 for k in SHIFT_TYPE[2:end]))
@constraint(model, work_count - 4 <= is_renkin[s, d])
@constraint(model, work_count - 5 + 0.001 >= -5 * (1 - is_renkin[s, d]))
end
end
renkin = sum(is_renkin[s,d] for s in staff_data.id for d in days_list[1:end-4])
「5日すべて勤務」を取った区間だけを is_renkin[s,d] = 1 として検出し、目的関数でその合計を最小化します。
2.5 目的関数
連休推奨/非推奨、5連勤最小化の変数を目的関数に追加します
@objective(model, Min, max_yakin + max_nikkin - min_nikkin + renkyu + renkin)
3.実行例
終了ステータス: OPTIMAL
名前 | 1日 | 2日 | 3日 | 4日 | 5日 | 6日 | 7日 | 8日 | 9日 | 10日 | 11日 | 12日 | 13日 | 14日 | 15日 | 16日 | 17日 | 18日 | 19日 | 20日 | 21日 | 22日 | 23日 | 24日 | 25日 | 26日 | 27日 | 28日 | 29日 | 30日 | 31日 | 休み | 夜勤 | 3連休 | 5連勤 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
看護師1 | 休 | 日 | 早 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 休 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 日 | 日 | 12 | 2 | 1 | 0 |
看護師2 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 休 | 日 | 休 | 日 | 12 | 3 | 1 | 0 |
看護師3 | 早 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 休 | 休 | 休 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 早 | 12 | 4 | 0 | 0 |
看護師4 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 休 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 12 | 2 | 1 | 0 |
看護師5 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 12 | 2 | 0 | 0 |
看護師6 | 休 | 日 | 日 | 夜 | 明 | 休 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 日 | 日 | 夜 | 明 | 休 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 日 | 休 | 日 | 12 | 4 | 0 | 0 |
看護師7 | 日 | 日 | 日 | 休 | 日 | 休 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 休 | 日 | 夜 | 明 | 12 | 3 | 0 | 0 |
看護師8 | 休 | 日 | 休 | 日 | 日 | 休 | 休 | 日 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 日 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 日 | 11 | 2 | 0 | 0 |
看護師9 | 日 | 休 | 休 | 日 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 12 | 3 | 0 | 0 |
看護師10 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 早 | 日 | 休 | 日 | 日 | 休 | 夜 | 明 | 休 | 日 | 早 | 日 | 休 | 休 | 休 | 休 | 休 | 休 | 休 | 休 | 休 | 日 | 夜 | 明 | 14 | 3 | 0 | 0 |
看護師11 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 早 | 日 | 休 | 日 | 夜 | 明 | 休 | 休 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 12 | 4 | 1 | 0 |
看護師12 | 日 | 早 | 休 | 休 | 日 | 休 | 夜 | 明 | 休 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 日 | 早 | 日 | 11 | 3 | 0 | 0 |
看護師13 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 休 | 休 | 日 | 日 | 休 | 日 | 日 | 日 | 休 | 日 | 日 | 休 | 休 | 日 | 休 | 日 | 12 | 3 | 1 | 0 |
看護師14 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 休 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 12 | 2 | 0 | 0 |
看護師15 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 13 | 3 | 0 | 0 |
看護師16 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 早 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 12 | 1 | 0 | 0 |
看護師17 | 日 | 休 | 日 | 日 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 休 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 12 | 3 | 1 | 0 |
看護師18 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 早 | 日 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 休 | 日 | 日 | 休 | 12 | 1 | 0 | 0 |
看護師19 | 日 | 休 | 日 | 日 | 日 | 休 | 休 | 日 | 早 | 日 | 休 | 日 | 休 | 休 | 休 | 日 | 日 | 日 | 日 | 休 | 休 | 日 | 休 | 日 | 日 | 日 | 休 | 休 | 日 | 日 | 日 | 12 | 0 | 1 | 0 |
看護師20 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 13 | 2 | 0 | 0 |
看護師21 | 休 | 日 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 日 | 早 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 12 | 2 | 0 | 0 |
看護師22 | 明 | 休 | 日 | 日 | 早 | 休 | 休 | 日 | 日 | 早 | 休 | 日 | 休 | 日 | 休 | 日 | 早 | 休 | 日 | 休 | 日 | 早 | 休 | 日 | 早 | 早 | 休 | 日 | 早 | 日 | 休 | 11 | 0 | 0 | 0 |
看護師23 | 休 | 日 | 休 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 日 | 休 | 休 | 休 | 休 | 日 | 早 | 夜 | 明 | 休 | 日 | 夜 | 明 | 休 | 休 | 日 | 休 | 日 | 日 | 13 | 3 | 0 | 0 |
看護師24 | 休 | 日 | 日 | 夜 | 明 | 休 | 休 | 日 | 日 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 日 | 夜 | 明 | 休 | 休 | 休 | 日 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 12 | 4 | 1 | 0 |
看護師25 | 日 | 休 | 日 | 早 | 夜 | 明 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 休 | 日 | 日 | 休 | 休 | 休 | 休 | 日 | 日 | 夜 | 明 | 休 | 日 | 日 | 夜 | 12 | 3 | 0 | 0 |
休 | 10 | 8 | 9 | 8 | 6 | 14 | 14 | 6 | 9 | 10 | 9 | 6 | 14 | 14 | 11 | 8 | 10 | 9 | 6 | 14 | 11 | 10 | 14 | 7 | 10 | 6 | 14 | 14 | 6 | 9 | 6 | 0 | 0 | 0 | 0 |
日 | 10 | 12 | 11 | 12 | 14 | 7 | 7 | 14 | 11 | 10 | 11 | 14 | 7 | 7 | 10 | 12 | 10 | 11 | 14 | 7 | 10 | 10 | 7 | 13 | 10 | 14 | 7 | 7 | 14 | 11 | 14 | 0 | 0 | 0 | 0 |
夜 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 0 | 0 | 0 | 0 |
明 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 0 | 0 | 0 | 0 |
早 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
4.まとめ
今回は前回までのモデルに加えて、
- 相性NGペアを夜勤同時割当NGにする制約
- 男性のみ夜勤禁止(女性必須配置)制約
- 3連休を抑制する機能
- 5連続勤務を抑制する機能
を実装しました。
今回の実行結果では3連休・5連続勤務は生成されませんでした(各スタッフの希望による3連休を除く)。条件次第では3連休・5連続勤務が生成される場合もありますが、目的関数に設定されているので極力少なくなります。
関連記事
-
‹‹ 前回:「Julia×JuMPで病棟シフト最適化③:Excelから希望休を読込&最適化」
https://qiita.com/kaz_ict_nurse/items/0bfa866b2bf114b6dcf3 -
次回 ››:「Julia×JuMPで病棟シフト最適化⑤(最終回):最後の機能追加で実務レベルを高める」
https://qiita.com/kaz_ict_nurse/items/d5185cd6ed65b2366eb4