Make offers to your banking customer based on their preferences and probability of acceptance

#This program is from the IBM Decision Optimization Documentationfrom pandas import DataFrame, Series
names = {
139987 : "Guadalupe J. Martinez", 140030 : "Michelle M. Lopez", 140089 : "Terry L. Ridgley",
140097 : "Miranda B. Roush", 139068 : "Sandra J. Wynkoop", 139154 : "Roland Guérette", 139158 : "Fabien Mailhot",
139169 : "Christian Austerlitz", 139220 : "Steffen Meister", 139261 : "Wolfgang Sanger",
139416 : "Lee Tsou", 139422 : "Sanaa' Hikmah Hakimi", 139532 : "Miroslav Škaroupka",
139549 : "George Blomqvist", 139560 : "Will Henderson", 139577 : "Yuina Ohira", 139580 : "Vlad Alekseeva",
139636 : "Cassio Lombardo", 139647 : "Trinity Zelaya Miramontes", 139649 : "Eldar Muravyov", 139665 : "Shu T'an",
139667 : "Jameel Abdul-Ghani Gerges", 139696 : "Zeeb Longoria Marrero", 139752 : "Matheus Azevedo Melo",
139832 : "Earl B. Wood", 139859 : "Gabrielly Sousa Martins", 139881 : "Franca Palermo"}
data = [(139987, "Pension", 0.13221, "Mortgage", 0.10675), (140030, "Savings", 0.95678, "Pension", 0.84446), (140089, "Savings", 0.95678, "Pension", 0.80233),
(140097, "Pension", 0.13221, "Mortgage", 0.10675), (139068, "Pension", 0.80506, "Savings", 0.28391), (139154, "Pension", 0.13221, "Mortgage", 0.10675),
(139158, "Pension", 0.13221, "Mortgage", 0.10675),(139169, "Pension", 0.13221, "Mortgage", 0.10675), (139220, "Pension", 0.13221, "Mortgage", 0.10675),
(139261, "Pension", 0.13221, "Mortgage", 0.10675), (139416, "Pension", 0.13221, "Mortgage", 0.10675), (139422, "Pension", 0.13221, "Mortgage", 0.10675),
(139532, "Savings", 0.95676, "Mortgage", 0.82269), (139549, "Savings", 0.16428, "Pension", 0.13221), (139560, "Savings", 0.95678, "Pension", 0.86779),
(139577, "Pension", 0.13225, "Mortgage", 0.10675), (139580, "Pension", 0.13221, "Mortgage", 0.10675), (139636, "Pension", 0.13221, "Mortgage", 0.10675),
(139647, "Savings", 0.28934, "Pension", 0.13221), (139649, "Pension", 0.13221, "Mortgage", 0.10675), (139665, "Savings", 0.95675, "Pension", 0.27248),
(139667, "Pension", 0.13221, "Mortgage", 0.10675), (139696, "Savings", 0.16188, "Pension", 0.13221), (139752, "Pension", 0.13221, "Mortgage", 0.10675),
(139832, "Savings", 0.95678, "Pension", 0.83426), (139859, "Savings", 0.95678, "Pension", 0.75925), (139881, "Pension", 0.13221, "Mortgage", 0.10675)]
products = ["Savings", "Mortgage", "Pension"]
product_value = [200, 300, 400]
budget_share = [0.2, 0.5, 0.3]
available_budget = 500
channels = DataFrame(data=[("gift", 20.0, 0.20), ("newsletter", 15.0, 0.05), ("seminar", 23.0, 0.30)], columns=["name", "cost", "factor"])
channels.head(5)| name | cost | factor | |
|---|---|---|---|
| 0 | gift | 20.0 | 0.20 |
| 1 | newsletter | 15.0 | 0.05 |
| 2 | seminar | 23.0 | 0.30 |
Offers are stored in a pandas DataFrame.
offers = DataFrame(data=data, index=range(0, len(data)), columns=["customerid", "Product1", "Confidence1", "Product2", "Confidence2"])
offers.insert(0,'name', Series(names[i[0]] for i in data))Customize the display of this data and show the confidence forecast for each customer.
CSS = """
body {
margin: 0;
font-family: Helvetica;
}
table.dataframe {
border-collapse: collapse;
border: none;
}
table.dataframe tr {
border: none;
}
table.dataframe td, table.dataframe th {
margin: 0;
border: 1px solid white;
padding-left: 0.25em;
padding-right: 0.25em;
}
table.dataframe th:not(:empty) {
background-color: #fec;
text-align: left;
font-weight: normal;
}
table.dataframe tr:nth-child(2) th:empty {
border-left: none;
border-right: 1px dashed #888;
}
table.dataframe td {
border: 2px solid #ccf;
background-color: #f4f4ff;
}
table.dataframe thead th:first-child {
display: none;
}
table.dataframe tbody th {
display: none;
}
"""from IPython.core.display import HTML
HTML('<style>{}</style>'.format(CSS))
from IPython.display import display
try:
display(offers.drop(columns='customerid').sort_values(by='name')) #Pandas >= 0.17
except:
display(offers.drop(columns='customerid').sort('name')) #Pandas < 0.17| name | Product1 | Confidence1 | Product2 | Confidence2 | |
|---|---|---|---|---|---|
| 17 | Cassio Lombardo | Pension | 0.13221 | Mortgage | 0.10675 |
| 7 | Christian Austerlitz | Pension | 0.13221 | Mortgage | 0.10675 |
| 24 | Earl B. Wood | Savings | 0.95678 | Pension | 0.83426 |
| 19 | Eldar Muravyov | Pension | 0.13221 | Mortgage | 0.10675 |
| 6 | Fabien Mailhot | Pension | 0.13221 | Mortgage | 0.10675 |
| 26 | Franca Palermo | Pension | 0.13221 | Mortgage | 0.10675 |
| 25 | Gabrielly Sousa Martins | Savings | 0.95678 | Pension | 0.75925 |
| 13 | George Blomqvist | Savings | 0.16428 | Pension | 0.13221 |
| 0 | Guadalupe J. Martinez | Pension | 0.13221 | Mortgage | 0.10675 |
| 21 | Jameel Abdul-Ghani Gerges | Pension | 0.13221 | Mortgage | 0.10675 |
| 10 | Lee Tsou | Pension | 0.13221 | Mortgage | 0.10675 |
| 23 | Matheus Azevedo Melo | Pension | 0.13221 | Mortgage | 0.10675 |
| 1 | Michelle M. Lopez | Savings | 0.95678 | Pension | 0.84446 |
| 3 | Miranda B. Roush | Pension | 0.13221 | Mortgage | 0.10675 |
| 12 | Miroslav Škaroupka | Savings | 0.95676 | Mortgage | 0.82269 |
| 5 | Roland Guérette | Pension | 0.13221 | Mortgage | 0.10675 |
| 11 | Sanaa’ Hikmah Hakimi | Pension | 0.13221 | Mortgage | 0.10675 |
| 4 | Sandra J. Wynkoop | Pension | 0.80506 | Savings | 0.28391 |
| 20 | Shu T’an | Savings | 0.95675 | Pension | 0.27248 |
| 8 | Steffen Meister | Pension | 0.13221 | Mortgage | 0.10675 |
| 2 | Terry L. Ridgley | Savings | 0.95678 | Pension | 0.80233 |
| 18 | Trinity Zelaya Miramontes | Savings | 0.28934 | Pension | 0.13221 |
| 16 | Vlad Alekseeva | Pension | 0.13221 | Mortgage | 0.10675 |
| 14 | Will Henderson | Savings | 0.95678 | Pension | 0.86779 |
| 9 | Wolfgang Sanger | Pension | 0.13221 | Mortgage | 0.10675 |
| 15 | Yuina Ohira | Pension | 0.13225 | Mortgage | 0.10675 |
| 22 | Zeeb Longoria Marrero | Savings | 0.16188 | Pension | 0.13221 |
Use IBM Decision Optimization CPLEX Modeling for Python
Create the optimization model to select the best ways to contact customers and stay within the limited budget.
Step 1: Import the library
Run the following code to import the Decision Optimization CPLEX Modeling library. The DOcplex library contains the two modeling packages, Mathematical Programming (docplex.mp) and Constraint Programming (docplex.cp).
import sys
try:
import docplex.mp
except:
raise Exception('Please install docplex. See https://pypi.org/project/docplex/')If cplex is not installed, you can install CPLEX Community edition.
try:
import cplex
except:
raise Exception('Please install CPLEX. See https://pypi.org/project/cplex/')Step 2: Set up the prescriptive model
Create the model
from docplex.mp.model import Model
mdl = Model(name="marketing_campaign")
mdl.round_solution = True # make sure integer vars are automatically roundedDefine the decision variables
- The integer decision variables
channel_vars, represent whether or not a customer will be made an offer for a particular product via a particular channel. - The integer decision variable
total_offersrepresents the total number of offers made. - The continuous variable
budget_pentrepresents the total cost of the offers made.
offersR = range(0, len(offers))
productsR = range(0, len(products))
channelsR = range(0, len(channels))
# names of channels
channel_names = channels['name'].tolist()
# this function is used to coin names for channel variables
def name_chan_var(opch):
offer, p, ch = opch
return f"ch_{offer}_{p}_{channel_names[ch]}"
channel_vars = mdl.binary_var_cube(offersR, productsR, channelsR, name=name_chan_var)
total_offers = mdl.integer_var(name="total_offers")
budget_spent = mdl.continuous_var(name="spent")
def name_prod_budget(p):
return f"product_budget_{products[p]}"
budget_per_product = mdl.continuous_var_list(productsR, name=name_prod_budget)
mdl.print_information()Model: marketing_campaign
- number of variables: 248
- binary=243, integer=1, continuous=4
- number of constraints: 0
- linear=0
- parameters: defaults
- objective: none
- problem type is: MILP
Set up the constraints
- Offer only one product per customer.
- Compute the budget and set a maximum for it.
- Compute the number of offers to be made.
# Only 1 product is offered to each customer
mdl.add( mdl.sum(channel_vars[o,p,c] for p in productsR for c in channelsR) <= 1
for o in offersR)
# total offers is simply sum of channel vars
mdl.add( total_offers == mdl.sum(channel_vars))
# define per product budgets
for p in productsR:
mdl.add(budget_per_product[p] == mdl.sum(channel_vars[o, p, c] * channels.at[c, "cost"]
for o in offersR
for c in channelsR))
mdl.add( budget_spent == mdl.sum(budget_per_product))
# Balance the offers among products
assert sum(budget_share) == 1 # shares equal 1
for p in productsR:
mdl.add( mdl.sum(channel_vars[o,p,c] for o in offersR for c in channelsR)
<= budget_share[p] * total_offers )
# Do not exceed the budget
mdl.add_constraint( budget_spent <= available_budget )
mdl.print_information()Model: marketing_campaign
- number of variables: 248
- binary=243, integer=1, continuous=4
- number of constraints: 36
- linear=36
- parameters: defaults
- objective: none
- problem type is: MILP
Express the objective
Maximize the expected revenue.
expected_p1 = mdl.sum( channel_vars[idx,p,idx2] * c.factor * product_value[p]* o.Confidence1
for p in productsR
for idx,o in offers[offers['Product1'] == products[p]].iterrows()
for idx2, c in channels.iterrows())
expected_p2 = mdl.sum( channel_vars[idx,p,idx2] * c.factor * product_value[p]* o.Confidence2
for p in productsR
for idx,o in offers[offers['Product2'] == products[p]].iterrows()
for idx2, c in channels.iterrows())
mdl.maximize(expected_p1 + expected_p2)Define KPIs
KPIs are numbers, computed to give insights, not necessarily used in the optimization process.
for p in productsR:
mdl.add_kpi(budget_per_product[p], budget_per_product[p].name)
mdl.add_kpi(expected_p1, "Expected P1 return");
mdl.add_kpi(expected_p2, "Expected P2 return");Solve the model
Depending on the size of the problem, the solve stage might fail and require the Commercial Edition of CPLEX engines, which is included in the premium environments in Watson Studio.
s = mdl.solve(log_output=True)
assert s, "No Solution !!!"WARNING: Number of workers has been reduced to 2 to comply with platform limitations.
Version identifier: 22.1.1.0 | 2023-06-15 | d64d5bd77
CPXPARAM_Read_DataCheck 1
CPXPARAM_Threads 2
Found incumbent of value 0.000000 after 0.00 sec. (0.01 ticks)
Tried aggregator 2 times.
MIP Presolve eliminated 1 rows and 55 columns.
MIP Presolve modified 3 coefficients.
Aggregator did 3 substitutions.
Reduced MIP has 32 rows, 190 columns, and 760 nonzeros.
Reduced MIP has 189 binaries, 1 generals, 0 SOSs, and 0 indicators.
Presolve time = 0.01 sec. (1.36 ticks)
Probing time = 0.00 sec. (0.40 ticks)
Tried aggregator 1 time.
Detecting symmetries...
Reduced MIP has 32 rows, 190 columns, and 760 nonzeros.
Reduced MIP has 189 binaries, 1 generals, 0 SOSs, and 0 indicators.
Presolve time = 0.01 sec. (0.59 ticks)
Probing time = 0.00 sec. (0.40 ticks)
Clique table members: 27.
MIP emphasis: balance optimality and feasibility.
MIP search method: dynamic search.
Parallel mode: deterministic, using up to 2 threads.
Root relaxation solution time = 0.00 sec. (0.31 ticks)
Nodes Cuts/
Node Left Objective IInf Best Integer Best Bound ItCnt Gap
* 0+ 0 0.0000 2946.6993 ---
0 0 862.3006 5 0.0000 862.3006 30 ---
0 0 844.9441 9 0.0000 Cuts: 7 57 ---
0 0 844.9441 11 0.0000 Cuts: 6 65 ---
* 0 0 integral 0 844.4226 Cuts: 3 75 0.00%
* 0+ 0 844.4226 844.4226 0.00%
0 0 cutoff 844.4226 844.4226 75 0.00%
Elapsed time = 0.07 sec. (10.39 ticks, tree = 0.01 MB, solutions = 3)
Cover cuts applied: 1
Mixed integer rounding cuts applied: 2
Zero-half cuts applied: 3
Lift and project cuts applied: 1
Gomory fractional cuts applied: 1
Root node processing (before b&c):
Real time = 0.08 sec. (10.41 ticks)
Parallel b&c, 2 threads:
Real time = 0.00 sec. (0.00 ticks)
Sync time (average) = 0.00 sec.
Wait time (average) = 0.00 sec.
------------
Total (root+branch&cut) = 0.08 sec. (10.41 ticks)
# print objectives and kpis
mdl.report()* model marketing_campaign solved with objective = 844.423
* KPI: product_budget_Savings = 92.000
* KPI: product_budget_Mortgage = 230.000
* KPI: product_budget_Pension = 138.000
* KPI: Expected P1 return = 190.942
* KPI: Expected P2 return = 653.480
Step 3: Analyze the solution
First, display the Optimal Marketing Channel per customer.
report = [(channels.at[c, "name"], products[p], names[offers.at[o, "customerid"]])
for c in channelsR
for p in productsR
for o in offersR if abs(channel_vars[o,p,c].solution_value-1) <= 1e-6]
assert len(report) == round(total_offers.solution_value)
print("Marketing plan has {0} offers costing {1}".format(total_offers.solution_value, budget_spent.solution_value))
report_bd = DataFrame(report, columns=['channel', 'product', 'customer'])
display(report_bd)Marketing plan has 20.0 offers costing 460.0
| channel | product | customer | |
|---|---|---|---|
| 0 | seminar | Savings | George Blomqvist |
| 1 | seminar | Savings | Trinity Zelaya Miramontes |
| 2 | seminar | Savings | Shu T’an |
| 3 | seminar | Savings | Zeeb Longoria Marrero |
| 4 | seminar | Mortgage | Guadalupe J. Martinez |
| 5 | seminar | Mortgage | Roland Guérette |
| 6 | seminar | Mortgage | Fabien Mailhot |
| 7 | seminar | Mortgage | Steffen Meister |
| 8 | seminar | Mortgage | Wolfgang Sanger |
| 9 | seminar | Mortgage | Lee Tsou |
| 10 | seminar | Mortgage | Miroslav Škaroupka |
| 11 | seminar | Mortgage | Vlad Alekseeva |
| 12 | seminar | Mortgage | Matheus Azevedo Melo |
| 13 | seminar | Mortgage | Franca Palermo |
| 14 | seminar | Pension | Michelle M. Lopez |
| 15 | seminar | Pension | Terry L. Ridgley |
| 16 | seminar | Pension | Sandra J. Wynkoop |
| 17 | seminar | Pension | Will Henderson |
| 18 | seminar | Pension | Earl B. Wood |
| 19 | seminar | Pension | Gabrielly Sousa Martins |
Now focus on seminar.
display(report_bd[report_bd['channel'] == "seminar"].drop(columns='channel'))| product | customer | |
|---|---|---|
| 0 | Savings | George Blomqvist |
| 1 | Savings | Trinity Zelaya Miramontes |
| 2 | Savings | Shu T’an |
| 3 | Savings | Zeeb Longoria Marrero |
| 4 | Mortgage | Guadalupe J. Martinez |
| 5 | Mortgage | Roland Guérette |
| 6 | Mortgage | Fabien Mailhot |
| 7 | Mortgage | Steffen Meister |
| 8 | Mortgage | Wolfgang Sanger |
| 9 | Mortgage | Lee Tsou |
| 10 | Mortgage | Miroslav Škaroupka |
| 11 | Mortgage | Vlad Alekseeva |
| 12 | Mortgage | Matheus Azevedo Melo |
| 13 | Mortgage | Franca Palermo |
| 14 | Pension | Michelle M. Lopez |
| 15 | Pension | Terry L. Ridgley |
| 16 | Pension | Sandra J. Wynkoop |
| 17 | Pension | Will Henderson |
| 18 | Pension | Earl B. Wood |
| 19 | Pension | Gabrielly Sousa Martins |
Summary
You have learned how to set up and use IBM Decision Optimization CPLEX Modeling for Python to formulate a Mathematical Programming model and solve it with CPLEX.
References
- CPLEX Modeling for Python documentation
- IBM Decision Optimization
- Need help with DOcplex or to report a bug? Please go here.
Copyright © 2017-2024 IBM. IPLA licensed Sample Materials.