How to make targeted offers to customers

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

link to the notebook ==> How to make targeted offers to customers github ipynb
#This program is from the IBM Decision Optimization Documentation
from 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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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 rounded

Define 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_offers represents the total number of offers made.
  • The continuous variable budget_pent represents 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
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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'))
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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

Copyright © 2017-2024 IBM. IPLA licensed Sample Materials.

https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015

Leave a comment