2015年にシフトを自動で組むプログラムをPythonで書いた - matsu7874のブログを書いているのですが、これはランダムで100案作って制約を満たすか確認する雑な実装なので、もう少し勉強して書き直してみるシリーズの第一回目です。
問題設定
- N日分のシフト枠をM人のワーカーで埋める
入力
N,Mと各ワーカーのNG日程のリスト
例:5日3人の場合、下記の入力でworker0はday2がNG、worker3はday0,1,2がNGということを表します。
[ [2,], [3,], [4,], [0,1,2,], [2,3,4,], ]
満たすべき制約
- 各ワーカーのNGを優先する。
- 毎日capacity人以下の割当を行う。
なるべく実現したいこと
- 毎日の割当はなるべくcapacity人にする。
- ワーカーの割当回数はなるべく均等にする。
方針
まずはシンプルな貪欲法で実装したいと思います。
- リストの先頭から働ける人に働いてもらう。
- 割当回数を均等化するために、割当日数の少ない人から割り当てていく。
最悪時は毎日全員が働けるかをチェックするため、NGが重ならない限りは割当が可能です。
実装
import sys import random import heapq class Worker: def __init__(self, name, ng): self.name = name self.ng = ng def __str__(self): return str((self.name, str(self.ng))) class Shift: def __init__(self, days, capacity): self.days = days self.capacity = capacity self.shift = [['UNASSIGNED']*capacity for i in range(days)] def assign(self, workers): hq = [(0, i) for i in range(len(workers))] used_hq = [] for d in range(self.days): day_assigned_cnt = 0 while hq and day_assigned_cnt < self.capacity: worker_assigned_cnt, worker_id = heapq.heappop(hq) if d in workers[worker_id].ng: heapq.heappush(used_hq, (worker_assigned_cnt, worker_id)) else: self.shift[d][day_assigned_cnt] = workers[worker_id].name day_assigned_cnt += 1 heapq.heappush(used_hq, (worker_assigned_cnt+1, worker_id)) while used_hq: heapq.heappush(hq, heapq.heappop(used_hq)) self.shift[d].sort() worker_assigned_cnt = [0 for i in range(len(workers))] for wa_cnt, worker_id in hq: worker_assigned_cnt[worker_id] = wa_cnt print(worker_assigned_cnt, file=sys.stderr) def __str__(self): return ',\n'.join([str((i, d)) for i, d in enumerate(self.shift)]) def main(): n_days = 10 n_workers = 5 # 各workerはランダムで最大n_days/2日分までNGを出す。 workers = [Worker('worker {:02}'.format(i), sorted(random.sample( range(n_days), random.randint(0, n_days//2)))) for i in range(n_workers)] print(*workers, sep='\n', file=sys.stderr) shift = Shift(n_days, 2) shift.assign(workers) for d, ws in enumerate(shift.shift): print(d, '\t'.join(ws), sep='\t') if __name__ == "__main__": main()
実行結果
上記のプログラムを実行すると、乱数を使っているので同じ結果にはならないと思いますが、下記のような結果を得ることができます。
('worker 00', '[]') ('worker 01', '[5, 9]') ('worker 02', '[0, 9]') ('worker 03', '[1, 2, 4, 7, 9]') ('worker 04', '[1, 3, 7, 8, 9]') [5, 4, 4, 4, 2] 0 worker 00 worker 01 1 worker 00 worker 02 2 worker 01 worker 04 3 worker 02 worker 03 4 worker 00 worker 04 5 worker 02 worker 03 6 worker 01 worker 03 7 worker 00 worker 01 8 worker 02 worker 03 9 UNASSIGNED worker 00
4/5がNGを出しているday9以外は割当が行えていることがわかります。 一方で各ワーカーの割当回数には2から5のばらつきがあり、NGを5箇所出しているworker03,04間で割当回数が2,4と差がある点は課題です。 また、day0にworker00の代わりにworker04を割り当てることでより均等な割当が実現可能です。
最後に
ナース・スケジューリング:問題把握とモデリングを読んでいるのですが、いろいろなアルゴリズムが応用されていて非常に面白いです。 次回以降、内容を紹介しながらプログラムを改善していきたいと思います。