def team_divider(member_list, rate_list, number_of_teams): rate_list_copy = [[i] for i in rate_list] rate_list_copy = np.array(rate_list_copy) number_of_members = len(member_list) m = pulp.LpProblem() # 数理モデル x = np.array(addbinvars(number_of_members, number_of_teams)) # 割当 y = np.array(addvars(number_of_teams, 2)) # チーム内の最小と最大 z = addvars(2) # チーム間の最小と最大 m += pulp.lpSum(y[:, 1] - y[:, 0]) + 1.5 * (z[1] - z[0]) # 目的関数 for i in range(number_of_members): m += pulp.lpSum(x[i]) == 1 # どこかのチームに所属 for j in range(number_of_teams): m += pulp.lpDot(rate_list_copy.sum(1), x[:, j]) >= z[0] m += pulp.lpDot(rate_list_copy.sum(1), x[:, j]) <= z[1] for k in range(rate_list_copy.shape[1]): m += pulp.lpDot(rate_list_copy[:, k], x[:, j]) >= y[j, 0] m += pulp.lpDot(rate_list_copy[:, k], x[:, j]) <= y[j, 1] m.solve() # 求解 divide_result = np.vectorize(pulp.value)(x) teamdivide = [ i for i in (divide_result @ range(number_of_teams)).astype(int) ] return teamdivide
def pulp_mosaic(v_num, h_num, ch_num, box_array): from pulp import LpProblem, lpSum, lpDot, value, LpStatus from ortoolpy import addbinvars import time start = time.time() print("Calculate the optimal combination") print("Variables : {}".format(v_num * h_num * ch_num)) if box_array.shape != (v_num, h_num, ch_num): raise Exception("shape does not match!!") # 変数宣言 variable = np.array(addbinvars(v_num, h_num, ch_num)) # 目的関数の最小化 prob = LpProblem() # 目的関数の設定 prob += (box_array * variable).sum() # 制約条件の設定 for ch in range(ch_num): prob += variable[:, :, ch].sum() <= 1 for v in range(v_num): for h in range(h_num): prob += variable[v, h, :].sum() == 1 # 問題を解く prob.solve() if LpStatus[prob.status] != "Optimal": raise Exception("could not be optimized!!") else: print("status = " + LpStatus[prob.status]) # 最適化された変数の値を抽出 opt_var = np.vectorize(value)(variable).astype(int) # 全体で最小となるインデックスの組み合わせ bit_index = np.empty((v_num, h_num), dtype=np.int32) for v in range(v_num): for h in range(h_num): bit_index[v, h] = np.where(opt_var[v, h, :] == 1)[0] elapsed_time = time.time() - start print("elapsed_time:{0}".format(elapsed_time) + "[sec]") return bit_index
n_user = 100 n_cand_firehouse, n_firehouse = 100, 3 loc_user = np.random.rand(n_user, 2) cand_loc_firehouse = copy.deepcopy(loc_user) plt.scatter(*loc_user.T) # - 施設配置問題の定式化 # In[39]: from pulp import * from ortoolpy import addbinvars, addvars m = LpProblem() x = addbinvars(n_user, n_cand_firehouse) y = addbinvars(n_cand_firehouse) m += lpSum(((loc_user[i][0] - cand_loc_firehouse[j][0])**2 + (loc_user[i][1] - cand_loc_firehouse[j][1])**2) * x[i][j] for i in range(n_user) for j in range(n_cand_firehouse)) m += lpSum(y) <= n_firehouse for i in range(n_user): #利用者iは,1つの消防署のみに割り当てられる m += lpSum(x[i]) == 1 for j in range(n_cand_firehouse): #消防署のない場所に、利用者を割り当てられない m += x[i][j] <= y[j] # - 最適化 # In[40]: m.solve()
list2 = [] for _, elem in enumerate(list1): list2.append(str(elem.weekday())) return list2 workMembers = ["浜田", "松本", "田中", "遠藤", "方正"] dateset = makeDateSetOfMonth() listLenDateset = list(range(0, len(dateset))) df = pd.DataFrame(index=listLenDateset, columns=workMembers) df['日付'] = dateset df['曜日'] = makeDaysSetOfMonth() # print(df) v割当 = np.array(op.addbinvars(df.shape[0], df.shape[1] - 2)) # 希望休に関しては、V割当の中の、一部を固定する方向でとりあえずいこうか?qiitaの希望不可扱いでいってしまえばよい #制約 # //必要な条件はなんでしょうか? # //① 平日は2人基本的に必要 => 定数y行 のxの合計が3となればよい c平日 = 100 c平日soft = 50 v平日不足 = op.addvars(len(dateset)) v平日不足soft = op.addvars(len(dateset)) #dateset分配列を作るが、平日以外の部分に0以外の数字が入る可能性がないので問題なし。=>本当に???????? # //② 土日は4人必要 => 定数y行 のxの合計が5 c休日 = 100
member = member.T detail = pd.read_excel('1月作成用.xlsx',sheet_name='Detail',index_col=0) # Shiftシートのカラムとインデックス date = shift.index employee = shift.columns # 確認用にコピー shift_show = shift # 従業員数と月の日数 days, number_employee = shift.shape[0], shift.shape[1] holidays = play_ground.holiday_list(days) # 変数作成 var = pd.DataFrame(np.array(addbinvars(days, number_employee)), columns=employee, index=date) # 0と1を逆転させている shift_rev = shift[shift.columns].apply(lambda r: 1-r[shift.columns],1) k = LpProblem() # 希望していない場所には入らないようにする。 for (_, h),(_, n) in zip(shift_rev.iterrows(),var.iterrows()): k += lpDot(h, n) <= 0 # 変数の追加 shift['V_need_dif'] = addvars(days) # 必要人数に達していない時のペナルティ変数 shift['V_gender_rate'] = addvars(days) # 女性が少ない時のペナルティ shift['V_experience'] = addvars(days) # 新人だけになった時のペナルティ shift['need'] = manage.need # 必要人数
def divide_users(): # デフォルトだと 54 x 54 で読まれるので、不要行は読み込まないようにする skip_rows = [ i + GROUP_COUNT for i in range(1, USER_COUNT - GROUP_COUNT + 1) ] df = pd.read_excel(FILE_NAME, header=0, index_col=0, skiprows=skip_rows) # 当番回数 event_count = df.shape[0] # print(f'{type(box_size)}: {box_size}') # => <class 'int'>: 18 # ユーザ数 user_count = df.shape[1] # print(f'{type(user_size)}: {user_size}') # => <class 'int'>: 54 # 数理モデル model = LpProblem() # 変数を準備(当番/非当番の2値なので、0-1変数リスト) # https://docs.pyq.jp/python/math_opt/pdopt.html var_schedule = np.array(addbinvars(event_count, user_count)) df['必要人数差'] = addvars(event_count) # 重み weight = 1 # 目的関数の割り当て model += lpSum(df.必要人数差) * weight # 制約 # 1当番あたり3人 for idx, row in df.iterrows(): model += row.必要人数差 >= (lpSum(var_schedule[row.name]) - 3) model += row.必要人数差 >= -(lpSum(var_schedule[row.name]) - 3) # 一人あたり1回当番すればよい for user in range(user_count): scheduled = [var_schedule[event, user] for event in range(event_count)] model += lpSum(pd.Series(scheduled)) <= 1 # 当番可能な時だけ割り当てる df_rev = df[df.columns].apply(lambda r: 1 - r[df.columns], 1) for (_, d), (_, s) in zip(df_rev.iterrows(), pd.DataFrame(var_schedule).iterrows()): model += lpDot(d, s) <= 0 # 実行 model.solve() # 結果取得 vectorized_results = np.vectorize(value)(var_schedule).astype(int) # print(type(vectorized_results)) # => <class 'numpy.ndarray'> group = [[] for _ in range(event_count)] for i, vectorized_result in enumerate(vectorized_results): for result, name in zip(vectorized_result, df.columns): if result * name: group[i].append(name) return group
def temporal(): dateset = makeDateSetOfMonth(strDate) listLenDateset = list(range(0, len(dateset))) df = pd.DataFrame(index=listLenDateset, columns=workMembers) df['日付'] = dateset df['曜日'] = makeDaysSetOfMonth() # print(df) v割当 = np.array(op.addbinvars(df.shape[0], df.shape[1] - 2)) # 希望休に関しては、V割当の中の、一部を固定する方向でとりあえずいこうか?qiitaの希望不可扱いでいってしまえばよい #制約 # //必要な条件はなんでしょうか? # //① 平日は2人基本的に必要 => 定数y行 のxの合計が3となればよい c平日 = 100 c平日必要人数 = getNumOfMemberWorkingWeekDay() c平日soft = 50 v平日不足 = op.addvars(len(dateset)) v平日不足soft = op.addvars(len(dateset)) #dateset分配列を作るが、平日以外の部分に0以外の数字が入る可能性がないので問題なし。=>本当に???????? # //② 土日は4人必要 => 定数y行 のxの合計が5 c休日 = 100 c休日必要人数 = getNumOfMemberWorkingWeekEnd() v休日不足 = op.addvars(len(dateset)) c休日soft = 50 v休日不足soft = op.addvars(len(dateset)) # //③ 希望休の場所には0固定でいれるようにする => 指定した[y][x]の値が0固定 c希望休 = 100 # //④ メンバそれぞれに休みが10日必要 => xの列はそれぞれ、合計20とならなければならない(有給がある場合は19) 必要な休日数はそれぞれ設定できたほうがよいね(月によっても違うので) なので、月の日数 - 必要休日数 = あるxの列の必要値 c計休 = 100 v計休不足 = op.addvars(len(workMembers)) #休暇はメンバごとに日数がちがうな。。。とりあえず10日としとこう # //⑤二連休が欲しい人 =>これは希望休みに組み込めるからどうでもよい # //⑥最大連勤数nの設定が必要 => これには縦列のx1~x30を、nずつ区切り、そこの和がn-1以下にならなければならない。(つまりどこかで休みがないといけない) #5連勤がないようにする(つまりn = 4) c連勤 = 100 # v連勤違反 = op.addvars() # //⑦最大連勤数は前の月からも考慮しなきゃいけないので、前の月が終わった段階での連続出勤数を計算に組み込んで置く必要がある。 dfWeekend = df[(df['曜日'] == 1)] #この書きかたは、trueが返る場所だけを取得できる dfWeekday = df[(df['曜日'] == 0)] dfWork = df.iloc[:, :len(workMembers)] # 目的関数 m = pp.LpProblem(sense=pp.LpMinimize) m += c平日 * pp.lpSum(v平日不足) +\ c平日soft * pp.lpSum(v平日不足soft) +\ c休日 * pp.lpSum(v休日不足) +\ c休日soft * pp.lpSum(v休日不足soft) +\ c計休 * pp.lpSum(v計休不足) #制約関数 #dfWeekday の 行ごと(日にちごと)のV割当変数のseriesが2以上になるようにする。dfWeekendなら4以上 #V割当は二次元配列。 for _, elem in dfWeekday.iterrows(): # print(v平日不足[elem.name]) m += pp.lpSum(v割当[elem.name]) + v平日不足[ elem. name] >= c平日必要人数 - 1 # V割当の合計が、2以上でないとき、V平日不足が自動的に補うように設定され、スコアが大きくなってしまう for _, elem in dfWeekday.iterrows(): m += pp.lpSum(v割当[elem.name]) + v平日不足soft[elem.name] >= c平日必要人数 for _, elem in dfWeekend.iterrows(): m += pp.lpSum( v割当[elem.name]) + v休日不足[elem.name] >= c休日必要人数 - 1 #同上。休日ver for _, elem in dfWeekend.iterrows(): m += pp.lpSum(v割当[elem.name]) + v休日不足soft[elem.name] >= c休日必要人数 #従業員ごとの計休が出されてるか。チェック(列ごと) def fuckingFunc(): fuckingN = 0 global m #python ではグローバル変数を関数内で操るためには、global宣言が必要 for colName, series in dfWork.iteritems(): #elemにはseriesが返ってきてる。colNameにはseriesの列名。 m += pp.lpSum(v割当[:, fuckingN]) == len(dateset) - 10 # m += pp.lpSum(v割当[:,fuckingN]) - v計休不足[fuckingN] fuckingN += 1 fuckingFunc() #n連勤チェック #V割当から、1日~30-n日目までの配列。1+1 ~30-(n+1)日目までの配列・・・・n日~30日目までの配列を用意する。 #=>この1日目 v + 2日目 v + .... + n日目 vが一つ一つの要素(Xとする)となる。連勤チェック配列が出来上がる。 #この要素Xが、n以上にならないようにすれば、n連勤にならないようチェックができる。 for i, elem in enumerate(workMembers): print(elem, i) for n, elem in enumerate((v割当[:-4, i] + v割当[1:-3, i] + v割当[2:-2, i] + v割当[3:-1, i] + v割当[4:, i]).flat): m += elem <= 4 #これで5つの要素すべてに1が入る(5連勤する)と5を超える #希望休。希望休をいれる部分と対応するv割当に、1を確定でいれる。 #はまだ 12,1,26 #まつもと 19,20,5 #えんどう 26,27,13 #たなか 19,12,20 #ほうせい 13,5,6 # workMembers = ["浜田","松本","田中","遠藤","方正"] def holidayRequest(array2d): global m for i, elem in enumerate(array2d): for i2, elem2 in enumerate(elem): if i2 == 0: continue # 0番目名前管理用の番号なのでスキップ m += v割当[elem2 - 1][elem[0]] == 0 holidayRequest(希望休リスト) status = m.solve() print(pp.LpStatus[status]) funcH = np.vectorize(pp.value) result = funcH(v割当).astype(int) Res = result.astype(str) for i in range(0, Res.shape[0]): for j in range(0, Res.shape[1]): if Res[i][j] == '1': Res[i][j] = '〇' else: Res[i][j] = '×' # numでしっかり個々の値を拾えてるけど、なんでnoneになっちゃうんだろう。。。。。 #=> ndarrayが同じデータ型しか保持できないから。....ではないらしい。DFにつっこんでもだめ。 # astypeでキャストしてからでもだめ。=>キャストは、copyを作るものなので、代入しないといけない。 #とにもかくにもできた!mapとかvectorizeじゃできんかったわ。。。 df.iloc[:, 0:len(workMembers)] = Res #結果をdfに代入 print(df) df.index = df['日付'] df.drop(df.columns[[5, 6]], axis=1, inplace=True) #日付を一番左にもってきて、右のいらない曜日と日付を消す #日付の部分がセル空白狭いので、そこの整形と、土日祝の色付け #あとエクセル出力した際、条件つけれるように?、 # print(df) df.to_excel(r'C:\Users\mouse\Desktop\Python\シフト出力.xlsx', sheet_name='シフト結果')