<jupyter_start><jupyter_text>ECR multi-week planning=====================The ECR call center has about 200 agentswith the following contract types:- 50% part-time: thet work 4 hours- 75% part-time: they work 6 hours- full-time: they work 8 hoursA plan is made each week based on the call forecast based on the following rules- agents work from monday to sunday- they work on 4,6,8 hours shifts based on their contract- agent can choose an entry time window---- there are 3 kind of rest assignment pattern: | name         | description                                           ||--------------|-------------------------------------------------------|| *weekend*    | rest is assigned on sat and sun                       || *split-rest* | rest is assigned on sun an on one other day mon-thu   || *infra-week* | rest is assigned on two consecutive week days mon-sat |- on the very first week the agent can be assigned any kind- on the following weeks the rest rotation rule is weekend -> infra-week -> split-rest -> weekend---- the plan must be **optimal** that is it must follow as closely as possible the target---In order to specify these rules we use a simple *domain specific language*:- **shifts** are defined based on a specification line: for example```A1 = Shift.fromSpec("A1", "09:00-12:00")              a 4 hour shift from 9 to 12A2 = Shift.fromSpec("A2", "10:00-13:00")              a 4 hour shift from 9 to 12...M1 = Shift.fromSpec("M1", "09:00-12:00,13:00-17:00")  a 8 hour shift from 9 to 17 with a 1 hour pause...R  = Shift("R")                                       a rest shift```- shifts can be *combined* to form **shift rules**```A1 + A2       assign either A1 or A2 on a single dayA1 * A2       a two day assigment: assign A1 on the first day and A2 on the second(A1+A2) * R   a two day assignment: assign either A1 or A2 on the first day and R on the secondW = A1 + A2X = W * W * W * W * W * R * R     a 7 days assignment:                                    assign either A1 or A2 on the first 5 days                                    assign R on the weekend```---To produce a plan we have to- define the shifts and the agents and the target staffing- based on the contract, availability, etc. we find the shifts that can be assigned to an agent- we create a planner- we form the shift rules and add them to the planner- we run the planner- we retrieve the optimized plan<jupyter_code>import re
import json
import matplotlib.pyplot as plt

from datetime import time
from random import randint
from functools import reduce
from tabulate import tabulate

from pywfplan import Shift, StaffPlanner
# first we import the shift specifications
# note that the pauses are specified in the shift definition

with open("shifts_ecr.json") as f:
        shifts = [Shift.fromSpec(s[0], *s[1]) for s in json.load(f).items()]
# we implement a function to retrieve the shifts that can be assigned to an agent

def work_shifts_for_agent(agent):
    p = re.compile("^(\d\d):(\d\d)$")
    
    m0 = p.match(agent["from"])
    m1 = p.match(agent["to"])
    
    t0 = time(hour=int(m0.group(1)), minute=int(m0.group(2)))
    t1 = time(hour=int(m1.group(1)), minute=int(m1.group(2)))

    # during the week agents can be assigned shifts in a 1 hour window around their chosen time `t0`
    agent_shifts = [s for s in shifts if s.is_work() and s.attrs["contract"] == agent["contract"] and t0 <= s.start_time() and s.start_time() <= t1]
    
    return reduce(lambda a,b: a+b, agent_shifts)
# the rest shift

R = Shift.fromSpec("R")
# the the agent definitions

with open("agents_ecr.json") as f:
        agents = json.load(f)

len(agents)
# initialize cycles

cycle = {agent: randint(0,3) for agent in agents}
# the target curves from the call forecast
with open("target_ecr.json") as f:
        target = json.load(f)

# rescale
N1 = 4042.5 / (sum(target["week1"])*30/60)
target["week1"] = [t * N1 for t in target["week1"]]

N2 = 4042.5 / (sum(target["week2"])*30/60)
target["week2"] = [t * N2 for t in target["week2"]]
        
plt.subplot(2, 1, 1)
plt.plot(target["week1"], label="week 1")
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(target["week2"], label="week 2")
plt.legend()<jupyter_output><empty_output><jupyter_text>Week 1----------<jupyter_code># create a planner for the first week

week1Planner = StaffPlanner()
# configure the planner

week1Planner.setStaffingTarget(target["week1"], days=7, slot_length=30)

for code, agent in agents.items():
    
    # W is the work shift that can be assigned on woork days
    W = work_shifts_for_agent(agent)
    
    if cycle[code] == 0:
        # weekend
        rule = W * W * W * W * W * R * R

    elif cycle[code] == 1 or cycle[code] == 2:
        # infra-week
        iw1 = W * W * W * W * R * R * W
        iw2 = W * W * W * R * R * W * W
        iw3 = W * W * R * R * W * W * W
        iw4 = W * R * R * W * W * W * W
        iw5 = R * R * W * W * W * W * W
        rule  = iw1 + iw2 + iw3 + iw4 + iw5

    elif cycle[code] == 3:
        # split-rest
        sr1 = W * W * W * W * R * W * R
        sr2 = W * W * W * R * W * W * R
        sr3 = W * W * R * W * W * W * R
        sr4 = W * R * W * W * W * W * R
        sr5 = R * W * W * W * W * W * R
        rule  = sr1 + sr2 + sr3 + sr4 + sr5
    
    # finally we add the rule to the planner
    week1Planner.addAgentRule(code, rule)
# at this point the planner is ready to be launched
# (it takes a few minutes)

week1Planner.run(annealing_schedule=0.85, comfort_energy_weight=0.0)
# retrieve the optimized plan

print(tabulate([[code, agents[code]["contract"], *week1Planner.getAgentPlan(code)] for code in agents], 
               headers=["agent", "contract", "sun", "mon", "tue", "wed", "thu", "fri", "sat", "sun"]))
# and compare the planned staffing curve with the target

plt.plot(week1Planner.getTargetStaffing(), label="Target")
plt.plot(week1Planner.getPlannedStaffing(), label="Planned")
plt.legend()
plt.show()
# the optimization report 

print(week1Planner.getReport())
# save plan for later

week1Plan = {code: week1Planner.getAgentPlan(code) for code in agents}
# update cycle

for agent in agents:
    cycle[agent] = (cycle[agent] + 1) % 4<jupyter_output><empty_output><jupyter_text>Week 2----------<jupyter_code># create a new planner for the second week

week2Planner = StaffPlanner()
# configure the planner

week2Planner.setStaffingTarget(target["week2"], days=7, slot_length=30)

for code, agent in agents.items():
    
    # W is the work shift that can be assigned on work days
    W = work_shifts_for_agent(agent)
    
    if cycle[code] == 0:
        # weekend
        rule = W * W * W * W * W * R * R

    elif cycle[code] == 1 or cycle[code] == 2:
        # infra-week
        iw1 = W * W * W * W * R * R * W
        iw2 = W * W * W * R * R * W * W
        iw3 = W * W * R * R * W * W * W
        iw4 = W * R * R * W * W * W * W
        iw5 = R * R * W * W * W * W * W
        rule  = iw1 + iw2 + iw3 + iw4 + iw5

    elif cycle[code] == 3:
        # split-rest
        sr1 = W * W * W * W * R * W * R
        sr2 = W * W * W * R * W * W * R
        sr3 = W * W * R * W * W * W * R
        sr4 = W * R * W * W * W * W * R
        sr5 = R * W * W * W * W * W * R
        rule  = sr1 + sr2 + sr3 + sr4 + sr5
    
    # finally we add the rule to the planner
    week2Planner.addAgentRule(code, rule)
# run planner

week2Planner.run(annealing_schedule=0.85, comfort_energy_weight=0.0)
# retrieve the optimized plan

print(tabulate([[code, agents[code]["contract"], *week2Planner.getAgentPlan(code)] for code in agents], 
               headers=["agent", "contract", "sun", "mon", "tue", "wed", "thu", "fri", "sat", "sun"]))
# and compare the planned staffing curve with the target

plt.plot(week2Planner.getTargetStaffing(), label="Target")
plt.plot(week2Planner.getPlannedStaffing(), label="Planned")
plt.legend()
plt.show()
# the optimization report 

print(week2Planner.getReport())<jupyter_output>===========================================================================

          turning length: 7
                 week n°: 0
             slot length: 5 minutes
               agents n°: 129
         target staffing: 4042.50 hrs
      simulated staffing: 4042.50 hrs

   comfort energy weight: 0.00000

         annealing steps: 72
       temperature range: 16.00000 -> 0.00014
    temperature schedule: 0.85
       optimization time: 3.8 minutes

         staffing energy: 162.52092 -> 2.63742
          comfort energy: 81061.71429 -> 137885.14286
            TOTAL ENERGY: 162.52092 -> 2.63742

     day by day staffing:
                 day   1: 659.50 hrs (target 657.29 hrs error -0.34%)
                 day   2: 636.50 hrs (target 630.78 hrs error -0.91%)
                 day   3: 618.50 hrs (target 617.80 hrs error -0.11%)
                 day   4: 605.50 hrs (target 603.13 hrs error -0.39%)
                 day   5: 598.50 hrs (target 598.62 hrs error 0.02%)
                 day[...]
{'fraction_non_alphanumeric': 0.13705980994969258, 'fraction_numerical': 0.043823365008384574, 'mean_word_length': 2.4394463667820068, 'pattern_counts': {'":': 0, '<': 14, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 52, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '432798', 'license': 'MIT', 'n_tokens_mistral': 3181, 'n_tokens_neox': 2873, 'n_words': 1090, 'path': 'examples/plan_ecr.ipynb', 'repo_name': 'lucamarx/pywfplan', 'size': '8945'}