[{"data":1,"prerenderedAt":1783},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python":1749,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python":1758,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python":1763,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python":1764},{"id":4,"title":5,"body":6,"dateModified":1715,"datePublished":1715,"description":1716,"extension":1717,"faq":1718,"keyword":1727,"kind":1728,"meta":1729,"navigation":244,"path":1738,"seo":1739,"slug":1742,"stem":1743,"topics":1744,"type":1747,"__hash__":1748},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python\u002Findex.md","Replace Excel Solver with Python",{"type":7,"value":8,"toc":1700},"minimark",[9,28,33,64,78,196,200,203,391,418,422,431,606,623,795,799,809,972,975,979,993,1125,1128,1195,1199,1202,1365,1368,1372,1375,1514,1529,1533,1551,1555,1568,1572,1622,1626,1635,1639,1645,1651,1657,1663,1667,1696],[10,11,12,13,17,18,21,22,27],"p",{},"Excel Solver is where a lot of practical optimisation lives: which products to make with limited machine time, how to allocate a budget across channels, which warehouse should serve which customer. The model is usually sound; the problems are around it. The setup lives in a dialog that nobody can review, runs cannot be scheduled, the 200-variable limit of the built-in engine arrives sooner than expected, and the sensitivity report is a separate sheet that is overwritten on the next run. SciPy ships the HiGHS solver, one of the fastest open-source linear and integer solvers, behind two functions — ",[14,15,16],"code",{},"linprog"," and ",[14,19,20],{},"milp"," — so the same model becomes a short script that reads its data from the workbook and writes the plan back. This page is part of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install scipy pandas openpyxl\n","bash","",[14,41,42],{"__ignoreMap":39},[43,44,47,51,55,58,61],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"sMTad","pip",[43,52,54],{"class":53},"srMev"," install",[43,56,57],{"class":53}," scipy",[43,59,60],{"class":53}," pandas",[43,62,63],{"class":53}," openpyxl\n",[10,65,66,69,70,73,74,77],{},[14,67,68],{},"production_plan.xlsx"," holds a classic product-mix problem, the kind usually set up in Solver. The ",[14,71,72],{},"Products"," sheet lists four products with their profit per unit, the assembly hours, finishing hours and metres of wood each one uses, and the most the market will take. The ",[14,75,76],{},"Resources"," sheet says how much of each resource is available this month. The question: how many of each product should the workshop make to earn the most?",[79,80,88,89,88,93,88,97,88,105,88,115,88,126,88,133,88,138,88,142,88,147,88,152,88,159,88,164,88,168,88,172,88,175,88,180,88,187,88,192],"svg",{"viewBox":81,"role":82,"ariaLabelledBy":83,"xmlns":86,"style":87},"54 0 652 383","img",[84,85],"sv-parts-t","sv-parts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:652px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[90,91,92],"title",{"id":84},"The three parts of every Solver model",[94,95,96],"desc",{"id":85},"A stack of three layers. Decision variables are the quantities to choose, the objective is the profit to maximise, and constraints are the resource limits and demand caps.",[98,99],"rect",{"x":100,"y":101,"width":102,"height":103,"fill":104},"54","0","652","383","#ffffff",[98,106],{"x":107,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":113,"style":114},"70.0","16.0","620.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[116,117,119],"g",{"transform":118},"translate(368.0,29.0) scale(1.000)",[120,121],"path",{"d":122,"fill":123,"stroke":124,"style":125},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[127,128,132],"text",{"x":129,"y":130,"style":131},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Decision variables",[127,134,137],{"x":129,"y":135,"style":136},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","how many chairs, tables, shelves, desks — Solver's 'By Changing Cells'",[45,139],{"x1":129,"y1":140,"x2":129,"y2":141,"stroke":113,"style":114},"114.0","136.0",[143,144],"polygon",{"points":145,"fill":146},"380.0,140.0 375.0,131.0 385.0,131.0","#5b5cf0",[98,148],{"x":107,"y":149,"width":109,"height":110,"rx":111,"fill":150,"stroke":151,"style":114},"145.0","#d9f4f1","var(--teal,#0f9488)",[116,153,155],{"transform":154},"translate(368.0,158.0) scale(1.000)",[120,156],{"d":157,"fill":123,"stroke":158,"style":125},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--teal-ink,#0b6157)",[127,160,163],{"x":129,"y":161,"style":162},"204.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Objective",[127,165,167],{"x":129,"y":166,"style":136},"224.0","total profit = Σ profit × quantity — 'Set Objective: Max'",[45,169],{"x1":129,"y1":170,"x2":129,"y2":171,"stroke":113,"style":114},"243.0","265.0",[143,173],{"points":174,"fill":146},"380.0,269.0 375.0,260.0 385.0,260.0",[98,176],{"x":107,"y":177,"width":109,"height":110,"rx":111,"fill":178,"stroke":179,"style":114},"274.0","#fdefd8","var(--gold,#b4740a)",[116,181,183],{"transform":182},"translate(368.0,287.0) scale(1.000)",[120,184],{"d":185,"fill":123,"stroke":186,"style":125},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[127,188,191],{"x":129,"y":189,"style":190},"333.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Constraints",[127,193,195],{"x":129,"y":194,"style":136},"353.0","hours and wood used ≤ available; 0 ≤ quantity ≤ demand",[29,197,199],{"id":198},"step-1-read-the-models-data-from-the-workbook","Step 1 — Read the model's data from the workbook",[10,201,202],{},"In Solver the data and the model are tangled together in cells. In Python they separate cleanly: the numbers stay in the workbook, where the planners maintain them, and the script turns them into arrays:",[34,204,208],{"className":205,"code":206,"language":207,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\nproducts = pd.read_excel(\"production_plan.xlsx\", sheet_name=\"Products\")\nresources = pd.read_excel(\"production_plan.xlsx\", sheet_name=\"Resources\")\n\nprofit = products[\"Profit\"].to_numpy()\nusage = products[resources[\"Resource\"]].T.to_numpy()    # one row per resource\navailable = resources[\"Available\"].to_numpy()\ncaps = products[\"Max demand\"].to_numpy()\nprint(usage)\nprint(available, caps)\n","python",[14,209,210,226,239,246,276,299,304,321,342,358,373,383],{"__ignoreMap":39},[43,211,212,216,220,223],{"class":45,"line":46},[43,213,215],{"class":214},"s-kum","import",[43,217,219],{"class":218},"skGVy"," numpy ",[43,221,222],{"class":214},"as",[43,224,225],{"class":218}," np\n",[43,227,229,231,234,236],{"class":45,"line":228},2,[43,230,215],{"class":214},[43,232,233],{"class":218}," pandas ",[43,235,222],{"class":214},[43,237,238],{"class":218}," pd\n",[43,240,242],{"class":45,"line":241},3,[43,243,245],{"emptyLinePlaceholder":244},true,"\n",[43,247,249,252,255,258,261,264,268,270,273],{"class":45,"line":248},4,[43,250,251],{"class":218},"products ",[43,253,254],{"class":214},"=",[43,256,257],{"class":218}," pd.read_excel(",[43,259,260],{"class":53},"\"production_plan.xlsx\"",[43,262,263],{"class":218},", ",[43,265,267],{"class":266},"sa561","sheet_name",[43,269,254],{"class":214},[43,271,272],{"class":53},"\"Products\"",[43,274,275],{"class":218},")\n",[43,277,279,282,284,286,288,290,292,294,297],{"class":45,"line":278},5,[43,280,281],{"class":218},"resources ",[43,283,254],{"class":214},[43,285,257],{"class":218},[43,287,260],{"class":53},[43,289,263],{"class":218},[43,291,267],{"class":266},[43,293,254],{"class":214},[43,295,296],{"class":53},"\"Resources\"",[43,298,275],{"class":218},[43,300,302],{"class":45,"line":301},6,[43,303,245],{"emptyLinePlaceholder":244},[43,305,307,310,312,315,318],{"class":45,"line":306},7,[43,308,309],{"class":218},"profit ",[43,311,254],{"class":214},[43,313,314],{"class":218}," products[",[43,316,317],{"class":53},"\"Profit\"",[43,319,320],{"class":218},"].to_numpy()\n",[43,322,324,327,329,332,335,338],{"class":45,"line":323},8,[43,325,326],{"class":218},"usage ",[43,328,254],{"class":214},[43,330,331],{"class":218}," products[resources[",[43,333,334],{"class":53},"\"Resource\"",[43,336,337],{"class":218},"]].T.to_numpy()    ",[43,339,341],{"class":340},"s-wDw","# one row per resource\n",[43,343,345,348,350,353,356],{"class":45,"line":344},9,[43,346,347],{"class":218},"available ",[43,349,254],{"class":214},[43,351,352],{"class":218}," resources[",[43,354,355],{"class":53},"\"Available\"",[43,357,320],{"class":218},[43,359,361,364,366,368,371],{"class":45,"line":360},10,[43,362,363],{"class":218},"caps ",[43,365,254],{"class":214},[43,367,314],{"class":218},[43,369,370],{"class":53},"\"Max demand\"",[43,372,320],{"class":218},[43,374,376,380],{"class":45,"line":375},11,[43,377,379],{"class":378},"sP0c6","print",[43,381,382],{"class":218},"(usage)\n",[43,384,386,388],{"class":45,"line":385},12,[43,387,379],{"class":378},[43,389,390],{"class":218},"(available, caps)\n",[10,392,393,396,397,401,402,405,406,408,409,411,412,414,415,417],{},[14,394,395],{},"usage"," is the constraint matrix: row ",[398,399,400],"em",{},"i",", column ",[398,403,404],{},"j"," is how much of resource ",[398,407,400],{}," one unit of product ",[398,410,404],{}," consumes. Building it by selecting the columns named on the ",[14,413,76],{}," sheet means adding a resource is a matter of adding a row there and a column on ",[14,416,72],{}," — the script does not change.",[29,419,421],{"id":420},"step-2-solve-the-linear-programme","Step 2 — Solve the linear programme",[10,423,424,426,427,430],{},[14,425,16],{}," minimises, so maximising profit means minimising negative profit. Resource limits go in as ",[14,428,429],{},"A_ub @ x \u003C= b_ub",", and the demand caps as bounds on each variable:",[34,432,434],{"className":205,"code":433,"language":207,"meta":39,"style":39},"from scipy.optimize import linprog\n\nres = linprog(-profit, A_ub=usage, b_ub=available,\n              bounds=list(zip(np.zeros(len(caps)), caps)), method=\"highs\")\nprint(res.status, res.message)\nplan = products[[\"Product\"]].assign(Quantity=res.x.round(2))\nprint(plan.to_string(index=False))\nprint(f\"profit {-res.fun:,.2f}\")\n",[14,435,436,449,453,485,520,527,557,574],{"__ignoreMap":39},[43,437,438,441,444,446],{"class":45,"line":46},[43,439,440],{"class":214},"from",[43,442,443],{"class":218}," scipy.optimize ",[43,445,215],{"class":214},[43,447,448],{"class":218}," linprog\n",[43,450,451],{"class":45,"line":228},[43,452,245],{"emptyLinePlaceholder":244},[43,454,455,458,460,463,466,469,472,474,477,480,482],{"class":45,"line":241},[43,456,457],{"class":218},"res ",[43,459,254],{"class":214},[43,461,462],{"class":218}," linprog(",[43,464,465],{"class":214},"-",[43,467,468],{"class":218},"profit, ",[43,470,471],{"class":266},"A_ub",[43,473,254],{"class":214},[43,475,476],{"class":218},"usage, ",[43,478,479],{"class":266},"b_ub",[43,481,254],{"class":214},[43,483,484],{"class":218},"available,\n",[43,486,487,490,492,495,498,501,504,507,510,513,515,518],{"class":45,"line":248},[43,488,489],{"class":266},"              bounds",[43,491,254],{"class":214},[43,493,494],{"class":378},"list",[43,496,497],{"class":218},"(",[43,499,500],{"class":378},"zip",[43,502,503],{"class":218},"(np.zeros(",[43,505,506],{"class":378},"len",[43,508,509],{"class":218},"(caps)), caps)), ",[43,511,512],{"class":266},"method",[43,514,254],{"class":214},[43,516,517],{"class":53},"\"highs\"",[43,519,275],{"class":218},[43,521,522,524],{"class":45,"line":278},[43,523,379],{"class":378},[43,525,526],{"class":218},"(res.status, res.message)\n",[43,528,529,532,534,537,540,543,546,548,551,554],{"class":45,"line":301},[43,530,531],{"class":218},"plan ",[43,533,254],{"class":214},[43,535,536],{"class":218}," products[[",[43,538,539],{"class":53},"\"Product\"",[43,541,542],{"class":218},"]].assign(",[43,544,545],{"class":266},"Quantity",[43,547,254],{"class":214},[43,549,550],{"class":218},"res.x.round(",[43,552,553],{"class":378},"2",[43,555,556],{"class":218},"))\n",[43,558,559,561,564,567,569,572],{"class":45,"line":306},[43,560,379],{"class":378},[43,562,563],{"class":218},"(plan.to_string(",[43,565,566],{"class":266},"index",[43,568,254],{"class":214},[43,570,571],{"class":378},"False",[43,573,556],{"class":218},[43,575,576,578,580,583,586,590,592,595,598,601,604],{"class":45,"line":323},[43,577,379],{"class":378},[43,579,497],{"class":218},[43,581,582],{"class":214},"f",[43,584,585],{"class":53},"\"profit ",[43,587,589],{"class":588},"sSjpA","{",[43,591,465],{"class":214},[43,593,594],{"class":218},"res.fun",[43,596,597],{"class":214},":,.2f",[43,599,600],{"class":588},"}",[43,602,603],{"class":53},"\"",[43,605,275],{"class":218},[10,607,608,609,612,613,615,616,618,619,622],{},"The best plan makes no chairs, the full 120 tables, about 274 shelves and 131 desks, for a profit of about 35,114. Always check ",[14,610,611],{},"res.status"," before using the answer: ",[14,614,101],{}," is optimal, ",[14,617,553],{}," means the constraints contradict each other (infeasible), and ",[14,620,621],{},"3"," means the objective can grow without limit — usually a missing constraint. Solver reports the same outcomes in its result dialog; in a scheduled job they need to stop the run.",[79,624,88,630,88,633,88,636,88,641,88,647,88,653,88,658,88,662,88,668,88,673,88,677,88,681,88,684,88,688,88,691,88,695,88,698,88,702,88,705,88,710,88,714,88,718,88,722,88,727,88,732,88,737,88,742,88,747,88,752,88,757,88,762,88,766,88,771,88,774,88,777,88,781,88,785,88,790],{"viewBox":625,"role":82,"ariaLabelledBy":626,"xmlns":86,"style":629},"4 0 748 260",[627,628],"sv-plan-t","sv-plan-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[90,631,632],{"id":627},"The optimal plan against demand",[94,634,635],{"id":628},"Column chart comparing the optimal quantity of each product with its maximum demand. Chairs 0 of 400, tables 120 of 120, shelves 274 of 300, desks 131 of 150.",[98,637],{"x":638,"y":101,"width":639,"height":640,"fill":104},"4","748","260",[98,642],{"x":643,"y":644,"width":645,"height":645,"rx":621,"fill":112,"stroke":113,"style":646},"64.0","18.0","14.0","stroke-width:1.5px",[127,648,652],{"x":649,"y":650,"style":651},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","Plan",[98,654],{"x":655,"y":644,"width":645,"height":645,"rx":621,"fill":656,"stroke":657,"style":646},"138.7","#e7ebef","var(--line,#cdd5e6)",[127,659,661],{"x":660,"y":650,"style":651},"158.7","Max demand",[45,663],{"x1":664,"y1":665,"x2":666,"y2":665,"stroke":657,"style":667},"54.0","222.0","736.0","stroke-width:1px",[127,669,101],{"x":670,"y":671,"style":672},"31.0","226.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[45,674],{"x1":664,"y1":675,"x2":666,"y2":675,"stroke":657,"style":676},"179.5","stroke-width:1px;stroke-dasharray:5 4",[127,678,680],{"x":670,"y":679,"style":672},"183.5","100",[45,682],{"x1":664,"y1":683,"x2":666,"y2":683,"stroke":657,"style":676},"137.0",[127,685,687],{"x":670,"y":686,"style":672},"141.0","200",[45,689],{"x1":664,"y1":690,"x2":666,"y2":690,"stroke":657,"style":676},"94.5",[127,692,694],{"x":670,"y":693,"style":672},"98.5","300",[45,696],{"x1":664,"y1":697,"x2":666,"y2":697,"stroke":657,"style":676},"52.0",[127,699,701],{"x":670,"y":700,"style":672},"56.0","400",[45,703],{"x1":664,"y1":665,"x2":666,"y2":665,"stroke":704,"style":646},"var(--muted,#5b6780)",[127,706,709],{"x":707,"y":708,"style":136},"139.3","240.0","Chair",[127,711,713],{"x":712,"y":708,"style":136},"309.8","Table",[127,715,717],{"x":716,"y":708,"style":136},"480.3","Shelf",[127,719,721],{"x":720,"y":708,"style":136},"650.8","Desk",[98,723],{"x":724,"y":665,"width":725,"height":726,"rx":621,"fill":112,"stroke":113,"style":646},"102.3","36.0","0.0",[127,728,101],{"x":729,"y":730,"style":731},"120.3","216.0","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[98,733],{"x":734,"y":735,"width":725,"height":736,"rx":621,"fill":112,"stroke":113,"style":646},"272.8","171.0","51.0",[127,738,741],{"x":739,"y":740,"style":731},"290.8","165.0","120",[98,743],{"x":744,"y":745,"width":725,"height":746,"rx":621,"fill":112,"stroke":113,"style":646},"443.3","105.5","116.5",[127,748,751],{"x":749,"y":750,"style":731},"461.3","99.5","274",[98,753],{"x":754,"y":755,"width":725,"height":756,"rx":621,"fill":112,"stroke":113,"style":646},"613.8","166.3","55.7",[127,758,761],{"x":759,"y":760,"style":731},"631.8","160.3","131",[98,763],{"x":764,"y":697,"width":725,"height":765,"rx":621,"fill":656,"stroke":657,"style":646},"140.3","170.0",[127,767,701],{"x":768,"y":769,"style":770},"158.3","46.0","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[98,772],{"x":773,"y":735,"width":725,"height":736,"rx":621,"fill":656,"stroke":657,"style":646},"310.8",[127,775,741],{"x":776,"y":740,"style":770},"328.8",[98,778],{"x":779,"y":690,"width":725,"height":780,"rx":621,"fill":656,"stroke":657,"style":646},"481.3","127.5",[127,782,694],{"x":783,"y":784,"style":770},"499.3","88.5",[98,786],{"x":787,"y":788,"width":725,"height":789,"rx":621,"fill":656,"stroke":657,"style":646},"651.8","158.2","63.8",[127,791,794],{"x":792,"y":793,"style":770},"669.8","152.2","150",[29,796,798],{"id":797},"step-3-whole-units-with-milp","Step 3 — Whole units with milp",[10,800,801,802,804,805,808],{},"Nobody makes 274.29 shelves. Solver has an \"int\" constraint for this; SciPy has ",[14,803,20],{},", which takes the same data in matrix form plus an ",[14,806,807],{},"integrality"," flag per variable:",[34,810,812],{"className":205,"code":811,"language":207,"meta":39,"style":39},"from scipy.optimize import milp, LinearConstraint, Bounds\n\nres_int = milp(-profit,\n               constraints=LinearConstraint(usage, -np.inf, available),\n               integrality=np.ones(len(profit)),\n               bounds=Bounds(0, caps))\nprint(res_int.status, res_int.message)\nplan[\"Whole units\"] = res_int.x.round().astype(int)\nprint(plan.to_string(index=False))\nprint(f\"profit {-res_int.fun:,.0f}; resources used {usage @ res_int.x}\")\n",[14,813,814,825,829,844,859,874,889,896,917,931],{"__ignoreMap":39},[43,815,816,818,820,822],{"class":45,"line":46},[43,817,440],{"class":214},[43,819,443],{"class":218},[43,821,215],{"class":214},[43,823,824],{"class":218}," milp, LinearConstraint, Bounds\n",[43,826,827],{"class":45,"line":228},[43,828,245],{"emptyLinePlaceholder":244},[43,830,831,834,836,839,841],{"class":45,"line":241},[43,832,833],{"class":218},"res_int ",[43,835,254],{"class":214},[43,837,838],{"class":218}," milp(",[43,840,465],{"class":214},[43,842,843],{"class":218},"profit,\n",[43,845,846,849,851,854,856],{"class":45,"line":248},[43,847,848],{"class":266},"               constraints",[43,850,254],{"class":214},[43,852,853],{"class":218},"LinearConstraint(usage, ",[43,855,465],{"class":214},[43,857,858],{"class":218},"np.inf, available),\n",[43,860,861,864,866,869,871],{"class":45,"line":278},[43,862,863],{"class":266},"               integrality",[43,865,254],{"class":214},[43,867,868],{"class":218},"np.ones(",[43,870,506],{"class":378},[43,872,873],{"class":218},"(profit)),\n",[43,875,876,879,881,884,886],{"class":45,"line":301},[43,877,878],{"class":266},"               bounds",[43,880,254],{"class":214},[43,882,883],{"class":218},"Bounds(",[43,885,101],{"class":378},[43,887,888],{"class":218},", caps))\n",[43,890,891,893],{"class":45,"line":306},[43,892,379],{"class":378},[43,894,895],{"class":218},"(res_int.status, res_int.message)\n",[43,897,898,901,904,907,909,912,915],{"class":45,"line":323},[43,899,900],{"class":218},"plan[",[43,902,903],{"class":53},"\"Whole units\"",[43,905,906],{"class":218},"] ",[43,908,254],{"class":214},[43,910,911],{"class":218}," res_int.x.round().astype(",[43,913,914],{"class":378},"int",[43,916,275],{"class":218},[43,918,919,921,923,925,927,929],{"class":45,"line":344},[43,920,379],{"class":378},[43,922,563],{"class":218},[43,924,566],{"class":266},[43,926,254],{"class":214},[43,928,571],{"class":378},[43,930,556],{"class":218},[43,932,933,935,937,939,941,943,945,948,951,953,956,958,960,963,966,968,970],{"class":45,"line":360},[43,934,379],{"class":378},[43,936,497],{"class":218},[43,938,582],{"class":214},[43,940,585],{"class":53},[43,942,589],{"class":588},[43,944,465],{"class":214},[43,946,947],{"class":218},"res_int.fun",[43,949,950],{"class":214},":,.0f",[43,952,600],{"class":588},[43,954,955],{"class":53},"; resources used ",[43,957,589],{"class":588},[43,959,326],{"class":218},[43,961,962],{"class":214},"@",[43,964,965],{"class":218}," res_int.x",[43,967,600],{"class":588},[43,969,603],{"class":53},[43,971,275],{"class":218},[10,973,974],{},"Rounding the continuous answer down would have been feasible but not necessarily best. The integer solution happens to be close to it — one chair appears, filling a gap the rounding leaves — at a profit of 35,110. On larger problems the integer optimum can look quite different from the rounded continuous one, which is why the integer solver exists.",[29,976,978],{"id":977},"step-4-read-the-sensitivity-report","Step 4 — Read the sensitivity report",[10,980,981,982,984,985,988,989,992],{},"Solver's sensitivity report answers \"what is an extra hour of finishing worth?\" ",[14,983,16],{}," returns the same information for the continuous model: the ",[398,986,987],{},"slack"," of each constraint and its ",[398,990,991],{},"marginal"," value, the shadow price:",[34,994,996],{"className":205,"code":995,"language":207,"meta":39,"style":39},"report = resources.assign(\n    Used=(usage @ res.x).round(1),\n    Slack=res.ineqlin.residual.round(1),\n    ShadowPrice=(-res.ineqlin.marginals).round(2),\n)\nprint(report.to_string(index=False))\nprint(\"value of one more unit of demand:\", dict(zip(products[\"Product\"], ((-res.upper.marginals).round(2) + 0.0).tolist())))\n",[14,997,998,1008,1029,1043,1061,1065,1080],{"__ignoreMap":39},[43,999,1000,1003,1005],{"class":45,"line":46},[43,1001,1002],{"class":218},"report ",[43,1004,254],{"class":214},[43,1006,1007],{"class":218}," resources.assign(\n",[43,1009,1010,1013,1015,1018,1020,1023,1026],{"class":45,"line":228},[43,1011,1012],{"class":266},"    Used",[43,1014,254],{"class":214},[43,1016,1017],{"class":218},"(usage ",[43,1019,962],{"class":214},[43,1021,1022],{"class":218}," res.x).round(",[43,1024,1025],{"class":378},"1",[43,1027,1028],{"class":218},"),\n",[43,1030,1031,1034,1036,1039,1041],{"class":45,"line":241},[43,1032,1033],{"class":266},"    Slack",[43,1035,254],{"class":214},[43,1037,1038],{"class":218},"res.ineqlin.residual.round(",[43,1040,1025],{"class":378},[43,1042,1028],{"class":218},[43,1044,1045,1048,1050,1052,1054,1057,1059],{"class":45,"line":248},[43,1046,1047],{"class":266},"    ShadowPrice",[43,1049,254],{"class":214},[43,1051,497],{"class":218},[43,1053,465],{"class":214},[43,1055,1056],{"class":218},"res.ineqlin.marginals).round(",[43,1058,553],{"class":378},[43,1060,1028],{"class":218},[43,1062,1063],{"class":45,"line":278},[43,1064,275],{"class":218},[43,1066,1067,1069,1072,1074,1076,1078],{"class":45,"line":301},[43,1068,379],{"class":378},[43,1070,1071],{"class":218},"(report.to_string(",[43,1073,566],{"class":266},[43,1075,254],{"class":214},[43,1077,571],{"class":378},[43,1079,556],{"class":218},[43,1081,1082,1084,1086,1089,1091,1094,1096,1098,1101,1103,1106,1108,1111,1113,1116,1119,1122],{"class":45,"line":306},[43,1083,379],{"class":378},[43,1085,497],{"class":218},[43,1087,1088],{"class":53},"\"value of one more unit of demand:\"",[43,1090,263],{"class":218},[43,1092,1093],{"class":378},"dict",[43,1095,497],{"class":218},[43,1097,500],{"class":378},[43,1099,1100],{"class":218},"(products[",[43,1102,539],{"class":53},[43,1104,1105],{"class":218},"], ((",[43,1107,465],{"class":214},[43,1109,1110],{"class":218},"res.upper.marginals).round(",[43,1112,553],{"class":378},[43,1114,1115],{"class":218},") ",[43,1117,1118],{"class":214},"+",[43,1120,1121],{"class":378}," 0.0",[43,1123,1124],{"class":218},").tolist())))\n",[10,1126,1127],{},"Finishing time is the real bottleneck: every hour is used, and one more would add about 38.57 of profit. Wood is also fully used, but a metre is worth only about 3.57. Assembly has over 50 hours to spare, so its shadow price is zero — buying more assembly capacity would earn nothing. Among the demand caps, only the table limit binds: one more table of demand would be worth about 2.14. That is the list a production manager needs: pay up to 38 an hour for overtime in finishing, do not bother with assembly.",[79,1129,88,1135,88,1138,88,1141,88,1145,88,1151,88,1157,88,1161,88,1166,88,1168,88,1172,88,1175,88,1180,88,1183,88,1187,88,1190],{"viewBox":1130,"role":82,"ariaLabelledBy":1131,"xmlns":86,"style":1134},"4 0 697 140",[1132,1133],"sv-shadow-t","sv-shadow-d","width:100%;max-width:697px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[90,1136,1137],{"id":1132},"What one more unit of each resource is worth",[94,1139,1140],{"id":1133},"Horizontal bars of shadow prices from the linear model. A finishing hour is worth 38.57, a metre of wood 3.57, and an assembly hour nothing because assembly has spare capacity.",[98,1142],{"x":638,"y":101,"width":1143,"height":1144,"fill":104},"697","140",[98,1146],{"x":1147,"y":108,"width":1148,"height":1149,"rx":1150,"fill":656,"stroke":657,"style":667},"20.0","122.7","28.0","7",[127,1152,1156],{"x":1153,"y":1154,"style":1155},"81.4","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","finishing hour",[98,1158],{"x":1159,"y":108,"width":1160,"height":1149,"rx":1150,"fill":112,"stroke":113,"style":646},"152.7","488.3",[127,1162,1165],{"x":1163,"y":1154,"style":1164},"649.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","38.57",[98,1167],{"x":1147,"y":700,"width":1148,"height":1149,"rx":1150,"fill":656,"stroke":657,"style":667},[127,1169,1171],{"x":1153,"y":1170,"style":1155},"74.5","metre of wood",[98,1173],{"x":1159,"y":700,"width":1174,"height":1149,"rx":1150,"fill":150,"stroke":151,"style":646},"45.2",[127,1176,1179],{"x":1177,"y":1170,"style":1178},"205.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","3.57",[98,1181],{"x":1147,"y":1182,"width":1148,"height":1149,"rx":1150,"fill":656,"stroke":657,"style":667},"96.0",[127,1184,1186],{"x":1153,"y":1185,"style":1155},"114.5","assembly hour",[98,1188],{"x":1159,"y":1182,"width":1189,"height":1149,"rx":1150,"fill":656,"stroke":657,"style":646},"6.0",[127,1191,1194],{"x":1192,"y":1185,"style":1193},"166.7","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start","0 (51 h spare)",[29,1196,1198],{"id":1197},"step-5-add-a-business-rule","Step 5 — Add a business rule",[10,1200,1201],{},"Real plans have rules that the pure optimum ignores. Suppose the showroom needs at least 50 chairs. In Solver that is another row in the constraint list; here it is a lower bound:",[34,1203,1205],{"className":205,"code":1204,"language":207,"meta":39,"style":39},"lower = np.zeros(len(caps))\nlower[products[\"Product\"].tolist().index(\"Chair\")] = 50\nres_rule = milp(-profit, constraints=LinearConstraint(usage, -np.inf, available),\n                integrality=np.ones(len(profit)), bounds=Bounds(lower, caps))\nplan[\"With 50 chairs\"] = res_rule.x.round().astype(int)\nprint(plan.to_string(index=False))\nprint(f\"profit {-res_rule.fun:,.0f} — the rule costs {-res_int.fun + res_rule.fun:,.0f}\")\n",[14,1206,1207,1222,1243,1267,1289,1307,1321],{"__ignoreMap":39},[43,1208,1209,1212,1214,1217,1219],{"class":45,"line":46},[43,1210,1211],{"class":218},"lower ",[43,1213,254],{"class":214},[43,1215,1216],{"class":218}," np.zeros(",[43,1218,506],{"class":378},[43,1220,1221],{"class":218},"(caps))\n",[43,1223,1224,1227,1229,1232,1235,1238,1240],{"class":45,"line":228},[43,1225,1226],{"class":218},"lower[products[",[43,1228,539],{"class":53},[43,1230,1231],{"class":218},"].tolist().index(",[43,1233,1234],{"class":53},"\"Chair\"",[43,1236,1237],{"class":218},")] ",[43,1239,254],{"class":214},[43,1241,1242],{"class":378}," 50\n",[43,1244,1245,1248,1250,1252,1254,1256,1259,1261,1263,1265],{"class":45,"line":241},[43,1246,1247],{"class":218},"res_rule ",[43,1249,254],{"class":214},[43,1251,838],{"class":218},[43,1253,465],{"class":214},[43,1255,468],{"class":218},[43,1257,1258],{"class":266},"constraints",[43,1260,254],{"class":214},[43,1262,853],{"class":218},[43,1264,465],{"class":214},[43,1266,858],{"class":218},[43,1268,1269,1272,1274,1276,1278,1281,1284,1286],{"class":45,"line":248},[43,1270,1271],{"class":266},"                integrality",[43,1273,254],{"class":214},[43,1275,868],{"class":218},[43,1277,506],{"class":378},[43,1279,1280],{"class":218},"(profit)), ",[43,1282,1283],{"class":266},"bounds",[43,1285,254],{"class":214},[43,1287,1288],{"class":218},"Bounds(lower, caps))\n",[43,1290,1291,1293,1296,1298,1300,1303,1305],{"class":45,"line":278},[43,1292,900],{"class":218},[43,1294,1295],{"class":53},"\"With 50 chairs\"",[43,1297,906],{"class":218},[43,1299,254],{"class":214},[43,1301,1302],{"class":218}," res_rule.x.round().astype(",[43,1304,914],{"class":378},[43,1306,275],{"class":218},[43,1308,1309,1311,1313,1315,1317,1319],{"class":45,"line":301},[43,1310,379],{"class":378},[43,1312,563],{"class":218},[43,1314,566],{"class":266},[43,1316,254],{"class":214},[43,1318,571],{"class":378},[43,1320,556],{"class":218},[43,1322,1323,1325,1327,1329,1331,1333,1335,1338,1340,1342,1345,1347,1349,1352,1354,1357,1359,1361,1363],{"class":45,"line":306},[43,1324,379],{"class":378},[43,1326,497],{"class":218},[43,1328,582],{"class":214},[43,1330,585],{"class":53},[43,1332,589],{"class":588},[43,1334,465],{"class":214},[43,1336,1337],{"class":218},"res_rule.fun",[43,1339,950],{"class":214},[43,1341,600],{"class":588},[43,1343,1344],{"class":53}," — the rule costs ",[43,1346,589],{"class":588},[43,1348,465],{"class":214},[43,1350,1351],{"class":218},"res_int.fun ",[43,1353,1118],{"class":214},[43,1355,1356],{"class":218}," res_rule.fun",[43,1358,950],{"class":214},[43,1360,600],{"class":588},[43,1362,603],{"class":53},[43,1364,275],{"class":218},[10,1366,1367],{},"The rule costs only 35 of profit: the optimiser makes up for the chairs by switching some desks to shelves. Quantifying the cost of a rule like this is one of the most useful things an optimisation model does, and with the model in a script, each variant is a few lines instead of a new Solver setup.",[29,1369,1371],{"id":1370},"step-6-write-the-plan-back-to-the-workbook","Step 6 — Write the plan back to the workbook",[10,1373,1374],{},"Return the results to the file the planners already use, next to their data:",[34,1376,1378],{"className":205,"code":1377,"language":207,"meta":39,"style":39},"with pd.ExcelWriter(\"production_plan.xlsx\", engine=\"openpyxl\", mode=\"a\", if_sheet_exists=\"replace\") as xw:\n    plan.assign(Profit=lambda d: d[\"Whole units\"] * profit).to_excel(xw, sheet_name=\"Plan\", index=False)\n    report.to_excel(xw, sheet_name=\"Constraints\", index=False)\nwith pd.ExcelFile(\"production_plan.xlsx\") as xl:\n    print(xl.sheet_names)\n",[14,1379,1380,1427,1468,1490,1506],{"__ignoreMap":39},[43,1381,1382,1385,1388,1390,1392,1395,1397,1400,1402,1405,1407,1410,1412,1415,1417,1420,1422,1424],{"class":45,"line":46},[43,1383,1384],{"class":214},"with",[43,1386,1387],{"class":218}," pd.ExcelWriter(",[43,1389,260],{"class":53},[43,1391,263],{"class":218},[43,1393,1394],{"class":266},"engine",[43,1396,254],{"class":214},[43,1398,1399],{"class":53},"\"openpyxl\"",[43,1401,263],{"class":218},[43,1403,1404],{"class":266},"mode",[43,1406,254],{"class":214},[43,1408,1409],{"class":53},"\"a\"",[43,1411,263],{"class":218},[43,1413,1414],{"class":266},"if_sheet_exists",[43,1416,254],{"class":214},[43,1418,1419],{"class":53},"\"replace\"",[43,1421,1115],{"class":218},[43,1423,222],{"class":214},[43,1425,1426],{"class":218}," xw:\n",[43,1428,1429,1432,1435,1438,1441,1443,1445,1448,1451,1453,1455,1458,1460,1462,1464,1466],{"class":45,"line":228},[43,1430,1431],{"class":218},"    plan.assign(",[43,1433,1434],{"class":266},"Profit",[43,1436,1437],{"class":214},"=lambda",[43,1439,1440],{"class":218}," d: d[",[43,1442,903],{"class":53},[43,1444,906],{"class":218},[43,1446,1447],{"class":214},"*",[43,1449,1450],{"class":218}," profit).to_excel(xw, ",[43,1452,267],{"class":266},[43,1454,254],{"class":214},[43,1456,1457],{"class":53},"\"Plan\"",[43,1459,263],{"class":218},[43,1461,566],{"class":266},[43,1463,254],{"class":214},[43,1465,571],{"class":378},[43,1467,275],{"class":218},[43,1469,1470,1473,1475,1477,1480,1482,1484,1486,1488],{"class":45,"line":241},[43,1471,1472],{"class":218},"    report.to_excel(xw, ",[43,1474,267],{"class":266},[43,1476,254],{"class":214},[43,1478,1479],{"class":53},"\"Constraints\"",[43,1481,263],{"class":218},[43,1483,566],{"class":266},[43,1485,254],{"class":214},[43,1487,571],{"class":378},[43,1489,275],{"class":218},[43,1491,1492,1494,1497,1499,1501,1503],{"class":45,"line":248},[43,1493,1384],{"class":214},[43,1495,1496],{"class":218}," pd.ExcelFile(",[43,1498,260],{"class":53},[43,1500,1115],{"class":218},[43,1502,222],{"class":214},[43,1504,1505],{"class":218}," xl:\n",[43,1507,1508,1511],{"class":45,"line":278},[43,1509,1510],{"class":378},"    print",[43,1512,1513],{"class":218},"(xl.sheet_names)\n",[10,1515,1516,1519,1520,1523,1524,1528],{},[14,1517,1518],{},"mode=\"a\""," with ",[14,1521,1522],{},"if_sheet_exists=\"replace\""," keeps the input sheets and replaces last run's results, so the workbook always shows the current plan. ",[23,1525,1527],{"href":1526},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas\u002F","Append a DataFrame to an Existing Excel File with pandas"," explains the options.",[29,1530,1532],{"id":1531},"beyond-linear-models","Beyond linear models",[10,1534,1535,17,1537,1539,1540,1519,1543,1546,1547,1550],{},[14,1536,16],{},[14,1538,20],{}," cover linear objectives and constraints, which is most of what Solver's Simplex LP and integer settings are used for. Solver's GRG Nonlinear engine corresponds to ",[14,1541,1542],{},"scipy.optimize.minimize",[14,1544,1545],{},"method=\"SLSQP\""," or ",[14,1548,1549],{},"\"trust-constr\"",", which handle smooth nonlinear objectives with constraints — portfolio variance, pricing with a demand curve. For larger models with many named constraints, modelling libraries such as PuLP, Pyomo or OR-Tools let you write constraints by name instead of as matrices; check the current documentation of whichever you choose, since PuLP's 4.0 release, for one, changed how variables are created.",[29,1552,1554],{"id":1553},"performance-notes","Performance notes",[10,1556,1557,1558,1561,1562,1564,1565,1567],{},"Excel's built-in Solver engine is limited to 200 decision variables, which a realistic planning model — products by week by site — passes quickly. HiGHS has no such limit: linear models with tens of thousands of variables solve in seconds, and integer models of a few thousand variables are usually fine, though integer problems can get hard in ways that have little to do with their size. Pass ",[14,1559,1560],{},"options={\"time_limit\": 60}"," to ",[14,1563,20],{}," so a difficult instance returns its best solution so far instead of running all night, and check ",[14,1566,611],{}," to see whether it was proven optimal.",[29,1569,1571],{"id":1570},"common-pitfalls","Common pitfalls",[1573,1574,1575,1583,1596,1605,1616],"ul",{},[1576,1577,1578,1582],"li",{},[1579,1580,1581],"strong",{},"Forgetting that linprog minimises."," Negate the objective to maximise, and negate the result back.",[1576,1584,1585,1591,1592,1595],{},[1579,1586,1587,1588,27],{},"Using the answer without checking ",[14,1589,1590],{},"status"," An infeasible model still returns an object; only ",[14,1593,1594],{},"status == 0"," means optimal.",[1576,1597,1598,1601,1602,1604],{},[1579,1599,1600],{},"Rounding a continuous solution."," Rounding up can break constraints and rounding down can leave profit on the table; use ",[14,1603,20],{}," for whole units.",[1576,1606,1607,1610,1611,1613,1614,27],{},[1579,1608,1609],{},"Shadow prices from the integer model."," Marginals are well defined for the continuous problem; read sensitivity from ",[14,1612,16],{},", not ",[14,1615,20],{},[1576,1617,1618,1621],{},[1579,1619,1620],{},"Units mixed across rows."," Hours, metres and kilograms in one matrix are fine, but each row's available amount must use that row's unit.",[29,1623,1625],{"id":1624},"conclusion","Conclusion",[10,1627,1628,1629,1631,1632,1634],{},"Keep the numbers in the workbook and move the model into a script: read the profit, usage matrix, availability and caps with pandas, solve with ",[14,1630,16],{}," for the continuous plan and its shadow prices, switch to ",[14,1633,20],{}," for whole units and business rules, check the status every time, and write the plan and the constraint report back to the workbook. The result is Solver's answer, reviewable, repeatable and without its size limits.",[29,1636,1638],{"id":1637},"frequently-asked-questions","Frequently asked questions",[10,1640,1641,1644],{},[1579,1642,1643],{},"What is the Python equivalent of Excel Solver?","\nFor linear and integer models, scipy.optimize.linprog and scipy.optimize.milp, which use the HiGHS solver. For nonlinear models, scipy.optimize.minimize. Modelling libraries such as PuLP, Pyomo and OR-Tools add a friendlier way to write large models.",[10,1646,1647,1650],{},[1579,1648,1649],{},"How do I maximise instead of minimise with linprog?","\nPass the negative of the objective coefficients and negate the returned objective value. The optimal decision variables are the same.",[10,1652,1653,1656],{},[1579,1654,1655],{},"How do I get integer solutions like Solver's int constraint?","\nUse scipy.optimize.milp and set integrality to 1 for every variable that must be a whole number. linprog alone always returns continuous values.",[10,1658,1659,1662],{},[1579,1660,1661],{},"Can Python produce Solver's sensitivity report?","\nFor linear models, yes: linprog's result has ineqlin.residual (slack) and ineqlin.marginals (shadow prices) for each constraint, and upper.marginals for variable bounds. Negate the marginals when you maximised by negating the objective.",[29,1664,1666],{"id":1665},"related","Related",[1573,1668,1669,1675,1682,1689],{},[1576,1670,1671,1672,1674],{},"Up one level: ",[23,1673,26],{"href":25}," — Excel functions and tools translated to Python.",[1576,1676,1677,1681],{},[23,1678,1680],{"href":1679},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fgoal-seek-in-python-instead-of-excel\u002F","Goal Seek in Python Instead of Excel"," — hitting a single target instead of optimising.",[1576,1683,1684,1688],{},[23,1685,1687],{"href":1686},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments\u002F","Allocate Shared Costs Across Departments"," — another allocation problem, solved with rules.",[1576,1690,1691,1695],{},[23,1692,1694],{"href":1693},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumproduct-and-weighted-average-in-pandas\u002F","SUMPRODUCT and Weighted Average in pandas"," — the matrix product behind resource usage.",[1697,1698,1699],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":228,"depth":228,"links":1701},[1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714],{"id":31,"depth":228,"text":32},{"id":198,"depth":228,"text":199},{"id":420,"depth":228,"text":421},{"id":797,"depth":228,"text":798},{"id":977,"depth":228,"text":978},{"id":1197,"depth":228,"text":1198},{"id":1370,"depth":228,"text":1371},{"id":1531,"depth":228,"text":1532},{"id":1553,"depth":228,"text":1554},{"id":1570,"depth":228,"text":1571},{"id":1624,"depth":228,"text":1625},{"id":1637,"depth":228,"text":1638},{"id":1665,"depth":228,"text":1666},"2026-09-29","Move Solver models out of a dialog box and into a script: read the decision data from a workbook, solve linear programmes with scipy.optimize.linprog, whole-number problems with milp, report which constraints bind and what an extra unit of each resource is worth, and write the plan back to Excel.","md",[1719,1721,1723,1725],{"q":1643,"a":1720},"For linear and integer models, scipy.optimize.linprog and scipy.optimize.milp, which use the HiGHS solver. For nonlinear models, scipy.optimize.minimize. Modelling libraries such as PuLP, Pyomo and OR-Tools add a friendlier way to write large models.",{"q":1649,"a":1722},"Pass the negative of the objective coefficients and negate the returned objective value. The optimal decision variables are the same.",{"q":1655,"a":1724},"Use scipy.optimize.milp and set integrality to 1 for every variable that must be a whole number. linprog alone always returns continuous values.",{"q":1661,"a":1726},"For linear models, yes: linprog's result has ineqlin.residual (slack) and ineqlin.marginals (shadow prices) for each constraint, and upper.marginals for variable bounds. Negate the marginals when you maximised by negating the objective.","replace excel solver with python",null,{"breadcrumb":1730},[1731,1734,1737],{"name":1732,"item":1733},"Home","\u002F",{"name":1735,"item":1736},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python",{"title":1740,"description":1741},"Excel Solver Alternative in Python with SciPy linprog and milp","Replace Excel Solver with scipy.optimize: a product-mix model read from Excel, linprog with the HiGHS solver, integer solutions with milp, shadow prices and slack as a sensitivity report, extra constraints, and results written back to a workbook.","replace-excel-solver-with-python","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Freplace-excel-solver-with-python\u002Findex",[1745,1746],"formula-equivalents","pandas","how-to","2GC6VWHtOqagU1mWYQNdYQjBhC4j2im3jxRtqaSt9PI",[1750,1754],{"title":1751,"path":1752,"stem":1753,"children":-1},"REGEXTEST, REGEXEXTRACT and REGEXREPLACE Equivalents in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fregexextract-regextest-and-regexreplace-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fregexextract-regextest-and-regexreplace-equivalents-in-pandas\u002Findex",{"title":1755,"path":1756,"stem":1757,"children":-1},"ROUND, ROUNDUP, MROUND and CEILING Equivalents in Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fround-roundup-mround-and-ceiling-equivalents-in-python","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fround-roundup-mround-and-ceiling-equivalents-in-python\u002Findex",[1759,1761],{"path":1760,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":1762,"title":1735},"\u002Fadvanced-data-transformation-and-cleaning",[],[1765,1768,1771,1774,1777,1780],{"path":1766,"title":1767,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files","The VLOOKUP Equivalent in pandas for Excel Files",{"path":1769,"title":1770,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas","Extract Numbers and Codes from Text with pandas",{"path":1772,"title":1773,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-full-names-into-first-and-last-names-with-pandas","Split Full Names into First and Last Names with pandas",{"path":1775,"title":1776,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":1778,"title":1779,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","Calculate Percent of Total per Group in pandas",{"path":1781,"title":1782,"kind":1747},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","Count Distinct Values per Group in pandas",1790972088490]