{ "cells": [ { "attachments": {}, "cell_type": "markdown", "id": "1682b237", "metadata": {}, "source": [ "#
Global Supply Chain: Optimizing for Costs Under Known Constraints Using PuLP" ] }, { "attachments": {}, "cell_type": "markdown", "id": "6fd5cd37", "metadata": {}, "source": [ "This notebook develops a linear programming (LP) algorithm that automates and streamlines decision-making for a supply chain system of a major retailer. I use PuLP, a linear programming modeler written in Python, to design an optimization algorithm. The retailer uses the services of five carriers. These carriers ship containers across 290 Asia-U.S. routes. The retailer would like to have software to determine which carrier should supply which route to minimize the whole operation costs while satisfying a set of constraints. " ] }, { "attachments": {}, "cell_type": "markdown", "id": "b9368f19-4b93-4a46-9c71-7147ff0e1cb2", "metadata": {}, "source": [ "### Data Description\n", "The dataset contains 290 instances of Asia-US routes. The route column indicates route id, while demand columns reflect the number of containers the retailer wants to ship using a given route. The dataset also contains information about five carries: A, B, C, D, and E. Capacity columns show the number of containers a given carrier can ship using a given route, while cost columns indicate shipments costs. The dataset has no missing values.\n", "\n", "The data is based on shipment carriers' information about their current shipping prices and capacity for each route. After the retailer receives this information from carriers, a data analyst compiles a spreadsheet, such as the one used in the current notebook. This information is provided periodically with updates for the next period in spreadsheets." ] }, { "cell_type": "code", "execution_count": 1, "id": "04861300", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
routedemandcapacity_Acost_Acapacity_Bcost_Bcapacity_Ccost_Ccapacity_Dcost_Dcapacity_Ecost_E
1031041015712841571116.0621700.0671781.0911792.0
3536221572000531712.01052193.6532081.6532078.1
28428515861618561460.0621700.0671781.0911792.0
\n", "
" ], "text/plain": [ " route demand capacity_A cost_A capacity_B cost_B capacity_C \n", "103 104 10 157 1284 157 1116.0 62 \\\n", "35 36 22 157 2000 53 1712.0 105 \n", "284 285 15 86 1618 56 1460.0 62 \n", "\n", " cost_C capacity_D cost_D capacity_E cost_E \n", "103 1700.0 67 1781.0 91 1792.0 \n", "35 2193.6 53 2081.6 53 2078.1 \n", "284 1700.0 67 1781.0 91 1792.0 " ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# import Python modules\n", "from pulp import *\n", "import pandas as pd \n", "\n", "# read csv file and disply randomly selected rows\n", "df = pd.read_csv('data.csv')\n", "df.sample(3)" ] }, { "cell_type": "code", "execution_count": 2, "id": "26a41847", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 290 entries, 0 to 289\n", "Data columns (total 12 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 route 290 non-null int64 \n", " 1 demand 290 non-null int64 \n", " 2 capacity_A 290 non-null int64 \n", " 3 cost_A 290 non-null int64 \n", " 4 capacity_B 290 non-null int64 \n", " 5 cost_B 290 non-null float64\n", " 6 capacity_C 290 non-null int64 \n", " 7 cost_C 290 non-null float64\n", " 8 capacity_D 290 non-null int64 \n", " 9 cost_D 290 non-null float64\n", " 10 capacity_E 290 non-null int64 \n", " 11 cost_E 290 non-null float64\n", "dtypes: float64(4), int64(8)\n", "memory usage: 27.3 KB\n" ] } ], "source": [ "df.info()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "5df49b7f", "metadata": {}, "source": [ "### Preparation\n", "All variables are put into lists to prepare data for the optimization analysis. Then, cost, capacity, and demand are made into dictionaries." ] }, { "cell_type": "code", "execution_count": 3, "id": "9fb1ffaf-f95f-4819-b6b4-842128e16db1", "metadata": {}, "outputs": [], "source": [ "routes = df['route'].tolist() \n", "carriers = ['A', 'B', 'C', 'D', 'E']\n", "demand = df['demand'].tolist()\n", "\n", "# creates costs list of lists\n", "costs = []\n", "costs.append(df['cost_A'].tolist())\n", "costs.append(df['cost_B'].tolist())\n", "costs.append(df['cost_C'].tolist())\n", "costs.append(df['cost_D'].tolist())\n", "costs.append(df['cost_E'].tolist())\n", "\n", "# creates capacity list of lists\n", "capacity = []\n", "capacity.append(df['capacity_A'].tolist())\n", "capacity.append(df['capacity_B'].tolist())\n", "capacity.append(df['capacity_C'].tolist())\n", "capacity.append(df['capacity_D'].tolist())\n", "capacity.append(df['capacity_E'].tolist())\n", "\n", "# Creates list of keys\n", "carrier_route = [(c, r) for c in carriers for r in routes]\n", "\n", "# Data is made into dictionaries\n", "costs = makeDict([carriers,routes],costs,0)\n", "capacity = makeDict([carriers, routes],capacity,0)\n", "demand = makeDict([routes],demand,0)" ] }, { "attachments": {}, "cell_type": "markdown", "id": "06331439-82f1-4cf4-a4ac-5b72b865145c", "metadata": {}, "source": [ "### Model Initiation, Decision Variables, and Objective Function\n", "The model variable is created using the LpProblem function. Then, a dictionary called shipments is created that contains LP variables. Finally, the objective function is added to the variable model using a list comprehension." ] }, { "cell_type": "code", "execution_count": 4, "id": "eaf2ea75-470a-43c7-8edc-2b76a748d204", "metadata": {}, "outputs": [], "source": [ "# Initiates model\n", "model = LpProblem('supply_chain', LpMinimize)\n", "\n", "# Dictionary containing decision variables - numbers of shipments by a given carrier for a given route\n", "shipments = LpVariable.dicts('shipment',(carriers, routes),0,None,LpInteger)\n", "\n", "# Objective function (multiplies shipping cost for each route by a given carrier and sums all of those costs together)\n", "model +=lpSum([shipments[c][r]*costs[c][r] for c in carriers for r in routes])" ] }, { "attachments": {}, "cell_type": "markdown", "id": "d54ecd1d-6dd5-464b-8089-a9364fec7a1d", "metadata": {}, "source": [ "### Constraints\n", "The retailer needs to transport a set number of containers for each of the 290 routes. Each carrier has a limited container capacity per route of transportation. Also, carriers can charge different prices for their services. \n", "\n", "Given the high fixed costs of preparing transportation contracts, the retailer would like to work with only those carriers who transport at least a certain number of containers. The retailer prefers to ship at least 500 containers with the B carrier because of the higher quality service." ] }, { "cell_type": "code", "execution_count": 5, "id": "377ce6a5-00f6-4893-8874-22874a55ea6e", "metadata": {}, "outputs": [], "source": [ "# Maximimum number of shipments per route by a given carrier can transport\n", "for (c, r) in carrier_route:\n", " model += shipments[c][r] <= capacity[c][r]\n", " \n", "# Number of shipments that are required by the retailer per a given route\n", "for r in demand:\n", " model +=lpSum([shipments[c][r] for c in carriers]) >= demand[r]\n", "\n", "# Overall minimum number of shipments that should be transported by a carrier across routes\n", "for c in carriers:\n", " model += lpSum([shipments[c][r] for r in routes]) >= 10\n", " \n", "# Minimum number of shipments that should be transported by the B carrier across routes\n", "model += lpSum([shipments['B'][r] for r in routes]) >= 500" ] }, { "attachments": {}, "cell_type": "markdown", "id": "d7f1cd13-53b2-438b-b05d-0065ca25415a", "metadata": {}, "source": [ "### Solve the Problem\n", "Now that all the problem data is entered, the writeLP() function can be used to copy this information into a .lp file into the directory that your code-block is running from. The LP is solved using the solver that PuLP chooses." ] }, { "cell_type": "code", "execution_count": 6, "id": "cea7d735-52cb-4c06-be91-cfa27edf747e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# The problem data is written to an .lp file\n", "model.writeLP('supply_chain.lp')\n", "\n", "# The problem is solved using PuLP's choice of Solver\n", "model.solve()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "97fc5a39-6299-4b4c-9c54-e0d6c57d83a7", "metadata": {}, "source": [ "### Results\n", "The results of the solver call can be displayed as output. First, I request the status of the solution, which can be one of “Not Solved,” “Infeasible,” “Unbounded,” “Undefined” or “Optimal.” The optimized objective function value is printed to the screen using the value function. \n", "\n", "The variables and their resolved optimum values can now be printed. The printed output describes the number of containers that the retailer should ship with a given carrier for a given route to minimize costs under the given constraints (10 randomly selected rows are displayed). The output also contains the total costs of shipping all containers across all routes demanded by the retailer." ] }, { "cell_type": "code", "execution_count": 7, "id": "aec1b0c3-6206-451d-8d9f-0c2602f9896b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Status: Optimal\n", "Total shipment costs = 5312280\n" ] } ], "source": [ "# The status of the solution is printed \n", "print('Status:', LpStatus[model.status])\n", "\n", "# The optimised objective function value is printed \n", "print('Total shipment costs = ', round(value(model.objective)))" ] }, { "cell_type": "code", "execution_count": 10, "id": "337414b3", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
carrier and routeoptimum number of containers
257shipment_A_70.0
1197shipment_E_13210.0
774shipment_C_2740.0
449shipment_B_24216.0
1329shipment_E_2510.0
1080shipment_D_2890.0
132shipment_A_2180.0
1415shipment_E_680.0
240shipment_A_540.0
1156shipment_D_960.0
\n", "
" ], "text/plain": [ " carrier and route optimum number of containers\n", "257 shipment_A_7 0.0\n", "1197 shipment_E_132 10.0\n", "774 shipment_C_274 0.0\n", "449 shipment_B_242 16.0\n", "1329 shipment_E_251 0.0\n", "1080 shipment_D_289 0.0\n", "132 shipment_A_218 0.0\n", "1415 shipment_E_68 0.0\n", "240 shipment_A_54 0.0\n", "1156 shipment_D_96 0.0" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Each of the variables is displayed with it's resolved optimum value\n", "result = [(v.name, v.varValue) for v in model.variables()]\n", "result= pd.DataFrame(result, columns=['carrier and route', 'optimum number of containers'])\n", "result.sample(10)" ] }, { "cell_type": "code", "execution_count": null, "id": "f125e803", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "base", "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.11.2" }, "vscode": { "interpreter": { "hash": "2f2c054dce7f3ebd1e76d58cbab2bcb69b44b6a1fb131f28c8fa296b61f3b710" } } }, "nbformat": 4, "nbformat_minor": 5 }