/* Business scenario:
A company needs to staff a customer support call center. There is a forecast
of the number of operators needed per time period. There are three options
for covering the forecast: full-time resources, part-time resources, and
understaffing (reflecting missed calls). Each type of resource has a
cost per period and standard shift duration. Longer or shorter shifts are not
allowed. The cost of understaffing for a period and missing calls is estimated.
The time horizon repeats, so resources starting at later time periods will
wrap around and be available for the beginning of the next cycle.
Create a schedule that minimizes total cost over the time horizon.
*/
/* Data for forecasted demand for call center operators by time period.
The index of a period is the observation number. */
data Forecast_data;
input Forecast;
datalines;
20
30
45
15
10
20
20
40
70
90
130
120
105
95
90
100
80
60
50
30
15
10
15
20
;
run;
/* Data defining options for handling demand.
Each option has a cost per time period and a fixed shift length. */
data Resource_data;
length Type $ 10;
input Type $ Cost_Per_Period Duration;
datalines;
fulltime 15 8
parttime 20 4
understaff 40 1
;
run;
/* Create a model to minimize the total cost while assigning enough
resources to the forecasted demand. */
proc optmodel;
/* Declare sets */
set TIMES;
number max_time = max{time in TIMES} time;
number forecast{TIMES};
set RESOURCES;
number cost{RESOURCES};
number duration{RESOURCES};
/* Read data */
read data Forecast_data into TIMES=[_n_] forecast;
read data Resource_data into
RESOURCES=[type] cost=Cost_Per_Period duration=Duration;
call symput('uscost',compress(put(cost['understaff'],best.)));
/* Store data about which resource types and
starting times 'cover' forecasted demand for
each hour of the day. */
set COVERS{demand_time in TIMES, resource in RESOURCES}
= {start_time in TIMES:
(start_time in demand_time-duration[resource]+1..demand_time)
or (start_time > max_time + demand_time - duration[resource])};
/* Alternate definition of COVERS set */
/* set COVERS{demand_time in TIMES, resource in RESOURCES}
= setof {start_time in demand_time-duration[resource]+1..demand_time}
(mod(start_time+max_time-1,max_time)+1); */
/* Write COVERS set information to the log */
for {t in TIMES, r in RESOURCES} put COVERS[t,r]=;
/* Declare variables */
var Staffing{TIMES, RESOURCES} integer >= 0;
/* = number of resources of a given type starting at a given time */
/* Declare constraints */
constraint coverage{time in TIMES}:
sum{resource in RESOURCES, start_time in COVERS[time,resource]}
Staffing[start_time,resource] >= forecast[time];
/* Declare objective function */
min Total_Cost = sum{time in TIMES, resource in RESOURCES}
cost[resource]*duration[resource]*Staffing[time,resource];
/* Expand first-hour coverage constraint */
expand coverage[1];
/* Solve */
solve;
/* Collect total number of resources of each type available at each time */
number total_coverage{time in TIMES, resource in RESOURCES} =
sum{start_time in COVERS[time,resource]} Staffing[start_time,resource].sol;
/* Create a dense output table */
create data Solution
from [Time]={time in TIMES} Demand=forecast
{resource in RESOURCES}
{resource in RESOURCES}
;
/* Create a sparse output table to be used for graphing */
create data SolutionSparse
from [Time Type] =
{time in TIMES, resource in RESOURCES: total_coverage[time,resource] > 0}
Amount=total_coverage[time,resource]
Demand=(if resource = "fulltime" then forecast[time])
;
quit;
/* View the solution as a table */
proc print data=Solution;
run;
proc sort data=SolutionSparse;
by Time;
run;
data Solution2;
set Solution;
rename Demand=Forecast;
run;
data Solution2(keep=Time Type Amount Demand Forecast);
merge SolutionSparse Solution2;
by Time;
run;
proc sort data=Solution2;
by Time Type;
run;
data Solution3;
set Solution2;
by Time;
if not(first.Time) then Forecast=0;
run;
/* Set axis labels for graphs */
axis1 label=("Resources");
axis2 label=("Demand");
legend1 position=bottom FRAME;
/* Create a graph of 'ideal' coverage of demand */
title 'Ideal Coverage of Forecasted Demand by a Single Resource';
*title2 "Understaff Unit Cost=&uscost";
proc gbarline data=Solution3;
bar Time / sumvar=Forecast DISCRETE axis=axis1;
plot / sumvar=Forecast axis=axis2;
run;
/* Create a graph with the solution of resources against demand */
title 'Solution Coverage of Forecasted Demand by Resources';
title2 "Understaff Unit Cost=&uscost";
proc gbarline data=Solution3;
bar Time / sumvar=Amount subgroup=Type DISCRETE axis=axis1 legend=legend1;
plot / sumvar=Forecast axis=axis2;
run;
/* Reset global options */
title;
axis1;
axis2;
quit;