We use cookies and other tracking technologies to improve your browsing experience on our website, to show you personalized content and targeted ads, to analyze our website traffic, and to understand where our visitors are coming from.
Optimizing Conference Budget Allocation with Integer Programming in Python
A practical guide to allocating conference attendance seats under travel, registration, and demand constraints using integer programming in Python
Conference attendnce planning is a natural optimization problem. We may have a fixed travel and expense (T&E) budget, a separate registration budget, a set of flagship conferences, and several competing objectives: technical learning, brand presence, recruiting, and broad associate participation.
In this post, we will model a conference attendance strategy as a linear and integer programming problem. The setup is inspired by a conference strategy proposal I recently presented that evaluates five anonymized flagship conferences:
Flagship conference A
Flagship conference B
Flagship conference C
Flagship conference D
Flagship conference E
My proposal compares three intuitive attendance strategies:
Strong presence: send 7 associates to every flagship conference
SME efficiency: send 3 subject matter experts to every flagship conference
Balanced reach: send 5 associates to every flagship conference
These strategies are useful starting points. However, they force the same headcount at every flagship conference regardless of location, cost, and associate demand. Optimization gives us a more flexible way to answer the same planning question: if we have a balanced-reach target of 25 flagship seats, how should those seats be distributed across conferences?
Tools From The Python Ecosystem
We will be using the following libraries and packages:
The core optimization work is handled by two packages:
scipy.optimize.linprog for the continuous linear programming relaxation, which relaxes the integer constraints to allow for continuous solutions
PuLP for the integer programming formulation, see documentation
Show code
from typing import Dict, List, Optionalimport numpy as npimport polars as plimport pulpfrom scipy.optimize import linprog
Data Inputs
Let us generate some data. In practice, I find it cleaner to keep the planning assumptions in separate tables before joining them into the modeling dataset. This mirrors how the information often arrives in real planning work: the conference list may come from the committee, flight assumptions may come from a travel policy, and hotel/food estimates may come from a finance planning template.
The first input table contains the conference-level planning information: region, number of days, registration fee, and a simulated demand score. The demand score can be interpreted as expected associate demand, e.g., the number of associates likely to attend the conference.
The third input table stores the hotel and food cost estimate per attendee per day. In this simplified example, we use one cost profile for all conferences. However, we can easily extend this to allow for cost profiles based on the conference location.
The average flagship T&E cost is therefore $4,820.
Baseline Scenarios
Before moving into optimization, it is useful to reproduce the three fixed-headcount scenarios. These scenarios send the same number of attendees to each flagship conference. We use one shared budget set throughout the post, so the fixed strategies are intentionally sized to stay feasible under the same caps used by the optimization model:
The fixed scenarios are easy to explain, and under the shared budget caps all three are feasible:
Strong presence sends 7 associates to every flagship.
SME efficiency sends 3 associates to every flagship. This preserves the most remaining T&E budget, which can be redeployed into many smaller non-flagship events.
Balanced reach sends 5 associates to every flagship and sits between the two extremes.
These scenarios are intentionally simple. They are useful for communicating the trade-off between flagship depth and broader participation before introducing an optimization model.
However, the fixed-headcount approach has one important limitation: it assumes every conference should receive the same number of attendees. If one flagship has much higher associate demand but is also more expensive, while another is less expensive but has lower demand, we should allow the model to trade off cost and demand directly.
For the optimization model, we will use the same shared budget caps and target the balanced-reach 25-seat flagship footprint. The key difference is that optimization can vary the allocation by conference instead of requiring the same headcount everywhere. This creates a cleaner comparison: the optimized strategy keeps the same total footprint as the balanced fixed strategy while using the budget more flexibly across cost and demand.
Optimization Problem
Let:
We want to choose the vector that maximizes associate demand while respecting T&E and registration budgets.
Objective
We will optimize directly against expected demand. Let be the demand score for flagship conference :
The objective is then:
Constraints
The model includes the following constraints:
The LP version relaxes the final integer constraint; the ILP version enforces it fully.
Finally, each relaxed decision variable is bounded between the minimum and maximum practical delegation sizes. In this example, the lower bound matches the SME-efficiency strategy, while the upper bound allows one seat above the strong-presence strategy:
Show code
bounds = [ (min_flagship_attendees, max_flagship_attendees)for _ in conference_names]
Putting these pieces together, the formulation passed to linprog is:
The LP relaxation is useful for understanding the shape of the optimization problem. However, conference attendance seats cannot be fractional (even though sometimes the optional solution does not contain any fractional values). This is where integer programming becomes the more appropriate formulation.
Solve the ILP with PuLP
For the integer programming formulation, each decision variable represents a whole number of attendees. The optimization model is initialized as a maximization problem:
Show code
model = pulp.LpProblem("conference_attendance_allocation", pulp.LpMaximize)
Before building PuLP expressions, we export the columns needed by the solver into plain Python dictionaries. This keeps avoids row-wise iteration over polars DataFrame, which is columnar.
The optimized solution does not need to pick the same number of attendees for every conference. Instead, it allocates the 25 flagship seats toward conferences with stronger demand, while still maintaining a minimum level of participation at every flagship.
Compare Fixed Scenarios with Optimized Allocation
To compare fixed scenarios and the optimized plan, we can compute their objective values using the same demand score and the same budget caps.
Show code
def evaluate_fixed_strategy(attendees_per_flagship: int) -> Dict[str, object]:""" Evaluate a fixed-headcount conference allocation strategy. Parameters ---------- attendees_per_flagship : int Number of attendees assigned to every flagship conference. Returns ------- Dict[str, object] Summary statistics for the fixed strategy, including total attendees, budget spend, remaining budget, feasibility, and objective value. """ attendees = np.repeat(attendees_per_flagship, len(conference_data)) te_spend =float( (attendees * conference_data["te_cost_per_person"].to_numpy()).sum() ) registration_spend =float( (attendees * conference_data["registration_fee"].to_numpy()).sum() ) objective_value =float( (attendees * conference_data["demand_score"].to_numpy()).sum() )return {"model_type": "Fixed","strategy": f"{attendees_per_flagship} per flagship","flagship_attendees": int(attendees.sum()),"te_spend": te_spend,"registration_spend": registration_spend,"remaining_te": te_budget - te_spend,"remaining_registration": registration_budget - registration_spend,"is_budget_feasible": (te_spend <= te_budget)and (registration_spend <= registration_budget),"objective_value": objective_value, }strategy_comparison = pl.DataFrame( [ evaluate_fixed_strategy(strong_presence), evaluate_fixed_strategy(sme_efficiency), evaluate_fixed_strategy(balanced_reach), {"model_type": "Optimized","strategy": "optimized integer allocation","flagship_attendees": int(ilp_solution["attendees"].sum()),"te_spend": float(ilp_solution["te_spend"].sum()),"registration_spend": float(ilp_solution["registration_spend"].sum()),"remaining_te": te_budget -float(ilp_solution["te_spend"].sum()),"remaining_registration": registration_budget-float(ilp_solution["registration_spend"].sum()),"is_budget_feasible": True,"objective_value": float(ilp_solution["demand_contribution"].sum()), }, ])strategy_comparison_table = ( strategy_comparison .to_dicts())
Fixed Strategies vs Optimized Allocation
model_type
strategy
flagship_attendees
te_spend
registration_spend
remaining_te
remaining_registration
is_budget_feasible
objective_value
Fixed
7 per flagship
35
$168,700
$34,300
$1,300
$700
Yes
2,982.00
Fixed
3 per flagship
15
$72,300
$14,700
$97,700
$20,300
Yes
1,278.00
Fixed
5 per flagship
25
$120,500
$24,500
$49,500
$10,500
Yes
2,130.00
Optimized
optimized integer allocation
25
$129,100
$25,500
$40,900
$9,500
Yes
2,434.00
This comparison highlights the practical value of optimization. The fixed strategies are clear and communicable, while the optimized allocation keeps the planned 25-seat footprint without requiring a rigid equal-headcount rule.
Add Additional Non-Flagship Conference Seats
The original scenario framing also considers how many additional non-flagship event seats could be funded after flagship commitments. We can add this as a second-stage calculation.
Let be the total T&E budget, be the optimized flagship T&E spend, and be the estimated T&E cost for one additional non-flagship event seat. The remaining budget and additional seats are:
This is a pragmatic way to combine flagship planning with broader participation goals. The first-stage model determines the flagship footprint. The remaining budget can then be used for smaller non-flagship, affinity, or niche conferences.
Scenario Analysis
The biggest benefit of formulating the planning problem as code is that scenario analysis becomes straightforward. We can define a function that solves the ILP for a given set of demand adjustments, minimum attendees, maximum attendees, and flagship seat target.
Show code
def solve_conference_ilp( conference_data: pl.DataFrame, demand_adjustments: Optional[Dict[str, int]] =None, min_attendees: int= min_flagship_attendees, max_attendees: int= max_flagship_attendees, flagship_seat_target: int= planning_flagship_seat_target,) -> pl.DataFrame:""" Solve the conference attendance allocation problem with integer programming. Parameters ---------- conference_data: pl.DataFrame A DataFrame with the conference names, demand scores, te costs, registration fees, and attendees. demand_adjustments: Optional[Dict[str, int]] A dictionary of conference names and demand adjustments. min_attendees: int The minimum number of attendees for each conference. max_attendees: int The maximum number of attendees for each conference. flagship_seat_target: int The target number of flagship seats. Returns ------- pl.DataFrame A DataFrame with the conference names, demand scores, te costs, registration fees, and attendees. """if demand_adjustments: demand_adjustment_data = pl.DataFrame( {"conference": list(demand_adjustments.keys()),"demand_adjustment": list(demand_adjustments.values()), } ) scenario_data = ( conference_data .join(demand_adjustment_data, on="conference", how="left") .with_columns( ( pl.col("demand_score")+ pl.col("demand_adjustment").fill_null(0) ).alias("demand_score") ) .drop("demand_adjustment") )else: scenario_data = conference_data scenario_names = scenario_data["conference"].to_list() scenario_demand_score =dict(zip( scenario_names, scenario_data["demand_score"].to_list(), ) ) scenario_te_cost =dict(zip( scenario_names, scenario_data["te_cost_per_person"].to_list(), ) ) scenario_registration_fee =dict(zip( scenario_names, scenario_data["registration_fee"].to_list(), ) ) scenario_model = pulp.LpProblem("conference_attendance_scenario", pulp.LpMaximize) x_scenario = { conference: pulp.LpVariable(f"attendees_{conference}", lowBound=min_attendees, upBound=max_attendees, cat="Integer", )for conference in scenario_names } scenario_model += pulp.lpSum( scenario_demand_score[conference] * x_scenario[conference]for conference in scenario_names ) scenario_model += pulp.lpSum( scenario_te_cost[conference] * x_scenario[conference]for conference in scenario_names ) <= te_budget scenario_model += pulp.lpSum( scenario_registration_fee[conference] * x_scenario[conference]for conference in scenario_names ) <= registration_budget scenario_model += pulp.lpSum( x_scenario[conference] for conference in scenario_names ) == flagship_seat_target scenario_model.solve(pulp.PULP_CBC_CMD(msg=False)) solution = ( scenario_data .select( ["conference","demand_score","te_cost_per_person","registration_fee", ] ) .with_columns( pl.Series("attendees", [int(x_scenario[conference].value())for conference in scenario_names ], ) ) .with_columns( [ (pl.col("attendees") * pl.col("te_cost_per_person")).alias("te_spend" ), (pl.col("attendees") * pl.col("registration_fee")).alias("registration_spend" ), (pl.col("attendees") * pl.col("demand_score")).alias("objective_value" ), ] ) )return solution
Scenario A: Updated Demand
Suppose updated planning data comes in after the initial planning pass. Demand for Flagship A, Flagship D, and Flagship E increases sharply, while demand for Flagship B and Flagship C softens. This is intentionally a meaningful demand shock so the scenario produces a visibly different allocation rather than a cosmetic score change.
If the committee wants to reserve more budget for non-flagship events, we can reduce the flagship seat target from 25 to 20 while keeping the same budget caps.
The objective values here represent the total demand score for the allocation.Scenario analysis helps move the discussion away from a single answer. Instead, the committee can ask how sensitive the allocation is to new demand, minimum coverage requirements, or changes in travel assumptions.
Wrapping Up
The fixed scenario approach is a good executive communication tool: 3x, 5x, and 7x flagship attendance levels are easy to understand. Integer programming adds another layer by letting the attendance plan vary by conference while still respecting budget, demand, and operational constraints.
The general workflow is:
Translate the planning proposal into structured data.
Define the objective function explicitly.
Add budget and operational constraints.
Solve the ILP to produce an executable attendance plan.
Run scenarios to test how assumptions change the recommendation.
One final modeling note is worth emphasizing: in this example, represents expected associate demand. In other settings, could be a recruiting value, learning value, leadership priority score, or another single value signal.
By combining domain judgment with optimization in Python, we can make conference planning decisions that are more transparent, reproducible, and aligned with planning priorities.