質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

4181閲覧

Pythonによるpulpの組合せ最適化を使用したシフト作成について

k-shiokawa

総合スコア11

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

1クリップ

投稿2019/06/20 01:58

編集2019/06/21 03:43

前提・実現したいこと

Python初心者です。宜しくお願いいたします。
稚拙な説明で申し訳ありませんが、ご指導いただけますと幸いです。

Pythonにてpulpの組合せ最適化を使用しシフト作成を行うことを検討しています。作成するシフトについての条件は次のとおりとなります。
・1チーム5名(A~E)で成り立っています。
・日勤→夜勤→明け→休暇 のローテーションで繰り返される勤務となります。
・日勤でも早番と遅番がありますので、日勤シフトの場合、早番で勤務するメンバーと遅番で勤務するメンバーにわかれています。
作成するシフトについて「0」「1」「2」で表現することとし、「0:休暇」「1:早番」「2:遅番」としたいです。
例として6/20が日勤で「1:早番」が2名「2:遅番」が2名(1名は休暇)のシフト場合、その日の数値の合計値は「6」となりますが、1名は休暇でありますので、4名で「6」を表す場合の組合せは「1」が2名「2」が2名(「0」が1名)となるので、これを制約式で表現したいと思っています。

発生している問題・エラーメッセージ

元々存在していたソースは「0:休暇」「1:出勤」の組合せを制約式にしたものだったのですが、この制約式を修正し、0~2の値を取り得ることと、該当の日における「0:休暇」の人数を制約式として追加することを試みました。
######0~2の値を取り得る

HTML

1for i in range(n_members): 2 for j in range(n_days): 3 model.addConstraint(a.iloc[i,j] <= 2)

######該当の日における「0:休暇」の人数

HTML

1for i in range(n_days): 2 n_rest = num_rest_members[duty_list[i]] 3 for j in range(n_members): 4 if value(a.iloc[j,i]) == 0: 5 x += 1 6 model.addConstraint(x <= n_rest)

実行すると後半部分の制約式に誤りがあり、次のようなエラーが発生します。
TypeError: Can only add LpConstraint objects

該当のソースコード

html

1import numpy as np 2import pandas as pd 3from pulp import * 4from ortoolpy import addvar, addvars, addbinvars 5import random 6import string 7 8model = LpProblem() 9 10n_members = 5 11members = list(string.ascii_uppercase[:n_members]) 12days = range(1, 31) 13n_days = 30 14 15""" 16変数の準備 17""" 18a = pd.DataFrame(addvars(5, 30), index=members, columns=days) 19members = list(string.ascii_uppercase[:n_members]) 20 21num_required_members = {'日勤': 6, '夜勤': 7, '明け': 5, '休暇': 0} 22num_rest_members = {'日勤': 1, '夜勤': 0, '明け': 0, '休暇': 5} 23 24duty_list = ['明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇', '日勤', '夜勤', '明け', '休暇'] 25 26t = 30 27y = addvar() 28x = addvar() 29""" 30目的関数と制約式 31""" 32model.setObjective(y) 33""" 34各メンバーの出勤日数をなるべく小さくする 35""" 36for i, s in enumerate(a.sum()): 37 model.addConstraint(s - t <= y) 38 model.addConstraint(s - t >= -y) 39 40""" 41取り得る値は0~2 42""" 43for i in range(n_members): 44 for j in range(n_days): 45 model.addConstraint(a.iloc[i,j] <= 2) 46 47""" 48該当の日の数値の合計が一致する。 49""" 50for i in range(n_days): 51 n_mem = num_required_members[duty_list[i]] 52 model.addConstraint(a.iloc[:,i].sum() == n_mem) 53 54""" 55該当の日の休暇(0)の数が一致する。 56""" 57for i in range(n_days): 58 n_rest = num_rest_members[duty_list[i]] 59 for j in range(n_members): 60 if value(a.iloc[j,i]) == 0: 61 x += 1 62 model.addConstraint(x <= n_rest) 63 64model.solve()

試したこと

前述でも記載させていただきましたが、0~2の値を取り得ることと、該当の日における「0:休暇」の人数を制約式として追加することを試みましたが、休暇(0)の数の制御がうまく動作しませんでした。

補足情報(FW/ツールのバージョンなど)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tiitoi

2019/06/20 04:28

Python のコードの記載はマークダウン記法を使っていただけますでしょうか。(インデントが崩れてしまうため)
k-shiokawa

2019/06/20 04:40

大変失礼いたしました。マークダウン記法にてコードを修正いたしました。
tiitoi

2019/06/20 06:44 編集

訂正ありがとうございます。 すみませんが、質問のコードは定義されていない変数等があり、動きません。 一旦コードは置いておいて、文章でルール (制約条件) を整理していただけますか? シフトは「早番、遅番、夜勤、休暇」の4種類だと解釈したのですが、「明け」とはどういう意味でしょうか。 また、日勤→夜勤→休暇を繰り返すということは、日勤、夜勤、休暇が連続したりすることはないということでしょうか。
k-shiokawa

2019/06/20 07:00

ご質問有難うございます。コードについては主に制約条件をピックアップして記載させていただいておりまして、うまく動作しないとのことで失礼いたしました。 またシフト勤務ですが、日勤(早番または遅番)は8時間勤務となるのですが、夜勤については前日から翌日朝までの16時間(2日分)勤務し、勤務が終了した日については「明け」扱いとなり、出社しないで良いルールとなっています。なお「明け」は休暇ではありませんので、「休暇」とは別扱いのシフトとしています。 そのため夜勤→夜勤といった連続した勤務はありません。日勤(早番または遅番)→夜勤→明け→休暇→日勤・・・を繰り返す形式となります。 (厳密には他のチームのメンバーの都合等により日勤→日勤となる場合もありますが、初めからそこまで考慮すると難解となってしまうとの考えから対象外とさせていただいています)
tiitoi

2019/06/20 11:08

> TypeError: Can only add LpConstraint objects pulp に model.addConstraint() で制約を追加する際、引数が pulp.pulp.LpConstraint でないというエラーです。 print(type(x <= n_rest)) を入れた場合、pulp.pulp.LpConstraint 型になっていますか?
k-shiokawa

2019/06/21 01:57

改めてご指摘いただいた点を元にコードを見直ししましたところ、同様のエラーは発生しなくなりました。有難うございました。また次の点について修正を試みました。 ①元来の仕様では「0」,「1」のみをシフトの対象としていたのですが、今回「2」も対象となるので以下のとおり「1」を外しました。 for i, s in enumerate(a.sum(1)): →for i, s in enumerate(a.sum()): ②該当の日の数値の合計が一致する必要がありますので、以下のとおり等号に修正しました。 model.addConstraint(a.iloc[:,i].sum() <= n_mem) →model.addConstraint(a.iloc[:,i].sum() == n_mem) 一応動作はするのですが、明けの日の場合、5名全員に「1」が入って欲しいのですが、「2」「2」「1」「0」「0」といったシフトになってしまいます。また日勤の日も、「2」「2」「1」「1」「0」となって欲しいのですが、、「2」「2」「2」「0」「0」となってしまいます。 「該当の日の数値の合計が一致する」の制約は守られているのですが、追加した「該当の日の休暇(0)の数が一致する」の制約が全く反映できない状況に陥っています。 また今回うまく動作しない形式でのソースで質問させていただきましたが、問題ありましたら改めて動作するソースで投稿し直させていただきたいとも思います。
k-shiokawa

2019/06/21 03:44

ソースについてそのままでは動作できなかったため、載せ替えさせていただきました。
k-shiokawa

2019/06/21 06:30

丁寧なご説明とサンプルコードのご提供をありがとうございました。当方が作成したいシフトの説明が悪かったようですので、改めてご説明させていただきます。 ・1チーム5名で成り立っていて、チーム制のシフト勤務を行っています。 ・日勤→夜勤→明け→休暇 のローテーションで繰り返される勤務となります。  同じチームに所属する5名は全員同じローテーションを行います。(日勤であれば5名全員が日勤シフト、夜勤でれば全員が夜勤シフト、同様に休暇であれば全員が休暇) ・仮に上述のチームをAチームとした場合、Aチームが休暇の場合、日勤や夜勤が不在となってしまいますが、それはBチームやCチームのメンバーが行うこととなります。 ・最終的には全チームのシフトを作成する必要がありますが、一旦はAチームに限定して検討したいです。 ・元々のソースは出勤「1」or休暇「0」の2択だったのですが、日勤の日については早番or遅番の別がありますので、それを「1」or「2」で区別したいです。 ・Aチームでは当日がどのシフトとなるかは予め判明していますので、「明け」や「夜勤」は「1」で表せれば問題ありません。 ・上述のソースの「duty_list 」がAチームのシフトとなっています。日々のシフトを満たすにはどのような人数構成となっていればよいかを「num_required_members 」や「num_rest_members 」でうまく制約がつけられれば良いと考えました。 お手間を取らさせてしまい申し訳ありませんが、可能でありましたらお力添えいただけますと幸いです。
tiitoi

2019/06/21 06:47 編集

人数は5人で全員が同じシフトで動くのだとすると、何を最適化したいのでしょうか。 例えば、30日分のシフトを作るとして、各日付の '日勤', '夜勤', '明け', '休暇' のパターンも決まっているのだとすると、日勤のシフトの日に5人がそれぞれ早番、遅番のどちらにするかしか選択肢がないように思えますが、いかがでしょうか。 また「5名は全員同じローテーション」という説明と質問文に記載がある以下は矛盾しませんか。 > 「1:早番」が2名「2:遅番」が2名(1名は休暇)
k-shiokawa

2019/06/21 06:56

制約に記載させていただかなかったのですが、勤務地が2か所ありまして、夜勤シフトでA勤務地、B勤務地に分かれるのですが、これも「1」「2」等で表せればと考えていました。他休日の場合は出勤人数も少なくなるのですが、出勤日にばらつきが出ないようにしたいと考えています。 ご指摘の件もっともなのですが、条件を色々と出すと複雑になってしまうかと思い、また今回のケースがクリアできれば自ら検討ができるかと思った次第です。 申し訳ありません。
tiitoi

2019/06/21 07:11 編集

簡易的な条件から取り組む方針はよいと思いますよ。 しかし、現状の問題設定ですと、日勤の日に早番、遅番のどちらかを選択するかどうかしか選択肢がないので、早番2名必要、遅番3名必要となっていたら、2名を早番に割り当てる、3名を遅番に割り当てるだけなので、最適化する要素がありません。 あと、選択肢がA, B, C あったとして、0, 1, 2 の値をとる変数1つで表すという方式だと制約式が書けません。 基本的にその場合はAするかどうか、Bするかどうか、Cするかどうかの3つの2値変数で表すのがよいかと思います。 (2値変数というのは、取る値が0,1ということ。1だったら ~ する、0だったら ~ しない を意味する。) まずは類似問題などで変数定義、制約条件の書き方等を勉強してみてはどうでしょうか。 「集合被覆問題」 http://www.msi.co.jp/nuopt/docs/v19/examples/html/02-06-00.html 「スケジューリング問題」 https://tech.unifa-e.com/entry/2019/06/21/064243?utm_source=feed
k-shiokawa

2019/06/21 07:15

ご回答ありがとうとございました。教えていただいたリンク先の内容を確認して再考いたします。
guest

回答1

0

ベストアンサー

問題設定について

元々出勤 or 休暇の2択を2値変数で表されていたのを "早番", "遅番", "夜勤", "明け", "休暇" の5種類に拡張するのであれば、変数は (日付の数, 従業員の数, 5) だけ変数が必要になるのではないでしょうか。

質問にあるような変数を整数にして早番なら0、遅番なら1、夜勤なら2、... のようなやり方だと、「各時間帯で必要な人数以上が割り当てられている」という制約や「早番 or 遅番 → 夜勤 → 明け → 休暇のパターンのみ OK」という制約を数式で表す方法が思いつきませんでした。

まずはコードは一旦置いておいて、要求を満たすようにするにはどのようなモデルを作成すればいいか数式で考えたほうがよいかと思います。

必要な情報を定義

イメージ説明

目標関数

イメージ説明

制約条件

イメージ説明

サンプルコード

必要な情報を定義

python

1import itertools 2 3import pandas as pd 4from pulp import LpProblem, LpVariable, lpSum, LpMinimize 5 6# 入力 7num_employees = 20 8 9days = pd.date_range("2019-06-01", "2019-06-30") # 日付一覧 10employees = pd.Series([f"employees{i}" for i in range(num_employees)]) # 従業員一覧 11shifts = pd.Series(["早番", "遅番", "夜勤", "明け", "休暇"]) # シフト一覧 12 13# 形状が (日付, 3) の DataFrame。(i, k) 成分は日付 i のシフト k の必要人数を表す。 14need = pd.DataFrame([[2, 2, 2]], index=days, columns=["早番", "遅番", "夜勤"]) 15need 16 17# 許可するパターン 18# 早番 or 遅番 → 夜勤 → 明け → 休暇のパターンのみ OK 19ok_patterns = [ 20 ("早番", "夜勤"), 21 ("遅番", "夜勤"), 22 ("夜勤", "明け"), 23 ("明け", "休暇"), 24 ("休暇", "早番"), 25 ("休暇", "遅番"), 26]

モデルの作成及びソルバー実行

python

1# モデルを作成する。 2model = LpProblem(sense=LpMinimize) 3 4# 変数を作成する。 5X = [ 6 [[LpVariable(f"{d}_{e}_{s}", cat="Binary") for s in shifts] for e in employees] 7 for d in days 8] 9 10# 目的変数及び制約を作成する。 11########################################### 12objective = 0 13for i in range(days.size): 14 for k in range(need.columns.size): 15 # 日付 days[i] のシフト k に実際に働いた人数 16 actual = lpSum(X[i][j][k] for j in range(employees.size)) 17 # 日付 days[i] のシフト k に必要だった人数 18 expected = need.iloc[i, k] 19 # 余剰人数を目的関数に足す。 20 objective += actual - expected 21 # (制約1) シフト k の必要人数は満たす制約 22 model += actual >= expected 23model += objective # 余剰人数を最小化する。 24 25# (制約2) 一人が同じ日に複数のシフトはできない。 26for i in range(days.size): 27 for j in range(employees.size): 28 model += lpSum(X[i][j]) == 1 29 30# OK のパターンをインデックスに変換する。 31name_to_idx = {v: i for i, v in shifts.items()} 32ok = [tuple([name_to_idx[name] for name in p]) for p in ok_patterns] 33 34# NG のパターンを作成する。 35ng = [p for p in itertools.product(name_to_idx.values(), repeat=2) if p not in ok] 36 37# (制約3) NG のパターンが出ないように制約に追加する。 38for j in range(employees.size): 39 for pattern in ng: 40 for i in range(days.size - len(pattern) + 1): 41 model += ( 42 lpSum(X[i + k][j][pattern[k]] for k in range(len(pattern))) 43 <= len(pattern) - 1 44 ) 45 46 47# モデルを解く。 48ret = model.solve() 49assert ret == 1, f"No Solution found, status: {ret}"

結果を元にシフト表作成

python

1# 結果を取得して、データフレームを作成する。 2schedule = pd.DataFrame(index=days, columns=employees) 3for i in range(days.size): 4 for j in range(employees.size): 5 # one-hot 表現からインデックスを取得する。 例: [0, 0, 1, 0] -> 2 6 binary_repr = [X[i][j][k].value() for k in range(len(shifts))] 7 assert np.sum(binary_repr) == 1 8 k = np.argmax(binary_repr) 9 schedule.iloc[i, j] = shifts.iloc[k] 10 11schedule.to_csv("schedule.csv")

出力結果

python

1,employees0,employees1,employees2,employees3,employees4,employees5,employees6,employees7,employees8,employees9,employees10,employees11,employees12,employees13,employees14,employees15,employees16,employees17,employees18,employees19 22019-06-01,明け,夜勤,休暇,遅番,夜勤,明け,明け,明け,明け,遅番,早番,明け,明け,休暇,明け,夜勤,休暇,夜勤,早番,休暇 32019-06-02,休暇,明け,早番,夜勤,明け,休暇,休暇,休暇,休暇,夜勤,夜勤,休暇,休暇,遅番,休暇,明け,遅番,明け,夜勤,早番 42019-06-03,遅番,休暇,夜勤,明け,休暇,遅番,早番,早番,早番,明け,明け,遅番,早番,夜勤,遅番,休暇,夜勤,休暇,明け,夜勤 52019-06-04,夜勤,遅番,明け,休暇,早番,夜勤,夜勤,夜勤,夜勤,休暇,休暇,夜勤,夜勤,明け,夜勤,遅番,明け,早番,休暇,明け 62019-06-05,明け,夜勤,休暇,遅番,夜勤,明け,明け,明け,明け,遅番,早番,明け,明け,休暇,明け,夜勤,休暇,夜勤,早番,休暇 72019-06-06,休暇,明け,遅番,夜勤,明け,休暇,休暇,休暇,休暇,夜勤,夜勤,休暇,休暇,早番,休暇,明け,遅番,明け,夜勤,早番 82019-06-07,早番,休暇,夜勤,明け,休暇,遅番,早番,遅番,早番,明け,明け,早番,遅番,夜勤,早番,休暇,夜勤,休暇,明け,夜勤 92019-06-08,夜勤,早番,明け,休暇,遅番,夜勤,夜勤,夜勤,夜勤,休暇,休暇,夜勤,夜勤,明け,夜勤,早番,明け,遅番,休暇,明け 102019-06-09,明け,夜勤,休暇,早番,夜勤,明け,明け,明け,明け,遅番,早番,明け,明け,休暇,明け,夜勤,休暇,夜勤,遅番,休暇 112019-06-10,休暇,明け,遅番,夜勤,明け,休暇,休暇,休暇,休暇,夜勤,夜勤,休暇,休暇,早番,休暇,明け,遅番,明け,夜勤,早番 122019-06-11,早番,休暇,夜勤,明け,休暇,早番,遅番,遅番,遅番,明け,明け,早番,早番,夜勤,遅番,休暇,夜勤,休暇,明け,夜勤 132019-06-12,夜勤,早番,明け,休暇,早番,夜勤,夜勤,夜勤,夜勤,休暇,休暇,夜勤,夜勤,明け,夜勤,遅番,明け,遅番,休暇,明け 142019-06-13,明け,夜勤,休暇,早番,夜勤,明け,明け,明け,明け,遅番,早番,明け,明け,休暇,明け,夜勤,休暇,夜勤,遅番,休暇 152019-06-14,休暇,明け,遅番,夜勤,明け,休暇,休暇,休暇,休暇,夜勤,夜勤,休暇,休暇,早番,休暇,明け,早番,明け,夜勤,遅番 16 17以下略

3つの制約に関しては満たされていることは確認しました。

所感

早番 or 遅番 → 夜勤 → 明け → 休暇のパターンのみ OK という制約のため、ある程度の人数がいないとそのようなパターンが存在しなくなってしまうし、余剰人数が出る日が出てきてしまうので、日勤 (早番 or 遅番) は2回までは OK にする等もう少し制約を緩めたほうが無駄が出ないかもしれません。


最適化については専門ではないので、間違い等ありましたらすみません。
意図と違いましたら、コメントしてください。

投稿2019/06/21 05:40

編集2019/06/21 05:41
tiitoi

総合スコア21956

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

k-shiokawa

2019/07/03 05:40

ご連携いただいたリンク先の資料を基に再考したところ、実現することができました。ご丁寧に説明等くださり大変お世話になりました。感謝申し上げます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問