Building A Decision Support Tool For Capital Budgeting With Python Pulp
By: Zhijing Eu
Tags: Capital Budgeting, Pythonpulp, Python Programming, Optimization, Linear Programming

Summary

Optimization techniques like Linear Programming are often still the reliable work-horses behind many decision support systems. Unfortunately they often do not get the attention that they deserve when compared to fancy Machine Learning algorithms. In this article , we look at the basic principles of Linear Programming as applied to the Capital Budgeting Optimization problem and how to optimize Capital Budgeting with PuLP , a Python library for Linear Programming

Outline

Source : Alamy Stock Photos

1 What Is Capital Budgeting ?

Image Source : Dreamstime.com Stock Photos

The “‘Capital Budgeting” problem is a situation many organisations face where there is a long list of projects to be done but a limited budget (or other resources such as manpower) that constraints which projects can be executed.

Thus the challenge is how make the “best” selection of projects in the portfolio under these scarce resources to maximize value for the company

2 What Is Linear /Integer Programming ?

There are a number of approaches to optimizing Capital Budgeting process but Linear Programming is relatively straight-forward to apply and intuitive to understand.

Linear Programming is an technique that can be used to solve optimisation problems if the relationships (i.e ≤ , ≥ , =) between the variables are linear in nature (i.e X + Y = Z rather than X² + Y³ = Z which would be non-linear)

For example, as per the below — if the objective is to maximize/minimize the “y” variable, all that needs to be done is to move a straight horizontal line up and down and reading off the y coordinate (y max = 6 or y min = 3) for the intersect with the grey triangle

Image Source : http://www.purplemath.com/modules/linprog.htm

Binary Integer Linear Programming is a special case of Linear Programming where the decision variables are constrained to be either 1 or 0 and is the main approach that can be used to ‘solve’ the Capital Budgeting Optimization Problem

3 Optimizing Capital Budgeting Using Python PuLP

If you want to follow along , the source code and input files are available at this link :

3.1 Why Python ?

Some of you may be familiar with Excel’s Solver Add-In which is a tool that provides an easy way to model Optimization problems.

However there are a number of issues with using spreadsheets to run your optimization models:

Spreadsheets ‘couple up’ the data model and the logic of the solver model — while this is sometimes convenient for ad hoc modelling, this can limit the ability to generalize the model across larger datasets as the ‘logic’ is closely tied to how the data looks

Unlike programming code that have more robust version control tools like Git or SVN , revision management for spreadsheets is limited making errors more likely especially if multiple parties work on the same model (It is not uncommon too that VBA code is used IN ADDITION to the Solver Add-In making things messier)

Spreadsheets are (generally) “stand-alone” tools whereas a programming language like Python can allow you to move information to and from databases or visualization tools etc

Image Source : https://www.decusoft.com/nightmare-on-spreadsheet/

Therefore instead of Excel, we will use PuLP a Python programming library for Linear Programming to model the problem.

3.2 Problem Description

A company has 5 potential projects that each have individual CAPEX cost phasing and NPV estimates as follows:

A shortlist of these projects that best maximizes the total NPV has to be selected with these constraints:-

a) There is a 3 Yr CAPEX threshold that needs to be met for each year for 10Mil , 10 Mil and 6 Mil respectively

b) Projects 1 & 2 are CONTINGENT on one another i.e must either be selected together or not at all

c) There Projects 3 and 5 are MUTUALLY EXCLUSIVE i.e cannot be selected together (although both could be not selected as well)

3.3. Translating The Problem Into Relationships

The “Decision Variable” is what we are trying to solve. Here it’s the Selection Status for all 5 projects which we can model as a a list = [ StatusProject1, StatusProject2, …., StatusProject5] where each row is either 1 (Yes) or 0 (No)

The “Objective” we are trying to maximize is the NPV so it is just sum of Selection Status of each project multiplied by the NPV of each project. (i.e the yellow cell in the table above)

Total NPV = SUM ( [Selection Status] X [NPV] For Each Project)

Constraints : For Constraint (a), it is the similar where it is the sum of each CAPEX Yr 1, 2, 3 multiplied by Selection Status (Blue cells) which must be less than the Annual Limits (Green cells)

Total Selected CAPEX Yr 1 =

SUM ([Selection Status]X[CAPEX Yr 1] For Each Project) ...and so on for Yr 2 , 3

For Constraint (b) , it is handled by saying that

Selection Status[Project1] = SelectionStatus[Project2]

because this means they will always either be selected or not selected together

and constraint(c)

Selection Status[Project3] + SelectionStatus[Project5] <= 1

because this means that either BOTH are not selected so 0+0 ≤ 1 or only one of two can be ‘on’ so 0+1 ≤ 1 or 1+ 0 ≤ 1 they are mutually exclusive

3.4. Representing The Problem In Python PuLP

Not Me — Even Though I Wish I Had 3 Screens (Source : Dreamstime.Com Stock Image)

The full code as follows

Steps 1 & 2 are straightforward as it is just loading libraries and getting the data into Python.

#Step 1 : Import Libraries

import pandas as pd

import numpy as np

import pulp #Step 2: Load Data for Project List and Yrly CAPEX Limits

proj_list=

pd.read_csv('Example1_ProjList.csv', index_col=['ID']).fillna(0) capexconstraints=

pd.read_csv('Example1_AnnualCAPEX.csv')['CAPEX'].values.tolist()

Step 3 is where it begins to get a bit interesting as we are starting to builds list of all the pairs of Mutually Exclusive Projects , Contingent Projects , etc. This will be something that we make use of later…

#Step 3: Build Sub-Lists Of Projects With Dependency Relationships relationships=proj_list[['Relationship','RelationshipProjID']].dropna(thresh=2)

relationships['RelationshipProjID2']=relationships.index MutuallyExclusive=relationships.loc[relationships['Relationship'] == 'Mutually_Exclusive'].sort_values(['RelationshipProjID2'])

MutuallyExclProjectsList=list(set(map(lambda x: tuple(sorted(x)),[[i , j] for i, j in zip(MutuallyExclusive['RelationshipProjID2'].values.tolist(), MutuallyExclusive['RelationshipProjID'].values.tolist())]))) Contingent=relationships.loc[relationships['Relationship'] == 'Contingent'].sort_values(['RelationshipProjID2'])

ContingentProjectsList = list(set(map(lambda x: tuple(sorted(x)),[[i , j] for i, j in zip(Contingent['RelationshipProjID2'].values.tolist(), Contingent['RelationshipProjID'].values.tolist())]))) Mandatory=relationships.loc[relationships['Relationship'] == 'Mandatory'].sort_values(['RelationshipProjID2'])

MandatoryProjectsList = Mandatory['RelationshipProjID2'].values.tolist()

In Steps 4–5 is that actual PuLP code and the process is similar where the Decision Variables and Objectives are being defined.

# Step 4: Create A Model phasing = pulp.LpProblem("Maximise", pulp.LpMaximize) Selection = pulp.LpVariable.dicts("Selection", proj_list.index, cat='Binary') # Step 5 : Set The Objective Function

phasing += pulp.lpSum(Selection[idx]*proj_list.loc[idx]["NPV"] for idx in proj_list.index)

Step 6 is the most interesting one because that rather than DEFINING each constraint line by line , the code uses the power of Python programming to iterate over the constraints…

For example, for Mutually Exclusive Projects, the code does not explicitly say Selection Status[Project3] + SelectionStatus[Project5] = 1 but instead, the code uses the list of Mutually Exclusive Projects and passes the pairs into a loop to assign the relationship.

Therefore — the logic of the solver model is now generalized without being tied to the input data format (i.e no of rows or even no of columns)

# Step 6 : Set The Constraints #Loop over the individual Years

for m in range(0,len(capexconstraints)):

year=str(m+1)

phasing += sum([Selection[idx] * proj_list.loc[idx]["CAPEX Yr"+year] for idx in proj_list.index]) <= capexconstraints[m] # Loop over for mutually exclusive projects

for i in range(0,len(MutuallyExclProjectsList)):

phasing += Selection[MutuallyExclProjectsList[i][0]] + Selection[MutuallyExclProjectsList[i][1]] == 1



# Loop over contingent projects

for j in range(0,len(ContingentProjectsList)):

phasing += Selection[ContingentProjectsList[j][0]] == Selection[ContingentProjectsList[j][1]] # Loop over mandatory projects for k in range(0,len(MandatoryProjectsList)):

phasing += Selection[MandatoryProjectsList[k]] == 1 # Step 7: Run The Solver(s) %time phasing.solve() #equivalent to phasing.solve(pulp.PULP_CBC_CMD()) as CBC is PulP's default solver pulp.LpStatus[phasing.status] # Print our objective function value and Output Solution

print (pulp.value(phasing.objective))

The final step after PulP runs the solving algorithm is to output the data into a user friendly format.

# Step 8 : Convert output into user friendly output for viewing or downloading

outputtablecolumns=[]

outputtablecolumns.append('Selection Y/N')

for n in range(0,len(capexconstraints)):

year=str(n+1)

outputtablecolumns.append("CAPEX Yr"+year+" Selected")

outputtablecolumns.append('NPV Selected') pulpsolution=pd.DataFrame()

pulpsolution['Selection Y/N']= [Selection[idx].value() for idx in proj_list.index] for n in range(0,len(capexconstraints)):

year=str(n+1)

pulpsolution["CAPEX Yr"+year+" Selected"]= [Selection[idx].value()*proj_list.loc[idx]["CAPEX Yr"+year] for idx in proj_list.index] pulpsolution['NPV Selected']= [Selection[idx].value()*proj_list.loc[idx]["NPV"] for idx in proj_list.index] pulpsolution pulpsolution.index += 1

pulpsolution pulpoutput = pd.concat([proj_list, pulpsolution], axis=1)

pulpoutput yearSumCapexColumns=[]

for p in range(0,len(capexconstraints)):

year=str(p+1)

yearSumCapexColumns.append("CAPEX Yr"+year+" Selected")

yearSumCapexColumns CAPEX_Totals=[pulpsolution[yr].sum() for yr in yearSumCapexColumns] CAPEX_CheckSum= pd.DataFrame()

CAPEX_CheckSum['Year'] = np.arange(len(yearSumCapexColumns))+1

CAPEX_CheckSum['CAPEX Phasing'] = CAPEX_Totals

CAPEX_CheckSum['Constraints'] = capexconstraints

CAPEX_CheckSum['Budget Utilisation%'] = round(CAPEX_CheckSum['CAPEX Phasing']/CAPEX_CheckSum['Constraints'],2)*100 print("Summary")

print()

print('Total NPV=', round(pulpsolution['NPV Selected'].sum(),2))

print()

print(len(pulpoutput.loc[pulpoutput['Selection Y/N'] == 1]), "out of" ,len(proj_list.index) ,"total projects submitted were shortlisted for execution" )

print()

print(CAPEX_CheckSum.to_string(index = False)) print("Selected Projects")

print("-----------------")

pulpoutput.loc[pulpoutput['Selection Y/N'] == 1] print("Dropped Projects")

print("----------------")

pulpoutput.loc[pulpoutput['Selection Y/N'] == 0]

In any event, solving for the current problem produces the following result:

Projects 1 , 2 , 5 were Selected

Now this doesn’t look like much but if we try this again but with a much longer list of 30 Projects and 4 Yrs worth of CAPEX Phasing (instead of 3 Yrs), the code still manages to discover the optimal solution WITHOUT the need for any modifications to the code…

This table has 30 Projects X 4 Years CAPEX Phasing vs the earlier 5 Projects X 3 Years CAPEX Phasing

3.5 To The Cloud And Beyond!

Image Source : Getty Images

In the example above , the input was taken from CSV files and the output was just displayed in the Python JupyterNotebook file.

However it is possible to use Python to directly load live inputs from a centralised Database (e.g SAP etc) and send the outputs to a Visualization tool (e.g Power BI , Tableau or other dashboards) to be shared with others.

Since this is just a code snippet , it could even be hosted and run from a virtual machine to leverage cloud computing resources (similar to how some machine learning models work)

This may not make sense for Capital Budgeting as this is often tied to annual financial planning cycles but the same Integer/Linear Programming techniques are also often used for Scheduling, Production Planning or Inventory Management (Often with hundreds or even thousands of variables so ‘solving’ for the optimum becomes computationally harder) that need operational decisions to be weekly, daily or even hourly where this approach would definitely help.

4. Conclusion

Hopefully, through the example above, I’ve managed to…

help you understand the basic ideas behind how Linear Programming works

demonstrate how to optimize Capital Budgeting using PuLP

put forward some strong points around why programming languages should be the preferred method to to build and maintain complex optimization models vs spreadsheet solver add-ins models.

5. Further Resources

If you’ve found this topic interesting, this is actually part of a much more comprehensive tutorial series that goes into more detail into the Capital Budgeting problem and Linear Programming

Some of the topics covered include :-