[{"data":1,"prerenderedAt":889},["ShallowReactive",2],{"dev-\u002Fdeveloper\u002Ftutorials\u002Fresource-allocation":3},{"id":4,"title":5,"body":6,"description":871,"extension":872,"faq":873,"meta":883,"navigation":185,"noindex":884,"path":885,"seo":886,"stem":887,"__hash__":888},"content\u002Fdeveloper\u002Ftutorials\u002Fresource-allocation.md","Allocate a budget to maximize return in Python",{"type":7,"value":8,"toc":865},"minimark",[9,13,29,36,41,44,364,392,395,399,410,769,774,778,810,813,816,820,846,856,861],[10,11,5],"h1",{"id":12},"allocate-a-budget-to-maximize-return-in-python",[14,15,16,20,21,24,25,28],"p",{},[17,18,19],"strong",{},"Resource allocation"," — splitting a fixed budget, headcount, or capacity across\ncompeting options to get the most out of it — is one of the most common\noptimization problems in day-to-day code: allocate marketing spend across\nchannels, capital across projects, compute across jobs. With ",[17,22,23],{},"quicopt"," it is a\nshort ",[17,26,27],{},"linear program (LP)"," in Python.",[14,30,31,32,35],{},"Real allocations are rarely limited by money alone. Here each channel also costs\n",[17,33,34],{},"management hours",", and the team only has so many — and the highest-return\nchannel is by far the most hours-hungry. Two scarce resources, and the choices\nstop being independent.",[37,38,40],"h2",{"id":39},"the-naive-approach","The naive approach",[14,42,43],{},"Without the vocabulary, the instinct is to pour money into the highest-return\nchannel first, then the next:",[45,46,52],"pre",{"className":47,"code":48,"filename":49,"language":50,"meta":51,"style":51},"language-python shiki shiki-themes github-dark","ret   = [0.12, 0.10, 0.15, 0.08]   # return per unit spent\nhours = [1.0,  1.0,  4.0,  1.0]    # management hours per unit spent\ncap   = [40.0, 50.0, 30.0, 60.0]   # max spend per channel\nbudget, team_hours = 100.0, 160.0\n\nspend = [0.0] * len(ret)\nfor i in sorted(range(len(ret)), key=lambda i: -ret[i]):\n    take = min(cap[i], budget, team_hours \u002F hours[i])\n    spend[i] = take\n    budget -= take\n    team_hours -= take * hours[i]\n\nprint(\"return:\", sum(ret[i] * spend[i] for i in range(len(ret))))  # -> 9.3\n","greedy.py","python","",[53,54,55,98,131,164,180,187,212,257,277,288,299,315,320],"code",{"__ignoreMap":51},[56,57,60,64,68,71,75,78,81,83,86,88,91,94],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"s95oV","ret   ",[56,65,67],{"class":66},"snl16","=",[56,69,70],{"class":62}," [",[56,72,74],{"class":73},"sDLfK","0.12",[56,76,77],{"class":62},", ",[56,79,80],{"class":73},"0.10",[56,82,77],{"class":62},[56,84,85],{"class":73},"0.15",[56,87,77],{"class":62},[56,89,90],{"class":73},"0.08",[56,92,93],{"class":62},"]   ",[56,95,97],{"class":96},"sAwPA","# return per unit spent\n",[56,99,101,104,106,108,111,114,116,118,121,123,125,128],{"class":58,"line":100},2,[56,102,103],{"class":62},"hours ",[56,105,67],{"class":66},[56,107,70],{"class":62},[56,109,110],{"class":73},"1.0",[56,112,113],{"class":62},",  ",[56,115,110],{"class":73},[56,117,113],{"class":62},[56,119,120],{"class":73},"4.0",[56,122,113],{"class":62},[56,124,110],{"class":73},[56,126,127],{"class":62},"]    ",[56,129,130],{"class":96},"# management hours per unit spent\n",[56,132,134,137,139,141,144,146,149,151,154,156,159,161],{"class":58,"line":133},3,[56,135,136],{"class":62},"cap   ",[56,138,67],{"class":66},[56,140,70],{"class":62},[56,142,143],{"class":73},"40.0",[56,145,77],{"class":62},[56,147,148],{"class":73},"50.0",[56,150,77],{"class":62},[56,152,153],{"class":73},"30.0",[56,155,77],{"class":62},[56,157,158],{"class":73},"60.0",[56,160,93],{"class":62},[56,162,163],{"class":96},"# max spend per channel\n",[56,165,167,170,172,175,177],{"class":58,"line":166},4,[56,168,169],{"class":62},"budget, team_hours ",[56,171,67],{"class":66},[56,173,174],{"class":73}," 100.0",[56,176,77],{"class":62},[56,178,179],{"class":73},"160.0\n",[56,181,183],{"class":58,"line":182},5,[56,184,186],{"emptyLinePlaceholder":185},true,"\n",[56,188,190,193,195,197,200,203,206,209],{"class":58,"line":189},6,[56,191,192],{"class":62},"spend ",[56,194,67],{"class":66},[56,196,70],{"class":62},[56,198,199],{"class":73},"0.0",[56,201,202],{"class":62},"] ",[56,204,205],{"class":66},"*",[56,207,208],{"class":73}," len",[56,210,211],{"class":62},"(ret)\n",[56,213,215,218,221,224,227,230,233,235,238,241,245,248,251,254],{"class":58,"line":214},7,[56,216,217],{"class":66},"for",[56,219,220],{"class":62}," i ",[56,222,223],{"class":66},"in",[56,225,226],{"class":73}," sorted",[56,228,229],{"class":62},"(",[56,231,232],{"class":73},"range",[56,234,229],{"class":62},[56,236,237],{"class":73},"len",[56,239,240],{"class":62},"(ret)), ",[56,242,244],{"class":243},"s9osk","key",[56,246,247],{"class":66},"=lambda",[56,249,250],{"class":62}," i: ",[56,252,253],{"class":66},"-",[56,255,256],{"class":62},"ret[i]):\n",[56,258,260,263,265,268,271,274],{"class":58,"line":259},8,[56,261,262],{"class":62},"    take ",[56,264,67],{"class":66},[56,266,267],{"class":73}," min",[56,269,270],{"class":62},"(cap[i], budget, team_hours ",[56,272,273],{"class":66},"\u002F",[56,275,276],{"class":62}," hours[i])\n",[56,278,280,283,285],{"class":58,"line":279},9,[56,281,282],{"class":62},"    spend[i] ",[56,284,67],{"class":66},[56,286,287],{"class":62}," take\n",[56,289,291,294,297],{"class":58,"line":290},10,[56,292,293],{"class":62},"    budget ",[56,295,296],{"class":66},"-=",[56,298,287],{"class":62},[56,300,302,305,307,310,312],{"class":58,"line":301},11,[56,303,304],{"class":62},"    team_hours ",[56,306,296],{"class":66},[56,308,309],{"class":62}," take ",[56,311,205],{"class":66},[56,313,314],{"class":62}," hours[i]\n",[56,316,318],{"class":58,"line":317},12,[56,319,186],{"emptyLinePlaceholder":185},[56,321,323,326,328,332,334,337,340,342,345,347,349,351,354,356,358,361],{"class":58,"line":322},13,[56,324,325],{"class":73},"print",[56,327,229],{"class":62},[56,329,331],{"class":330},"sU2Wk","\"return:\"",[56,333,77],{"class":62},[56,335,336],{"class":73},"sum",[56,338,339],{"class":62},"(ret[i] ",[56,341,205],{"class":66},[56,343,344],{"class":62}," spend[i] ",[56,346,217],{"class":66},[56,348,220],{"class":62},[56,350,223],{"class":66},[56,352,353],{"class":73}," range",[56,355,229],{"class":62},[56,357,237],{"class":73},[56,359,360],{"class":62},"(ret))))  ",[56,362,363],{"class":96},"# -> 9.3\n",[14,365,366,367,370,371,374,375,378,379,382,383,387,388,391],{},"Greedy spends ",[53,368,369],{},"40"," on channel 0 and ",[53,372,373],{},"30"," on channel 2 and returns ",[17,376,377],{},"9.3",". It\nwalked into the trap: channel 2 has the best return per euro, but at 4 hours per\nunit it burns 120 of the 160 team hours. The hours run out, and ",[17,380,381],{},"€30 of the\nbudget is left unspendable",". The best return ",[384,385,386],"em",{},"per euro"," is not the best return\n",[384,389,390],{},"per hour",", and with two limits you cannot rank channels on one number at all.",[14,393,394],{},"Sorting by return-per-hour instead just moves the trap; grid-searching the splits\nexplodes with the number of channels. The problem is continuous and linear — so\nmodel it and solve it exactly.",[37,396,398],{"id":397},"model-it-as-an-lp","Model it as an LP",[14,400,401,402,405,406,409],{},"Each channel gets a continuous variable ",[53,403,404],{},"spend[i]"," bounded by its cap. ",[17,407,408],{},"Two","\nconstraints — one per scarce resource — and the objective maximizes total return:",[45,411,414],{"className":47,"code":412,"filename":413,"language":50,"meta":51,"style":51},"from ortools.math_opt.python import mathopt\nfrom quicopt import Client\n\n# Allocate a fixed budget across channels to maximize return. Each channel has a\n# per-unit return, a spend cap, AND a management-hours cost per unit spent — and\n# the team has a limited number of hours. Two scarce resources (budget + hours),\n# so pouring money into the highest-return channel first is NOT optimal.\nret   = [0.12, 0.10, 0.15, 0.08]   # return per unit spent\nhours = [1.0,  1.0,  4.0,  1.0]    # management hours needed per unit spent\ncap   = [40.0, 50.0, 30.0, 60.0]   # max spend per channel\nbudget     = 100.0\nteam_hours = 160.0\nN = len(ret)\n\nmodel = mathopt.Model(name=\"resource_allocation\")\nx = [model.add_variable(lb=0.0, ub=cap[i], name=f\"spend_{i}\") for i in range(N)]\nmodel.add_linear_constraint(sum(x) \u003C= budget)\nmodel.add_linear_constraint(sum(hours[i] * x[i] for i in range(N)) \u003C= team_hours)\nmodel.maximize(sum(ret[i] * x[i] for i in range(N)))\n\nclient = Client(\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\")\nprint(client.solve(model).display)\n","resource_allocation.py",[53,415,416,430,442,446,451,456,461,466,492,519,545,555,565,576,581,603,667,684,715,740,745,761],{"__ignoreMap":51},[56,417,418,421,424,427],{"class":58,"line":59},[56,419,420],{"class":66},"from",[56,422,423],{"class":62}," ortools.math_opt.python ",[56,425,426],{"class":66},"import",[56,428,429],{"class":62}," mathopt\n",[56,431,432,434,437,439],{"class":58,"line":100},[56,433,420],{"class":66},[56,435,436],{"class":62}," quicopt ",[56,438,426],{"class":66},[56,440,441],{"class":62}," Client\n",[56,443,444],{"class":58,"line":133},[56,445,186],{"emptyLinePlaceholder":185},[56,447,448],{"class":58,"line":166},[56,449,450],{"class":96},"# Allocate a fixed budget across channels to maximize return. Each channel has a\n",[56,452,453],{"class":58,"line":182},[56,454,455],{"class":96},"# per-unit return, a spend cap, AND a management-hours cost per unit spent — and\n",[56,457,458],{"class":58,"line":189},[56,459,460],{"class":96},"# the team has a limited number of hours. Two scarce resources (budget + hours),\n",[56,462,463],{"class":58,"line":214},[56,464,465],{"class":96},"# so pouring money into the highest-return channel first is NOT optimal.\n",[56,467,468,470,472,474,476,478,480,482,484,486,488,490],{"class":58,"line":259},[56,469,63],{"class":62},[56,471,67],{"class":66},[56,473,70],{"class":62},[56,475,74],{"class":73},[56,477,77],{"class":62},[56,479,80],{"class":73},[56,481,77],{"class":62},[56,483,85],{"class":73},[56,485,77],{"class":62},[56,487,90],{"class":73},[56,489,93],{"class":62},[56,491,97],{"class":96},[56,493,494,496,498,500,502,504,506,508,510,512,514,516],{"class":58,"line":279},[56,495,103],{"class":62},[56,497,67],{"class":66},[56,499,70],{"class":62},[56,501,110],{"class":73},[56,503,113],{"class":62},[56,505,110],{"class":73},[56,507,113],{"class":62},[56,509,120],{"class":73},[56,511,113],{"class":62},[56,513,110],{"class":73},[56,515,127],{"class":62},[56,517,518],{"class":96},"# management hours needed per unit spent\n",[56,520,521,523,525,527,529,531,533,535,537,539,541,543],{"class":58,"line":290},[56,522,136],{"class":62},[56,524,67],{"class":66},[56,526,70],{"class":62},[56,528,143],{"class":73},[56,530,77],{"class":62},[56,532,148],{"class":73},[56,534,77],{"class":62},[56,536,153],{"class":73},[56,538,77],{"class":62},[56,540,158],{"class":73},[56,542,93],{"class":62},[56,544,163],{"class":96},[56,546,547,550,552],{"class":58,"line":301},[56,548,549],{"class":62},"budget     ",[56,551,67],{"class":66},[56,553,554],{"class":73}," 100.0\n",[56,556,557,560,562],{"class":58,"line":317},[56,558,559],{"class":62},"team_hours ",[56,561,67],{"class":66},[56,563,564],{"class":73}," 160.0\n",[56,566,567,570,572,574],{"class":58,"line":322},[56,568,569],{"class":62},"N ",[56,571,67],{"class":66},[56,573,208],{"class":73},[56,575,211],{"class":62},[56,577,579],{"class":58,"line":578},14,[56,580,186],{"emptyLinePlaceholder":185},[56,582,584,587,589,592,595,597,600],{"class":58,"line":583},15,[56,585,586],{"class":62},"model ",[56,588,67],{"class":66},[56,590,591],{"class":62}," mathopt.Model(",[56,593,594],{"class":243},"name",[56,596,67],{"class":66},[56,598,599],{"class":330},"\"resource_allocation\"",[56,601,602],{"class":62},")\n",[56,604,606,609,611,614,617,619,621,623,626,628,631,633,635,638,641,644,647,650,653,656,658,660,662,664],{"class":58,"line":605},16,[56,607,608],{"class":62},"x ",[56,610,67],{"class":66},[56,612,613],{"class":62}," [model.add_variable(",[56,615,616],{"class":243},"lb",[56,618,67],{"class":66},[56,620,199],{"class":73},[56,622,77],{"class":62},[56,624,625],{"class":243},"ub",[56,627,67],{"class":66},[56,629,630],{"class":62},"cap[i], ",[56,632,594],{"class":243},[56,634,67],{"class":66},[56,636,637],{"class":66},"f",[56,639,640],{"class":330},"\"spend_",[56,642,643],{"class":73},"{",[56,645,646],{"class":62},"i",[56,648,649],{"class":73},"}",[56,651,652],{"class":330},"\"",[56,654,655],{"class":62},") ",[56,657,217],{"class":66},[56,659,220],{"class":62},[56,661,223],{"class":66},[56,663,353],{"class":73},[56,665,666],{"class":62},"(N)]\n",[56,668,670,673,675,678,681],{"class":58,"line":669},17,[56,671,672],{"class":62},"model.add_linear_constraint(",[56,674,336],{"class":73},[56,676,677],{"class":62},"(x) ",[56,679,680],{"class":66},"\u003C=",[56,682,683],{"class":62}," budget)\n",[56,685,687,689,691,694,696,699,701,703,705,707,710,712],{"class":58,"line":686},18,[56,688,672],{"class":62},[56,690,336],{"class":73},[56,692,693],{"class":62},"(hours[i] ",[56,695,205],{"class":66},[56,697,698],{"class":62}," x[i] ",[56,700,217],{"class":66},[56,702,220],{"class":62},[56,704,223],{"class":66},[56,706,353],{"class":73},[56,708,709],{"class":62},"(N)) ",[56,711,680],{"class":66},[56,713,714],{"class":62}," team_hours)\n",[56,716,718,721,723,725,727,729,731,733,735,737],{"class":58,"line":717},19,[56,719,720],{"class":62},"model.maximize(",[56,722,336],{"class":73},[56,724,339],{"class":62},[56,726,205],{"class":66},[56,728,698],{"class":62},[56,730,217],{"class":66},[56,732,220],{"class":62},[56,734,223],{"class":66},[56,736,353],{"class":73},[56,738,739],{"class":62},"(N)))\n",[56,741,743],{"class":58,"line":742},20,[56,744,186],{"emptyLinePlaceholder":185},[56,746,748,751,753,756,759],{"class":58,"line":747},21,[56,749,750],{"class":62},"client ",[56,752,67],{"class":66},[56,754,755],{"class":62}," Client(",[56,757,758],{"class":330},"\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\"",[56,760,602],{"class":62},[56,762,764,766],{"class":58,"line":763},22,[56,765,325],{"class":73},[56,767,768],{"class":62},"(client.solve(model).display)\n",[770,771],"term-result",{":rows":772,"cmd":773},"[\"├── status:     optimal\",\"├── feasible:   true\",\"├── objective:  11.8\",\"├── x:          spend_0=40, spend_1=40, spend_2=20, spend_3=0  (4 variables)\",\"└── solve_time: 0.4199 s\"]","$ python resource_allocation.py",[37,775,777],{"id":776},"what-you-get","What you get",[14,779,780,783,784,787,788,791,792,794,795,798,799,802,803,806,807,809],{},[53,781,782],{},"status: optimal"," means the split is ",[17,785,786],{},"proven"," best — return ",[53,789,790],{},"11.8",", against\ngreedy's ",[53,793,377],{},". That is ",[17,796,797],{},"27% more return from the same budget and the same\nteam",", and it comes from a split no ranking rule would produce: the solver\n",[384,800,801],{},"holds back"," on the highest-return channel (",[53,804,805],{},"20"," instead of its ",[53,808,373],{}," cap) and\nputs the freed hours into channel 1, which greedy skipped entirely. Every euro\nand every hour gets used.",[14,811,812],{},"That is the difference between a heuristic and a solver. Greedy commits to one\nresource's ranking and cannot see the trade; the LP prices both resources at\nonce and proves there is nothing better.",[14,814,815],{},"Because it is an LP, the same model scales from four options to four thousand,\nand from two scarce resources to twenty — you change the data, not the method.",[37,817,819],{"id":818},"next","Next",[821,822,823,832,839],"ul",{},[824,825,826,827],"li",{},"The problem class behind this: ",[828,829,831],"a",{"href":830},"\u002Fproblems\u002Flp","Linear programming (LP)",[824,833,834,835],{},"A runnable model for every supported class: ",[828,836,838],{"href":837},"\u002Fdeveloper\u002Ftutorials\u002Fexamples","Examples",[824,840,841,842],{},"Set up the client and solve your first model: ",[828,843,845],{"href":844},"\u002Fdeveloper\u002Fgetting-started","Getting started",[14,847,848,851,852,855],{},[17,849,850],{},"Reference:"," G. B. Dantzig, ",[384,853,854],{},"Linear Programming and Extensions",", Princeton\nUniversity Press, 1963.",[857,858],"contact-cta",{"sub":859,"title":860},"Tell us what you're optimizing. We'll help you model it and point you at the right approach.","A bigger allocation problem — or a different one?",[862,863,864],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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":51,"searchDepth":100,"depth":100,"links":866},[867,868,869,870],{"id":39,"depth":100,"text":40},{"id":397,"depth":100,"text":398},{"id":776,"depth":100,"text":777},{"id":818,"depth":100,"text":819},"Split a fixed budget and limited team hours across options to maximize return in Python — the resource-allocation problem modeled as a linear program (LP) and solved with quicopt in a few lines, instead of a greedy ranking that leaves budget unused.","md",[874,877,880],{"q":875,"a":876},"Isn't this just spending everything on the highest-return option?","Only if money is the single limit. Here the best-return channel is also the most management-hours-expensive, so greedily filling it burns the team's hours and leaves budget unspendable — greedy returns 9.3 where the proven optimum is 11.8. Two scarce resources couple the choices, which is exactly what the LP solves.",{"q":878,"a":879},"Can I add per-group budgets, minimum spends, or other rules?","Yes. They are just more linear constraints on the same variables; the model and the solve call stay the same.",{"q":881,"a":882},"Is quicopt free to use?","Yes — pip install quicopt and your first call sets up a free key, no license.",{},false,"\u002Fdeveloper\u002Ftutorials\u002Fresource-allocation",{"title":5,"description":871},"developer\u002Ftutorials\u002Fresource-allocation","B7nbIRm5NX5NfozYCEzVNAew4U-jci_O4RQno9G3sS0",1785144831817]