In the PuLP scheduling problem, how to bring/group the consecutive Zeros together and still get an optimal solution? For meal breaks in the schedule - linear-programming

Solving an agent scheduling problem using PuLP.
8 hours of shift, 4 agents.
I have to generate an output where the 8 hrs of shift is divided into 15-minute intervals. Hence, 32 total periods. (the 15 minute periods of an 8 hour shift is a fixed input, which is not allowed to be tweaked.)
At any given period, there need to be a minimum of 3 agents working (i.e.not on break)
Now, there needs to be a 1 hour meal break, and a 30 min short break.
So, for 1 hour meal break, I will have to combine 4 periods of 15 mins, and for the 30 min short break, I'll have to combine 2 periods of 15 mins.
I tried getting 26 counts of 1... and, 6 counts of 0.
The idea was to then combine 4 zeros together (meal break), and the remaining 2 zeros together (short break).
The current LpStatus is 'infeasible'... if i remove the constraint where i am trying to club the zeros, then the solution is optimal, or else it shows infeasible.
Have also pasted my final dataframe output screenshots.
import pandas as pd
import numpy as np
import scipy as sp
import seaborn as sns
import matplotlib.pyplot as plt
from pandas.plotting import table
import os, sys, json
from pulp import *
%matplotlib inline
# agent works either 1, 2, 3, 4, 5, 6, 7, 8 hours per week.
working_periods = [26]
# maximum periods that agent will work (7 hrs in each shift)
max_periods = 26
# planning_length
planning_length = 1 # TODO: Total number of shifts i.e. 21 in our case
# Number of periods per shift:
daily_periods = [0, 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]
# Label the days from Monday to Sunday.
s = ['Period']
# Create the required_agents dataframe
col_2 = range(0, 1*planning_length)
required_agents_per_period = pd.DataFrame(data = None, columns=s, index = daily_periods)
for j in col_2:
# Small number is better for visualization.
required_agents_per_period.iloc[0][j] = 3
required_agents_per_period.iloc[1][j] = 3
required_agents_per_period.iloc[2][j] = 3
required_agents_per_period.iloc[3][j] = 3
required_agents_per_period.iloc[4][j] = 3
required_agents_per_period.iloc[5][j] = 3
required_agents_per_period.iloc[6][j] = 3
required_agents_per_period.iloc[7][j] = 3
required_agents_per_period.iloc[8][j] = 3
required_agents_per_period.iloc[9][j] = 3
required_agents_per_period.iloc[10][j] = 3
required_agents_per_period.iloc[11][j] = 3
required_agents_per_period.iloc[12][j] = 3
required_agents_per_period.iloc[13][j] = 3
required_agents_per_period.iloc[14][j] = 3
required_agents_per_period.iloc[15][j] = 3
required_agents_per_period.iloc[16][j] = 3
required_agents_per_period.iloc[17][j] = 3
required_agents_per_period.iloc[18][j] = 3
required_agents_per_period.iloc[19][j] = 3
required_agents_per_period.iloc[20][j] = 3
required_agents_per_period.iloc[21][j] = 3
required_agents_per_period.iloc[22][j] = 3
required_agents_per_period.iloc[23][j] = 3
required_agents_per_period.iloc[24][j] = 3
required_agents_per_period.iloc[25][j] = 3
required_agents_per_period.iloc[26][j] = 3
required_agents_per_period.iloc[27][j] = 3
required_agents_per_period.iloc[28][j] = 3
required_agents_per_period.iloc[29][j] = 3
required_agents_per_period.iloc[30][j] = 3
required_agents_per_period.iloc[31][j] = 3
# List of number of agents required in specific periods
r_p = required_agents_per_period.values.swapaxes(0,1).ravel()
print("The number of agents required for each period is: ")
print (r_p)
print("Total no. of periods is: ", len(r_p))
print ("\nIn matrix form:")
print (required_agents_per_period)
# Total number of the agents
total = 4
print ("\nTotal number of agents are: {}".format(total))
# Create agents_id tag
agent_id_working_in_shift = ['agent7', 'agent10', 'agent13', 'agent18'] # TODO: Important: Here agent_id will be array of agents that will be extracted from dataframe.
print ("\nThe agents are: ")
print (agent_id_working_in_shift)
# Total periods
periods = range(1*32)
agents_per_shift = range(total)
## Create shift names based on index:
period_name = []
for p in periods:
period_name.append(s[0] + '_' + 'P' + str(p))
print("The periods are: ")
print(periods)
print("\nThe names of corresponding periods are: ")
print(period_name)
print("\nThe agents are: ")
print(agents_per_shift)
def LpProb():
# The prob variable is created to contain the problem data
prob = LpProblem("Agents Meal Scheduling Per Shift",LpMinimize)
# Creating the variables.
var = {
(n, p): pulp.LpVariable(
"schdule_{0}_{1}".format(n, p), cat = "Binary")
for n in agents_per_shift for p in periods
}
# add constraints:
for n in agents_per_shift:
for p in periods:
prob.addConstraint(var[(n,p)] <= 1)
# add constraints:
# Exactly 32 working periods per shift
for n in agents_per_shift:
prob.addConstraint(
sum(var[(n,p)] for p in periods) == 26
)
for n in agents_per_shift:
for p in periods:
if(p == periods[-1] or p == periods[-2] or p == periods[-3]):
continue
prob.addConstraint(var[(n,p)] + var[(n,p+1)] + var[(n,p+2)] + var[(n,p+3)] == 0)
# add constraints
# for each shift, the numbers of working agents should be greater than or equal to
# the required numbers of agents
for p in periods:
try:
prob.addConstraint(
sum(var[(n,p)] for n in agents_per_shift) >= 3
)
except:
print("len(periods) should be equal to len(agents_per_shift)")
sys.exit(-1)
prob.objective = sum(var[(n,p)] for n in agents_per_shift for p in periods)
return var, prob
# Run the solver
var, prob = LpProb()
prob.solve()
print(LpStatus[prob.status])
def agent_scheduling(var = var):
schedule = pd.DataFrame(data=None, index = agent_id_working_in_shift, columns = period_name)
for k, v in var.items():
n, p = k[0], k[1]
schedule.iloc[n][p] = int(value(v)) # pulp.value()
return schedule
schedule = agent_scheduling()
schedule.T
I was expecting an output of only zeros and one.
Want to combine four 0's as meal break, that need to be consecutive, and then a two 0's consecutive for short break (remaining 26 cells should be 1's)
Also, only 1 agent can be on a break (0), the other 3 agents needs to be working in that period (1)
OUTPUTS:-
enter image description here
enter image description here
enter image description here

Related

Counting rows per record

I'm trying count the total number of invoices where record is concurent.
I am unsure about what it is you exactly want. I'm going to base my expectations on your code.
There are a three errors in the code:
Function arguments should a variable name.
def count_consecutive_invoice (df, invoiceNumber):
retlist = []
for i in range(len(df[InvoiceNumber]) - 1):
Notice the removal of quotes around invoiceNumber. You set, what it's equal to later, when calling the function.
You are trying to call the function instead of accessing a variable:
if count_consecutive_invoice[i] + 1 == count_consecutive_invoice[i
+ 1]:
Should be
if df[InvoiceNumber][i] + 1 == df[InvoiceNumber][i + 1]:
You need to declare all variables, including count.
To do this, just add count = 1 after retlist = [].
This code should work:
df = pd.read_excel(r'MYPath\Book1.xlsx')
def count_consecutive_invoice (df, invoiceNumber):
retlist = []
count = 1
for i in range(len(df[ivoiceNumber]) - 1):
# Check if the next number is consecutive
if df[invoiceNumber][i] + 1 == df[invoiceNumber][i+1]:
count += 1
elif count > 1:
# If it is not and count > 1 append the count and restart counting
retlist.append(count)
count = 1
# Since we stopped the loop one early append the last count
retlist.append(count)
return retlist
output = count_consecutive_invoice(df, 'Invoice Number')
print(output)
output:
[4]
Here is my commented solution.
It does recreate a panda frame, you need to pass the rows name for the id and the one on which we count the invoicing.
def count_consecutive_invoice(table, invoice_row_name, id_row_name):
invoiced_table = {} # the output
for row in table:
if row != invoice_row_name:
invoiced_table[row] = []
invoiced_table['Cont of Consecutive Invoices'] = []
streak = False # keep track of streaking invoices cause on first invoice we need to add 2, not 1
for line in range(len(table[invoice_row_name]) - 1):
id = table[id_row_name][line]
if not id in invoiced_table[id_row_name]:
for row in table:
if row != invoice_row_name:
invoiced_table[row].append(table[row][line])
invoiced_table['Cont of Consecutive Invoices'].append(0)
if id == table[id_row_name][line+1]: #check the vendor id so if you get the invoicing for each
if table[invoice_row_name][line]+1 == table[invoice_row_name][line+1]: # check the actual invoicing
itable_line = invoiced_table[id_row_name].index(id)
invoiced_table['Cont of Consecutive Invoices'][itable_line] += 1 + int(not streak) #otherwise we add 1 or 2 depending on the streak status
streak = True
continue
streak = False
return invoiced_table
invoiced = count_consecutive_invoice(df, "Invoice ID", "Vendor ID")
print(pd.DataFrame.from_dict(invoiced))

how do you draw random numbers from a list and put them in another

I do not know how to draw 2 or more different numbers and put them in another list, so far I have tried this:
import random
def jugar(m):
m = list(range(1, 14, 1)) * m
random.shuffle(m)
player1 = []
for n in m:
random.choice(m)
player1.append(n)
if n + n == 21:
print("Nano jack")
elif n + n < 21:
random.choice(m)
player1.append(n)
elif n + n > 21:
print("Loser")
return player1
jugar(1)
but this returns me 2 equal numbers, it is similar to the game of blackjack, I want it to keep adding random numbers until it reaches 21 or more, thanks for the help in advance
You can use choice method to randomly select an item from a given list.
Use it in a for loop to randomly select more items.
import random
the_list = [1, 2, 3, 4]
new_list = []
for i in range(3):
value = random.choice(the_list)
new_list.append(value)

A permutation considering one item has to be prior to another

I have a problem regarding the permutation problem.
I have a list of (1,2,3,4,5,6), then if I apply the following code:
import itertools
a = list(itertools.permutations([1,2,3,4,5,6],6))
I would get 720 permutations. Instead, I have a specific sequence that:
1 has to be ahead of 5,
2 has to be ahead of 6,
3 has to be ahead of 7,
4 has to be ahead of 8,
so does anyone know how to generate permutations consider the rules above?
Thanks,
You create your permutations and filter them:
import itertools
a = list(itertools.permutations([1,2,3,4,5,6,7,8],8))
# your rules for filter
def IsOk(tupl):
one = tupl.index(1)
two = tupl.index(2)
thr = tupl.index(3)
fou = tupl.index(4)
fiv = tupl.index(5)
six = tupl.index(6)
sev = tupl.index(7)
eig = tupl.index(8)
return one > fiv and two > six and thr > sev and fou > eig
print(a)
filtered = filter(lambda x: IsOk(x), a)
print(*filtered)
print(len(*filtered))
I had to increase your permutations to 8 so all your rules could be processed.
This reduces the permutations from 40320 down to 2520.

Find category-of-4 for number starting at 2

I'm tasked with categorizing numbers into groups with a range of four, starting at 2 (2, 6, 10, 14...). So for the number 9, the category would be 6 (between 6 and 10). I've developed the following function but I'm guessing there's a more efficient means and one that isn't limited in range.
>>> def FindCategory (num):
categories = [2]
lastVal = 2
for i in range (100):
lastVal = lastVal + 4
categories += [lastVal]
try:
return [cat for cat in categories if cat < num and num < cat + 4] [0]
except:
return
>>> FindCategory (56)
54
>>> FindCategory (99999999999999999999999999)
>>>
Just use math?
def category(n):
return (((n + 2) // 4) * 4) - 2
Examples:
>>> category(2)
2
>>> category(56)
54
>>> category(99)
98
>>> category(99999999999999999999999999)
99999999999999999999999998
By way of explanation: without the shift-by-2, you're just looking for the closest (lower) multiple of four, which can be found just by integer-division and then multiplication by 4 (i.e. (n//4)*4). The +2 and -2 account for the shift in your categories.

pandas how to split twice a given field

I would like to count the top subject in a Column. Some fields have commas or dot I would like to create a new row with them.
import pandas as pd
from pandas import DataFrame, Series
sbj = DataFrame(["Africa, Business", "Oceania",
"Business.Biology.Pharmacology.Therapeutics",
"French Litterature, Philosophy, Arts", "Biology,Business", ""
])
sbj
I would like to split into a new any field that has a '.' or '.'
sbj_top = sbj[0].apply(lambda x: pd.value_counts(x.split(",")) if not pd.isnull(x) else pd.value_counts('---'.split(","))).sum(axis = 0)
sbj_top
I'm getting an error (AttributeError) here while try to re-split('.') it
sbj_top = sbj_top.apply(lambda x: pd.value_counts(x.split(".")) if not pd.isnull(x) else pd.value_counts('---'.split(","))).sum(axis = 0)
sbj_top
My desired output
sbj_top.sort(ascending=False)
plt.title("Distribution of the top 10 subjects")
plt.ylabel("Frequency")
sbj_top.head(10).plot(kind='bar', color="#348ABD")
You can use Counter together with chain from itertools. Note that I first replace periods with commas before parsing.
from collections import Counter
import itertools
from string import whitespace
trimmed_list = [i.replace('.', ',').split(',') for i in sbj[0].tolist() if i != ""]
item_list = [item.strip(whitespace) for item in itertools.chain(*trimmed_list)]
item_count = Counter(item_list)
>>> item_count.most_common()
[('Business', 3),
('Biology', 2),
('Oceania', 1),
('Pharmacology', 1),
('Philosophy', 1),
('Africa', 1),
('French Litterature', 1),
('Therapeutics', 1),
('Arts', 1)]
if you need the output in the form of a DataFrame:
df = pd.DataFrame(item_list, columns=['subject'])
>>> df
subject
0 Africa
1 Business
2 Oceania
3 Business
4 Biology
5 Pharmacology
6 Therapeutics
7 French Litterature
8 Philosophy
9 Arts
10 Biology
11 Business