In this article, we will create a 10-day shift for 7 employees. There are variations of matrix elements: 4-hour work, 8-hour work, and 0-hour work (holiday). After reading this article, you will finally be able to create the following roster.
Month | fire | water | wood | Money | soil | Day | Month | fire | water | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 8 | 8 | 8 | 4 | 0 | 8 | 8 | 8 | 4 |
1 | 4 | 8 | 4 | 4 | 4 | 8 | 0 | 8 | 4 | 4 |
2 | 0 | 8 | 4 | 0 | 0 | 4 | 8 | 4 | 4 | 8 |
3 | 4 | 0 | 8 | 0 | 0 | 4 | 4 | 8 | 4 | 8 |
4 | 8 | 0 | 4 | 4 | 4 | 0 | 0 | 8 | 4 | 8 |
5 | 4 | 4 | 4 | 0 | 4 | 8 | 0 | 8 | 8 | 8 |
6 | 0 | 8 | 4 | 8 | 4 | 8 | 0 | 4 | 4 | 0 |
Let's start with the algorithm.
① Creation of initial solution ② Creation of neighborhood solution ③ Optimize using a penalty to meet the constraint conditions
That's it in a nutshell. It is an image that randomly extracts from the array of [0, 4, 8] and fills the array so as to satisfy the constraint condition.
self.ns = np.zeros((person_num, days_num))
self.ns2 = np.zeros((person_num, days_num))
self.ns3 = np.zeros((person_num, days_num))
We are creating a matrix with all 0 elements (number of employees x number of days to calculate).
index_num = self.choice_index()
index_num2 = self.choice_index()
index_num3 = self.choice_index()
We have selected 3 indexes each to change to create 3 neighborhood solutions.
if not(sum(self.ns[i]) < 64 and sum(self.ns[i]) >= 40):
penalty = penalty + 1
if not(sum(self.ns2[i]) < 64 and sum(self.ns2[i]) >= 40):
penalty2 = penalty2 + 1
if not(sum(self.ns3[i]) < 64 and sum(self.ns3[i]) >= 40):
penalty3 = penalty3 + 1
We define a constraint that the weekly working hours of the three neighborhood solutions are within 40 to 64 hours. If the constraints are not met, the penalty will be increased by one.
Using the "taboo list" that records the transitions that have fallen into a bad solution, we will investigate other than the one searched once. As a result, convergence becomes faster and an approximate solution can be obtained.
%load_ext Cython
import numpy as np
import itertools
import matplotlib.pyplot as plt
import pandas as pd
class TabuSearch():
tabu_list = []
penalty_arr = []
def __init__(self, person_num = 7, days_num = 10, select_num = 3):
self.person_num = person_num
self.penalty_old = 7 * 2 + 10
self.days_num = 10
self.select_num = select_num
self.jikkou_num = 1
#Initialization of approximate solution
self.s_good = np.zeros((person_num, days_num))
self.index_list = list(itertools.product(range(0, person_num), range(0, days_num), repeat=1))
self.ns = np.zeros((person_num, days_num))
self.ns2 = np.zeros((person_num, days_num))
self.ns3 = np.zeros((person_num, days_num))
def choice_index(self):
return np.random.choice(list(range(0, self.person_num*self.days_num)), self.select_num, replace=False)
def generate_near(self):
#Creating a neighborhood solution
#Since the neighborhood solution is locally transformed, select three elements. (3 is appropriate)
#Index to replace
index_num = self.choice_index()
index_num2 = self.choice_index()
index_num3 = self.choice_index()
penalty = 0
penalty2 = 0
penalty3 = 0
ns = np.zeros((self.person_num, self.days_num))
ns2 = np.zeros((self.person_num, self.days_num))
ns3 = np.zeros((self.person_num, self.days_num))
chg_flag = True
#Value to change
new_var = [np.random.choice([8,4,0]) for i in range(0, self.select_num)]
for j in range(0, len(self.tabu_list)):
# tabu_See the first 7 values in the list
if j == 7:
break
for k in range(0, self.select_num):
if index_num[k] == TabuSearch.tabu_list[j][k]:
chg_flag = False
if index_num2[k] == TabuSearch.tabu_list[j][k]:
chg_flag = False
if index_num3[k] == TabuSearch.tabu_list[j][k]:
chg_flag = False
#Do not update the value if it is in the taboo list
if chg_flag == True:
for i in range(0, len(index_num)):
self.ns[self.index_list[index_num[i]][0], self.index_list[index_num[i]][1]] = new_var[i]
self.ns2[self.index_list[index_num2[i]][0], self.index_list[index_num2[i]][1]] = new_var[i]
self.ns3[self.index_list[index_num3[i]][0], self.index_list[index_num3[i]][1]] = new_var[i]
for i in range(0, len(self.ns)):
if not(sum(self.ns[i]) < 64 and sum(self.ns[i]) >= 40):
penalty = penalty + 1
if not(sum(self.ns2[i]) < 64 and sum(self.ns2[i]) >= 40):
penalty2 = penalty2 + 1
if not(sum(self.ns3[i]) < 64 and sum(self.ns3[i]) >= 40):
penalty3 = penalty3 + 1
if penalty < self.penalty_old and penalty <= penalty2 and penalty <= penalty3:
self.s_good = self.ns
#Penalty only when there is no value in the taboo list_Add a value to arr
TabuSearch.penalty_arr.append(penalty)
for j in range(0, len(self.ns)):
print(f"{j+1}Total value of the line", str(sum(self.ns[j])))
self.jikkou_num = self.jikkou_num + 1
return penalty
elif penalty2 < self.penalty_old and penalty2 <= penalty3:
self.s_good = self.ns2
TabuSearch.penalty_arr.append(penalty2)
for j in range(0, len(self.ns)):
print(f"{j+1}Total value of the line", str(sum(self.ns2[j])))
self.jikkou_num = self.jikkou_num + 1
return penalty2
elif penalty3 < self.penalty_old:
self.s_good = self.ns3
TabuSearch.penalty_arr.append(penalty3)
for j in range(0, len(self.ns)):
print(f"{j+1}Total value of the line", str(sum(self.ns3[j])))
self.jikkou_num = self.jikkou_num + 1
return penalty3
else:
#Record bad things and try to optimize good directions
TabuSearch.tabu_list.insert(0, index_num)
TabuSearch.tabu_list.insert(0, index_num2)
TabuSearch.tabu_list.insert(0, index_num3)
return self.penalty_old
else:
self.jikkou_num = self.jikkou_num + 1
TabuSearch.penalty_arr.append(self.penalty_old)
return self.penalty_old
def execution(self, times=1000000):
#Code to end if the sum of each line is within a certain range
for i in range(0, times):
penalty = self.generate_near()
if penalty < self.penalty_old:
print(f"{self.jikkou_num}Second execution")
print("Penalty value", penalty)
print("Overall penalty value", self.penalty_old)
self.penalty_old = penalty
if penalty == 0:
df = pd.DataFrame(self.s_good, columns=("Month" , "fire", "water", "wood", "Money", "soil", "Day", "Month", "fire", "water"))
print(df.to_markdown())
break
def plot_loss(self):
plt.plot(TabuSearch.penalty_arr)
def output_markdown(self):
df = pd.DataFrame(self.s_good, columns=("Month" , "fire", "water", "wood", "Money", "soil", "Day", "Month", "fire", "water"))
print(df.to_markdown())
ts = TabuSearch()
ts.execution(100000)
Total value on the first line 56.0
Total value on the second line 48.0
Total value on the third line 40.0
Total value on the 4th line 40.0
Total value on the 5th line 40.0
Total value on line 6 48.0
Total value on line 7 40.0
232nd execution
Penalty value 0
Overall penalty value 1
| |Month|fire|water|wood|Money|soil|Day|Month|fire|water|
|---:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|
| 0 | 0 | 8 | 8 | 8 | 4 | 0 | 8 | 8 | 8 | 4 |
| 1 | 4 | 8 | 4 | 4 | 4 | 8 | 0 | 8 | 4 | 4 |
| 2 | 0 | 8 | 4 | 0 | 0 | 4 | 8 | 4 | 4 | 8 |
| 3 | 4 | 0 | 8 | 0 | 0 | 4 | 4 | 8 | 4 | 8 |
| 4 | 8 | 0 | 4 | 4 | 4 | 0 | 0 | 8 | 4 | 8 |
| 5 | 4 | 4 | 4 | 0 | 4 | 8 | 0 | 8 | 8 | 8 |
| 6 | 0 | 8 | 4 | 8 | 4 | 8 | 0 | 4 | 4 | 0 |
ts.plot_loss()
ts.output_markdown()
| |Month|fire|water|wood|Money|soil|Day|Month|fire|water|
|---:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|-----:|
| 0 | 0 | 8 | 8 | 8 | 4 | 0 | 8 | 8 | 8 | 4 |
| 1 | 4 | 8 | 4 | 4 | 4 | 8 | 0 | 8 | 4 | 4 |
| 2 | 0 | 8 | 4 | 0 | 0 | 4 | 8 | 4 | 4 | 8 |
| 3 | 4 | 0 | 8 | 0 | 0 | 4 | 4 | 8 | 4 | 8 |
| 4 | 8 | 0 | 4 | 4 | 4 | 0 | 0 | 8 | 4 | 8 |
| 5 | 4 | 4 | 4 | 0 | 4 | 8 | 0 | 8 | 8 | 8 |
| 6 | 0 | 8 | 4 | 8 | 4 | 8 | 0 | 4 | 4 | 0 |