├── apps
└── .keep
├── domodel
├── .keep
└── Nurses
│ ├── .scenarios
│ ├── Scenario 1
│ │ ├── kpis.csv
│ │ ├── solveStatus.json
│ │ ├── shift_assignments.csv
│ │ ├── log.txt
│ │ ├── model.py
│ │ └── scenario.json
│ └── dashboard
│ │ ├── scenario.json
│ │ └── dashboard.json
│ └── .decision.json
├── flows
└── .keep
├── jobs
└── .keep
├── jupyter
└── .keep
├── misc
└── .keep
├── models
└── .keep
├── rstudio
└── .keep
├── scripts
└── .keep
├── shaper
└── .keep
├── dashboards
└── .keep
├── datasets
├── .keep
├── Departments.csv
├── NurseAssociations.csv
├── SkillsRequirements.csv
├── Skills.csv
├── NurseIncompatibilities.csv
├── NurseSkills.csv
├── Nurses.csv
├── NurseVacations.csv
└── Shifts.csv
├── datasources
├── .keep
├── credentials
│ └── .keep
└── remotedatasets
│ └── .keep
├── model-groups
└── .keep
├── packages
├── R
│ └── .keep
├── python
│ └── .keep
└── scala
│ └── .keep
├── zeppelin
└── .keep
├── README.md
└── info.json
/apps/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/domodel/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flows/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jobs/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jupyter/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/misc/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rstudio/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/shaper/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dashboards/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/datasets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/datasources/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/model-groups/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/R/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/zeppelin/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/python/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/scala/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/datasources/credentials/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/datasources/remotedatasets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/datasets/Departments.csv:
--------------------------------------------------------------------------------
1 | id
2 | Emergency
3 | Consultation
4 |
--------------------------------------------------------------------------------
/datasets/NurseAssociations.csv:
--------------------------------------------------------------------------------
1 | nurse1,nurse2
2 | Isabelle,Dee
3 | Anne,Patrick
4 |
--------------------------------------------------------------------------------
/datasets/SkillsRequirements.csv:
--------------------------------------------------------------------------------
1 | department,skill,req
2 | Emergency,Cardiac Care,1
3 |
--------------------------------------------------------------------------------
/datasets/Skills.csv:
--------------------------------------------------------------------------------
1 | id
2 | Anaesthesiology
3 | Cardiac Care
4 | Geriatrics
5 | Oncology
6 | Pediatrics
7 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/kpis.csv:
--------------------------------------------------------------------------------
1 | Total salary cost,25032.0
2 | Total worked hours,1146.0
3 | objective_,25032.0
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This DO for DSX example introduces basic concepts of model builder and dashboard using a python CPLEX model.
2 |
3 | It solves a simple Nurse Scheduling example.
4 |
--------------------------------------------------------------------------------
/domodel/Nurses/.decision.json:
--------------------------------------------------------------------------------
1 | {"name":"Nurses","creator":"alain","createdAt":1523362081004,"usage":{"lastModificationTime":1523362081004,"lastModifier":"alain"},"type":"DecisionArtifact"}
--------------------------------------------------------------------------------
/datasets/NurseIncompatibilities.csv:
--------------------------------------------------------------------------------
1 | nurse1,nurse2
2 | Patricia,Patrick
3 | Janice,Wendie
4 | Suzanne,Betsy
5 | Janelle,Jane
6 | Gloria,David
7 | Dee,Jemma
8 | Bethanie,Dee
9 | Roberta,Zoe
10 | Nicole,Patricia
11 | Vickie,Dee
12 | Joan,Anne
13 |
--------------------------------------------------------------------------------
/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "provider": ["IBM"],
3 | "name": "DO for WS Nurses example" ,
4 | "model": [ "CPLEX" ],
5 | "development": [ "Python" ],
6 | "category": [ {"DOforWS":["DO for WS"]} ],
7 | "industry": [ "Health" ],
8 | "abstract": "According to the expected demand for different hospital departments, nurses are assigned to shifts."
9 | }
10 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/dashboard/scenario.json:
--------------------------------------------------------------------------------
1 | {"parentId":"Nurses","category":"dashboard","creator":"alain","createdAt":1523362538392,"usage":{"lastModificationTime":1523366000542,"lastModifier":"alain"},"dataUsagePerCategory":{"":{"lastModificationTime":1523366000542,"lastModifier":"alain"}},"state":"available","assets":[{"name":"dashboard.json","creator":"alain","createdAt":1523362540348,"lastUpdater":"alain","updatedAt":1523366000542,"contentType":"application/json","path":"dashboard.json"}]}
--------------------------------------------------------------------------------
/datasets/NurseSkills.csv:
--------------------------------------------------------------------------------
1 | nurse,skill
2 | Anne,Anaesthesiology
3 | Anne,Oncology
4 | Anne,Pediatrics
5 | Betsy,Cardiac Care
6 | Cathy,Anaesthesiology
7 | Cecilia,Anaesthesiology
8 | Cecilia,Oncology
9 | Cecilia,Pediatrics
10 | Chris,Cardiac Care
11 | Chris,Oncology
12 | Chris,Geriatrics
13 | Gloria,Pediatrics
14 | Jemma,Cardiac Care
15 | Joyce,Anaesthesiology
16 | Joyce,Pediatrics
17 | Julie,Geriatrics
18 | Juliet,Pediatrics
19 | Kate,Pediatrics
20 | Nancy,Cardiac Care
21 | Nathalie,Anaesthesiology
22 | Nathalie,Geriatrics
23 | Patrick,Oncology
24 | Suzanne,Pediatrics
25 | Wendie,Geriatrics
26 | Zoe,Cardiac Care
27 |
--------------------------------------------------------------------------------
/datasets/Nurses.csv:
--------------------------------------------------------------------------------
1 | name,seniority,qualification,pay_rate
2 | Anne,11,1,25
3 | Bethanie,4,5,28
4 | Betsy,2,2,17
5 | Cathy,2,2,17
6 | Cecilia,9,5,38
7 | Chris,11,4,38
8 | Cindy,5,2,21
9 | David,1,2,15
10 | Debbie,7,2,24
11 | Dee,3,3,21
12 | Gloria,8,2,25
13 | Isabelle,3,1,16
14 | Jane,3,4,23
15 | Janelle,4,3,22
16 | Janice,2,2,17
17 | Jemma,2,4,22
18 | Joan,5,3,24
19 | Joyce,8,3,29
20 | Jude,4,3,22
21 | Julie,6,2,22
22 | Juliet,7,4,31
23 | Kate,5,3,24
24 | Nancy,8,4,32
25 | Nathalie,9,5,38
26 | Nicole,0,2,14
27 | Patricia,1,1,13
28 | Patrick,6,1,19
29 | Roberta,3,5,26
30 | Suzanne,5,1,18
31 | Vickie,7,1,20
32 | Wendie,5,2,21
33 | Zoe,8,3,29
34 |
--------------------------------------------------------------------------------
/datasets/NurseVacations.csv:
--------------------------------------------------------------------------------
1 | nurse,day
2 | Anne,Friday
3 | Anne,Sunday
4 | Cathy,Thursday
5 | Cathy,Tuesday
6 | Joan,Thursday
7 | Joan,Saturday
8 | Juliet,Monday
9 | Juliet,Tuesday
10 | Juliet,Thursday
11 | Nathalie,Sunday
12 | Nathalie,Thursday
13 | Isabelle,Monday
14 | Isabelle,Thursday
15 | Patricia,Saturday
16 | Patricia,Wednesday
17 | Nicole,Friday
18 | Nicole,Wednesday
19 | Jude,Tuesday
20 | Jude,Friday
21 | Debbie,Saturday
22 | Debbie,Wednesday
23 | Joyce,Sunday
24 | Joyce,Thursday
25 | Chris,Thursday
26 | Chris,Tuesday
27 | Cecilia,Friday
28 | Cecilia,Wednesday
29 | Patrick,Saturday
30 | Patrick,Sunday
31 | Cindy,Sunday
32 | Dee,Tuesday
33 | Dee,Friday
34 | Jemma,Friday
35 | Jemma,Wednesday
36 | Bethanie,Wednesday
37 | Bethanie,Tuesday
38 | Betsy,Monday
39 | Betsy,Thursday
40 | David,Monday
41 | Gloria,Monday
42 | Jane,Saturday
43 | Jane,Sunday
44 | Janelle,Wednesday
45 | Janelle,Friday
46 | Julie,Sunday
47 | Kate,Tuesday
48 | Kate,Monday
49 | Nancy,Sunday
50 | Roberta,Friday
51 | Roberta,Saturday
52 | Janice,Tuesday
53 | Janice,Friday
54 | Suzanne,Monday
55 | Vickie,Wednesday
56 | Vickie,Friday
57 | Wendie,Thursday
58 | Wendie,Saturday
59 | Zoe,Saturday
60 | Zoe,Sunday
61 |
--------------------------------------------------------------------------------
/datasets/Shifts.csv:
--------------------------------------------------------------------------------
1 | department,day,start_time,end_time,min_req,max_req
2 | Emergency,Monday,2,8,3,5
3 | Emergency,Monday,8,12,4,7
4 | Emergency,Monday,12,18,2,5
5 | Emergency,Monday,18,2,3,7
6 | Consultation,Monday,8,12,10,13
7 | Consultation,Monday,12,18,8,12
8 | Emergency,Tuesday,2,8,3,5
9 | Emergency,Tuesday,8,12,4,7
10 | Emergency,Tuesday,12,18,2,5
11 | Emergency,Tuesday,18,2,3,7
12 | Consultation,Tuesday,8,12,10,13
13 | Consultation,Tuesday,12,18,8,12
14 | Emergency,Wednesday,2,8,3,5
15 | Emergency,Wednesday,8,12,4,7
16 | Emergency,Wednesday,12,18,2,5
17 | Emergency,Wednesday,18,2,3,7
18 | Consultation,Wednesday,8,12,10,13
19 | Consultation,Wednesday,12,18,8,12
20 | Emergency,Thursday,2,8,3,5
21 | Emergency,Thursday,8,12,4,7
22 | Emergency,Thursday,12,18,2,5
23 | Emergency,Thursday,18,2,3,7
24 | Consultation,Thursday,8,12,10,13
25 | Consultation,Thursday,12,18,8,12
26 | Emergency,Friday,2,8,3,5
27 | Emergency,Friday,8,12,4,7
28 | Emergency,Friday,12,18,2,5
29 | Emergency,Friday,18,2,3,7
30 | Consultation,Friday,8,12,10,13
31 | Consultation,Friday,12,18,8,12
32 | Emergency,Saturday,2,12,5,7
33 | Emergency,Saturday,12,20,7,9
34 | Emergency,Saturday,20,2,12,12
35 | Emergency,Sunday,2,12,5,7
36 | Emergency,Sunday,12,20,7,9
37 | Emergency,Sunday,20,2,12,12
38 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/dashboard/dashboard.json:
--------------------------------------------------------------------------------
1 | {"id":"076c622a-c310-45b2-a8c8-918db617566e","name":"Dashboard","version":"0.3.0","locked":false,"activePage":0,"pages":[{"id":"cb32a9f1-606c-467c-8754-8906d91454a8","name":"Input","widgets":{"4b3cd697-bb13-4d4d-9acd-0354515722e5":{"name":"","type":"Table","props":{"container":"","data":"Nurses","spec":{"numbered":true,"compact":true,"columnExpand":true,"columnShrink":false,"columnWidth":0,"sortOrder":[],"columns":[{"property":"name","label":"name","type":"String","visible":true,"width":0,"style":{}},{"property":"seniority","label":"seniority","type":"Number","visible":true,"width":0,"style":{}},{"property":"qualification","label":"qualification","type":"Number","visible":true,"width":0,"style":{}},{"property":"pay_rate","label":"pay_rate","type":"Number","visible":true,"width":0,"style":{}}]},"search":""}},"995adae6-d186-42b4-8b82-a8a10d7cf882":{"name":"","type":"Table","props":{"container":"","data":"Skills","spec":{"numbered":true,"compact":true,"columnExpand":true,"columnShrink":false,"columnWidth":0,"sortOrder":[],"columns":[{"property":"id","label":"id","type":"String","visible":true,"width":0,"style":{}}]},"search":""}},"4dba641f-26c5-44c6-a32b-ead3fd444fdf":{"name":"","type":"Table","props":{"container":"","data":"Shifts","spec":{"numbered":true,"compact":true,"columnExpand":true,"columnShrink":false,"columnWidth":0,"sortOrder":[],"columns":[{"property":"department","label":"department","type":"String","visible":true,"width":0,"style":{}},{"property":"day","label":"day","type":"String","visible":true,"width":0,"style":{}},{"property":"start_time","label":"start_time","type":"Number","visible":true,"width":0,"style":{}},{"property":"end_time","label":"end_time","type":"Number","visible":true,"width":0,"style":{}},{"property":"min_req","label":"min_req","type":"Number","visible":true,"width":0,"style":{}},{"property":"max_req","label":"max_req","type":"Number","visible":true,"width":0,"style":{}}]},"search":""}}},"layouts":{"SM":[{"i":"4b3cd697-bb13-4d4d-9acd-0354515722e5","x":0,"y":2,"w":1,"h":2,"moved":false,"static":false},{"i":"995adae6-d186-42b4-8b82-a8a10d7cf882","x":0,"y":0,"w":1,"h":2,"moved":false,"static":false},{"i":"4dba641f-26c5-44c6-a32b-ead3fd444fdf","x":0,"y":4,"w":1,"h":2,"moved":false,"static":false}],"MD":[{"i":"4b3cd697-bb13-4d4d-9acd-0354515722e5","x":0,"y":2,"w":3,"h":2,"moved":false,"static":false},{"i":"995adae6-d186-42b4-8b82-a8a10d7cf882","x":0,"y":0,"w":3,"h":2,"moved":false,"static":false},{"i":"4dba641f-26c5-44c6-a32b-ead3fd444fdf","x":0,"y":4,"w":3,"h":2,"moved":false,"static":false}],"LG":[{"w":3,"h":2,"x":0,"y":0,"i":"4b3cd697-bb13-4d4d-9acd-0354515722e5","moved":false,"static":false},{"w":3,"h":2,"x":3,"y":0,"i":"995adae6-d186-42b4-8b82-a8a10d7cf882","moved":false,"static":false},{"w":6,"h":2,"x":0,"y":2,"i":"4dba641f-26c5-44c6-a32b-ead3fd444fdf","moved":false,"static":false}]}},{"id":"b0347448-a941-473e-a5a9-3fc31ec98dcd","name":"Shft Assignments","widgets":{"2a6c81a9-1fbd-452f-9422-d6b7717327f0":{"name":"","type":"Chart","props":{"container":"","data":"shift_assignments","spec":{"mark":"rect","encoding":{"x":{"field":"nurse_name","type":"nominal","axis":{"title":"Nurse"}},"y":{"field":"shift_day","type":"nominal","axis":{"title":"Day"}},"color":{"field":"shift_department","type":"nominal","legend":{"title":"Department"}}},"config":{"overlay":{"line":true},"scale":{"useUnaggregatedDomain":true}}},"search":""}},"85b1a527-6087-44e7-b796-77c59700e18a":{"name":"","type":"Table","props":{"container":"","data":"shift_assignments","spec":{"numbered":true,"compact":true,"columnExpand":true,"columnShrink":false,"columnWidth":0,"sortOrder":[],"columns":[{"property":"\"\"","label":"\"\"","type":"Number","visible":true,"width":0,"style":{}},{"property":"nurse_id","label":"nurse_id","type":"Number","visible":true,"width":0,"style":{}},{"property":"shift","label":"shift","type":"Number","visible":true,"width":0,"style":{}},{"property":"nurse_name","label":"nurse_name","type":"String","visible":true,"width":0,"style":{}},{"property":"shift_department","label":"shift_department","type":"String","visible":true,"width":0,"style":{}},{"property":"shift_day","label":"shift_day","type":"String","visible":true,"width":0,"style":{}},{"property":"shift_duration","label":"shift_duration","type":"Number","visible":true,"width":0,"style":{}}]},"search":""}},"e5955592-b417-4d83-ab76-e7081421d941":{"name":"","type":"Chart","props":{"container":"","data":"shift_assignments","spec":{"mark":"bar","encoding":{"x":{"field":"nurse_name","type":"nominal","axis":{"title":"Nurse"}},"y":{"field":"shift_duration","type":"quantitative","axis":{"title":"Duration"}},"color":{"field":"shift_department","type":"nominal","legend":{"title":"Department"}}},"config":{"overlay":{"line":true},"scale":{"useUnaggregatedDomain":true}}},"search":""}},"b5a57e3e-5c7a-4a85-9268-bb86c733baa5":{"name":"","type":"Chart","props":{"container":"","data":"shift_assignments","spec":{"mark":"bar","encoding":{"x":{"field":"nurse_name","type":"nominal","axis":{"title":"Nurse"}},"y":{"field":"shift_duration","type":"quantitative","aggregate":"sum","axis":{"title":"Duration"}},"color":{"field":"shift_day","type":"nominal","legend":{"title":"Day"}}},"config":{"overlay":{"line":true},"scale":{"useUnaggregatedDomain":true}}},"search":""}}},"layouts":{"SM":[{"i":"2a6c81a9-1fbd-452f-9422-d6b7717327f0","x":0,"y":0,"w":1,"h":2,"moved":false,"static":false},{"i":"85b1a527-6087-44e7-b796-77c59700e18a","x":0,"y":2,"w":1,"h":2,"moved":false,"static":false},{"i":"e5955592-b417-4d83-ab76-e7081421d941","x":0,"y":4,"w":1,"h":2,"moved":false,"static":false},{"i":"b5a57e3e-5c7a-4a85-9268-bb86c733baa5","x":0,"y":6,"w":1,"h":2,"moved":false,"static":false}],"MD":[{"i":"2a6c81a9-1fbd-452f-9422-d6b7717327f0","x":0,"y":0,"w":1,"h":2,"moved":false,"static":false},{"i":"85b1a527-6087-44e7-b796-77c59700e18a","x":0,"y":2,"w":3,"h":2,"moved":false,"static":false},{"i":"e5955592-b417-4d83-ab76-e7081421d941","x":1,"y":0,"w":1,"h":2,"moved":false,"static":false},{"i":"b5a57e3e-5c7a-4a85-9268-bb86c733baa5","x":2,"y":0,"w":1,"h":2,"moved":false,"static":false}],"LG":[{"w":3,"h":3,"x":3,"y":2,"i":"2a6c81a9-1fbd-452f-9422-d6b7717327f0","moved":false,"static":false},{"w":6,"h":2,"x":0,"y":0,"i":"85b1a527-6087-44e7-b796-77c59700e18a","moved":false,"static":false},{"w":3,"h":3,"x":0,"y":2,"i":"e5955592-b417-4d83-ab76-e7081421d941","moved":false,"static":false},{"w":3,"h":2,"x":0,"y":5,"i":"b5a57e3e-5c7a-4a85-9268-bb86c733baa5","moved":false,"static":false}]}}],"breakpoints":[{"name":"SM","width":0,"columns":1},{"name":"MD","width":480,"columns":3},{"name":"LG","width":960,"columns":6}],"rowHeight":120}
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/solveStatus.json:
--------------------------------------------------------------------------------
1 | {"retry":0,"creationTime":1523364445238,"lastModificationTime":1523364456068,"state":"TERMINATED","solveConfig":{"containerId":"Scenario 1","collectEngineLog":true,"solveParameters":{"oaas.logAttachmentName":"log.txt","oaas.logTailEnabled":"true"}},"jobId":"job.ec0f9241-0f8b-4e77-ac05-0c476fdb77a0","transactionId":"ce955898-e32a-44e5-aa6e-7d5bb60bdb27","dataContainerId":"c900759e-cab2-4213-9d1c-8d3f8fbb9cf7","jobDetails":{"id":"job.ec0f9241-0f8b-4e77-ac05-0c476fdb77a0","createdAt":1523364447599,"startedAt":1523364447693,"endedAt":1523364457891,"endReportedAt":1523364457909,"submittedAt":1523364447599,"updatedAt":1523364457891,"applicationVersionUsed":"1.0-R1-b4181","details":{"MODEL_DETAIL_BOOLEAN_VARS":"1152","MODEL_DETAIL_CONSTRAINTS":"388","MODEL_DETAIL_CONTINUOUS_VARS":"32","MODEL_DETAIL_KPIS":"[\"Total salary cost\", \"Total worked hours\"]","MODEL_DETAIL_INTEGER_VARS":"0","KPI.Total worked hours":"1146.0","KPI.Total salary cost":"25032.0","PROGRESS_CURRENT_OBJECTIVE":"25032.0","KPI._time":"7.4368531703948975","PROGRESS_BEST_OBJECTIVE":"25032.0","MODEL_DETAIL_TYPE":"MILP","PROGRESS_GAP":"0.0","MODEL_DETAIL_NONZEROS":"2976"},"systemDetails":{"worker.process.max.memory":"524288","worker.cores.total":"8","worker.cores.default":"1","worker.process.cpu.set":null,"worker.heap.max":"524288","worker.data.read.bytes":"34583","worker.data.write.duration":"260","worker.processing.duration":"10147","worker.process.peak.swap":"0","worker.log.write.duration":"2","worker.process.processing.duration":"9505","worker.data.read.duration":"150","worker.jms.write.chars":"53010","worker.log.write.chars":"1076","worker.data.write.bytes":"19873","worker.heap.used":"139027","worker.process.peak.memory":"116560","worker.setup.duration":"43","worker.jms.write.duration":"113"},"executionStatus":"PROCESSED","solveStatus":"OPTIMAL_SOLUTION","logTail":["[2018-04-10T12:47:37Z, INFO] 18 27 20 Roberta Emergency Thursday 6","[2018-04-10T12:47:37Z, INFO] 19 25 4 Patricia Consultation Monday 4","[2018-04-10T12:47:37Z, INFO] 20 29 22 Vickie Consultation Thursday 4","[2018-04-10T12:47:37Z, INFO] 21 19 32 Julie Emergency Saturday 6","[2018-04-10T12:47:37Z, INFO] 22 18 32 Jude Emergency Saturday 6","[2018-04-10T12:47:37Z, INFO] 23 10 5 Gloria Consultation Monday 6","[2018-04-10T12:47:37Z, INFO] 24 0 19 Anne Emergency Thursday 4","[2018-04-10T12:47:37Z, INFO] 25 15 6 Jemma Emergency Tuesday 6","[2018-04-10T12:47:37Z, INFO] 26 3 4 Cathy Consultation Monday 4","[2018-04-10T12:47:37Z, INFO] 27 28 4 Suzanne Consultation Monday 4","[2018-04-10T12:47:37Z, INFO] 28 31 9 Zoe Emergency Tuesday 8","[2018-04-10T12:47:37Z, INFO] 29 17 3 Joyce Emergency Monday 8","[2018-04-10T12:47:37Z, INFO] .. ... ... ... ... ... ...","[2018-04-10T12:47:37Z, INFO] 168 26 29 Patrick Consultation Friday 6","[2018-04-10T12:47:37Z, INFO] 169 24 5 Nicole Consultation Monday 6","[2018-04-10T12:47:37Z, INFO] 170 7 1 David Emergency Monday 4","[2018-04-10T12:47:37Z, INFO] 171 20 31 Juliet Emergency Saturday 8","[2018-04-10T12:47:37Z, INFO] 172 17 22 Joyce Consultation Thursday 4","[2018-04-10T12:47:37Z, INFO] 173 12 35 Jane Emergency Sunday 6","[2018-04-10T12:47:37Z, INFO] 174 24 12 Nicole Emergency Wednesday 6","[2018-04-10T12:47:37Z, INFO] 175 14 3 Janice Emergency Monday 8","[2018-04-10T12:47:37Z, INFO] 176 1 30 Bethanie Emergency Saturday 10","[2018-04-10T12:47:37Z, INFO] 177 13 22 Janelle Consultation Thursday 4","[2018-04-10T12:47:37Z, INFO] 178 2 29 Betsy Consultation Friday 6","[2018-04-10T12:47:37Z, INFO] 179 15 32 Jemma Emergency Saturday 6","[2018-04-10T12:47:37Z, INFO] 180 6 17 Cindy Consultation Wednesday 6","[2018-04-10T12:47:37Z, INFO] 181 17 15 Joyce Emergency Wednesday 8","[2018-04-10T12:47:37Z, INFO] 182 21 1 Kate Emergency Monday 4","[2018-04-10T12:47:37Z, INFO] 183 8 10 Debbie Consultation Tuesday 4","[2018-04-10T12:47:37Z, INFO] 184 9 11 Dee Consultation Tuesday 6","[2018-04-10T12:47:37Z, INFO] 185 8 16 Debbie Consultation Wednesday 4","[2018-04-10T12:47:37Z, INFO] 186 0 34 Anne Emergency Sunday 8","[2018-04-10T12:47:37Z, INFO] 187 31 10 Zoe Consultation Tuesday 4","[2018-04-10T12:47:37Z, INFO] 188 21 32 Kate Emergency Saturday 6","[2018-04-10T12:47:37Z, INFO] 189 31 16 Zoe Consultation Wednesday 4","[2018-04-10T12:47:37Z, INFO] 190 8 23 Debbie Consultation Thursday 6","[2018-04-10T12:47:37Z, INFO] 191 26 16 Patrick Consultation Wednesday 4","[2018-04-10T12:47:37Z, INFO] 192 19 25 Julie Emergency Friday 4","[2018-04-10T12:47:37Z, INFO] 193 15 17 Jemma Consultation Wednesday 6","[2018-04-10T12:47:37Z, INFO] 194 13 1 Janelle Emergency Monday 4","[2018-04-10T12:47:37Z, INFO] 195 11 17 Isabelle Consultation Wednesday 6","[2018-04-10T12:47:37Z, INFO] 196 29 24 Vickie Emergency Friday 6","[2018-04-10T12:47:37Z, INFO] 197 3 29 Cathy Consultation Friday 6","[2018-04-10T12:47:37Z, INFO] ","[2018-04-10T12:47:37Z, INFO] [198 rows x 6 columns]"]},"runtimeDetails":{"state":"STARTED"},"taskSetupMillis":341,"taskSolveMillis":10291,"taskTearDownMillis":0,"jobUri":"http://dods-processor-server-1523356102860-1168-svc/dd-execution/api/v1/solve/job.ec0f9241-0f8b-4e77-ac05-0c476fdb77a0"}
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/shift_assignments.csv:
--------------------------------------------------------------------------------
1 | 20,25,Juliet,Emergency,Friday,4
2 | 3,35,Cathy,Emergency,Sunday,6
3 | 21,28,Kate,Consultation,Friday,4
4 | 13,32,Janelle,Emergency,Saturday,6
5 | 9,0,Dee,Emergency,Monday,6
6 | 1,28,Bethanie,Consultation,Friday,4
7 | 30,35,Wendie,Emergency,Sunday,6
8 | 28,10,Suzanne,Consultation,Tuesday,4
9 | 1,21,Bethanie,Emergency,Thursday,8
10 | 25,16,Patricia,Consultation,Wednesday,4
11 | 0,14,Anne,Emergency,Wednesday,6
12 | 29,4,Vickie,Consultation,Monday,4
13 | 3,17,Cathy,Consultation,Wednesday,6
14 | 2,7,Betsy,Emergency,Tuesday,4
15 | 31,27,Zoe,Emergency,Friday,8
16 | 19,1,Julie,Emergency,Monday,4
17 | 12,32,Jane,Emergency,Saturday,6
18 | 0,28,Anne,Consultation,Friday,4
19 | 27,20,Roberta,Emergency,Thursday,6
20 | 25,4,Patricia,Consultation,Monday,4
21 | 29,22,Vickie,Consultation,Thursday,4
22 | 19,32,Julie,Emergency,Saturday,6
23 | 18,32,Jude,Emergency,Saturday,6
24 | 10,5,Gloria,Consultation,Monday,6
25 | 0,19,Anne,Emergency,Thursday,4
26 | 15,6,Jemma,Emergency,Tuesday,6
27 | 3,4,Cathy,Consultation,Monday,4
28 | 28,4,Suzanne,Consultation,Monday,4
29 | 31,9,Zoe,Emergency,Tuesday,8
30 | 17,3,Joyce,Emergency,Monday,8
31 | 20,8,Juliet,Emergency,Tuesday,6
32 | 18,8,Jude,Emergency,Tuesday,6
33 | 21,13,Kate,Emergency,Wednesday,4
34 | 12,10,Jane,Consultation,Tuesday,4
35 | 25,22,Patricia,Consultation,Thursday,4
36 | 13,11,Janelle,Consultation,Tuesday,6
37 | 2,16,Betsy,Consultation,Wednesday,4
38 | 1,13,Bethanie,Emergency,Wednesday,4
39 | 6,12,Cindy,Emergency,Wednesday,6
40 | 16,31,Joan,Emergency,Saturday,8
41 | 21,22,Kate,Consultation,Thursday,4
42 | 22,5,Nancy,Consultation,Monday,6
43 | 11,32,Isabelle,Emergency,Saturday,6
44 | 8,27,Debbie,Emergency,Friday,8
45 | 11,6,Isabelle,Emergency,Tuesday,6
46 | 10,17,Gloria,Consultation,Wednesday,6
47 | 28,32,Suzanne,Emergency,Saturday,6
48 | 26,28,Patrick,Consultation,Friday,4
49 | 24,4,Nicole,Consultation,Monday,4
50 | 2,5,Betsy,Consultation,Monday,6
51 | 30,0,Wendie,Emergency,Monday,6
52 | 19,29,Julie,Consultation,Friday,6
53 | 26,32,Patrick,Emergency,Saturday,6
54 | 31,5,Zoe,Consultation,Monday,6
55 | 16,22,Joan,Consultation,Thursday,4
56 | 6,25,Cindy,Emergency,Friday,4
57 | 13,35,Janelle,Emergency,Sunday,6
58 | 14,24,Janice,Emergency,Friday,6
59 | 25,10,Patricia,Consultation,Tuesday,4
60 | 28,17,Suzanne,Consultation,Wednesday,6
61 | 2,28,Betsy,Consultation,Friday,4
62 | 15,33,Jemma,Emergency,Sunday,10
63 | 30,11,Wendie,Consultation,Tuesday,6
64 | 19,34,Julie,Emergency,Sunday,8
65 | 16,13,Joan,Emergency,Wednesday,4
66 | 6,16,Cindy,Consultation,Wednesday,4
67 | 9,34,Dee,Emergency,Sunday,8
68 | 21,2,Kate,Emergency,Monday,6
69 | 12,13,Jane,Emergency,Wednesday,4
70 | 2,23,Betsy,Consultation,Thursday,6
71 | 0,11,Anne,Consultation,Tuesday,6
72 | 31,11,Zoe,Consultation,Tuesday,6
73 | 7,18,David,Emergency,Thursday,6
74 | 18,6,Jude,Emergency,Tuesday,6
75 | 10,32,Gloria,Emergency,Saturday,6
76 | 10,10,Gloria,Consultation,Tuesday,4
77 | 8,22,Debbie,Consultation,Thursday,4
78 | 3,21,Cathy,Emergency,Thursday,8
79 | 20,17,Juliet,Consultation,Wednesday,6
80 | 17,28,Joyce,Consultation,Friday,4
81 | 7,33,David,Emergency,Sunday,10
82 | 8,29,Debbie,Consultation,Friday,6
83 | 14,23,Janice,Consultation,Thursday,6
84 | 11,30,Isabelle,Emergency,Saturday,10
85 | 12,25,Jane,Emergency,Friday,4
86 | 27,19,Roberta,Emergency,Thursday,4
87 | 1,4,Bethanie,Consultation,Monday,4
88 | 27,9,Roberta,Emergency,Tuesday,8
89 | 6,5,Cindy,Consultation,Monday,6
90 | 17,9,Joyce,Emergency,Tuesday,8
91 | 6,31,Cindy,Emergency,Saturday,8
92 | 7,30,David,Emergency,Saturday,10
93 | 21,7,Kate,Emergency,Tuesday,4
94 | 8,4,Debbie,Consultation,Monday,4
95 | 13,33,Janelle,Emergency,Sunday,10
96 | 0,16,Anne,Consultation,Wednesday,4
97 | 1,29,Bethanie,Consultation,Friday,6
98 | 27,14,Roberta,Emergency,Wednesday,6
99 | 15,35,Jemma,Emergency,Sunday,6
100 | 16,15,Joan,Emergency,Wednesday,8
101 | 30,19,Wendie,Emergency,Thursday,4
102 | 9,32,Dee,Emergency,Saturday,6
103 | 9,10,Dee,Consultation,Tuesday,4
104 | 20,20,Juliet,Emergency,Thursday,6
105 | 26,17,Patrick,Consultation,Wednesday,6
106 | 0,4,Anne,Consultation,Monday,4
107 | 28,31,Suzanne,Emergency,Saturday,8
108 | 29,30,Vickie,Emergency,Saturday,10
109 | 3,23,Cathy,Consultation,Thursday,6
110 | 7,5,David,Consultation,Monday,6
111 | 20,19,Juliet,Emergency,Thursday,4
112 | 6,26,Cindy,Emergency,Friday,6
113 | 10,29,Gloria,Consultation,Friday,6
114 | 29,23,Vickie,Consultation,Thursday,6
115 | 3,28,Cathy,Consultation,Friday,4
116 | 30,4,Wendie,Consultation,Monday,4
117 | 20,26,Juliet,Emergency,Friday,6
118 | 27,33,Roberta,Emergency,Sunday,10
119 | 14,28,Janice,Consultation,Friday,4
120 | 29,32,Vickie,Emergency,Saturday,6
121 | 24,15,Nicole,Emergency,Wednesday,8
122 | 29,16,Vickie,Consultation,Wednesday,4
123 | 19,12,Julie,Emergency,Wednesday,6
124 | 18,33,Jude,Emergency,Sunday,10
125 | 30,23,Wendie,Consultation,Thursday,6
126 | 28,35,Suzanne,Emergency,Sunday,6
127 | 25,23,Patricia,Consultation,Thursday,6
128 | 24,34,Nicole,Emergency,Sunday,8
129 | 22,34,Nancy,Emergency,Sunday,8
130 | 17,31,Joyce,Emergency,Saturday,8
131 | 22,4,Nancy,Consultation,Monday,4
132 | 14,16,Janice,Consultation,Wednesday,4
133 | 25,24,Patricia,Emergency,Friday,6
134 | 11,29,Isabelle,Consultation,Friday,6
135 | 26,31,Patrick,Emergency,Saturday,8
136 | 24,3,Nicole,Emergency,Monday,8
137 | 12,30,Jane,Emergency,Saturday,10
138 | 1,7,Bethanie,Emergency,Tuesday,4
139 | 25,2,Patricia,Emergency,Monday,6
140 | 26,35,Patrick,Emergency,Sunday,6
141 | 7,7,David,Emergency,Tuesday,4
142 | 16,21,Joan,Emergency,Thursday,8
143 | 18,35,Jude,Emergency,Sunday,6
144 | 22,31,Nancy,Emergency,Saturday,8
145 | 14,27,Janice,Emergency,Friday,8
146 | 15,18,Jemma,Emergency,Thursday,6
147 | 11,18,Isabelle,Emergency,Thursday,6
148 | 25,11,Patricia,Consultation,Tuesday,6
149 | 30,10,Wendie,Consultation,Tuesday,4
150 | 19,35,Julie,Emergency,Sunday,6
151 | 27,35,Roberta,Emergency,Sunday,6
152 | 9,35,Dee,Emergency,Sunday,6
153 | 16,34,Joan,Emergency,Sunday,8
154 | 2,22,Betsy,Consultation,Thursday,4
155 | 0,10,Anne,Consultation,Tuesday,4
156 | 28,5,Suzanne,Consultation,Monday,6
157 | 30,28,Wendie,Consultation,Friday,4
158 | 21,34,Kate,Emergency,Sunday,8
159 | 31,22,Zoe,Consultation,Thursday,4
160 | 10,35,Gloria,Emergency,Sunday,6
161 | 9,16,Dee,Consultation,Wednesday,4
162 | 12,11,Jane,Consultation,Tuesday,6
163 | 10,23,Gloria,Consultation,Thursday,6
164 | 13,10,Janelle,Consultation,Tuesday,4
165 | 2,11,Betsy,Consultation,Tuesday,6
166 | 18,0,Jude,Emergency,Monday,6
167 | 8,28,Debbie,Consultation,Friday,4
168 | 14,22,Janice,Consultation,Thursday,4
169 | 26,29,Patrick,Consultation,Friday,6
170 | 24,5,Nicole,Consultation,Monday,6
171 | 7,1,David,Emergency,Monday,4
172 | 20,31,Juliet,Emergency,Saturday,8
173 | 17,22,Joyce,Consultation,Thursday,4
174 | 12,35,Jane,Emergency,Sunday,6
175 | 24,12,Nicole,Emergency,Wednesday,6
176 | 14,3,Janice,Emergency,Monday,8
177 | 1,30,Bethanie,Emergency,Saturday,10
178 | 13,22,Janelle,Consultation,Thursday,4
179 | 2,29,Betsy,Consultation,Friday,6
180 | 15,32,Jemma,Emergency,Saturday,6
181 | 6,17,Cindy,Consultation,Wednesday,6
182 | 17,15,Joyce,Emergency,Wednesday,8
183 | 21,1,Kate,Emergency,Monday,4
184 | 8,10,Debbie,Consultation,Tuesday,4
185 | 9,11,Dee,Consultation,Tuesday,6
186 | 8,16,Debbie,Consultation,Wednesday,4
187 | 0,34,Anne,Emergency,Sunday,8
188 | 31,10,Zoe,Consultation,Tuesday,4
189 | 21,32,Kate,Emergency,Saturday,6
190 | 31,16,Zoe,Consultation,Wednesday,4
191 | 8,23,Debbie,Consultation,Thursday,6
192 | 26,16,Patrick,Consultation,Wednesday,4
193 | 19,25,Julie,Emergency,Friday,4
194 | 15,17,Jemma,Consultation,Wednesday,6
195 | 13,1,Janelle,Emergency,Monday,4
196 | 11,17,Isabelle,Consultation,Wednesday,6
197 | 29,24,Vickie,Emergency,Friday,6
198 | 3,29,Cathy,Consultation,Friday,6
199 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/log.txt:
--------------------------------------------------------------------------------
1 | [2018-04-10T12:47:28Z, INFO] #nurses = 32
2 | [2018-04-10T12:47:28Z, INFO] #shifts = 36
3 | [2018-04-10T12:47:28Z, INFO] #vacations = 59
4 | [2018-04-10T12:47:28Z, INFO] * system is: Linux 64bit
5 | [2018-04-10T12:47:28Z, INFO] * Python is present, version is 2.7.13
6 | [2018-04-10T12:47:28Z, INFO] * docplex is present, version is (2, 5, 92)
7 | [2018-04-10T12:47:28Z, INFO] * CPLEX wrapper is present, version is 12.8.0.0, located at: /opt/ibm/lib/cplex/2.7/x86-64_linux
8 | [2018-04-10T12:47:29Z, INFO] #incompatible shift constraints: 320
9 | [2018-04-10T12:47:29Z, INFO] # vacation forbids: 0 assignments
10 | [2018-04-10T12:47:29Z, INFO] Model: nurses
11 | [2018-04-10T12:47:29Z, INFO] - number of variables: 1184
12 | [2018-04-10T12:47:29Z, INFO] - binary=1152, integer=0, continuous=32
13 | [2018-04-10T12:47:29Z, INFO] - number of constraints: 352
14 | [2018-04-10T12:47:29Z, INFO] - linear=352
15 | [2018-04-10T12:47:29Z, INFO] - parameters: defaults
16 | [2018-04-10T12:47:29Z, INFO] Model: nurses
17 | [2018-04-10T12:47:29Z, INFO] - number of variables: 1184
18 | [2018-04-10T12:47:29Z, INFO] - binary=1152, integer=0, continuous=32
19 | [2018-04-10T12:47:29Z, INFO] - number of constraints: 388
20 | [2018-04-10T12:47:29Z, INFO] - linear=388
21 | [2018-04-10T12:47:29Z, INFO] - parameters: defaults
22 | [2018-04-10T12:47:29Z, INFO] WARNING: Number of workers has been reduced to 1 to comply with platform limitations.
23 | [2018-04-10T12:47:29Z, INFO] CPXPARAM_Read_DataCheck 1
24 | [2018-04-10T12:47:29Z, INFO] CPXPARAM_Threads 1
25 | [2018-04-10T12:47:29Z, INFO] CPXPARAM_MIP_Tolerances_MIPGap 1.0000000000000001e-05
26 | [2018-04-10T12:47:29Z, INFO] Tried aggregator 1 time.
27 | [2018-04-10T12:47:29Z, INFO] Reduced MIP has 388 rows, 1184 columns, and 2976 nonzeros.
28 | [2018-04-10T12:47:29Z, INFO] Reduced MIP has 1152 binaries, 0 generals, 0 SOSs, and 0 indicators.
29 | [2018-04-10T12:47:29Z, INFO] Presolve time = 0.00 sec. (1.50 ticks)
30 | [2018-04-10T12:47:29Z, INFO] Found incumbent of value 26912.000000 after 0.01 sec. (5.42 ticks)
31 | [2018-04-10T12:47:29Z, INFO] Probing time = 0.00 sec. (0.54 ticks)
32 | [2018-04-10T12:47:29Z, INFO] Tried aggregator 1 time.
33 | [2018-04-10T12:47:29Z, INFO] Reduced MIP has 388 rows, 1184 columns, and 2976 nonzeros.
34 | [2018-04-10T12:47:29Z, INFO] Reduced MIP has 1152 binaries, 32 generals, 0 SOSs, and 0 indicators.
35 | [2018-04-10T12:47:29Z, INFO] Presolve time = 0.00 sec. (2.87 ticks)
36 | [2018-04-10T12:47:29Z, INFO] Probing time = 0.00 sec. (0.54 ticks)
37 | [2018-04-10T12:47:29Z, INFO] Clique table members: 320.
38 | [2018-04-10T12:47:29Z, INFO] MIP emphasis: balance optimality and feasibility.
39 | [2018-04-10T12:47:29Z, INFO] MIP search method: dynamic search.
40 | [2018-04-10T12:47:29Z, INFO] Parallel mode: none, using 1 thread.
41 | [2018-04-10T12:47:29Z, INFO] Root relaxation solution time = 0.01 sec. (4.25 ticks)
42 | [2018-04-10T12:47:29Z, INFO]
43 | [2018-04-10T12:47:29Z, INFO] Nodes Cuts/
44 | [2018-04-10T12:47:29Z, INFO] Node Left Objective IInf Best Integer Best Bound ItCnt Gap
45 | [2018-04-10T12:47:29Z, INFO] * 0+ 0 26912.0000 0.0000 100.00%
46 | [2018-04-10T12:47:29Z, INFO] 0 0 25032.0000 32 26912.0000 25032.0000 477 6.99%
47 | [2018-04-10T12:47:29Z, INFO] 0 0 25032.0000 35 26912.0000 Cuts: 73 566 6.99%
48 | [2018-04-10T12:47:30Z, INFO] 0 0 25032.0000 30 26912.0000 Cuts: 55 626 6.99%
49 | [2018-04-10T12:47:30Z, INFO] 0 0 25032.0000 28 26912.0000 Cuts: 57 710 6.99%
50 | [2018-04-10T12:47:30Z, INFO] * 0+ 0 25544.0000 25032.0000 2.00%
51 | [2018-04-10T12:47:30Z, INFO] * 0+ 0 25378.0000 25032.0000 1.36%
52 | [2018-04-10T12:47:30Z, INFO] * 0+ 0 25324.0000 25032.0000 1.15%
53 | [2018-04-10T12:47:30Z, INFO] * 0+ 0 25276.0000 25032.0000 0.97%
54 | [2018-04-10T12:47:30Z, INFO] * 0+ 0 25246.0000 25032.0000 0.85%
55 | [2018-04-10T12:47:30Z, INFO] 0 2 25032.0000 14 25246.0000 25032.0000 710 0.85%
56 | [2018-04-10T12:47:30Z, INFO] Elapsed time = 0.22 sec. (97.62 ticks, tree = 0.01 MB
57 | [2018-04-10T12:47:30Z, INFO] , solutions = 6
58 | [2018-04-10T12:47:30Z, INFO] )
59 | [2018-04-10T12:47:31Z, INFO] * 130+ 130 25220.0000 25032.0000 0.75%
60 | [2018-04-10T12:47:35Z, INFO] 362 364 25032.0000 10 25220.0000 25032.0000 6097 0.75%
61 | [2018-04-10T12:47:35Z, INFO] * 380+ 380 25190.0000 25032.0000 0.63%
62 | [2018-04-10T12:47:37Z, INFO] * 506+ 506 25138.0000 25032.0000 0.42%
63 | [2018-04-10T12:47:37Z, INFO] * 507+ 505 25102.0000 25032.0000 0.28%
64 | [2018-04-10T12:47:37Z, INFO] * 507+ 505 25072.0000 25032.0000 0.16%
65 | [2018-04-10T12:47:37Z, INFO] * 507+ 0 25032.0000 25032.0000 0.00%
66 | [2018-04-10T12:47:37Z, INFO] GUB cover cuts applied: 12
67 | [2018-04-10T12:47:37Z, INFO] Cover cuts applied: 2
68 | [2018-04-10T12:47:37Z, INFO] Flow cuts applied: 10
69 | [2018-04-10T12:47:37Z, INFO] Mixed integer rounding cuts applied: 11
70 | [2018-04-10T12:47:37Z, INFO] Zero-half cuts applied: 13
71 | [2018-04-10T12:47:37Z, INFO] Lift and project cuts applied: 7
72 | [2018-04-10T12:47:37Z, INFO] Gomory fractional cuts applied: 2
73 | [2018-04-10T12:47:37Z, INFO]
74 | [2018-04-10T12:47:37Z, INFO] Root node processing (before b&c):
75 | [2018-04-10T12:47:37Z, INFO] Real time = 0.22 sec. (97.62 ticks)
76 | [2018-04-10T12:47:37Z, INFO] Sequential b&c:
77 | [2018-04-10T12:47:37Z, INFO] Real time = 7.23 sec. (396.59 ticks)
78 | [2018-04-10T12:47:37Z, INFO] ------------
79 | [2018-04-10T12:47:37Z, INFO] Total (root+branch&cut) = 7.45 sec. (494.21 ticks)
80 | [2018-04-10T12:47:37Z, INFO] * model nurses solved with objective = 25032.000
81 | [2018-04-10T12:47:37Z, INFO] * KPI: Total salary cost = 25032.000
82 | [2018-04-10T12:47:37Z, INFO] * KPI: Total worked hours = 1146.000
83 | [2018-04-10T12:47:37Z, INFO] kpi value
84 | [2018-04-10T12:47:37Z, INFO] 0 Total salary cost 25032.0
85 | [2018-04-10T12:47:37Z, INFO] 1 Total worked hours 1146.0
86 | [2018-04-10T12:47:37Z, INFO] 2 objective_ 25032.0
87 | [2018-04-10T12:47:37Z, INFO] nurse_id shift nurse_name shift_department shift_day shift_duration
88 | [2018-04-10T12:47:37Z, INFO] 0 20 25 Juliet Emergency Friday 4
89 | [2018-04-10T12:47:37Z, INFO] 1 3 35 Cathy Emergency Sunday 6
90 | [2018-04-10T12:47:37Z, INFO] 2 21 28 Kate Consultation Friday 4
91 | [2018-04-10T12:47:37Z, INFO] 3 13 32 Janelle Emergency Saturday 6
92 | [2018-04-10T12:47:37Z, INFO] 4 9 0 Dee Emergency Monday 6
93 | [2018-04-10T12:47:37Z, INFO] 5 1 28 Bethanie Consultation Friday 4
94 | [2018-04-10T12:47:37Z, INFO] 6 30 35 Wendie Emergency Sunday 6
95 | [2018-04-10T12:47:37Z, INFO] 7 28 10 Suzanne Consultation Tuesday 4
96 | [2018-04-10T12:47:37Z, INFO] 8 1 21 Bethanie Emergency Thursday 8
97 | [2018-04-10T12:47:37Z, INFO] 9 25 16 Patricia Consultation Wednesday 4
98 | [2018-04-10T12:47:37Z, INFO] 10 0 14 Anne Emergency Wednesday 6
99 | [2018-04-10T12:47:37Z, INFO] 11 29 4 Vickie Consultation Monday 4
100 | [2018-04-10T12:47:37Z, INFO] 12 3 17 Cathy Consultation Wednesday 6
101 | [2018-04-10T12:47:37Z, INFO] 13 2 7 Betsy Emergency Tuesday 4
102 | [2018-04-10T12:47:37Z, INFO] 14 31 27 Zoe Emergency Friday 8
103 | [2018-04-10T12:47:37Z, INFO] 15 19 1 Julie Emergency Monday 4
104 | [2018-04-10T12:47:37Z, INFO] 16 12 32 Jane Emergency Saturday 6
105 | [2018-04-10T12:47:37Z, INFO] 17 0 28 Anne Consultation Friday 4
106 | [2018-04-10T12:47:37Z, INFO] 18 27 20 Roberta Emergency Thursday 6
107 | [2018-04-10T12:47:37Z, INFO] 19 25 4 Patricia Consultation Monday 4
108 | [2018-04-10T12:47:37Z, INFO] 20 29 22 Vickie Consultation Thursday 4
109 | [2018-04-10T12:47:37Z, INFO] 21 19 32 Julie Emergency Saturday 6
110 | [2018-04-10T12:47:37Z, INFO] 22 18 32 Jude Emergency Saturday 6
111 | [2018-04-10T12:47:37Z, INFO] 23 10 5 Gloria Consultation Monday 6
112 | [2018-04-10T12:47:37Z, INFO] 24 0 19 Anne Emergency Thursday 4
113 | [2018-04-10T12:47:37Z, INFO] 25 15 6 Jemma Emergency Tuesday 6
114 | [2018-04-10T12:47:37Z, INFO] 26 3 4 Cathy Consultation Monday 4
115 | [2018-04-10T12:47:37Z, INFO] 27 28 4 Suzanne Consultation Monday 4
116 | [2018-04-10T12:47:37Z, INFO] 28 31 9 Zoe Emergency Tuesday 8
117 | [2018-04-10T12:47:37Z, INFO] 29 17 3 Joyce Emergency Monday 8
118 | [2018-04-10T12:47:37Z, INFO] .. ... ... ... ... ... ...
119 | [2018-04-10T12:47:37Z, INFO] 168 26 29 Patrick Consultation Friday 6
120 | [2018-04-10T12:47:37Z, INFO] 169 24 5 Nicole Consultation Monday 6
121 | [2018-04-10T12:47:37Z, INFO] 170 7 1 David Emergency Monday 4
122 | [2018-04-10T12:47:37Z, INFO] 171 20 31 Juliet Emergency Saturday 8
123 | [2018-04-10T12:47:37Z, INFO] 172 17 22 Joyce Consultation Thursday 4
124 | [2018-04-10T12:47:37Z, INFO] 173 12 35 Jane Emergency Sunday 6
125 | [2018-04-10T12:47:37Z, INFO] 174 24 12 Nicole Emergency Wednesday 6
126 | [2018-04-10T12:47:37Z, INFO] 175 14 3 Janice Emergency Monday 8
127 | [2018-04-10T12:47:37Z, INFO] 176 1 30 Bethanie Emergency Saturday 10
128 | [2018-04-10T12:47:37Z, INFO] 177 13 22 Janelle Consultation Thursday 4
129 | [2018-04-10T12:47:37Z, INFO] 178 2 29 Betsy Consultation Friday 6
130 | [2018-04-10T12:47:37Z, INFO] 179 15 32 Jemma Emergency Saturday 6
131 | [2018-04-10T12:47:37Z, INFO] 180 6 17 Cindy Consultation Wednesday 6
132 | [2018-04-10T12:47:37Z, INFO] 181 17 15 Joyce Emergency Wednesday 8
133 | [2018-04-10T12:47:37Z, INFO] 182 21 1 Kate Emergency Monday 4
134 | [2018-04-10T12:47:37Z, INFO] 183 8 10 Debbie Consultation Tuesday 4
135 | [2018-04-10T12:47:37Z, INFO] 184 9 11 Dee Consultation Tuesday 6
136 | [2018-04-10T12:47:37Z, INFO] 185 8 16 Debbie Consultation Wednesday 4
137 | [2018-04-10T12:47:37Z, INFO] 186 0 34 Anne Emergency Sunday 8
138 | [2018-04-10T12:47:37Z, INFO] 187 31 10 Zoe Consultation Tuesday 4
139 | [2018-04-10T12:47:37Z, INFO] 188 21 32 Kate Emergency Saturday 6
140 | [2018-04-10T12:47:37Z, INFO] 189 31 16 Zoe Consultation Wednesday 4
141 | [2018-04-10T12:47:37Z, INFO] 190 8 23 Debbie Consultation Thursday 6
142 | [2018-04-10T12:47:37Z, INFO] 191 26 16 Patrick Consultation Wednesday 4
143 | [2018-04-10T12:47:37Z, INFO] 192 19 25 Julie Emergency Friday 4
144 | [2018-04-10T12:47:37Z, INFO] 193 15 17 Jemma Consultation Wednesday 6
145 | [2018-04-10T12:47:37Z, INFO] 194 13 1 Janelle Emergency Monday 4
146 | [2018-04-10T12:47:37Z, INFO] 195 11 17 Isabelle Consultation Wednesday 6
147 | [2018-04-10T12:47:37Z, INFO] 196 29 24 Vickie Emergency Friday 6
148 | [2018-04-10T12:47:37Z, INFO] 197 3 29 Cathy Consultation Friday 6
149 | [2018-04-10T12:47:37Z, INFO]
150 | [2018-04-10T12:47:37Z, INFO] [198 rows x 6 columns]
151 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/model.py:
--------------------------------------------------------------------------------
1 | #dd-markdown # The Nurse Assignment Problem
2 | #dd-markdown
3 | #dd-markdown This tutorial includes everything you need to set up IBM Decision Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical Programming model, and get its solution by solving the model on the cloud with IBM ILOG CPLEX Optimizer.
4 | #dd-markdown
5 | #dd-markdown When you finish this tutorial, you'll have a foundational knowledge of _Prescriptive Analytics_.
6 | #dd-markdown
7 | #dd-markdown >This notebook is part of [Prescriptive Analytics for Python](https://rawgit.com/IBMDecisionOptimization/docplex-doc/master/docs/index.html).
8 | #dd-markdown
9 | #dd-markdown >It requires a valid subscription to **Decision Optimization on Cloud** or a **local installation of CPLEX Optimizers**.
10 | #dd-markdown Discover us [here](https://developer.ibm.com/docloud).
11 | #dd-markdown
12 | #dd-markdown
13 | #dd-markdown Table of contents:
14 | #dd-markdown
15 | #dd-markdown - [Describe the business problem](#Describe-the-business-problem)
16 | #dd-markdown * [How decision optimization (prescriptive analytics) can help](#How--decision-optimization-can-help)
17 | #dd-markdown * [Use decision optimization](#Use-decision-optimization)
18 | #dd-markdown * [Step 1: Download the library](#Step-1:-Download-the-library)
19 | #dd-markdown * [Step 2: Set up the engines](#Step-2:-Set-up-the-prescriptive-engine)
20 | #dd-markdown - [Step 3: Model the data](#Step-3:-Model-the-data)
21 | #dd-markdown * [Step 4: Prepare the data](#Step-4:-Prepare-the-data)
22 | #dd-markdown - [Step 5: Set up the prescriptive model](#Step-5:-Set-up-the-prescriptive-model)
23 | #dd-markdown * [Define the decision variables](#Define-the-decision-variables)
24 | #dd-markdown * [Express the business constraints](#Express-the-business-constraints)
25 | #dd-markdown * [Express the objective](#Express-the-objective)
26 | #dd-markdown * [Solve with the Decision Optimization solve service](#Solve-with-the-Decision-Optimization-solve-service)
27 | #dd-markdown * [Step 6: Investigate the solution and run an example analysis](#Step-6:-Investigate-the-solution-and-then-run-an-example-analysis)
28 | #dd-markdown * [Summary](#Summary)
29 | #dd-markdown
30 | #dd-markdown ****
31 | #dd-markdown ## Describe the business problem
32 | #dd-markdown
33 | #dd-markdown This notebook describes how to use CPLEX Modeling for Python together with *pandas* to
34 | #dd-markdown manage the assignment of nurses to shifts in a hospital.
35 | #dd-markdown
36 | #dd-markdown Nurses must be assigned to hospital shifts in accordance with various skill and staffing constraints.
37 | #dd-markdown
38 | #dd-markdown The goal of the model is to find an efficient balance between the different objectives:
39 | #dd-markdown
40 | #dd-markdown * minimize the overall cost of the plan and
41 | #dd-markdown * assign shifts as fairly as possible.
42 | #dd-markdown ## How decision optimization can help
43 | #dd-markdown
44 | #dd-markdown * Prescriptive analytics (decision optimization) technology recommends actions that are based on desired outcomes. It takes into account specific scenarios, resources, and knowledge of past and current events. With this insight, your organization can make better decisions and have greater control of business outcomes.
45 | #dd-markdown
46 | #dd-markdown * Prescriptive analytics is the next step on the path to insight-based actions. It creates value through synergy with predictive analytics, which analyzes data to predict future outcomes.
47 | #dd-markdown
48 | #dd-markdown * Prescriptive analytics takes that insight to the next level by suggesting the optimal way to handle that future situation. Organizations that can act fast in dynamic conditions and make superior decisions in uncertain environments gain a strong competitive advantage.
49 | #dd-markdown
50 | #dd-markdown
51 | #dd-markdown With prescriptive analytics, you can:
52 | #dd-markdown
53 | #dd-markdown * Automate the complex decisions and trade-offs to better manage your limited resources.
54 | #dd-markdown * Take advantage of a future opportunity or mitigate a future risk.
55 | #dd-markdown * Proactively update recommendations based on changing events.
56 | #dd-markdown * Meet operational goals, increase customer loyalty, prevent threats and fraud, and optimize business processes.
57 | #dd-markdown ## Checking minimum requirements
58 | #dd-markdown This notebook uses some features of pandas that are available in version 0.17.1 or above.
59 | #dd-cell
60 | import pip
61 | REQUIRED_MINIMUM_PANDAS_VERSION = '0.17.1'
62 | try:
63 | import pandas as pd
64 | assert pd.__version__ >= REQUIRED_MINIMUM_PANDAS_VERSION
65 | except:
66 | raise Exception("Version %s or above of Pandas is required to run this notebook" % REQUIRED_MINIMUM_PANDAS_VERSION)
67 | #dd-markdown ### Step 2: Set up the prescriptive engine
68 | #dd-markdown
69 | #dd-markdown * Subscribe to our private cloud offer or Decision Optimization on Cloud solve service [here](https://developer.ibm.com/docloud) if you do not want to use a local solver.
70 | #dd-markdown * Get the service URL and your personal API key and enter your credentials here if accurate:
71 | #dd-cell
72 | # @hidden_cell
73 | url = "https://api-oaas.docloud.ibmcloud.com/job_manager/rest/v1/"
74 | key = "api_f550300e-8e52-4f3e-abf1-0fe1ac428d93"
75 | #dd-markdown ### Step 3: Model the data
76 | #dd-markdown
77 | #dd-markdown The input data consists of several tables:
78 | #dd-markdown
79 | #dd-markdown * The Departments table lists all departments in the scope of the assignment.
80 | #dd-markdown * The Skills table list all skills.
81 | #dd-markdown * The Shifts table lists all shifts to be staffed. A shift contains a department, a day in the week, plus the start and end times.
82 | #dd-markdown * The Nurses table lists all nurses, identified by their names.
83 | #dd-markdown * The NurseSkills table gives the skills of each nurse.
84 | #dd-markdown * The SkillRequirements table lists the minimum number of persons required for a given department and skill.
85 | #dd-markdown * The NurseVacations table lists days off for each nurse.
86 | #dd-markdown * The NurseAssociations table lists pairs of nurses who wish to work together.
87 | #dd-markdown * The NurseIncompatibilities table lists pairs of nurses who do not want to work together.
88 | #dd-markdown #### Loading data from Excel with pandas
89 | #dd-markdown
90 | #dd-markdown We load the data from an Excel file using *pandas*.
91 | #dd-markdown Each sheet is read into a separate *pandas* DataFrame.
92 | #dd-cell
93 | # This notebook requires pandas to work
94 | import pandas as pd
95 | from pandas import DataFrame
96 | #dd-cell
97 | df_skills = inputs['Skills']
98 | df_depts = inputs['Departments']
99 | df_shifts = inputs['Shifts']
100 | # Rename df_shifts index
101 | #df_shifts.index.name = 'shiftId'
102 |
103 | # Index is column 0: name
104 | df_nurses = inputs['Nurses']
105 | df_nurse_skilles = inputs['NurseSkills']
106 | df_vacations = inputs['NurseVacations']
107 | df_associations = inputs['NurseAssociations']
108 | df_incompatibilities = inputs['NurseIncompatibilities']
109 | #dd-cell
110 | df_shifts.index.name = 'shiftId'
111 | df_nurses.set_index('name', inplace=True)
112 |
113 | # Display the nurses dataframe
114 | print("#nurses = {}".format(len(df_nurses)))
115 | print("#shifts = {}".format(len(df_shifts)))
116 | print("#vacations = {}".format(len(df_vacations)))
117 | #dd-markdown In addition, we introduce some extra global data:
118 | #dd-markdown
119 | #dd-markdown * The maximum work time for each nurse.
120 | #dd-markdown * The maximum and minimum number of shifts worked by a nurse in a week.
121 | #dd-cell
122 | # maximum work time (in hours)
123 | max_work_time = 40
124 |
125 | # maximum number of shifts worked in a week.
126 | max_nb_shifts = 5
127 | #dd-markdown Shifts are stored in a separate DataFrame.
128 | #dd-cell
129 | df_shifts
130 | #dd-markdown ### Step 4: Prepare the data
131 | #dd-markdown
132 | #dd-markdown We need to precompute additional data for shifts.
133 | #dd-markdown For each shift, we need the start time and end time expressed in hours, counting from the beginning of the week: Monday 8am is converted to 8, Tuesday 8am is converted to 24+8 = 32, and so on.
134 | #dd-markdown
135 | #dd-markdown #### Sub-step #1
136 | #dd-markdown We start by adding an extra column `dow` (day of week) which converts the string "day" into an integer in 0..6 (Monday is 0, Sunday is 6).
137 | #dd-cell
138 | days = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
139 | day_of_weeks = dict(zip(days, range(7)))
140 |
141 | # utility to convert a day string e.g. "Monday" to an integer in 0..6
142 | def day_to_day_of_week(day):
143 | return day_of_weeks[day.strip().lower()]
144 |
145 | # for each day name, we normalize it by stripping whitespace and converting it to lowercase
146 | # " Monday" -> "monday"
147 | df_shifts["dow"] = df_shifts.day.apply(day_to_day_of_week)
148 | df_shifts
149 | #dd-markdown #### Sub-step #2 : Compute the absolute start time of each shift.
150 | #dd-markdown
151 | #dd-markdown Computing the start time in the week is easy: just add `24*dow` to column `start_time`. The result is stored in a new column `wstart`.
152 | #dd-cell
153 | df_shifts["wstart"] = df_shifts.start_time + 24 * df_shifts.dow
154 | #dd-markdown #### Sub-Step #3 : Compute the absolute end time of each shift.
155 | #dd-markdown
156 | #dd-markdown Computing the absolute end time is a little more complicated as certain shifts span across midnight. For example, Shift #3 starts on Monday at 18:00 and ends Tuesday at 2:00 AM. The absolute end time of Shift #3 is 26, not 2.
157 | #dd-markdown The general rule for computing absolute end time is:
158 | #dd-markdown
159 | #dd-markdown `abs_end_time = end_time + 24 * dow + (start_time>= end_time ? 24 : 0)`
160 | #dd-markdown
161 | #dd-markdown Again, we use *pandas* to add a new calculated column `wend`. This is done by using the *pandas* `apply` method with an anonymous `lambda` function over rows. The `raw=True` parameter prevents the creation of a *pandas* Series for each row, which improves the performance significantly on large data sets.
162 | #dd-cell
163 | # an auxiliary function to calculate absolute end time of a shift
164 | def calculate_absolute_endtime(start, end, dow):
165 | return 24*dow + end + (24 if start>=end else 0)
166 |
167 | # store the results in a new column
168 | df_shifts["wend"] = df_shifts.apply(lambda row: calculate_absolute_endtime(
169 | row.start_time, row.end_time, row.dow), axis=1, raw=True)
170 | #dd-markdown #### Sub-step #4 : Compute the duration of each shift.
171 | #dd-markdown
172 | #dd-markdown Computing the duration of each shift is now a straightforward difference of columns. The result is stored in column `duration`.
173 | #dd-cell
174 | df_shifts["duration"] = df_shifts.wend - df_shifts.wstart
175 | #dd-markdown #### Sub-step #5 : Compute the minimum demand for each shift.
176 | #dd-markdown
177 | #dd-markdown Minimum demand is the product of duration (in hours) by the minimum required number of nurses. Thus, in number of
178 | #dd-markdown nurse-hours, this demand is stored in another new column `min_demand`.
179 | #dd-markdown
180 | #dd-markdown Finally, we display the updated shifts DataFrame with all calculated columns.
181 | #dd-cell
182 | # also compute minimum demand in nurse-hours
183 | df_shifts["min_demand"] = df_shifts.min_req * df_shifts.duration
184 |
185 | # finally check the modified shifts dataframe
186 | df_shifts
187 | #dd-markdown ### Step 5: Set up the prescriptive model
188 | #dd-cell
189 | from docplex.mp.environment import Environment
190 | env = Environment()
191 | env.print_information()
192 | #dd-markdown #### Create the DOcplex model
193 | #dd-markdown The model contains all the business constraints and defines the objective.
194 | #dd-markdown
195 | #dd-markdown We now use CPLEX Modeling for Python to build a Mixed Integer Programming (MIP) model for this problem.
196 | #dd-cell
197 | from docplex.mp.model import Model
198 | mdl = Model(name="nurses")
199 | #dd-markdown #### Define the decision variables
200 | #dd-markdown
201 | #dd-markdown For each (nurse, shift) pair, we create one binary variable that is equal to 1 when the nurse is assigned to the shift.
202 | #dd-markdown
203 | #dd-markdown We use the `binary_var_matrix` method of class `Model`, as each binary variable is indexed by _two_ objects: one nurse and one shift.
204 | #dd-cell
205 | # first global collections to iterate upon
206 | all_nurses = df_nurses.index.values
207 | all_shifts = df_shifts.index.values
208 |
209 | # the assignment variables.
210 | def make_shift_name(shift_idx):
211 | shift_row = df_shifts.iloc[shift_idx]
212 | # keep first 3 characters in departement, uppercased
213 | dept2 = shift_row.department[0:4].upper()
214 | # keep 3 days of weekday
215 | dayname = shift_row.day[0:3].lower()
216 | return '%s_%s_%02d' % (dept2, dayname, shift_row.start_time)
217 |
218 | # the assignment variables.
219 | assigned = mdl.binary_var_matrix(keys1=all_nurses, keys2=all_shifts, name=lambda ns: "NurseAssigned_%s_%s" % (ns[0], make_shift_name(ns[1])))
220 | #dd-markdown #### Express the business constraints
221 | #dd-markdown
222 | #dd-markdown ##### Overlapping shifts
223 | #dd-markdown
224 | #dd-markdown Some shifts overlap in time, and thus cannot be assigned to the same nurse.
225 | #dd-markdown To check whether two shifts overlap in time, we start by ordering all shifts with respect to their *wstart* and *duration* properties. Then, for each shift, we iterate over the subsequent shifts in this ordered list to easily compute the subset of overlapping shifts.
226 | #dd-markdown
227 | #dd-markdown We use *pandas* operations to implement this algorithm. But first, we organize all decision variables in a DataFrame.
228 | #dd-markdown
229 | #dd-markdown For convenience, we also organize the decision variables in a pivot table with *nurses* as row index and *shifts* as columns. The *pandas* *unstack* operation does this.
230 | #dd-cell
231 | # Organize decision variables in a DataFrame
232 | df_assigned = DataFrame({'assigned': assigned})
233 | df_assigned.index.names=['all_nurses', 'all_shifts']
234 |
235 | # Re-organize the Data Frame as a pivot table with nurses as row index and shifts as columns:
236 | #df_assigned_pivot = df_assigned.unstack(level='all_shifts')
237 |
238 | # Create a pivot using nurses and shifts index as dimensions
239 | df_assigned_pivot = df_assigned.reset_index().pivot(index='all_nurses', columns='all_shifts', values='assigned')
240 |
241 | # Display first rows of the pivot table
242 | df_assigned_pivot.head()
243 | #dd-markdown We create a DataFrame representing a list of shifts sorted by *"wstart"* and *"duration"*.
244 | #dd-markdown This sorted list will be used to easily detect overlapping shifts.
245 | #dd-markdown
246 | #dd-markdown Note that indices are reset after sorting so that the DataFrame can be indexed with respect to
247 | #dd-markdown the index in the sorted list and not the original unsorted list. This is the purpose of the *reset_index()*
248 | #dd-markdown operation which also adds a new column named *"shiftId"* with the original index.
249 | #dd-cell
250 | # Create a Data Frame representing a list of shifts sorted by wstart and duration.
251 | # One keeps only the three relevant columns: 'shiftId', 'wstart' and 'wend' in the resulting Data Frame
252 | df_sorted_shifts = df_shifts.sort_values(['wstart','duration']).reset_index()[['shiftId', 'wstart', 'wend']]
253 |
254 | # Display the first rows of the newly created Data Frame
255 | df_sorted_shifts.head()
256 | #dd-markdown Next, we state that for any pair of shifts that overlap in time, a nurse can be assigned to only one of the two.
257 | #dd-cell
258 | number_of_incompatible_shift_constraints = 0
259 | for shift in df_sorted_shifts.itertuples():
260 | # Iterate over following shifts
261 | # 'shift[0]' contains the index of the current shift in the df_sorted_shifts Data Frame
262 | for shift_2 in df_sorted_shifts.iloc[shift[0] + 1:].itertuples():
263 | if (shift_2.wstart < shift.wend):
264 | # Iterate over all nurses to force incompatible assignment for the current pair of overlapping shifts
265 | for nurse_assignments in df_assigned_pivot[[shift.shiftId, shift_2.shiftId]].itertuples():
266 | # this is actually a logical OR
267 | mdl.add_constraint(nurse_assignments[1] + nurse_assignments[2] <= 1)
268 | number_of_incompatible_shift_constraints += 1
269 | else:
270 | # No need to test overlap with following shifts
271 | break
272 | print("#incompatible shift constraints: {}".format(number_of_incompatible_shift_constraints))
273 | #dd-markdown ##### Vacations
274 | #dd-markdown
275 | #dd-markdown When the nurse is on vacation, he cannot be assigned to any shift starting that day.
276 | #dd-markdown
277 | #dd-markdown We use the *pandas* *merge* operation to create a join between the *"df_vacations"*, *"df_shifts"*, and *"df_assigned"* DataFrames. Each row of the resulting DataFrame contains the assignment decision variable corresponding to the matching (nurse, shift) pair.
278 | #dd-cell
279 | # Add 'day of week' column to vacations Data Frame
280 | df_vacations['dow'] = df_vacations.day.apply(day_to_day_of_week)
281 |
282 | # Join 'df_vacations', 'df_shifts' and 'df_assigned' Data Frames to create the list of 'forbidden' assigments.
283 | # The 'reset_index()' function is invoked to move 'shiftId' index as a column in 'df_shifts' Data Frame, and
284 | # to move the index pair ('all_nurses', 'all_shifts') as columns in 'df_assigned' Data Frame.
285 | # 'reset_index()' is invoked so that a join can be performed between Data Frame, based on column names.
286 | df_assigned_reindexed = df_assigned.reset_index()
287 | df_vacation_forbidden_assignments = df_vacations.merge(df_shifts.reset_index()[['dow', 'shiftId']]).merge(
288 | df_assigned_reindexed, left_on=['nurse', 'shiftId'], right_on=['all_nurses', 'all_shifts'])
289 |
290 | # Here are the first few rows of the resulting Data Frames joins
291 | df_vacation_forbidden_assignments.head()
292 | #dd-cell
293 | for forbidden_assignment in df_vacation_forbidden_assignments.itertuples():
294 | # to forbid an assignment just set the variable to zero.
295 | mdl.add_constraint(forbidden_assignment.assigned == 0)
296 | print("# vacation forbids: {} assignments".format(len(df_vacation_forbidden_assignments)))
297 | #dd-markdown ##### Associations
298 | #dd-markdown
299 | #dd-markdown Some pairs of nurses get along particularly well, so we wish to assign them together as a team. In other words, for every such couple and for each shift, both assignment variables should always be equal.
300 | #dd-markdown Either both nurses work the shift, or both do not.
301 | #dd-markdown
302 | #dd-markdown In the same way we modeled *vacations*, we use the *pandas* merge operation to create a DataFrame for which each row contains the pair of nurse-shift assignment decision variables matching each association.
303 | #dd-cell
304 | # Join 'df_assignment' Data Frame twice, based on associations to get corresponding decision variables pairs for all shifts
305 | # The 'suffixes' parameter in the second merge indicates our preference for updating the name of columns that occur both
306 | # in the first and second argument Data Frames (in our case, these columns are 'all_nurses' and 'assigned').
307 | df_preferred_assign = df_associations.merge(
308 | df_assigned_reindexed, left_on='nurse1', right_on='all_nurses').merge(
309 | df_assigned_reindexed, left_on=['nurse2', 'all_shifts'], right_on=['all_nurses', 'all_shifts'], suffixes=('_1','_2'))
310 |
311 | # Here are the first few rows of the resulting Data Frames joins
312 | df_preferred_assign.head()
313 | #dd-markdown The associations constraint can now easily be formulated by iterating on the rows of the *"df_preferred_assign"* DataFrame.
314 | #dd-cell
315 | for preferred_assign in df_preferred_assign.itertuples():
316 | mdl.add_constraint(preferred_assign.assigned_1 == preferred_assign.assigned_2)
317 | #dd-markdown ##### Incompatibilities
318 | #dd-markdown
319 | #dd-markdown Similarly, certain pairs of nurses do not get along well, and we want to avoid having them together on a shift.
320 | #dd-markdown In other terms, for each shift, both nurses of an incompatible pair cannot be assigned together to the sift. Again, we state a logical OR between the two assignments: at most one nurse from the pair can work the shift.
321 | #dd-markdown
322 | #dd-markdown We first create a DataFrame whose rows contain pairs of invalid assignment decision variables, using the same *pandas* `merge` operations as in the previous step.
323 | #dd-cell
324 | # Join assignment Data Frame twice, based on incompatibilities Data Frame to get corresponding decision variables pairs
325 | # for all shifts
326 | df_incompatible_assign = df_incompatibilities.merge(
327 | df_assigned_reindexed, left_on='nurse1', right_on='all_nurses').merge(
328 | df_assigned_reindexed, left_on=['nurse2', 'all_shifts'], right_on=['all_nurses', 'all_shifts'], suffixes=('_1','_2'))
329 |
330 | # Here are the first few rows of the resulting Data Frames joins
331 | df_incompatible_assign.head()
332 | #dd-markdown The incompatibilities constraint can now easily be formulated, by iterating on the rows of the *"df_incompatible_assign"* DataFrame.
333 | #dd-cell
334 | for incompatible_assign in df_incompatible_assign.itertuples():
335 | mdl.add_constraint(incompatible_assign.assigned_1 + incompatible_assign.assigned_2 <= 1)
336 | #dd-markdown ##### Constraints on work time
337 | #dd-markdown
338 | #dd-markdown Regulations force constraints on the total work time over a week;
339 | #dd-markdown and we compute this total work time in a new variable. We store the variable in an extra column in the nurse DataFrame.
340 | #dd-markdown
341 | #dd-markdown The variable is declared as _continuous_ though it contains only integer values. This is done to avoid adding unnecessary integer variables for the _branch and bound_ algorithm.
342 | #dd-markdown These variables are not true decision variables; they are used to express work constraints.
343 | #dd-markdown
344 | #dd-markdown From a *pandas* perspective, we apply a function over the rows of the nurse DataFrame to create this variable and store it into a new column of the DataFrame.
345 | #dd-cell
346 | # auxiliary function to create worktime variable from a row
347 | def make_var(row, varname_fmt):
348 | return mdl.continuous_var(name=varname_fmt % row.name, lb=0)
349 |
350 | # apply the function over nurse rows and store result in a new column
351 | df_nurses["worktime"] = df_nurses.apply(lambda r: make_var(r, "worktime_%s"), axis=1)
352 |
353 | # display nurse dataframe
354 | df_nurses
355 | #dd-markdown ###### Define total work time
356 | #dd-markdown
357 | #dd-markdown Work time variables must be constrained to be equal to the sum of hours actually worked.
358 | #dd-markdown
359 | #dd-markdown We use the *pandas* *groupby* operation to collect all assignment decision variables for each nurse in a separate series. Then, we iterate over nurses to post a constraint calculating the actual worktime for each nurse as the dot product of the series of nurse-shift assignments with the series of shift durations.
360 | #dd-cell
361 | # Use pandas' groupby operation to enforce constraint calculating worktime for each nurse as the sum of all assigned
362 | # shifts times the duration of each shift
363 | for nurse, nurse_assignments in df_assigned.groupby(level='all_nurses'):
364 | mdl.add_constraint(df_nurses.worktime[nurse] == mdl.dot(nurse_assignments.assigned, df_shifts.duration))
365 |
366 | # print model information and check we now have 32 extra continuous variables
367 | mdl.print_information()
368 | #dd-markdown ###### Maximum work time
369 | #dd-markdown
370 | #dd-markdown For each nurse, we add a constraint to enforce the maximum work time for a week.
371 | #dd-markdown Again we use the `apply` method, this time with an anonymous lambda function.
372 | #dd-cell
373 | # we use pandas' apply() method to set an upper bound on all worktime variables.
374 | def set_max_work_time(v):
375 | v.ub = max_work_time
376 | # Optionally: return a string for fancy display of the constraint in the Output cell
377 | return str(v) + ' <= ' + str(v.ub)
378 |
379 | df_nurses["worktime"].apply(convert_dtype=False, func=set_max_work_time)
380 | #dd-markdown ##### Minimum requirement for shifts
381 | #dd-markdown
382 | #dd-markdown Each shift requires a minimum number of nurses.
383 | #dd-markdown For each shift, the sum over all nurses of assignments to this shift
384 | #dd-markdown must be greater than the minimum requirement.
385 | #dd-markdown
386 | #dd-markdown The *pandas* *groupby* operation is invoked to collect all assignment decision variables for each shift in a separate series. Then, we iterate over shifts to post the constraint enforcing the minimum number of nurse assignments for each shift.
387 | #dd-cell
388 | # Use pandas' groupby operation to enforce minimum requirement constraint for each shift
389 | for shift, shift_nurses in df_assigned.groupby(level='all_shifts'):
390 | mdl.add_constraint(mdl.sum(shift_nurses.assigned) >= df_shifts.min_req[shift])
391 | #dd-markdown #### Express the objective
392 | #dd-markdown
393 | #dd-markdown The objective mixes different (and contradictory) KPIs.
394 | #dd-markdown
395 | #dd-markdown The first KPI is the total salary cost, computed as the sum of work times over all nurses, weighted by pay rate.
396 | #dd-markdown
397 | #dd-markdown We compute this KPI as an expression from the variables we previously defined by using the panda summation over the DOcplex objects.
398 | #dd-cell
399 | # again leverage pandas to create a series of expressions: costs of each nurse
400 | total_salary_series = df_nurses.worktime * df_nurses.pay_rate
401 |
402 | # compute global salary cost using pandas sum()
403 | # Note that the result is a DOcplex expression: DOcplex if fully compatible with pandas
404 | total_salary_cost = total_salary_series.sum()
405 | mdl.add_kpi(total_salary_cost, "Total salary cost")
406 | mdl.add_kpi(df_nurses.worktime.sum(), "Total worked hours")
407 | #dd-markdown ##### Minimizing salary cost
408 | #dd-markdown
409 | #dd-markdown In a preliminary version of the model, we minimize the total salary cost. This is accomplished
410 | #dd-markdown using the `Model.minimize()` method.
411 | #dd-cell
412 | mdl.minimize(total_salary_cost)
413 | mdl.print_information()
414 | #dd-markdown ### Step 6: Investigate the solution and then run an example analysis
415 | #dd-markdown
416 | #dd-markdown We take advantage of *pandas* to analyze the results. First we store the solution values of the assignment variables into a new *pandas* Series.
417 | #dd-markdown
418 | #dd-markdown Calling `solution_value` on a DOcplex variable returns its value in the solution (provided the model has been successfully solved).
419 | #dd-cell
420 | # When using the solve_hook, a solution is passed. From the solution, I can access the model.
421 | # From the model, I can access any variable by its name. As I want to access them from their keys, I attach the matrix to the model.
422 | mdl.assigned = assigned
423 | #dd-cell
424 | from docplex.util.environment import get_environment
425 | #dd-cell
426 | import threading
427 | #dd-cell
428 | def make_kpi_df(solution):
429 | kpi_as_tuples = [(kp.name, kp.compute(solution)) for kp in solution.model.iter_kpis()]
430 | kpi_as_tuples.append(('objective_', solution.objective_value))
431 | return DataFrame.from_records(kpi_as_tuples, columns=['kpi', 'value'])
432 | #dd-cell
433 | from copy import deepcopy
434 |
435 | def build_solution(solution):
436 | assigned_vars = solution.model.assigned
437 | report = []
438 | for k,v in assigned_vars.iteritems():
439 | if v._get_solution_value(solution) >= 0.5:
440 | k2 = deepcopy(k)
441 | k2 = k2 + (k[0],)
442 | k2 = k2 + (df_shifts.get_value(k[1], 'department'),)
443 | k2 = k2 + (df_shifts.get_value(k[1], 'day'),)
444 | k2 = k2 + ( ((df_shifts.get_value(k[1], 'end_time') - df_shifts.get_value(k[1], 'start_time')) % 24 ) ,)
445 | report.append(k2)
446 | report_df = pd.DataFrame(report, columns=['nurse_id', 'shift', 'nurse_name', 'shift_department', 'shift_day', 'shift_duration'])
447 | df_kpis = make_kpi_df(solution)
448 |
449 | report = {}
450 | report["kpis"] = df_kpis
451 | report["shift_assignments"] = report_df
452 | return report
453 | #dd-cell
454 | #mdl.solution_hook = build_solution
455 | #dd-cell
456 | # Set Cplex mipgap to 1e-5 to enforce precision to be of the order of a unit (objective value magnitude is ~1e+5).
457 | mdl.parameters.mip.tolerances.mipgap = 1e-5
458 |
459 | s = mdl.solve(url=url, key=key, log_output=True)
460 | assert s, "solve failed"
461 | mdl.report()
462 | #dd-cell
463 | outputs = build_solution(s)
464 | #dd-cell
465 | for k,v in outputs.iteritems():
466 | print(v)
467 | #dd-cell
468 | outputs
469 |
--------------------------------------------------------------------------------
/domodel/Nurses/.scenarios/Scenario 1/scenario.json:
--------------------------------------------------------------------------------
1 | {"qualifiers":[{"name":"modelType","value":"python"},{"name":"modelMetadata","value":"{\"fileName\":\"model.py\",\"fileContent\":\"#dd-markdown # The Nurse Assignment Problem\\r\\n#dd-markdown \\r\\n#dd-markdown This tutorial includes everything you need to set up IBM Decision Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical Programming model, and get its solution by solving the model on the cloud with IBM ILOG CPLEX Optimizer.\\r\\n#dd-markdown \\r\\n#dd-markdown When you finish this tutorial, you'll have a foundational knowledge of _Prescriptive Analytics_.\\r\\n#dd-markdown \\r\\n#dd-markdown >This notebook is part of [Prescriptive Analytics for Python](https://rawgit.com/IBMDecisionOptimization/docplex-doc/master/docs/index.html).\\r\\n#dd-markdown \\r\\n#dd-markdown >It requires a valid subscription to **Decision Optimization on Cloud** or a **local installation of CPLEX Optimizers**. \\r\\n#dd-markdown Discover us [here](https://developer.ibm.com/docloud).\\r\\n#dd-markdown \\r\\n#dd-markdown \\r\\n#dd-markdown Table of contents:\\r\\n#dd-markdown \\r\\n#dd-markdown - [Describe the business problem](#Describe-the-business-problem)\\r\\n#dd-markdown * [How decision optimization (prescriptive analytics) can help](#How--decision-optimization-can-help)\\r\\n#dd-markdown * [Use decision optimization](#Use-decision-optimization)\\r\\n#dd-markdown * [Step 1: Download the library](#Step-1:-Download-the-library)\\r\\n#dd-markdown * [Step 2: Set up the engines](#Step-2:-Set-up-the-prescriptive-engine)\\r\\n#dd-markdown - [Step 3: Model the data](#Step-3:-Model-the-data)\\r\\n#dd-markdown * [Step 4: Prepare the data](#Step-4:-Prepare-the-data)\\r\\n#dd-markdown - [Step 5: Set up the prescriptive model](#Step-5:-Set-up-the-prescriptive-model)\\r\\n#dd-markdown * [Define the decision variables](#Define-the-decision-variables)\\r\\n#dd-markdown * [Express the business constraints](#Express-the-business-constraints)\\r\\n#dd-markdown * [Express the objective](#Express-the-objective)\\r\\n#dd-markdown * [Solve with the Decision Optimization solve service](#Solve-with-the-Decision-Optimization-solve-service)\\r\\n#dd-markdown * [Step 6: Investigate the solution and run an example analysis](#Step-6:-Investigate-the-solution-and-then-run-an-example-analysis)\\r\\n#dd-markdown * [Summary](#Summary)\\r\\n#dd-markdown \\r\\n#dd-markdown ****\\r\\n#dd-markdown ## Describe the business problem\\r\\n#dd-markdown \\r\\n#dd-markdown This notebook describes how to use CPLEX Modeling for Python together with *pandas* to\\r\\n#dd-markdown manage the assignment of nurses to shifts in a hospital.\\r\\n#dd-markdown \\r\\n#dd-markdown Nurses must be assigned to hospital shifts in accordance with various skill and staffing constraints.\\r\\n#dd-markdown \\r\\n#dd-markdown The goal of the model is to find an efficient balance between the different objectives:\\r\\n#dd-markdown \\r\\n#dd-markdown * minimize the overall cost of the plan and\\r\\n#dd-markdown * assign shifts as fairly as possible.\\r\\n#dd-markdown ## How decision optimization can help\\r\\n#dd-markdown \\r\\n#dd-markdown * Prescriptive analytics (decision optimization) technology recommends actions that are based on desired outcomes. It takes into account specific scenarios, resources, and knowledge of past and current events. With this insight, your organization can make better decisions and have greater control of business outcomes. \\r\\n#dd-markdown \\r\\n#dd-markdown * Prescriptive analytics is the next step on the path to insight-based actions. It creates value through synergy with predictive analytics, which analyzes data to predict future outcomes. \\r\\n#dd-markdown \\r\\n#dd-markdown * Prescriptive analytics takes that insight to the next level by suggesting the optimal way to handle that future situation. Organizations that can act fast in dynamic conditions and make superior decisions in uncertain environments gain a strong competitive advantage. \\r\\n#dd-markdown
\\r\\n#dd-markdown \\r\\n#dd-markdown With prescriptive analytics, you can: \\r\\n#dd-markdown \\r\\n#dd-markdown * Automate the complex decisions and trade-offs to better manage your limited resources.\\r\\n#dd-markdown * Take advantage of a future opportunity or mitigate a future risk.\\r\\n#dd-markdown * Proactively update recommendations based on changing events.\\r\\n#dd-markdown * Meet operational goals, increase customer loyalty, prevent threats and fraud, and optimize business processes.\\r\\n#dd-markdown ## Checking minimum requirements\\r\\n#dd-markdown This notebook uses some features of pandas that are available in version 0.17.1 or above.\\r\\n#dd-cell\\r\\nimport pip\\r\\nREQUIRED_MINIMUM_PANDAS_VERSION = '0.17.1'\\r\\ntry:\\r\\n import pandas as pd\\r\\n assert pd.__version__ >= REQUIRED_MINIMUM_PANDAS_VERSION\\r\\nexcept:\\r\\n raise Exception(\\\"Version %s or above of Pandas is required to run this notebook\\\" % REQUIRED_MINIMUM_PANDAS_VERSION)\\r\\n#dd-markdown ### Step 2: Set up the prescriptive engine\\r\\n#dd-markdown \\r\\n#dd-markdown * Subscribe to our private cloud offer or Decision Optimization on Cloud solve service [here](https://developer.ibm.com/docloud) if you do not want to use a local solver.\\r\\n#dd-markdown * Get the service URL and your personal API key and enter your credentials here if accurate:\\r\\n#dd-cell\\r\\n# @hidden_cell\\r\\nurl = \\\"https://api-oaas.docloud.ibmcloud.com/job_manager/rest/v1/\\\"\\r\\nkey = \\\"api_f550300e-8e52-4f3e-abf1-0fe1ac428d93\\\"\\r\\n#dd-markdown ### Step 3: Model the data\\r\\n#dd-markdown \\r\\n#dd-markdown The input data consists of several tables:\\r\\n#dd-markdown \\r\\n#dd-markdown * The Departments table lists all departments in the scope of the assignment.\\r\\n#dd-markdown * The Skills table list all skills.\\r\\n#dd-markdown * The Shifts table lists all shifts to be staffed. A shift contains a department, a day in the week, plus the start and end times.\\r\\n#dd-markdown * The Nurses table lists all nurses, identified by their names.\\r\\n#dd-markdown * The NurseSkills table gives the skills of each nurse.\\r\\n#dd-markdown * The SkillRequirements table lists the minimum number of persons required for a given department and skill.\\r\\n#dd-markdown * The NurseVacations table lists days off for each nurse.\\r\\n#dd-markdown * The NurseAssociations table lists pairs of nurses who wish to work together.\\r\\n#dd-markdown * The NurseIncompatibilities table lists pairs of nurses who do not want to work together.\\r\\n#dd-markdown #### Loading data from Excel with pandas\\r\\n#dd-markdown \\r\\n#dd-markdown We load the data from an Excel file using *pandas*.\\r\\n#dd-markdown Each sheet is read into a separate *pandas* DataFrame.\\r\\n#dd-cell\\r\\n# This notebook requires pandas to work\\r\\nimport pandas as pd\\r\\nfrom pandas import DataFrame\\r\\n#dd-cell\\r\\ndf_skills = inputs['Skills']\\r\\ndf_depts = inputs['Departments']\\r\\ndf_shifts = inputs['Shifts']\\r\\n# Rename df_shifts index\\r\\n#df_shifts.index.name = 'shiftId'\\r\\n\\r\\n# Index is column 0: name\\r\\ndf_nurses = inputs['Nurses']\\r\\ndf_nurse_skilles = inputs['NurseSkills']\\r\\ndf_vacations = inputs['NurseVacations']\\r\\ndf_associations = inputs['NurseAssociations']\\r\\ndf_incompatibilities = inputs['NurseIncompatibilities']\\r\\n#dd-cell\\r\\ndf_shifts.index.name = 'shiftId'\\r\\n\\r\\n# Display the nurses dataframe\\r\\nprint(\\\"#nurses = {}\\\".format(len(df_nurses)))\\r\\nprint(\\\"#shifts = {}\\\".format(len(df_shifts)))\\r\\nprint(\\\"#vacations = {}\\\".format(len(df_vacations)))\\r\\n#dd-markdown In addition, we introduce some extra global data:\\r\\n#dd-markdown \\r\\n#dd-markdown * The maximum work time for each nurse.\\r\\n#dd-markdown * The maximum and minimum number of shifts worked by a nurse in a week.\\r\\n#dd-cell\\r\\n# maximum work time (in hours)\\r\\nmax_work_time = 40\\r\\n\\r\\n# maximum number of shifts worked in a week.\\r\\nmax_nb_shifts = 5\\r\\n#dd-markdown Shifts are stored in a separate DataFrame.\\r\\n#dd-cell\\r\\ndf_shifts\\r\\n#dd-markdown ### Step 4: Prepare the data\\r\\n#dd-markdown \\r\\n#dd-markdown We need to precompute additional data for shifts. \\r\\n#dd-markdown For each shift, we need the start time and end time expressed in hours, counting from the beginning of the week: Monday 8am is converted to 8, Tuesday 8am is converted to 24+8 = 32, and so on.\\r\\n#dd-markdown \\r\\n#dd-markdown #### Sub-step #1\\r\\n#dd-markdown We start by adding an extra column `dow` (day of week) which converts the string \\\"day\\\" into an integer in 0..6 (Monday is 0, Sunday is 6).\\r\\n#dd-cell\\r\\ndays = [\\\"monday\\\", \\\"tuesday\\\", \\\"wednesday\\\", \\\"thursday\\\", \\\"friday\\\", \\\"saturday\\\", \\\"sunday\\\"]\\r\\nday_of_weeks = dict(zip(days, range(7)))\\r\\n\\r\\n# utility to convert a day string e.g. \\\"Monday\\\" to an integer in 0..6\\r\\ndef day_to_day_of_week(day):\\r\\n return day_of_weeks[day.strip().lower()]\\r\\n\\r\\n# for each day name, we normalize it by stripping whitespace and converting it to lowercase\\r\\n# \\\" Monday\\\" -> \\\"monday\\\"\\r\\ndf_shifts[\\\"dow\\\"] = df_shifts.day.apply(day_to_day_of_week)\\r\\ndf_shifts\\r\\n#dd-markdown #### Sub-step #2 : Compute the absolute start time of each shift.\\r\\n#dd-markdown \\r\\n#dd-markdown Computing the start time in the week is easy: just add `24*dow` to column `start_time`. The result is stored in a new column `wstart`.\\r\\n#dd-cell\\r\\ndf_shifts[\\\"wstart\\\"] = df_shifts.start_time + 24 * df_shifts.dow\\r\\n#dd-markdown #### Sub-Step #3 : Compute the absolute end time of each shift.\\r\\n#dd-markdown \\r\\n#dd-markdown Computing the absolute end time is a little more complicated as certain shifts span across midnight. For example, Shift #3 starts on Monday at 18:00 and ends Tuesday at 2:00 AM. The absolute end time of Shift #3 is 26, not 2.\\r\\n#dd-markdown The general rule for computing absolute end time is:\\r\\n#dd-markdown \\r\\n#dd-markdown `abs_end_time = end_time + 24 * dow + (start_time>= end_time ? 24 : 0)`\\r\\n#dd-markdown \\r\\n#dd-markdown Again, we use *pandas* to add a new calculated column `wend`. This is done by using the *pandas* `apply` method with an anonymous `lambda` function over rows. The `raw=True` parameter prevents the creation of a *pandas* Series for each row, which improves the performance significantly on large data sets.\\r\\n#dd-cell\\r\\n# an auxiliary function to calculate absolute end time of a shift\\r\\ndef calculate_absolute_endtime(start, end, dow):\\r\\n return 24*dow + end + (24 if start>=end else 0)\\r\\n\\r\\n# store the results in a new column\\r\\ndf_shifts[\\\"wend\\\"] = df_shifts.apply(lambda row: calculate_absolute_endtime(\\r\\n row.start_time, row.end_time, row.dow), axis=1, raw=True)\\r\\n#dd-markdown #### Sub-step #4 : Compute the duration of each shift.\\r\\n#dd-markdown \\r\\n#dd-markdown Computing the duration of each shift is now a straightforward difference of columns. The result is stored in column `duration`.\\r\\n#dd-cell\\r\\ndf_shifts[\\\"duration\\\"] = df_shifts.wend - df_shifts.wstart\\r\\n#dd-markdown #### Sub-step #5 : Compute the minimum demand for each shift.\\r\\n#dd-markdown \\r\\n#dd-markdown Minimum demand is the product of duration (in hours) by the minimum required number of nurses. Thus, in number of \\r\\n#dd-markdown nurse-hours, this demand is stored in another new column `min_demand`.\\r\\n#dd-markdown \\r\\n#dd-markdown Finally, we display the updated shifts DataFrame with all calculated columns.\\r\\n#dd-cell\\r\\n# also compute minimum demand in nurse-hours\\r\\ndf_shifts[\\\"min_demand\\\"] = df_shifts.min_req * df_shifts.duration\\r\\n\\r\\n# finally check the modified shifts dataframe\\r\\ndf_shifts\\r\\n#dd-markdown ### Step 5: Set up the prescriptive model\\r\\n#dd-cell\\r\\nfrom docplex.mp.environment import Environment\\r\\nenv = Environment()\\r\\nenv.print_information()\\r\\n#dd-markdown #### Create the DOcplex model\\r\\n#dd-markdown The model contains all the business constraints and defines the objective.\\r\\n#dd-markdown \\r\\n#dd-markdown We now use CPLEX Modeling for Python to build a Mixed Integer Programming (MIP) model for this problem.\\r\\n#dd-cell\\r\\nfrom docplex.mp.model import Model\\r\\nmdl = Model(name=\\\"nurses\\\")\\r\\n#dd-markdown #### Define the decision variables\\r\\n#dd-markdown \\r\\n#dd-markdown For each (nurse, shift) pair, we create one binary variable that is equal to 1 when the nurse is assigned to the shift.\\r\\n#dd-markdown \\r\\n#dd-markdown We use the `binary_var_matrix` method of class `Model`, as each binary variable is indexed by _two_ objects: one nurse and one shift.\\r\\n#dd-cell\\r\\n# first global collections to iterate upon\\r\\nall_nurses = df_nurses.index.values\\r\\nall_shifts = df_shifts.index.values\\r\\n\\r\\n# the assignment variables.\\r\\ndef make_shift_name(shift_idx):\\r\\n shift_row = df_shifts.iloc[shift_idx]\\r\\n # keep first 3 characters in departement, uppercased\\r\\n dept2 = shift_row.department[0:4].upper()\\r\\n # keep 3 days of weekday\\r\\n dayname = shift_row.day[0:3].lower()\\r\\n return '%s_%s_%02d' % (dept2, dayname, shift_row.start_time)\\r\\n\\r\\n# the assignment variables.\\r\\nassigned = mdl.binary_var_matrix(keys1=all_nurses, keys2=all_shifts, name=lambda ns: \\\"NurseAssigned_%s_%s\\\" % (ns[0], make_shift_name(ns[1])))\\r\\n#dd-markdown #### Express the business constraints\\r\\n#dd-markdown \\r\\n#dd-markdown ##### Overlapping shifts\\r\\n#dd-markdown \\r\\n#dd-markdown Some shifts overlap in time, and thus cannot be assigned to the same nurse.\\r\\n#dd-markdown To check whether two shifts overlap in time, we start by ordering all shifts with respect to their *wstart* and *duration* properties. Then, for each shift, we iterate over the subsequent shifts in this ordered list to easily compute the subset of overlapping shifts.\\r\\n#dd-markdown \\r\\n#dd-markdown We use *pandas* operations to implement this algorithm. But first, we organize all decision variables in a DataFrame.\\r\\n#dd-markdown \\r\\n#dd-markdown For convenience, we also organize the decision variables in a pivot table with *nurses* as row index and *shifts* as columns. The *pandas* *unstack* operation does this.\\r\\n#dd-cell\\r\\n# Organize decision variables in a DataFrame\\r\\ndf_assigned = DataFrame({'assigned': assigned})\\r\\ndf_assigned.index.names=['all_nurses', 'all_shifts']\\r\\n\\r\\n# Re-organize the Data Frame as a pivot table with nurses as row index and shifts as columns:\\r\\n#df_assigned_pivot = df_assigned.unstack(level='all_shifts')\\r\\n\\r\\n# Create a pivot using nurses and shifts index as dimensions\\r\\ndf_assigned_pivot = df_assigned.reset_index().pivot(index='all_nurses', columns='all_shifts', values='assigned')\\r\\n\\r\\n# Display first rows of the pivot table\\r\\ndf_assigned_pivot.head()\\r\\n#dd-markdown We create a DataFrame representing a list of shifts sorted by *\\\"wstart\\\"* and *\\\"duration\\\"*.\\r\\n#dd-markdown This sorted list will be used to easily detect overlapping shifts.\\r\\n#dd-markdown \\r\\n#dd-markdown Note that indices are reset after sorting so that the DataFrame can be indexed with respect to\\r\\n#dd-markdown the index in the sorted list and not the original unsorted list. This is the purpose of the *reset_index()*\\r\\n#dd-markdown operation which also adds a new column named *\\\"shiftId\\\"* with the original index.\\r\\n#dd-cell\\r\\n# Create a Data Frame representing a list of shifts sorted by wstart and duration.\\r\\n# One keeps only the three relevant columns: 'shiftId', 'wstart' and 'wend' in the resulting Data Frame \\r\\ndf_sorted_shifts = df_shifts.sort_values(['wstart','duration']).reset_index()[['shiftId', 'wstart', 'wend']]\\r\\n\\r\\n# Display the first rows of the newly created Data Frame\\r\\ndf_sorted_shifts.head()\\r\\n#dd-markdown Next, we state that for any pair of shifts that overlap in time, a nurse can be assigned to only one of the two.\\r\\n#dd-cell\\r\\nnumber_of_incompatible_shift_constraints = 0\\r\\nfor shift in df_sorted_shifts.itertuples():\\r\\n # Iterate over following shifts\\r\\n # 'shift[0]' contains the index of the current shift in the df_sorted_shifts Data Frame\\r\\n for shift_2 in df_sorted_shifts.iloc[shift[0] + 1:].itertuples():\\r\\n if (shift_2.wstart < shift.wend):\\r\\n # Iterate over all nurses to force incompatible assignment for the current pair of overlapping shifts\\r\\n for nurse_assignments in df_assigned_pivot[[shift.shiftId, shift_2.shiftId]].itertuples():\\r\\n # this is actually a logical OR\\r\\n mdl.add_constraint(nurse_assignments[1] + nurse_assignments[2] <= 1)\\r\\n number_of_incompatible_shift_constraints += 1\\r\\n else:\\r\\n # No need to test overlap with following shifts\\r\\n break\\r\\nprint(\\\"#incompatible shift constraints: {}\\\".format(number_of_incompatible_shift_constraints))\\r\\n#dd-markdown ##### Vacations\\r\\n#dd-markdown \\r\\n#dd-markdown When the nurse is on vacation, he cannot be assigned to any shift starting that day.\\r\\n#dd-markdown \\r\\n#dd-markdown We use the *pandas* *merge* operation to create a join between the *\\\"df_vacations\\\"*, *\\\"df_shifts\\\"*, and *\\\"df_assigned\\\"* DataFrames. Each row of the resulting DataFrame contains the assignment decision variable corresponding to the matching (nurse, shift) pair.\\r\\n#dd-cell\\r\\n# Add 'day of week' column to vacations Data Frame\\r\\ndf_vacations['dow'] = df_vacations.day.apply(day_to_day_of_week)\\r\\n\\r\\n# Join 'df_vacations', 'df_shifts' and 'df_assigned' Data Frames to create the list of 'forbidden' assigments.\\r\\n# The 'reset_index()' function is invoked to move 'shiftId' index as a column in 'df_shifts' Data Frame, and\\r\\n# to move the index pair ('all_nurses', 'all_shifts') as columns in 'df_assigned' Data Frame.\\r\\n# 'reset_index()' is invoked so that a join can be performed between Data Frame, based on column names.\\r\\ndf_assigned_reindexed = df_assigned.reset_index()\\r\\ndf_vacation_forbidden_assignments = df_vacations.merge(df_shifts.reset_index()[['dow', 'shiftId']]).merge(\\r\\n df_assigned_reindexed, left_on=['nurse', 'shiftId'], right_on=['all_nurses', 'all_shifts'])\\r\\n\\r\\n# Here are the first few rows of the resulting Data Frames joins\\r\\ndf_vacation_forbidden_assignments.head()\\r\\n#dd-cell\\r\\nfor forbidden_assignment in df_vacation_forbidden_assignments.itertuples():\\r\\n # to forbid an assignment just set the variable to zero.\\r\\n mdl.add_constraint(forbidden_assignment.assigned == 0)\\r\\nprint(\\\"# vacation forbids: {} assignments\\\".format(len(df_vacation_forbidden_assignments)))\\r\\n#dd-markdown ##### Associations\\r\\n#dd-markdown \\r\\n#dd-markdown Some pairs of nurses get along particularly well, so we wish to assign them together as a team. In other words, for every such couple and for each shift, both assignment variables should always be equal.\\r\\n#dd-markdown Either both nurses work the shift, or both do not.\\r\\n#dd-markdown \\r\\n#dd-markdown In the same way we modeled *vacations*, we use the *pandas* merge operation to create a DataFrame for which each row contains the pair of nurse-shift assignment decision variables matching each association.\\r\\n#dd-cell\\r\\n# Join 'df_assignment' Data Frame twice, based on associations to get corresponding decision variables pairs for all shifts\\r\\n# The 'suffixes' parameter in the second merge indicates our preference for updating the name of columns that occur both\\r\\n# in the first and second argument Data Frames (in our case, these columns are 'all_nurses' and 'assigned').\\r\\ndf_preferred_assign = df_associations.merge(\\r\\n df_assigned_reindexed, left_on='nurse1', right_on='all_nurses').merge(\\r\\n df_assigned_reindexed, left_on=['nurse2', 'all_shifts'], right_on=['all_nurses', 'all_shifts'], suffixes=('_1','_2'))\\r\\n\\r\\n# Here are the first few rows of the resulting Data Frames joins\\r\\ndf_preferred_assign.head()\\r\\n#dd-markdown The associations constraint can now easily be formulated by iterating on the rows of the *\\\"df_preferred_assign\\\"* DataFrame.\\r\\n#dd-cell\\r\\nfor preferred_assign in df_preferred_assign.itertuples():\\r\\n mdl.add_constraint(preferred_assign.assigned_1 == preferred_assign.assigned_2)\\r\\n#dd-markdown ##### Incompatibilities\\r\\n#dd-markdown \\r\\n#dd-markdown Similarly, certain pairs of nurses do not get along well, and we want to avoid having them together on a shift.\\r\\n#dd-markdown In other terms, for each shift, both nurses of an incompatible pair cannot be assigned together to the sift. Again, we state a logical OR between the two assignments: at most one nurse from the pair can work the shift.\\r\\n#dd-markdown \\r\\n#dd-markdown We first create a DataFrame whose rows contain pairs of invalid assignment decision variables, using the same *pandas* `merge` operations as in the previous step.\\r\\n#dd-cell\\r\\n# Join assignment Data Frame twice, based on incompatibilities Data Frame to get corresponding decision variables pairs\\r\\n# for all shifts\\r\\ndf_incompatible_assign = df_incompatibilities.merge(\\r\\n df_assigned_reindexed, left_on='nurse1', right_on='all_nurses').merge(\\r\\n df_assigned_reindexed, left_on=['nurse2', 'all_shifts'], right_on=['all_nurses', 'all_shifts'], suffixes=('_1','_2'))\\r\\n\\r\\n# Here are the first few rows of the resulting Data Frames joins\\r\\ndf_incompatible_assign.head()\\r\\n#dd-markdown The incompatibilities constraint can now easily be formulated, by iterating on the rows of the *\\\"df_incompatible_assign\\\"* DataFrame.\\r\\n#dd-cell\\r\\nfor incompatible_assign in df_incompatible_assign.itertuples():\\r\\n mdl.add_constraint(incompatible_assign.assigned_1 + incompatible_assign.assigned_2 <= 1)\\r\\n#dd-markdown ##### Constraints on work time\\r\\n#dd-markdown \\r\\n#dd-markdown Regulations force constraints on the total work time over a week;\\r\\n#dd-markdown and we compute this total work time in a new variable. We store the variable in an extra column in the nurse DataFrame.\\r\\n#dd-markdown \\r\\n#dd-markdown The variable is declared as _continuous_ though it contains only integer values. This is done to avoid adding unnecessary integer variables for the _branch and bound_ algorithm. \\r\\n#dd-markdown These variables are not true decision variables; they are used to express work constraints.\\r\\n#dd-markdown \\r\\n#dd-markdown From a *pandas* perspective, we apply a function over the rows of the nurse DataFrame to create this variable and store it into a new column of the DataFrame.\\r\\n#dd-cell\\r\\n# auxiliary function to create worktime variable from a row\\r\\ndef make_var(row, varname_fmt):\\r\\n return mdl.continuous_var(name=varname_fmt % row.name, lb=0)\\r\\n\\r\\n# apply the function over nurse rows and store result in a new column\\r\\ndf_nurses[\\\"worktime\\\"] = df_nurses.apply(lambda r: make_var(r, \\\"worktime_%s\\\"), axis=1)\\r\\n\\r\\n# display nurse dataframe\\r\\ndf_nurses\\r\\n#dd-markdown ###### Define total work time\\r\\n#dd-markdown \\r\\n#dd-markdown Work time variables must be constrained to be equal to the sum of hours actually worked.\\r\\n#dd-markdown \\r\\n#dd-markdown We use the *pandas* *groupby* operation to collect all assignment decision variables for each nurse in a separate series. Then, we iterate over nurses to post a constraint calculating the actual worktime for each nurse as the dot product of the series of nurse-shift assignments with the series of shift durations.\\r\\n#dd-cell\\r\\n# Use pandas' groupby operation to enforce constraint calculating worktime for each nurse as the sum of all assigned\\r\\n# shifts times the duration of each shift\\r\\nfor nurse, nurse_assignments in df_assigned.groupby(level='all_nurses'):\\r\\n mdl.add_constraint(df_nurses.worktime[nurse] == mdl.dot(nurse_assignments.assigned, df_shifts.duration))\\r\\n \\r\\n# print model information and check we now have 32 extra continuous variables\\r\\nmdl.print_information()\\r\\n#dd-markdown ###### Maximum work time\\r\\n#dd-markdown \\r\\n#dd-markdown For each nurse, we add a constraint to enforce the maximum work time for a week.\\r\\n#dd-markdown Again we use the `apply` method, this time with an anonymous lambda function.\\r\\n#dd-cell\\r\\n# we use pandas' apply() method to set an upper bound on all worktime variables.\\r\\ndef set_max_work_time(v):\\r\\n v.ub = max_work_time\\r\\n # Optionally: return a string for fancy display of the constraint in the Output cell\\r\\n return str(v) + ' <= ' + str(v.ub)\\r\\n\\r\\ndf_nurses[\\\"worktime\\\"].apply(convert_dtype=False, func=set_max_work_time)\\r\\n#dd-markdown ##### Minimum requirement for shifts\\r\\n#dd-markdown \\r\\n#dd-markdown Each shift requires a minimum number of nurses. \\r\\n#dd-markdown For each shift, the sum over all nurses of assignments to this shift\\r\\n#dd-markdown must be greater than the minimum requirement.\\r\\n#dd-markdown \\r\\n#dd-markdown The *pandas* *groupby* operation is invoked to collect all assignment decision variables for each shift in a separate series. Then, we iterate over shifts to post the constraint enforcing the minimum number of nurse assignments for each shift.\\r\\n#dd-cell\\r\\n# Use pandas' groupby operation to enforce minimum requirement constraint for each shift\\r\\nfor shift, shift_nurses in df_assigned.groupby(level='all_shifts'):\\r\\n mdl.add_constraint(mdl.sum(shift_nurses.assigned) >= df_shifts.min_req[shift])\\r\\n#dd-markdown #### Express the objective\\r\\n#dd-markdown \\r\\n#dd-markdown The objective mixes different (and contradictory) KPIs. \\r\\n#dd-markdown \\r\\n#dd-markdown The first KPI is the total salary cost, computed as the sum of work times over all nurses, weighted by pay rate.\\r\\n#dd-markdown \\r\\n#dd-markdown We compute this KPI as an expression from the variables we previously defined by using the panda summation over the DOcplex objects.\\r\\n#dd-cell\\r\\n# again leverage pandas to create a series of expressions: costs of each nurse\\r\\ntotal_salary_series = df_nurses.worktime * df_nurses.pay_rate\\r\\n\\r\\n# compute global salary cost using pandas sum()\\r\\n# Note that the result is a DOcplex expression: DOcplex if fully compatible with pandas\\r\\ntotal_salary_cost = total_salary_series.sum()\\r\\nmdl.add_kpi(total_salary_cost, \\\"Total salary cost\\\")\\r\\nmdl.add_kpi(df_nurses.worktime.sum(), \\\"Total worked hours\\\")\\r\\n#dd-markdown ##### Minimizing salary cost\\r\\n#dd-markdown \\r\\n#dd-markdown In a preliminary version of the model, we minimize the total salary cost. This is accomplished\\r\\n#dd-markdown using the `Model.minimize()` method.\\r\\n#dd-cell\\r\\nmdl.minimize(total_salary_cost)\\r\\nmdl.print_information()\\r\\n#dd-markdown ### Step 6: Investigate the solution and then run an example analysis\\r\\n#dd-markdown \\r\\n#dd-markdown We take advantage of *pandas* to analyze the results. First we store the solution values of the assignment variables into a new *pandas* Series.\\r\\n#dd-markdown \\r\\n#dd-markdown Calling `solution_value` on a DOcplex variable returns its value in the solution (provided the model has been successfully solved).\\r\\n#dd-cell\\r\\n# When using the solve_hook, a solution is passed. From the solution, I can access the model.\\r\\n# From the model, I can access any variable by its name. As I want to access them from their keys, I attach the matrix to the model.\\r\\nmdl.assigned = assigned\\r\\n#dd-cell\\r\\nfrom docplex.util.environment import get_environment\\r\\n#dd-cell\\r\\nimport threading\\r\\n#dd-cell\\r\\ndef make_kpi_df(solution):\\r\\n kpi_as_tuples = [(kp.name, kp.compute(solution)) for kp in solution.model.iter_kpis()]\\r\\n kpi_as_tuples.append(('objective_', solution.objective_value))\\r\\n return DataFrame.from_records(kpi_as_tuples, columns=['kpi', 'value'])\\r\\n#dd-cell\\r\\nfrom copy import deepcopy\\r\\n\\r\\ndef build_solution(solution):\\r\\n assigned_vars = solution.model.assigned\\r\\n report = []\\r\\n for k,v in assigned_vars.iteritems():\\r\\n if v._get_solution_value(solution) >= 0.5:\\r\\n k2 = deepcopy(k)\\r\\n k2 = k2 + (df_nurses.get_value(k[0], 'name'),)\\r\\n k2 = k2 + (df_shifts.get_value(k[1], 'department'),)\\r\\n k2 = k2 + (df_shifts.get_value(k[1], 'day'),)\\r\\n k2 = k2 + ( ((df_shifts.get_value(k[1], 'end_time') - df_shifts.get_value(k[1], 'start_time')) % 24 ) ,)\\r\\n report.append(k2)\\r\\n report_df = pd.DataFrame(report, columns=['nurse_id', 'shift', 'nurse_name', 'shift_department', 'shift_day', 'shift_duration'])\\r\\n df_kpis = make_kpi_df(solution)\\r\\n \\r\\n report = {}\\r\\n report[\\\"kpis\\\"] = df_kpis\\r\\n report[\\\"shift_assignments\\\"] = report_df\\r\\n return report\\r\\n#dd-cell\\r\\nmdl.solution_hook = build_solution\\r\\n#dd-cell\\r\\n# Set Cplex mipgap to 1e-5 to enforce precision to be of the order of a unit (objective value magnitude is ~1e+5).\\r\\nmdl.parameters.mip.tolerances.mipgap = 1e-5\\r\\n\\r\\ns = mdl.solve(url=url, key=key, log_output=True)\\r\\nassert s, \\\"solve failed\\\"\\r\\nmdl.report()\\r\\n#dd-cell\\r\\noutputs = build_solution(s)\\r\\n#dd-cell\\r\\nfor k,v in outputs.iteritems():\\r\\n print(v)\\r\\n#dd-cell\\r\\noutputs\\r\\n\",\"lastUploadTime\":1523364441001}"}],"parentId":"Nurses","category":"scenario","creator":"alain","createdAt":1523362081060,"usage":{"lastModificationTime":1523364455822,"lastModifier":"alain"},"dataUsagePerCategory":{"input":{"lastModificationTime":1523362516813,"lastModifier":"alain"},"model":{"lastModificationTime":1523364439491,"lastModifier":"alain"},"output":{"lastModificationTime":1523364455822,"lastModifier":"alain"}},"state":"available","tables":[{"tableType":{"columns":[{"key":"department","dataType":"String"},{"key":"skill","dataType":"String"},{"key":"req","dataType":"Number"}]},"name":"SkillsRequirements","category":"input","lineage":"Copied from SkillsRequirements.csv","numberOfRows":1,"creator":"alain","createdAt":1523362101565,"lastUpdater":"alain","updatedAt":1523362101651,"path":"../../../../datasets/SkillsRequirements.csv"},{"tableType":{"columns":[{"key":"nurse","dataType":"String"},{"key":"day","dataType":"String"}]},"name":"NurseVacations","category":"input","lineage":"Copied from NurseVacations.csv","numberOfRows":59,"creator":"alain","createdAt":1523362101577,"lastUpdater":"alain","updatedAt":1523362101683,"path":"../../../../datasets/NurseVacations.csv"},{"tableType":{"columns":[{"key":"id","dataType":"String"}]},"name":"Skills","category":"input","lineage":"Copied from Skills.csv","numberOfRows":5,"creator":"alain","createdAt":1523362101587,"lastUpdater":"alain","updatedAt":1523362101671,"path":"../../../../datasets/Skills.csv"},{"tableType":{"columns":[{"key":"id","dataType":"String"}]},"name":"Departments","category":"input","lineage":"Copied from Departments.csv","numberOfRows":2,"creator":"alain","createdAt":1523362121903,"lastUpdater":"alain","updatedAt":1523362189746,"path":"../../../../datasets/Departments.csv"},{"tableType":{"columns":[{"key":"nurse1","dataType":"String"},{"key":"nurse2","dataType":"String"}]},"name":"NurseAssociations","category":"input","lineage":"Copied from NurseAssociations.csv","numberOfRows":2,"creator":"alain","createdAt":1523362195049,"lastUpdater":"alain","updatedAt":1523362195147,"path":"../../../../datasets/NurseAssociations.csv"},{"tableType":{"columns":[{"key":"nurse1","dataType":"String"},{"key":"nurse2","dataType":"String"}]},"name":"NurseIncompatibilities","category":"input","lineage":"Copied from NurseIncompatibilities.csv","numberOfRows":11,"creator":"alain","createdAt":1523362211954,"lastUpdater":"alain","updatedAt":1523362212000,"path":"../../../../datasets/NurseIncompatibilities.csv"},{"tableType":{"columns":[{"key":"name","dataType":"String"},{"key":"seniority","dataType":"Number"},{"key":"qualification","dataType":"Number"},{"key":"pay_rate","dataType":"Number"}]},"name":"Nurses","category":"input","lineage":"Copied from Nurses.csv","numberOfRows":32,"creator":"alain","createdAt":1523362482231,"lastUpdater":"alain","updatedAt":1523362482340,"path":"../../../../datasets/Nurses.csv"},{"tableType":{"columns":[{"key":"department","dataType":"String"},{"key":"day","dataType":"String"},{"key":"start_time","dataType":"Number"},{"key":"end_time","dataType":"Number"},{"key":"min_req","dataType":"Number"},{"key":"max_req","dataType":"Number"}]},"name":"Shifts","category":"input","lineage":"Copied from Shifts.csv","numberOfRows":36,"creator":"alain","createdAt":1523362482250,"lastUpdater":"alain","updatedAt":1523362482380,"path":"../../../../datasets/Shifts.csv"},{"tableType":{"columns":[{"key":"nurse","dataType":"String"},{"key":"skill","dataType":"String"}]},"name":"NurseSkills","category":"input","lineage":"Copied from NurseSkills.csv","numberOfRows":25,"creator":"alain","createdAt":1523362516717,"lastUpdater":"alain","updatedAt":1523362516813,"path":"../../../../datasets/NurseSkills.csv"},{"tableType":{"columns":[{"key":"kpi","dataType":"String"},{"key":"value","dataType":"Number"}]},"name":"kpis","category":"output","numberOfRows":3,"creator":"alain","createdAt":1523364455714,"lastUpdater":"alain","updatedAt":1523364455714,"path":"kpis.csv"},{"tableType":{"columns":[{"key":"nurse_id","dataType":"Number"},{"key":"shift","dataType":"Number"},{"key":"nurse_name","dataType":"String"},{"key":"shift_department","dataType":"String"},{"key":"shift_day","dataType":"String"},{"key":"shift_duration","dataType":"Number"}]},"name":"shift_assignments","category":"output","numberOfRows":198,"creator":"alain","createdAt":1523364455764,"lastUpdater":"alain","updatedAt":1523364455765,"path":"shift_assignments.csv"}],"assets":[{"name":"model.py","category":"model","creator":"alain","createdAt":1523364439491,"lastUpdater":"alain","updatedAt":1523364439491,"contentType":"application/json","path":"model.py"},{"name":"log.txt","category":"output","creator":"alain","createdAt":1523364455822,"lastUpdater":"alain","updatedAt":1523364455822,"path":"log.txt"}]}
--------------------------------------------------------------------------------