Skip to content

Instantly share code, notes, and snippets.

@bhpayne
Created July 5, 2021 02:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bhpayne/c12e9669bf9bea65dc486d2bdb8de542 to your computer and use it in GitHub Desktop.
Save bhpayne/c12e9669bf9bea65dc486d2bdb8de542 to your computer and use it in GitHub Desktop.

Schedule a variable number of people into a variable number slots for multiple time steps.

Maximize for diversity of slots per person.

Not optimal

#!/usr/bin/env python3
import copy # https://docs.python.org/3/library/copy.html
import random
"""
This script focuses on three variables:
* (discrete) time
* slots
* people
The purpose of this script is to rotate people across slots
and maximize the diversity of each person's experience over time.
A person should not be in the same slot on consecutive times.
Two equivilant ways of viewing the relation are
provided in table 1 and table 2.
+--------+------------+-----------+---------+
| | time_1 | time_2 | time_3 |
+--------+------------+-----------+---------+
| slot_A | person_1 | person_3 | |
| | person_2 | person_4 | |
+--------+------------+-----------+---------+
| slot_B | person_3 | person_1 | |
+--------+------------+-----------+---------+
| slot_C | person_4 | person_2 | |
| | person_5 | | |
|--------+------------+-----------+---------+
| slot_D | N/A | person_5 | |
+--------+------------+-----------+---------+
| slot_E | N/A | N/A | |
+--------+------------+-----------+---------+
Table 1: slot versus time -- schedule of which slot is address by whom
source: https://asciiflow.com
Multiple people can be assigned one slot.
The number of slots and people is not one-to-one,
and the number of slots varies over time,
and the number of people varies over time.
In table 1, a person should not be in the same row for
two consecutive columns.
Also, every slot should be occupied by one or more people for
a given time (unless that slot is explicitly "N/A").
The people per slot should be uniformly distributed as much as feasible.
+---------+--------+--------+--------+--------+--------+
| | time_1 | time_2 | time_3 | time_4 | time_5 |
+---------+--------+--------+--------+--------+--------+
| person_1| slot_A | slot_B | | | |
+---------+--------+--------+--------+--------+--------+
| person_2| slot_A | slot_C | | | |
+---------+--------+--------+--------+--------+--------+
| person_3| slot_B | slot_A | | | |
+---------+--------+--------+--------+--------+--------+
| person_4| slot_C | slot_A | | | |
+---------+--------+--------+--------+--------+--------+
| person_5| slot_C | slot_D | | | |
+---------+--------+--------+--------+--------+--------+
| person_6| N/A | N/A | | | |
+---------+--------+--------+--------+--------+--------+
Table 2: person versus time -- schedule of who is covering which slot
source: https://asciiflow.com
Some people may not be present for each time.
constraint: Each person who is present for a given time
gets assigned one task.
"""
def print_slots_to_be_filled(slots_available_per_time: dict) -> None:
"""
Produces an ASCII table that looks like
slots to be filled
| t1 | t2 | t3 |
A | ___ | ___ | ___ |
B | ___ | ___ | ___ |
C | ___ | ___ | ___ |
D | N/A | ___ | ___ |
E | N/A | N/A | ___ |
"""
complete_list_of_slots = []
for this_time, list_of_slots in slots_available_per_time.items():
complete_list_of_slots+= list_of_slots
complete_list_of_slots = list(set(complete_list_of_slots))
complete_list_of_slots.sort()
print(" slots to be filled")
str_to_print = " |"
for this_time in slots_available_per_time.keys():
str_to_print+=" "
str_to_print+=this_time
str_to_print+=" |"
print(str_to_print)
for this_slot in complete_list_of_slots:
str_to_print=" " + this_slot + " |"
for this_time, list_of_slots in slots_available_per_time.items():
if this_slot in list_of_slots:
str_to_print+=" ___ |"
else:
str_to_print+=" N/A |"
print(str_to_print)
return
# def print_people_to_be_slotted(people_available_per_time: dict) -> None:
# """
# """
# return
def print_schedule(person_allocations:dict) -> None:
"""
Produces an ASCII table that looks like
schedule per person
| t1 | t2 |
p1 | A | B |
p2 | A | A |
p3 | B | B |
p4 | C | C |
p5 | C | C |
"""
#print("[Trace] def print_schedule")
complete_list_of_people = []
for this_time, people_slot_dict in person_allocations.items():
complete_list_of_people += list(people_slot_dict.keys())
complete_list_of_people = list(set(complete_list_of_people))
complete_list_of_people.sort()
print("\n schedule per person")
str_to_print = " |"
for this_time in person_allocations.keys():
str_to_print+=" "
str_to_print+=this_time
str_to_print+=" |"
print(str_to_print)
for person in complete_list_of_people:
str_to_print = " "
str_to_print += person
str_to_print += " |"
for this_time, people_slot_dict in person_allocations.items():
if person in people_slot_dict.keys():
str_to_print += " "
str_to_print += people_slot_dict[person]
str_to_print += " |"
else:
str_to_print += " N/A |"
print(str_to_print)
return
def create_candidate_schedule(slots_available_per_time:dict,
people_available_per_time:dict,
new_schedule:dict,
this_time:str) -> dict:
"""
for an unscheduled time (this_time), assign slots to people.
"""
#print("[Trace] def create_candidate_schedule")
new_schedule[this_time] = {}
# Create a random list of slots that is uniformly distributed
# and matches the number of people who need slots at this time
slots_to_assign = []
#print("len(people_available_per_time[this_time])=",len(people_available_per_time[this_time]))
while len(slots_to_assign)<len(people_available_per_time[this_time]):
slots_to_pick_from = random.sample( slots_available_per_time[this_time],
len(slots_available_per_time[this_time]))
#print('slots_to_pick_from=',slots_to_pick_from)
while ((len(slots_to_assign) < len(people_available_per_time[this_time]))
and (len(slots_to_pick_from)>0)):
#print('slots_to_assign=',slots_to_assign,'; slots_to_pick_from=',slots_to_pick_from)
slots_to_assign.append(slots_to_pick_from.pop())
#print("slots_to_assign=",slots_to_assign)
# assign each person the slot in the new time
for index, this_person in enumerate(people_available_per_time[this_time]):
new_schedule[this_time][this_person] = slots_to_assign[index]
#print_schedule(new_schedule)
# check whether any person has two consecutive slots for this_time and previous_time
index_of_this_time = list(slots_available_per_time.keys()).index(this_time)
previous_time = list(slots_available_per_time.keys())[index_of_this_time-1]
#print("previous_time =",previous_time,"currently",this_time)
for person, slot in new_schedule[this_time].items():
try:
#print("old:",person,new_schedule[previous_time][person],"new:",new_schedule[this_time][person])
if new_schedule[previous_time][person]==new_schedule[this_time][person]:
raise Exception("repetative slot for person")
except KeyError:
#print(person,"was not present")
pass
return new_schedule
def score_schedule(new_schedule: dict,this_time: str) -> int:
"""
"""
#print("[Trace] def score_schedule")
index_of_this_time = list(new_schedule.keys()).index(this_time)
previous_time = list(new_schedule.keys())[index_of_this_time-1]
#print("previous_time =",previous_time,"currently",this_time)
complete_list_of_people = []
for a_time, people_and_slot in new_schedule.items():
complete_list_of_people += list(people_and_slot.keys())
complete_list_of_people = list(set(complete_list_of_people))
#print("complete_list_of_people=",complete_list_of_people)
score = 0
for person in complete_list_of_people:
list_of_slots = []
for a_time, people_and_slot in new_schedule.items():
try:
list_of_slots.append(new_schedule[a_time][person])
except KeyError:
list_of_slots.append(" ")
#print(person,this_time,list_of_slots)
# maximize diversity of rotations
# TODO: the following includes " " in the scoring, which may skew results
for index in range(2,len(list_of_slots)+1):
#print('comparing',list_of_slots[-1],list_of_slots[-1*index])
if list_of_slots[-1]!=list_of_slots[-1*index]:
score+=1
#print("added 1")
return score
def find_schedule(slots_available_per_time:dict,
people_available_per_time:dict,
old_schedule:dict,
this_time:str,
number_of_candidate_schedules_per_time: int,
maximum_number_of_trials_per_time: int) -> dict:
"""
for an unscheduled time (this_time), assign slots to people.
"""
#print("[Trace] def find_schedule")
number_of_tries = 0
max_score = 0
list_of_scores = []
list_of_schedules = []
while ((len(list_of_schedules)<number_of_candidate_schedules_per_time)
and (number_of_tries<maximum_number_of_trials_per_time)):
number_of_tries+=1
try:
new_schedule = create_candidate_schedule(slots_available_per_time,
people_available_per_time,
old_schedule,
this_time)
score = score_schedule(new_schedule,this_time)
list_of_scores.append(score)
if score > max_score:
max_score = score
#print('score=',score)
#print_schedule(new_schedule)
list_of_schedules.append(new_schedule)
except Exception:
pass
#print("after creating multiple schedules, the scores are",list_of_scores)
highest_scoring_schedule = list_of_schedules[list_of_scores.index(max_score)]
#print_schedule(highest_scoring_schedule)
return highest_scoring_schedule
if __name__ == "__main__":
number_of_candidate_schedules_per_time = 10
maximum_number_of_trials_per_time = 1000
# To represent the slots available to be filled per time,
slots_available_per_time = {"t1": ["A", "B", "C"],
"t2": ["A", "B", "C", "D"],
"t3": ["A", "B", "C", "D", "E"],
"t4": ["A", "B", "C", "D"]}
# the ASCII table is just the transpose of the dict
print_slots_to_be_filled(slots_available_per_time)
# specify which people are available per time,
people_available_per_time = {"t1": ["p1", "p2", "p3", "p4", "p5"],
"t2": ["p1", "p2", "p3", "p4", "p5"],
"t3": ["p1", "p2", "p3", "p4", "p5", "p6"],
"t4": ["p1", "p3", "p4", "p6"]}
# creating an ASCII table of this isn't that interesting because it's just the transpose of the dict
#print_people_to_be_slotted(people_available_per_time)
# verify that the times are the same
time_diff = set(slots_available_per_time.keys()) - set(people_available_per_time.keys())
#print(time_diff)
assert(time_diff==set())
# what happened in the past?
historical_allocations = {"t1": {"p1":"A", "p2": "A", "p3":"B", "p4":"C", "p5":"C"},
"t2": {"p1":"B", "p2": "C", "p3":"A", "p4":"B", "p5":"A"}}
print_schedule(historical_allocations)
times_to_be_scheduled = set(people_available_per_time.keys()) - set(historical_allocations.keys())
times_to_be_scheduled = list(times_to_be_scheduled)
times_to_be_scheduled.sort()
print('times to schedule:', times_to_be_scheduled)
new_schedule = copy.deepcopy(historical_allocations) # https://docs.python.org/3/library/copy.html
for this_time in times_to_be_scheduled:
new_schedule = find_schedule(slots_available_per_time,
people_available_per_time,
new_schedule,
this_time,
number_of_candidate_schedules_per_time,
maximum_number_of_trials_per_time)
print("new schedule:")
print_schedule(new_schedule)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment