Schedule Optimisation using Linear Programming in Python | by Lewis Woolfson | Towards Data Science

Scheduling is an everyday challenge for many organisations. From allocating jobs on a manufacturing line to timetabling hospital surgery cases, the problem of how to efficiently manage limited resources pops up all the time.
While ‘back-of-the-envelope’ planning can take us so far, there are often times where more advanced prescriptive analytics tools such as linear programming can help decision-makers identify the best choices quickly.
The technology is not new — mathematical programming has been around for decades — but in recent years it has become more accessible. Improvements to the algorithmic performance of linear solvers have enabled more complex problems to be tackled in reasonable time frames; open-source libraries such as Pyomo make it possible to build models in familiar coding languages, and digitalisation has increased the availability of high-quality data.
Yet despite these advances, traditional optimisation methods are often overlooked by Data Scientists and Analysts.
In this post, I hope to demonstrate the value of linear programming and show how to get started with building models in Python. To do this we will construct a basic model to optimise theatre scheduling in hospitals.
I’ll assume familiarity with Python and basic knowledge of linear optimisation concepts.
Source code and data can be found here: https://github.com/Lewisw3/theatre-scheduling
Scheduling is crucial to the smooth running of an operating theatre department. Effective scheduling benefits all parties: the medical staff, the healthcare provider, and not least, the patients. But surgery planning remains a major challenge for hospitals. A recent study commissioned by NHS Improvement reported that an additional 290,000 operations could be completed each year with improved management of surgical lists and holiday bookings [1].
That was pre-Covid. The pandemic has since created a significant backlog in elective care so effective management of theatre schedules is even more pertinent than usual.
It’s a complex challenge and the solution does not lie with analytics alone.
In this post, we will consider just a simplified example of elective surgery planning for an individual surgeon. Our goal is to highlight the potential of analytics to support theatre planning — the model is not intended to be a real-world decision support tool!
There is plenty of literature on theatre schedule optimisation. If you are interested I recommend the review papers by Guerriero and Guido [2] and Cardoen et al. [3].
Suppose a hospital theatre department is planning elective surgeries for a consultant ophthalmologist. The hospital maintains two lists:
A list of operations (cases) assigned to the consultant to be performed before a deadline
A list of operating theatre time blocks (theatre sessions) allocated to the consultant
Theatre sessions can be either half-day (8.30am-1pm) or full-day (8.30am-6pm) sessions and the consultant is typically allocated one theatre session per week. The task is to assign the consultant’s upcoming cases to their theatre sessions in such a way that we maximise the utilisation of each session.
Utilisation is defined as the percentage of the theatre time block that is filled up by surgery cases.
Cases must be completed before their target deadline and at least 15% of a theatre session’s time block should be kept free for other activities (e.g. sterilisation, staff breaks, etc.)
We formulate the problem as a flexible job-shop scheduling problem where a surgical case is analogous to a job and a theatre session to a machine. We start by defining our decision variables, linear constraints, and a linear objective function.
Before we begin, let's look at the data. We have two data sources: cases.csv and sessions.csv.
cases.csv contains a list of all upcoming elective surgeries:
sessions.csv contains a list of all upcoming theatre sessions:
The full CSV files are available on the Github repo.
We start by importing the relevant data into a Pyomo ConcreteModel object using Sets (similar to arrays) and Params (key-value pairs).
We use a number of helper functions to initialize the Pyomo Sets and Params. These are omitted here for brevity but can be found on the Github repo.
We also define a Set called TASKS that contains a list of all possible combinations of (CaseID, SessionID). This represents all potential case allocation decisions available:
The main decision is assigning cases to sessions. This requires a binary yes/no decision to be made for each case-session combination in the TASKS Set above.
We also want to calculate the start time of each case and the utilisation of each session. All in, we have 3 decisions variables:
We define these decision variables in our Pyomo model as follows:
An advantage of linear programming is the flexibility to define an objective function that represents our business needs. We are free to define any (linear) function, and in our case, our goal is to maximise the utilisation of all sessions:
Next, we add our constraints. The constraints capture all the rules (not so realistic in this example!) that ensure the solution returned by the model constitutes a feasible theatre schedule. Here we have 6 rules:
The start time of a case must be after the start time of the session it is assigned toA case must end before the end of its allocated sessionA case can be assigned to at most one sessionCases cannot be assigned to a session after their deadline dateNo two cases can overlap: the start time of one case must be after the end time of another caseThe utilisation of a session is equal to the fraction of the session duration that is taken up by surgical cases
The start time of a case must be after the start time of the session it is assigned to
A case must end before the end of its allocated session
A case can be assigned to at most one session
Cases cannot be assigned to a session after their deadline date
No two cases can overlap: the start time of one case must be after the end time of another case
The utilisation of a session is equal to the fraction of the session duration that is taken up by surgical cases
We also restrict the bounds of our decision variables. The utilisation must be between 0 and 85% (because 15% of the session must be kept free for other activities) and that the start time of a case must be between 0 and the number of minutes in a day (1440).
The Constraints above are added to the model by writing a separate Python function for each constraint and using Pyomo’s Constraint method:
To define these constraints as linear equations we make use of two helpful techniques worth noting: Big M formulation and a logical disjunction.
Big M Formulation
The big M method is a trick to switch on and off constraints. For example, constraint 1 above states that a case must start after the session start time. This is only valid for the session that the case is assigned to. It shouldn’t hold for all the other sessions. We ensure this happens with the inequality:
case_start_time >= session_start_time - (1 - session_assigned)*M
where M is a sufficiently large constant and session_assigned is a binary variable. After staring at this long enough it begins to make sense. If session_assigned is equal to 1 then the rule must be held. However, if session_assigned is 0 then (assuming M is large enough) the second term on the RHS becomes much larger than the first so the entire RHS is always negative. Since our variable bounds force case_start_time ≥ 0, there is effectively no additional restriction.
Disjunctive Programming
Constraint number 5 is a logical disjunction: a set of constraints that are linked by an OR relationship. The disjunction states that for any pair of cases in the same session, either case 1 happens before case 2 or case 2 happens before case 1.
This relationship is non-linear so we must convert it into a linear constraint to work with standard linear solvers. To do this we used Pyomo’s Generalised Disjunctive Programming (GDP) modelling extension (see line 27 in the code snippet above). It allows us to define the disjunction as a simple Python function and all we need to do to convert the “disjunctive” model back to a standard mixed-inter programming (MIP) model is to include the following line:
pe.TransformationFactory("gdp.bigm").apply_to(model)
The GDP extension may be avoided by adding big M constraints and introducing a new binary variable to the model, but we’ll keep the disjunction as it works and is readable.
And finally, we are done! In full the linear programming model is:
An advantage of using an interface such as Pyomo is that it is easy to try out different linear solvers without rewriting the model in another coding language.
Here we use Coin-or Branch and Cut (CBC) — an open-source mixed-integer program solver (https://github.com/coin-or/Cbc).
The code snippet below solves the model using Pyomo’s SolverFactory class. The class can take a number of tuning parameters to control the operation of the chosen solver, but for simplicity, we keep default settings except for a time limit of 60 seconds.
Below is a Gantt chart to visualise the solution returned by the model. The input case and session data are available here:
The chart above shows a feasible schedule for cases and sessions that maximises the utilisation of all sessions subject to our constraints. The input data provided to the model had more demand (case time) than capacity (session time), and the model found that dropping case #2 (a Vitrectomy with 70 min duration) was the best option to maximise the total utilisation across all sessions.
Linear programming is a powerful tool for helping organisations make informed decisions quickly. It is a useful skill for Data Scientists, and with open-source libraries such as Pyomo it is easy to formulate models in Python.
In this post, we created a simple optimisation model for efficiently scheduling surgery cases. Despite not being a real-world solution, it demonstrates how optimisation methods like linear programming may support planners get the most out of their available resources.
Potential next steps with this model are to:
tune the solver parameters to improve performance and reduce solve time
reformulate the model to simplify the problem and reduce solve time
adjust the objective function to better represent performance targets
incorporate additional constraints to better represent real-world elective theatre scheduling
use a more sophisticated approach for predicting case times
On that last bullet point, prescriptive analytical techniques such as linear programming are increasingly being combined with predictive methods such as machine learning. For a scheduling application like the one presented here, machine learning could be used to predict the duration of tasks or even which tasks are going to occur. Once the demand is predicted, optimisation methods can help with the planning.
Thanks for reading! Please do not hesitate to reach out if you have any questions or comments.
[1] NHS Improvement (2019). Operating theatres: opportunities to reduce waiting lists. Available at: https://improvement.nhs.uk/resources/operating-theatres-opportunities-reduce-waiting-lists/
[2]Guerriero, F., Guido, R. Operational research in the management of the operating theatre: a survey. Health Care Manag Sci 14, 89–114 (2011). https://doi.org/10.1007/s10729-010-9143-6
[3] Cardoen et al. Operating room planning and scheduling: A literature review. European Journal of Operational Research 201(3), 921–932 (2010). https://doi.org/10.1016/j.ejor.2009.04.011