{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "963d23cf-65a6-4ca4-ba2d-d604ec9be655", "metadata": { "id": "x1bd_c_mjCvJ" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Last updated: Tue Dec 14 2021\n", "\n", "Python implementation: CPython\n", "Python version : 3.9.9\n", "IPython version : 7.27.0\n", "\n", "gurobipy: 9.5.0\n", "\n", "Watermark: 2.2.0\n", "\n" ] } ], "source": [ "import gurobipy as gp\n", "from gurobipy import GRB\n", "\n", "%load_ext watermark\n", "%watermark -n -u -v -iv -w" ] }, { "cell_type": "markdown", "id": "565f58ec-659d-4b6a-8e4b-e38a9196536e", "metadata": {}, "source": [ "# Data" ] }, { "cell_type": "markdown", "id": "9b0b42e2-2ed9-48ee-8c6a-0c771c32160b", "metadata": {}, "source": [ "## Product and cluster sets" ] }, { "cell_type": "code", "execution_count": 2, "id": "92f05341-f63b-441e-b1c7-24f7718e310d", "metadata": {}, "outputs": [], "source": [ "products = ['p1', 'p2']\n", "clusters = ['k1', 'k2']" ] }, { "cell_type": "markdown", "id": "317ec642-8fdf-4846-8ced-ce23dfb9cc02", "metadata": {}, "source": [ "## Expected profit per cluster and product" ] }, { "cell_type": "code", "execution_count": 3, "id": "0fca665c-08b2-4d1b-807a-f8a6f08a6038", "metadata": { "id": "9Kx_owENjCvK" }, "outputs": [], "source": [ "cp, expected_profit = gp.multidict({\n", " ('k1', 'p1'): 2000,\n", " ('k1', 'p2'): 1000,\n", " ('k2', 'p1'): 3000,\n", " ('k2', 'p2'): 2000\n", "})" ] }, { "cell_type": "markdown", "id": "64aa442d-f8b3-4614-a06b-8a2d916f6a93", "metadata": {}, "source": [ "## Expected cost per cluster and product" ] }, { "cell_type": "code", "execution_count": 4, "id": "dec6550d-a7f7-4ea1-a01a-72b65866ec3d", "metadata": { "id": "3NejHPuKjCvL" }, "outputs": [], "source": [ "cp, expected_cost = gp.multidict({\n", " ('k1', 'p1'): 200,\n", " ('k1', 'p2'): 100,\n", " ('k2', 'p1'): 300,\n", " ('k2', 'p2'): 200\n", "})" ] }, { "cell_type": "markdown", "id": "21317d67-1ddc-4b65-bd49-af77331c0bd8", "metadata": {}, "source": [ "## Number of customers in each cluster" ] }, { "cell_type": "code", "execution_count": 5, "id": "dee8eec0-5a35-4357-8c05-d0a7093034c0", "metadata": { "id": "wuFfsCCmjCvM" }, "outputs": [], "source": [ "clusters, number_customers = gp.multidict({\n", " ('k1'): 5,\n", " ('k2'): 5\n", "})" ] }, { "cell_type": "markdown", "id": "7d933758-7d04-469f-95ca-ef6c63b60d44", "metadata": {}, "source": [ "## Minimum number offers for each product" ] }, { "cell_type": "code", "execution_count": 6, "id": "dd4face1-baf8-4bae-a6c5-9773b4389bc1", "metadata": { "id": "IJa_hZumjCvM" }, "outputs": [], "source": [ "products, min_offers = gp.multidict({\n", " ('p1'): 2,\n", " ('p2'): 2\n", "})" ] }, { "cell_type": "markdown", "id": "a33ead68-48e4-4fe1-a555-042739cdff45", "metadata": { "id": "AygTRhcVjCvN" }, "source": [ "## Hurdle-rate is twenty percent and budget available for the marketing campaign\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "e978b251-6f20-4fa9-a6da-7d944ab7906a", "metadata": { "id": "5KpWIPUcjCvN" }, "outputs": [], "source": [ "# Define the corporate hurdle-rate\n", "R = 0.20\n", "\n", "# Define the value of budget available for campaign\n", "budget = 200 " ] }, { "cell_type": "markdown", "id": "ce4bf59f-8960-4591-bcad-f55d6bc5db3a", "metadata": { "id": "st89srb0jCvN" }, "source": [ "# Model Formulation" ] }, { "cell_type": "code", "execution_count": 8, "id": "fd522d41-e405-477a-8be0-b537c3f7bc96", "metadata": { "id": "mcy9dcrrjCvN", "outputId": "85228153-dacb-4187-fab2-2b366ec17c6d" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Restricted license - for non-production use only - expires 2023-10-25\n" ] } ], "source": [ "# Declare and initialize model\n", "mt = gp.Model('Tactical')\n", "\n", "# Define the decisions variables\n", "\n", "# Allocation of product offers to customers in clusters.\n", "y = mt.addVars(cp, name=\"allocate\")\n", "\n", "# Budget correction\n", "z = mt.addVar(name=\"budget_correction\")" ] }, { "cell_type": "markdown", "id": "25581dda-bda2-4e32-9ed0-55c016ebf50b", "metadata": {}, "source": [ "## Constraints" ] }, { "cell_type": "markdown", "id": "a5dcfef8-70c6-4117-90a3-6075fa8d37e8", "metadata": { "id": "1jwAnJ9ujCvO", "tags": [] }, "source": [ "### Max number of offers for each cluster" ] }, { "cell_type": "code", "execution_count": 9, "id": "1af996f6-5dc7-44fe-a828-535bc30afc5e", "metadata": { "id": "nPmOsmDjjCvP" }, "outputs": [], "source": [ "max_offers_c = mt.addConstrs((y.sum(k,'*') <= number_customers[k] for k in clusters), name='max_offers')" ] }, { "cell_type": "markdown", "id": "e5b2d7a4-6e14-4f5f-b892-0b05ee894cdd", "metadata": { "id": "gAR2Kx9HjCvP" }, "source": [ "### Max Budget" ] }, { "cell_type": "code", "execution_count": 10, "id": "cdcb1a6e-910c-4dba-aa9a-646969170b30", "metadata": { "id": "qcqhHVdCjCvP" }, "outputs": [], "source": [ "budget_c = mt.addConstr((y.prod(expected_cost) - z <= budget), name='budget')" ] }, { "cell_type": "markdown", "id": "c5785873-b951-4519-8c56-d716217dbd5d", "metadata": { "id": "Jw_8f7BHjCvP" }, "source": [ "### Minimum number of offers of each product" ] }, { "cell_type": "code", "execution_count": 11, "id": "ea614b40-1028-4621-b957-0bb50d623a0d", "metadata": { "id": "Rsv_JEakjCvQ" }, "outputs": [], "source": [ "min_offers_c = mt.addConstrs( (y.sum('*',j) >= min_offers[j] for j in products), name='min_offers')" ] }, { "cell_type": "markdown", "id": "9b020a6a-82d4-4dfa-b27e-d6427af66742", "metadata": { "id": "Nyb8VkuRjCvQ" }, "source": [ "### Minimum ROI" ] }, { "cell_type": "code", "execution_count": 12, "id": "ebada7d8-fa54-47be-b41a-b2c25067cb55", "metadata": { "id": "CWKyiLfOjCvQ" }, "outputs": [], "source": [ "roi_c = mt.addConstr((y.prod(expected_profit) - (1 + R)*y.prod(expected_cost) >= 0), name='roi')" ] }, { "cell_type": "markdown", "id": "bc2b49b7-f526-4ed1-a354-e3102f6bfc05", "metadata": { "id": "7ZyNh-qRjCvR" }, "source": [ "## Objective Function" ] }, { "cell_type": "code", "execution_count": 13, "id": "ef41e858-464e-4a05-b12c-f29b299c7e38", "metadata": { "id": "WLsgMEHajCvR" }, "outputs": [], "source": [ "# Maximize total expected profit\n", "\n", "# The value of 𝑀 should be higher than any of the expected profits to ensure \n", "# that the budget is increased only when the model is infeasible if this parameter \n", "# is not increased.\n", "M = 10000\n", "\n", "mt.setObjective(y.prod(expected_profit) -M*z, GRB.MAXIMIZE)" ] }, { "cell_type": "code", "execution_count": 14, "id": "a85c6838-5a53-4776-b7e4-cb6d7bfdc2fa", "metadata": { "id": "5dY26d5XjCvR" }, "outputs": [], "source": [ "# Verify model formulation\n", "\n", "mt.write('tactical.lp')" ] }, { "cell_type": "markdown", "id": "13b4f5de-0c0d-47e1-967f-499cb6c37509", "metadata": {}, "source": [ "# Optimize the Campaign" ] }, { "cell_type": "code", "execution_count": 15, "id": "e2a92f7d-7958-4d96-9a39-8f78fc05768b", "metadata": { "id": "d59476ZvjCvS", "outputId": "3300291b-e8cd-4596-c859-fcd4ae583110" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gurobi Optimizer version 9.5.0 build v9.5.0rc5 (linux64)\n", "Thread count: 8 physical cores, 8 logical processors, using up to 8 threads\n", "Optimize a model with 6 rows, 5 columns and 17 nonzeros\n", "Model fingerprint: 0x168ba328\n", "Coefficient statistics:\n", " Matrix range [1e+00, 3e+03]\n", " Objective range [1e+03, 1e+04]\n", " Bounds range [0e+00, 0e+00]\n", " RHS range [2e+00, 2e+02]\n", "Presolve removed 1 rows and 0 columns\n", "Presolve time: 0.09s\n", "Presolved: 5 rows, 5 columns, 13 nonzeros\n", "\n", "Iteration Objective Primal Inf. Dual Inf. Time\n", " 0 2.5000000e+04 8.787500e+01 0.000000e+00 0s\n", " 4 -3.9940000e+06 0.000000e+00 0.000000e+00 0s\n", "\n", "Solved in 4 iterations and 0.24 seconds (0.00 work units)\n", "Optimal objective -3.994000000e+06\n" ] } ], "source": [ "mt.optimize()" ] }, { "cell_type": "markdown", "id": "893bd50c-bf2e-4a9e-bb9c-f5d4ed66ba09", "metadata": {}, "source": [ "## Allocation of Product Offers to Clusters" ] }, { "cell_type": "code", "execution_count": 29, "id": "8e4aad53-11e0-4697-aaf7-37a74dfc09d3", "metadata": { "id": "thxKOnz-jCvT", "outputId": "c45bc8f2-1036-4137-825e-88b610fc6551" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The number of customers in cluster k1 that gets an offer of product p1 is: 2.0\n", "The number of customers in cluster k1 that gets an offer of product p2 is: 2.0\n", "The number of customers in cluster k2 that gets an offer of product p1 is: 0.0\n", "The number of customers in cluster k2 that gets an offer of product p2 is: 0.0\n" ] } ], "source": [ "total_expected_profit = 0\n", "total_expected_cost = 0\n", "\n", "for k,p in cp:\n", "# if y[k,p].x > 1e-6:\n", " print(f\"The number of customers in cluster {k} that gets an offer of product {p} is: {y[k,p].x}\")\n", " total_expected_profit += expected_profit[k,p]*y[k,p].x\n", " total_expected_cost += expected_cost[k,p]*y[k,p].x" ] }, { "cell_type": "markdown", "id": "515e3be9-9a38-4491-94da-bf6428222ebd", "metadata": {}, "source": [ "## Financial Reports" ] }, { "cell_type": "code", "execution_count": 30, "id": "bc09d04c-ca6a-4c80-b323-9bc0328397e4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The increase correction in the campaign budget is $400.00.\n", "Optimal total expected profit is $6,000.00.\n", "Optimal total expected cost is $600.00 with a budget of $200.00 and an extra amount of $400.00.\n", "Optimal ROI is 1000.0% with a minimum ROI of 120.0%.\n" ] } ], "source": [ "increased_budget = '${:,.2f}'.format(z.x)\n", "optimal_ROI = round(100*total_expected_profit/total_expected_cost,2)\n", "min_ROI = round(100*(1+R),2)\n", "\n", "money_expected_profit = '${:,.2f}'.format(total_expected_profit)\n", "money_expected_cost = '${:,.2f}'.format(total_expected_cost)\n", "money_budget = '${:,.2f}'.format(budget)\n", "\n", "print(f\"The increase correction in the campaign budget is {increased_budget}.\")\n", "print(f\"Optimal total expected profit is {money_expected_profit}.\")\n", "print(f\"Optimal total expected cost is {money_expected_cost} with a budget of {money_budget} and an extra amount of {increased_budget}.\")\n", "print(f\"Optimal ROI is {optimal_ROI}% with a minimum ROI of {min_ROI}%.\")" ] }, { "cell_type": "code", "execution_count": null, "id": "480d8ce9-cfd5-44e2-aa74-a2edbeae5356", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.9" }, "toc-autonumbering": true, "toc-showcode": false }, "nbformat": 4, "nbformat_minor": 5 }