├── 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"}]} --------------------------------------------------------------------------------