Maximizing Profits with a Simple Integer Linear Program in Python#
Below is an end-to-end example (in Python) of how you can:
Formulate the problem as an Integer Linear Program (ILP).
Generate some simulated data for banks, products, revenues, and costs.
Implement the model using PuLP.
Solve the model to maximize profits.
Visualize the resulting assignment (how many leads go to each bank-product combination).
Note: We are not using sub-products or hierarchical product constraints here, as per your request. We keep the model straightforward, but the same structure can be extended or adjusted to fit additional real-world constraints.
1. Key Model Overview#
We have a set of banks \( i \in \{1, \ldots, I\} \) and a set of products \( j \in \{1, \ldots, J\} \).
Decision variable: $\( x_{i,j} = \text{(integer) number of leads allocated to bank } i \text{ for product } j. \)$
Objective: Maximize $\( \sum_{i=1}^{I}\sum_{j=1}^{J} \bigl(r_{i,j} - c_{i,j}\bigr) \cdot x_{i,j}. \)\( Where \) r_{i,j} \( is revenue per lead (payout you get from the bank), and \) c_{i,j} $ is cost per lead (e.g., SMS cost).
Constraints:
Total lead constraint: $\( \sum_{i=1}^{I}\sum_{j=1}^{J} x_{i,j} \le L \)\( where \)L$ is the maximum number of leads you can send (limited by budget or available leads).
Capacity constraints (if needed): $\( x_{i,j} \le U_{i,j} \quad \forall\, i,j \)\( If each bank-product pair can only handle a certain maximum \) U_{i,j} $.
Non-negativity & integrality: $\( x_{i,j} \in \mathbb{Z}_{\ge 0}. \)$
2. Example Python Code#
Below is an illustrative Python script using PuLP. It includes:
Random/simulated data generation.
Model building.
Solver execution.
Extraction of results.
A simple bar chart to visualize the allocation of leads.
import random
import pandas as pd
import matplotlib.pyplot as plt
from pulp import LpMaximize, LpProblem, LpVariable, lpSum, LpInteger
# --------------------
# 1. SIMULATED DATA
# --------------------
# Let's say we have 3 banks and 2 products
banks = ["BankA", "BankB", "BankC"]
products = ["CreditCard", "PersonalLoan"]
# For each bank-product pair, define a random revenue and cost
random.seed(42) # for reproducibility
# Revenue per lead (r_{i,j})
revenue = {
(b, p): random.randint(50, 100) for b in banks for p in products
}
# Cost per lead (c_{i,j}), e.g., SMS or marketing cost
cost = {
(b, p): random.randint(5, 15) for b in banks for p in products
}
# We define max leads we can allocate in total (L)
L = 100 # Suppose we can only send 100 leads in total
# Optionally define capacity constraints for each bank-product
# e.g., each bank-product can handle up to 50 leads in this example
U = {
(b, p): 50 for b in banks for p in products
}
# --------------------
# 2. BUILD ILP MODEL
# --------------------
model = LpProblem("Maximize_Profit", LpMaximize)
# Create decision variables x_{i,j}, each must be an integer >= 0
x = {
(b, p): LpVariable(name=f"x_{b}_{p}", lowBound=0, cat=LpInteger)
for b in banks
for p in products
}
# Objective function: Maximize sum((revenue - cost)*x_{i,j})
model += lpSum((revenue[(b,p)] - cost[(b,p)]) * x[(b,p)] for b in banks for p in products), "Total_Profit"
# Constraint 1: total leads <= L
model += lpSum(x[(b,p)] for b in banks for p in products) <= L, "Total_Leads_Limit"
# Constraint 2: capacities x_{i,j} <= U_{i,j}
for b in banks:
for p in products:
model += x[(b,p)] <= U[(b,p)], f"Cap_{b}_{p}"
# --------------------
# 3. SOLVE MODEL
# --------------------
model.solve()
# --------------------
# 4. EXTRACT RESULTS
# --------------------
print("Status:", model.status)
print("Objective (Max Profit):", model.objective.value())
# Construct a dataframe for the solution
solution_data = []
for b in banks:
for p in products:
solution_data.append({
"Bank": b,
"Product": p,
"AllocatedLeads": x[(b,p)].value(),
"RevenuePerLead": revenue[(b,p)],
"CostPerLead": cost[(b,p)],
"ProfitPerLead": revenue[(b,p)] - cost[(b,p)]
})
df_solution = pd.DataFrame(solution_data)
print("\nSolution Allocations:")
print(df_solution)
# --------------------
# 5. VISUALIZE RESULT
# --------------------
# Example: Bar chart of leads allocated by (Bank, Product)
pivot_table = df_solution.pivot(index="Bank", columns="Product", values="AllocatedLeads")
pivot_table.plot(kind="bar", stacked=True, figsize=(8, 6))
plt.title("Optimal Leads Allocation by Bank and Product")
plt.xlabel("Bank")
plt.ylabel("Number of Leads Allocated")
plt.legend(title="Product")
plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[1], line 2
1 import random
----> 2 import pandas as pd
3 import matplotlib.pyplot as plt
4 from pulp import LpMaximize, LpProblem, LpVariable, lpSum, LpInteger
5
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/__init__.py:22
19 del _hard_dependencies, _dependency, _missing_dependencies
21 # numpy compat
---> 22 from pandas.compat import is_numpy_dev as _is_numpy_dev # pyright: ignore # noqa:F401
24 try:
25 from pandas._libs import hashtable as _hashtable, lib as _lib, tslib as _tslib
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/compat/__init__.py:18
15 from typing import TYPE_CHECKING
17 from pandas._typing import F
---> 18 from pandas.compat.numpy import (
19 is_numpy_dev,
20 np_version_under1p21,
21 )
22 from pandas.compat.pyarrow import (
23 pa_version_under1p01,
24 pa_version_under2p0,
(...) 31 pa_version_under9p0,
32 )
34 if TYPE_CHECKING:
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/compat/numpy/__init__.py:4
1 """ support numpy compatibility across versions """
2 import numpy as np
----> 4 from pandas.util.version import Version
6 # numpy versioning
7 _np_version = np.__version__
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/util/__init__.py:2
1 # pyright: reportUnusedImport = false
----> 2 from pandas.util._decorators import ( # noqa:F401
3 Appender,
4 Substitution,
5 cache_readonly,
6 )
8 from pandas.core.util.hashing import ( # noqa:F401
9 hash_array,
10 hash_pandas_object,
11 )
14 def __getattr__(name):
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/util/_decorators.py:14
6 from typing import (
7 Any,
8 Callable,
9 Mapping,
10 cast,
11 )
12 import warnings
---> 14 from pandas._libs.properties import cache_readonly
15 from pandas._typing import (
16 F,
17 T,
18 )
19 from pandas.util._exceptions import find_stack_level
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/_libs/__init__.py:13
1 __all__ = [
2 "NaT",
3 "NaTType",
(...) 9 "Interval",
10 ]
---> 13 from pandas._libs.interval import Interval
14 from pandas._libs.tslibs import (
15 NaT,
16 NaTType,
(...) 21 iNaT,
22 )
File pandas/_libs/interval.pyx:1, in init pandas._libs.interval()
----> 1 'Could not get source, probably due dynamically evaluated source code.'
ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject