[{"data":1,"prerenderedAt":493},["ShallowReactive",2],{"dev-\u002Fdeveloper\u002Fguides\u002Fshift-scheduling":3},{"id":4,"title":5,"body":6,"description":475,"extension":476,"faq":477,"meta":487,"navigation":488,"noindex":488,"path":489,"seo":490,"stem":491,"__hash__":492},"content\u002Fdeveloper\u002Fguides\u002Fshift-scheduling.md","Solve employee shift scheduling in Python",{"type":7,"value":8,"toc":469},"minimark",[9,13,40,45,48,52,55,396,401,405,419,422,426,450,460,465],[10,11,5],"h1",{"id":12},"solve-employee-shift-scheduling-in-python",[14,15,16,20,21,24,25,28,29,33,34,39],"p",{},[17,18,19],"strong",{},"Shift scheduling"," (workforce rostering) asks: given how many staff each day\nneeds and a set of allowed shift patterns, how few people cover every day? It is\nthe classic days-off scheduling problem. With ",[17,22,23],{},"quicopt"," it is a short ",[17,26,27],{},"MILP","\nin Python — and note this is ",[30,31,32],"em",{},"rostering",", distinct from balancing jobs across\nmachines (that's the ",[35,36,38],"a",{"href":37},"\u002Fdeveloper\u002Fguides\u002Fscheduling","makespan guide",").",[41,42,44],"h2",{"id":43},"the-naive-approach","The naive approach",[14,46,47],{},"Hand-rolled rostering loops or a greedy \"assign until covered\" pass produce a\nworkable schedule but rarely the leanest one, and they get brittle as patterns\nand rules multiply. The problem is a linear integer program — solve it exactly.",[41,49,51],{"id":50},"model-it-as-a-milp","Model it as a MILP",[14,53,54],{},"Each shift pattern covers a fixed set of days. An integer variable counts workers\non each pattern; every day's coverage must meet its demand; minimize total staff:",[56,57,63],"pre",{"className":58,"code":59,"filename":60,"language":61,"meta":62,"style":62},"language-python shiki shiki-themes github-dark","from ortools.math_opt.python import mathopt\nfrom quicopt import Client\ndemand = [17, 13, 15, 19, 14, 16, 11]  # staff needed per weekday (Mon..Sun)\nD = 7\n# pattern p works 5 consecutive days starting day p (off the other 2)\ncovers = [[(1 if (d - p) % 7 \u003C 5 else 0) for d in range(D)] for p in range(D)]\nmodel = mathopt.Model(name=\"shift_scheduling\")\nw = [model.add_integer_variable(lb=0.0, name=f\"pattern_{p}\") for p in range(D)]\nfor d in range(D):\n    model.add_linear_constraint(sum(covers[p][d] * w[p] for p in range(D)) >= demand[d])\nmodel.minimize(sum(w))\nprint(Client(\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\").solve(model).display)\n","shift_scheduling.py","python","",[64,65,66,85,98,152,163,169,243,267,321,335,370,381],"code",{"__ignoreMap":62},[67,68,71,75,79,82],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"snl16","from",[67,76,78],{"class":77},"s95oV"," ortools.math_opt.python ",[67,80,81],{"class":73},"import",[67,83,84],{"class":77}," mathopt\n",[67,86,88,90,93,95],{"class":69,"line":87},2,[67,89,74],{"class":73},[67,91,92],{"class":77}," quicopt ",[67,94,81],{"class":73},[67,96,97],{"class":77}," Client\n",[67,99,101,104,107,110,114,117,120,122,125,127,130,132,135,137,140,142,145,148],{"class":69,"line":100},3,[67,102,103],{"class":77},"demand ",[67,105,106],{"class":73},"=",[67,108,109],{"class":77}," [",[67,111,113],{"class":112},"sDLfK","17",[67,115,116],{"class":77},", ",[67,118,119],{"class":112},"13",[67,121,116],{"class":77},[67,123,124],{"class":112},"15",[67,126,116],{"class":77},[67,128,129],{"class":112},"19",[67,131,116],{"class":77},[67,133,134],{"class":112},"14",[67,136,116],{"class":77},[67,138,139],{"class":112},"16",[67,141,116],{"class":77},[67,143,144],{"class":112},"11",[67,146,147],{"class":77},"]  ",[67,149,151],{"class":150},"sAwPA","# staff needed per weekday (Mon..Sun)\n",[67,153,155,158,160],{"class":69,"line":154},4,[67,156,157],{"class":77},"D ",[67,159,106],{"class":73},[67,161,162],{"class":112}," 7\n",[67,164,166],{"class":69,"line":165},5,[67,167,168],{"class":150},"# pattern p works 5 consecutive days starting day p (off the other 2)\n",[67,170,172,175,177,180,183,186,189,192,195,198,201,204,207,210,213,216,219,222,225,228,231,233,236,238,240],{"class":69,"line":171},6,[67,173,174],{"class":77},"covers ",[67,176,106],{"class":73},[67,178,179],{"class":77}," [[(",[67,181,182],{"class":112},"1",[67,184,185],{"class":73}," if",[67,187,188],{"class":77}," (d ",[67,190,191],{"class":73},"-",[67,193,194],{"class":77}," p) ",[67,196,197],{"class":73},"%",[67,199,200],{"class":112}," 7",[67,202,203],{"class":73}," \u003C",[67,205,206],{"class":112}," 5",[67,208,209],{"class":73}," else",[67,211,212],{"class":112}," 0",[67,214,215],{"class":77},") ",[67,217,218],{"class":73},"for",[67,220,221],{"class":77}," d ",[67,223,224],{"class":73},"in",[67,226,227],{"class":112}," range",[67,229,230],{"class":77},"(D)] ",[67,232,218],{"class":73},[67,234,235],{"class":77}," p ",[67,237,224],{"class":73},[67,239,227],{"class":112},[67,241,242],{"class":77},"(D)]\n",[67,244,246,249,251,254,258,260,264],{"class":69,"line":245},7,[67,247,248],{"class":77},"model ",[67,250,106],{"class":73},[67,252,253],{"class":77}," mathopt.Model(",[67,255,257],{"class":256},"s9osk","name",[67,259,106],{"class":73},[67,261,263],{"class":262},"sU2Wk","\"shift_scheduling\"",[67,265,266],{"class":77},")\n",[67,268,270,273,275,278,281,283,286,288,290,292,295,298,301,303,306,309,311,313,315,317,319],{"class":69,"line":269},8,[67,271,272],{"class":77},"w ",[67,274,106],{"class":73},[67,276,277],{"class":77}," [model.add_integer_variable(",[67,279,280],{"class":256},"lb",[67,282,106],{"class":73},[67,284,285],{"class":112},"0.0",[67,287,116],{"class":77},[67,289,257],{"class":256},[67,291,106],{"class":73},[67,293,294],{"class":73},"f",[67,296,297],{"class":262},"\"pattern_",[67,299,300],{"class":112},"{",[67,302,14],{"class":77},[67,304,305],{"class":112},"}",[67,307,308],{"class":262},"\"",[67,310,215],{"class":77},[67,312,218],{"class":73},[67,314,235],{"class":77},[67,316,224],{"class":73},[67,318,227],{"class":112},[67,320,242],{"class":77},[67,322,324,326,328,330,332],{"class":69,"line":323},9,[67,325,218],{"class":73},[67,327,221],{"class":77},[67,329,224],{"class":73},[67,331,227],{"class":112},[67,333,334],{"class":77},"(D):\n",[67,336,338,341,344,347,350,353,355,357,359,361,364,367],{"class":69,"line":337},10,[67,339,340],{"class":77},"    model.add_linear_constraint(",[67,342,343],{"class":112},"sum",[67,345,346],{"class":77},"(covers[p][d] ",[67,348,349],{"class":73},"*",[67,351,352],{"class":77}," w[p] ",[67,354,218],{"class":73},[67,356,235],{"class":77},[67,358,224],{"class":73},[67,360,227],{"class":112},[67,362,363],{"class":77},"(D)) ",[67,365,366],{"class":73},">=",[67,368,369],{"class":77}," demand[d])\n",[67,371,373,376,378],{"class":69,"line":372},11,[67,374,375],{"class":77},"model.minimize(",[67,377,343],{"class":112},[67,379,380],{"class":77},"(w))\n",[67,382,384,387,390,393],{"class":69,"line":383},12,[67,385,386],{"class":112},"print",[67,388,389],{"class":77},"(Client(",[67,391,392],{"class":262},"\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\"",[67,394,395],{"class":77},").solve(model).display)\n",[397,398],"term-result",{":rows":399,"cmd":400},"[\"├── status:     optimal\",\"├── feasible:   true\",\"├── objective:  23.0\",\"├── x:          pattern_0=7, pattern_1=5, pattern_2=1, pattern_3=8, pattern_4=0, pattern_5=2, …  (7 variables)\",\"└── solve_time: 0.0121 s\"]","$ python shift_scheduling.py",[41,402,404],{"id":403},"what-you-get","What you get",[14,406,407,410,411,414,415,418],{},[64,408,409],{},"status: optimal"," means ",[64,412,413],{},"23"," workers is ",[17,416,417],{},"proven"," the fewest that cover every\nday's demand with these five-on\u002Ftwo-off patterns. The solver picks how many staff\nstart on each weekday; a greedy roster would likely overshoot.",[14,420,421],{},"Coverage minimums, per-pattern costs, and skill requirements are all extra linear\nconstraints on the same model — you change the data, not the method.",[41,423,425],{"id":424},"next","Next",[427,428,429,437,443],"ul",{},[430,431,432,433],"li",{},"The problem class behind this: ",[35,434,436],{"href":435},"\u002Fproblems\u002Fmilp","Mixed-integer linear (MILP)",[430,438,439,440],{},"Balancing jobs across machines instead: ",[35,441,442],{"href":37},"Job scheduling (makespan)",[430,444,445,446],{},"Set up the client and solve your first model: ",[35,447,449],{"href":448},"\u002Fdeveloper\u002Fgetting-started","Getting started",[14,451,452,455,456,459],{},[17,453,454],{},"Reference:"," K. R. Baker, ",[30,457,458],{},"Workforce Allocation in Cyclical Scheduling\nProblems: A Survey",", Operational Research Quarterly, 1976.",[461,462],"contact-cta",{"sub":463,"title":464},"Tell us what you're optimizing. We'll help you model it and point you at the right approach.","A bigger roster — or a different problem?",[466,467,468],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":62,"searchDepth":87,"depth":87,"links":470},[471,472,473,474],{"id":43,"depth":87,"text":44},{"id":50,"depth":87,"text":51},{"id":403,"depth":87,"text":404},{"id":424,"depth":87,"text":425},"Cover each day's staffing demand with the fewest workers in Python — the days-off \u002F shift scheduling problem modeled as a MILP and solved to proven optimality with quicopt.","md",[478,481,484],{"q":479,"a":480},"Is this the same as the makespan scheduling guide?","No. That one balances jobs across machines to finish early. This one is workforce rostering — choosing shift patterns to cover each day's demand at minimum staff. Different model, both MILP.",{"q":482,"a":483},"Isn't this a nonlinear \u002F binary-only model?","No — it uses general integer variables (how many workers on each pattern), which linear models (MILP) accept. The binary-only restriction only applies to nonlinear models on the free tier.",{"q":485,"a":486},"Is quicopt free to use?","Yes — pip install quicopt and your first call sets up a free key, no license.",{},true,"\u002Fdeveloper\u002Fguides\u002Fshift-scheduling",{"title":5,"description":475},"developer\u002Fguides\u002Fshift-scheduling","WHpEcNxKe-oRUGpQ1GO3O6tU6U6koobxqfefte9KNAs",1784110685996]