Sensitivity Analysis¶
Sensitivity analysis helps you understand how changes in model parameters affect the optimal solution using shadow prices, reduced costs, and binding constraint analysis.
Overview¶
Sensitivity analysis answers questions like:
How much would increasing capacity improve profit?
Which constraints are limiting performance?
What’s the opportunity cost of a decision?
How sensitive is the solution to parameter changes?
The LXSensitivityAnalyzer provides these insights without re-solving
the model by analyzing dual values from the original solution.
Key Concepts¶
Shadow Prices (Dual Values)¶
The shadow price of a constraint represents the marginal value of relaxing that constraint by one unit.
Interpretation:
Positive shadow price: Relaxing the constraint would improve the objective
Zero shadow price: The constraint is not binding (has slack)
Magnitude: How much each unit of relaxation is worth
Example:
If a capacity constraint has a shadow price of $50:
Increasing capacity by 1 unit would improve profit by approximately $50
This constraint is binding (limiting performance)
This is a potential bottleneck
Reduced Costs¶
The reduced cost of a variable represents the opportunity cost of forcing that variable to be non-zero (if currently zero) or to increase its value (if already positive).
Interpretation:
Positive reduced cost (minimization): How much the objective would increase per unit
Negative reduced cost (maximization): How much profit you’d sacrifice per unit
Zero reduced cost: Variable is in the optimal basis
Binding Constraints¶
A constraint is binding if it’s satisfied as an equality at the optimal solution (slack = 0).
Characteristics:
Shadow price ≠ 0
Relaxing it would improve the objective
Potential bottleneck
Worth investigating for improvement
Basic Usage¶
Creating an Analyzer¶
from lumix.analysis import LXSensitivityAnalyzer
# After solving your model
solution = optimizer.solve(model)
# Create analyzer
analyzer = LXSensitivityAnalyzer(model, solution)
Analyzing Constraints¶
# Analyze a specific constraint
capacity_sens = analyzer.analyze_constraint("capacity")
print(f"Shadow Price: ${capacity_sens.shadow_price:.2f}")
print(f"Slack: {capacity_sens.slack:.2f}")
print(f"Binding: {capacity_sens.is_binding}")
# If solver provides range analysis
if capacity_sens.allowable_increase:
print(f"Valid up to +{capacity_sens.allowable_increase:.2f}")
Analyzing Variables¶
# Analyze a specific variable
production_sens = analyzer.analyze_variable("production")
print(f"Value: {production_sens.value:.2f}")
print(f"Reduced Cost: ${production_sens.reduced_cost:.2f}")
print(f"At Bound: {production_sens.is_at_bound}")
Generating Reports¶
# Generate comprehensive report
report = analyzer.generate_report()
print(report)
# Example output:
# Sensitivity Analysis Report
# ============================
#
# Binding Constraints:
# capacity: shadow price = $50.00, slack = 0.00
# budget: shadow price = $12.50, slack = 0.00
#
# Non-binding Constraints:
# min_production: slack = 25.00
#
# Variables with Reduced Costs:
# product_a: value = 100.0, reduced_cost = $0.00
# product_b: value = 0.0, reduced_cost = $-5.00
Identifying Bottlenecks¶
Finding All Bottlenecks¶
# Get all binding constraints
bottlenecks = analyzer.identify_bottlenecks()
for constraint_name in bottlenecks:
sens = analyzer.analyze_constraint(constraint_name)
print(f"{constraint_name}:")
print(f" Shadow Price: ${sens.shadow_price:.2f}")
print(f" Relaxing by 1 unit would improve objective by ${sens.shadow_price:.2f}")
Getting Most Sensitive Constraints¶
# Get top N constraints by absolute shadow price
top_constraints = analyzer.get_most_sensitive_constraints(top_n=5)
print("Top 5 Most Sensitive Constraints:")
for name, sensitivity in top_constraints:
print(f" {name}: ${sensitivity.shadow_price:.2f}/unit")
Getting Binding Constraints¶
# Get all binding constraints with their shadow prices
binding = analyzer.get_binding_constraints()
for name, sensitivity in binding.items():
if sensitivity.shadow_price > 0: # For maximization
print(f"{name} is a bottleneck worth ${sensitivity.shadow_price:.2f}/unit")
Practical Examples¶
Example 1: Production Planning¶
from dataclasses import dataclass
from lumix import LXModel, LXVariable, LXConstraint, LXLinearExpression, LXOptimizer
from lumix.analysis import LXSensitivityAnalyzer
@dataclass
class Product:
id: str
profit: float
labor_hours: float
material_cost: float
# Create products
products = [
Product("A", 100, 2, 30),
Product("B", 150, 3, 45),
Product("C", 120, 2.5, 35),
]
# Build model
production = (
LXVariable[Product, float]("production")
.continuous()
.bounds(lower=0)
.indexed_by(lambda p: p.id)
.from_data(products)
)
model = (
LXModel("production")
.add_variable(production)
.add_constraint(
LXConstraint("labor")
.expression(
LXLinearExpression()
.add_term(production, lambda p: p.labor_hours)
)
.le()
.rhs(100)
)
.add_constraint(
LXConstraint("material_budget")
.expression(
LXLinearExpression()
.add_term(production, lambda p: p.material_cost)
)
.le()
.rhs(1000)
)
.maximize(
LXLinearExpression()
.add_term(production, lambda p: p.profit)
)
)
# Solve
optimizer = LXOptimizer().use_solver("gurobi")
solution = optimizer.solve(model)
# Analyze sensitivity
analyzer = LXSensitivityAnalyzer(model, solution)
print(analyzer.generate_report())
# Find bottlenecks
bottlenecks = analyzer.identify_bottlenecks()
print(f"\nBottlenecks: {bottlenecks}")
# Should labor or material budget be increased?
labor_sens = analyzer.analyze_constraint("labor")
material_sens = analyzer.analyze_constraint("material_budget")
print(f"\nLabor shadow price: ${labor_sens.shadow_price:.2f}/hour")
print(f"Material shadow price: ${material_sens.shadow_price:.2f}/$")
if labor_sens.shadow_price > material_sens.shadow_price:
print("Priority: Increase labor capacity")
else:
print("Priority: Increase material budget")
Example 2: Resource Allocation¶
# After solving a resource allocation model...
analyzer = LXSensitivityAnalyzer(model, solution)
# Identify which resources are most valuable
resource_constraints = [
"warehouse_space",
"truck_capacity",
"driver_hours",
"fuel_budget",
]
print("Resource Value Analysis:")
print("-" * 50)
resource_values = []
for constraint_name in resource_constraints:
sens = analyzer.analyze_constraint(constraint_name)
resource_values.append((constraint_name, sens.shadow_price))
print(f"{constraint_name:20s}: ${sens.shadow_price:8.2f}/unit")
# Sort by shadow price
resource_values.sort(key=lambda x: x[1], reverse=True)
print(f"\nMost valuable resource: {resource_values[0][0]}")
print(f"Invest here for maximum ROI: ${resource_values[0][1]:.2f}/unit")
Example 3: Understanding Non-Binding Constraints¶
# Check which constraints have slack
non_binding = analyzer.get_non_binding_constraints()
print("Constraints with Slack (Not Limiting):")
for name, sensitivity in non_binding.items():
print(f" {name}: slack = {sensitivity.slack:.2f}")
print(f" Could reduce RHS by {sensitivity.slack:.2f} without impact")
Advanced Features¶
Sensitivity Ranges¶
Some solvers (Gurobi, CPLEX) provide sensitivity ranges:
sens = analyzer.analyze_constraint("capacity")
if sens.allowable_increase and sens.allowable_decrease:
print(f"Current RHS: 1000")
print(f"Valid range for shadow price:")
print(f" Lower: {1000 - sens.allowable_decrease:.2f}")
print(f" Upper: {1000 + sens.allowable_increase:.2f}")
print(f"Shadow price is valid within this range")
Comparing Multiple Solutions¶
# Solve with different parameters
solution1 = optimizer.solve(model)
analyzer1 = LXSensitivityAnalyzer(model, solution1)
# Modify and resolve
model.get_constraint("capacity").rhs_value = 1200
solution2 = optimizer.solve(model)
analyzer2 = LXSensitivityAnalyzer(model, solution2)
# Compare shadow prices
print("Shadow Price Comparison:")
for name in ["capacity", "budget"]:
sp1 = analyzer1.analyze_constraint(name).shadow_price
sp2 = analyzer2.analyze_constraint(name).shadow_price
print(f"{name}: ${sp1:.2f} -> ${sp2:.2f}")
Solver Support¶
Sensitivity analysis requires dual values from the solver:
Solver |
Shadow Prices |
Reduced Costs |
Sensitivity Ranges |
|---|---|---|---|
Gurobi |
✓ |
✓ |
✓ |
CPLEX |
✓ |
✓ |
✓ |
OR-Tools |
✓ (LP only) |
✓ (LP only) |
✗ |
GLPK |
✓ (LP only) |
✓ (LP only) |
✗ |
CP-SAT |
✗ |
✗ |
✗ |
Best Practices¶
Solve to Optimality First
Sensitivity analysis is only meaningful for optimal solutions.
solution = optimizer.solve(model) if not solution.is_optimal(): print("Warning: Solution is not optimal, sensitivity may not be meaningful")
Focus on Binding Constraints
Non-binding constraints have zero shadow price (by definition).
# Only analyze binding constraints for name in analyzer.identify_bottlenecks(): # These are the constraints worth investigating analyze_improvement_options(name)
Validate Ranges
Shadow prices are only valid within their allowable ranges.
sens = analyzer.analyze_constraint("capacity") change = 500 # Proposed increase if sens.allowable_increase and change > sens.allowable_increase: print(f"Warning: Change exceeds valid range") print(f"Shadow price may not apply for changes > {sens.allowable_increase}")
Combine with What-If Analysis
Use sensitivity analysis to identify opportunities, what-if analysis to quantify them.
from lumix.analysis import LXWhatIfAnalyzer # Sensitivity identifies bottleneck bottlenecks = analyzer.identify_bottlenecks() # What-if quantifies the impact whatif = LXWhatIfAnalyzer(model, optimizer) for constraint in bottlenecks[:3]: # Top 3 result = whatif.increase_constraint_rhs(constraint, by=100) print(f"{constraint}: ${result.delta_objective:,.2f}")
Next Steps¶
Scenario Analysis - Compare multiple scenarios systematically
What-If Analysis - Interactively explore parameter changes
Analysis Module API - Complete API reference
Analysis Architecture - Architecture details