Allocate a budget to maximize return in Python

Resource allocation — splitting a fixed budget, headcount, or capacity across competing options to get the most out of it — is one of the most common optimization problems in day-to-day code: allocate marketing spend across channels, capital across projects, compute across jobs. With quicopt it is a short linear program (LP) in Python.

Real allocations are rarely limited by money alone. Here each channel also costs management hours, and the team only has so many — and the highest-return channel is by far the most hours-hungry. Two scarce resources, and the choices stop being independent.

The naive approach

Without the vocabulary, the instinct is to pour money into the highest-return channel first, then the next:

greedy.py
ret   = [0.12, 0.10, 0.15, 0.08]   # return per unit spent
hours = [1.0,  1.0,  4.0,  1.0]    # management hours per unit spent
cap   = [40.0, 50.0, 30.0, 60.0]   # max spend per channel
budget, team_hours = 100.0, 160.0

spend = [0.0] * len(ret)
for i in sorted(range(len(ret)), key=lambda i: -ret[i]):
    take = min(cap[i], budget, team_hours / hours[i])
    spend[i] = take
    budget -= take
    team_hours -= take * hours[i]

print("return:", sum(ret[i] * spend[i] for i in range(len(ret))))  # -> 9.3

Greedy spends 40 on channel 0 and 30 on channel 2 and returns 9.3. It walked into the trap: channel 2 has the best return per euro, but at 4 hours per unit it burns 120 of the 160 team hours. The hours run out, and €30 of the budget is left unspendable. The best return per euro is not the best return per hour, and with two limits you cannot rank channels on one number at all.

Sorting by return-per-hour instead just moves the trap; grid-searching the splits explodes with the number of channels. The problem is continuous and linear — so model it and solve it exactly.

Model it as an LP

Each channel gets a continuous variable spend[i] bounded by its cap. Two constraints — one per scarce resource — and the objective maximizes total return:

resource_allocation.py
from ortools.math_opt.python import mathopt
from quicopt import Client

# Allocate a fixed budget across channels to maximize return. Each channel has a
# per-unit return, a spend cap, AND a management-hours cost per unit spent — and
# the team has a limited number of hours. Two scarce resources (budget + hours),
# so pouring money into the highest-return channel first is NOT optimal.
ret   = [0.12, 0.10, 0.15, 0.08]   # return per unit spent
hours = [1.0,  1.0,  4.0,  1.0]    # management hours needed per unit spent
cap   = [40.0, 50.0, 30.0, 60.0]   # max spend per channel
budget     = 100.0
team_hours = 160.0
N = len(ret)

model = mathopt.Model(name="resource_allocation")
x = [model.add_variable(lb=0.0, ub=cap[i], name=f"spend_{i}") for i in range(N)]
model.add_linear_constraint(sum(x) <= budget)
model.add_linear_constraint(sum(hours[i] * x[i] for i in range(N)) <= team_hours)
model.maximize(sum(ret[i] * x[i] for i in range(N)))

client = Client("https://try.quicoptapi.pgi.fz-juelich.de")
print(client.solve(model).display)
$ python resource_allocation.py
├── 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

What you get

status: optimal means the split is proven best — return 11.8, against greedy's 9.3. That is 27% more return from the same budget and the same team, and it comes from a split no ranking rule would produce: the solver holds back on the highest-return channel (20 instead of its 30 cap) and puts the freed hours into channel 1, which greedy skipped entirely. Every euro and every hour gets used.

That is the difference between a heuristic and a solver. Greedy commits to one resource's ranking and cannot see the trade; the LP prices both resources at once and proves there is nothing better.

Because it is an LP, the same model scales from four options to four thousand, and from two scarce resources to twenty — you change the data, not the method.

Next

Reference: G. B. Dantzig, Linear Programming and Extensions, Princeton University Press, 1963.

A bigger allocation problem — or a different one?

Tell us what you're optimizing. We'll help you model it and point you at the right approach.

Talk to us →

Frequently asked questions

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.

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.

Is quicopt free to use?

Yes — pip install quicopt and your first call sets up a free key, no license.