Anthony Jimenez
16 August 2021
http://www.fa-jimenez.com/
Assumptions:
import pandas as pd
import numpy as np
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
input_revenue = [1277.5, 1929.625, 1319.5, 901.75, 616.75, 421, 288, 197.25]
input_operating_cost = [207.6, 345.9, 278.2, 231.9, 200.4, 178.7, 163.9, 153.9]
input_investment_stream = [2000, 0, 0, 0, 0, 0, 0, 0]
initial_investment = 1000
royalty_percentage = 5
cost_recov_percentage = 80
depreciation_life = 4
depreciation_method = 'Straight-Line'
contractor_equity_percentage = 50
tax_rate_percentage = 35
# Main function for computing all columns required for a cash flow analysis
def net_cash_flow(revenue_stream, oper_cost_stream, investment_stream, initial_investment, royalty, cost_recovery, contractor_equity, tax_rate, depreciation_life, depreciation_method):
# Create initial pandas dataframe
df_data = np.array([revenue_stream, oper_cost_stream, investment_stream]).T
df = pd.DataFrame(data=df_data, columns=['Gross Revenue ($M)', 'Operating Costs ($M)', 'Investment ($M)'])
# Compute cash flow columns
df['Royalty ($M)'] = (royalty / 100) * df['Gross Revenue ($M)']
df['Cost Recovery Limit ($M)'] = (cost_recovery / 100) * df['Gross Revenue ($M)']
# Need a loop to compute the cost recovery
unrecovered_arr = np.zeros(len(df))
depreciation_arr = np.zeros(len(df))
cost_recovery_total = np.zeros(len(df))
cost_recovery_arr = np.zeros(len(df))
for i in range(len(df)):
# Define the depreciation streams throughout life of project
if i == 0:
investment = df['Investment ($M)'].iloc[i] + initial_investment
else:
investment = df['Investment ($M)'].iloc[i]
# Currently supports only Straight-Line depreciation method
if depreciation_method == 'Straight-Line':
depreciation_arr[i:(i+depreciation_life)] += investment / depreciation_life
# Compute total recoverable cost (idealized, 100% recovery case)
if i == 0:
cost_recovery_total[i] = 0 + df['Operating Costs ($M)'].iloc[i] + depreciation_arr[i]
else:
cost_recovery_total[i] = unrecovered_arr[i-1] + df['Operating Costs ($M)'].iloc[i] + depreciation_arr[i]
# Determine the recovered cost amount per year
cost_recovery_arr[i] = min(cost_recovery_total[i], df['Cost Recovery Limit ($M)'].iloc[i])
unrecovered_arr[i] = cost_recovery_total[i] - cost_recovery_arr[i]
# Append computed array columns to main pandas dataframe
unrecovered_prior_arr = np.insert(unrecovered_arr[0:len(unrecovered_arr)-1], 0, 0)
df['Unrecovered Costs ($M)'] = unrecovered_arr
df['Unrecovered Prior Costs ($M)'] = unrecovered_prior_arr
df['Depreciation ($M)'] = depreciation_arr
df['Cost Recovery Before Limit ($M)'] = cost_recovery_total
df['Cost Recovery ($M)'] = cost_recovery_arr
# Compute equity columns
df['Total Equity ($M)'] = df['Gross Revenue ($M)'] - df['Royalty ($M)'] - df['Cost Recovery ($M)']
df['Contractor\'s Equity ($M)'] = (contractor_equity / 100) * df['Total Equity ($M)']
# Compute taxable income and annual tax owed
df['Contractor\'s Income ($M)'] = df['Cost Recovery ($M)'] + df['Contractor\'s Equity ($M)']
df['Taxable Income ($M)'] = df['Contractor\'s Income ($M)'] - df['Operating Costs ($M)'] - df['Depreciation ($M)']
df['Income Tax ($M)'] = (tax_rate / 100) * df['Taxable Income ($M)']
# Compute NOI and NCF
df['Contractor Net Operating Income ($M)'] = df['Contractor\'s Income ($M)'] - df['Operating Costs ($M)'] - df['Income Tax ($M)']
df['Net Cash Flow ($M)'] = df['Contractor Net Operating Income ($M)'] - df['Investment ($M)']
return df
# Allows negative values to be displayed as "red" text in pandas dataframe
def color_negative_red(val):
color = 'red' if val < 0 else 'black'
return 'color: %s' % color
# Main function for generating output cash flow tables
def net_cash_flow_tables(df):
# Create the Net Cash Flow Table
df_ncf = df[['Gross Revenue ($M)', 'Royalty ($M)', 'Cost Recovery ($M)', 'Total Equity ($M)',
'Contractor\'s Equity ($M)', 'Contractor\'s Income ($M)', 'Operating Costs ($M)',
'Income Tax ($M)', 'Contractor Net Operating Income ($M)', 'Investment ($M)',
'Net Cash Flow ($M)']]
# Create the first row of the NCF table
df_ncf_row0 = np.empty(len(df_ncf.columns))
df_ncf_row0[:] = np.NaN
df_ncf_row0[9] = initial_investment
df_ncf_row0[10] = -initial_investment
df_ncf_row0 = pd.DataFrame(df_ncf_row0).T
df_ncf_row0.columns = df_ncf.columns
df_ncf = pd.concat([df_ncf_row0, df_ncf]).reset_index(drop=True)
df_ncf.index.name = 'Year'
df_ncf.loc['Total'] = df_ncf.sum()
#df_ncf = df_ncf.style.applymap(color_negative_red)
# Create the Cost Recovered Table
df_cost = df[['Unrecovered Prior Costs ($M)', 'Operating Costs ($M)', 'Depreciation ($M)',
'Cost Recovery Before Limit ($M)', 'Cost Recovery Limit ($M)', 'Cost Recovery ($M)',
'Unrecovered Costs ($M)']]
df_cost.index = np.arange(1, len(df_cost)+1)
df_cost.index.name = 'Year'
df_cost = df_cost.style.applymap(color_negative_red)
# Create the PSC Income Tax Table
df_tax = df[['Contractor\'s Income ($M)', 'Operating Costs ($M)', 'Depreciation ($M)',
'Taxable Income ($M)', 'Income Tax ($M)']]
df_tax.index = np.arange(1, len(df_tax)+1)
df_tax.index.name = 'Year'
df_tax.loc['Total'] = df_tax.sum()
df_tax = df_tax.style.applymap(color_negative_red)
return df_ncf, df_cost, df_tax
# Eggholder function for net cash flow
def net_cash_flow_function():
# Compute all NCF functions
df = net_cash_flow(input_revenue, input_operating_cost, input_investment_stream, initial_investment,
royalty_percentage, cost_recov_percentage, contractor_equity_percentage, tax_rate_percentage,
depreciation_life, depreciation_method)
# Create the summarized tables for output (i.e., NCF, Cost Recovery, Income Tax Report)
df_ncf, df_cost, df_tax = net_cash_flow_tables(df)
return df_ncf, df_cost, df_tax, df
df_ncf, df_cost, df_tax, df_raw = net_cash_flow_function()
C:\ProgramData\Anaconda3\lib\site-packages\pandas\core\indexing.py:723: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy iloc._setitem_with_indexer(indexer, value, self.name)
df_ncf
Gross Revenue ($M) | Royalty ($M) | Cost Recovery ($M) | Total Equity ($M) | Contractor's Equity ($M) | Contractor's Income ($M) | Operating Costs ($M) | Income Tax ($M) | Contractor Net Operating Income ($M) | Investment ($M) | Net Cash Flow ($M) | |
---|---|---|---|---|---|---|---|---|---|---|---|
Year | |||||||||||
0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1000.0 | -1000.000000 |
1 | 1277.500 | 63.87500 | 957.6 | 256.02500 | 128.012500 | 1085.612500 | 207.6 | 44.804375 | 833.208125 | 2000.0 | -1166.791875 |
2 | 1929.625 | 96.48125 | 1095.9 | 737.24375 | 368.621875 | 1464.521875 | 345.9 | 129.017656 | 989.604219 | 0.0 | 989.604219 |
3 | 1319.500 | 65.97500 | 1028.2 | 225.32500 | 112.662500 | 1140.862500 | 278.2 | 39.431875 | 823.230625 | 0.0 | 823.230625 |
4 | 901.750 | 45.08750 | 721.4 | 135.26250 | 67.631250 | 789.031250 | 231.9 | -67.504062 | 624.635313 | 0.0 | 624.635313 |
5 | 616.750 | 30.83750 | 460.9 | 125.01250 | 62.506250 | 523.406250 | 200.4 | 113.052188 | 209.954063 | 0.0 | 209.954063 |
6 | 421.000 | 21.05000 | 178.7 | 221.25000 | 110.625000 | 289.325000 | 178.7 | 38.718750 | 71.906250 | 0.0 | 71.906250 |
7 | 288.000 | 14.40000 | 163.9 | 109.70000 | 54.850000 | 218.750000 | 163.9 | 19.197500 | 35.652500 | 0.0 | 35.652500 |
8 | 197.250 | 9.86250 | 153.9 | 33.48750 | 16.743750 | 170.643750 | 153.9 | 5.860313 | 10.883438 | 0.0 | 10.883438 |
Total | 6951.375 | 347.56875 | 4760.5 | 1843.30625 | 921.653125 | 5682.153125 | 1760.5 | 322.578594 | 3599.074531 | 3000.0 | 599.074531 |
df_cost
Unrecovered Prior Costs ($M) | Operating Costs ($M) | Depreciation ($M) | Cost Recovery Before Limit ($M) | Cost Recovery Limit ($M) | Cost Recovery ($M) | Unrecovered Costs ($M) | |
---|---|---|---|---|---|---|---|
Year | |||||||
1 | 0.000000 | 207.600000 | 750.000000 | 957.600000 | 1022.000000 | 957.600000 | 0.000000 |
2 | 0.000000 | 345.900000 | 750.000000 | 1095.900000 | 1543.700000 | 1095.900000 | 0.000000 |
3 | 0.000000 | 278.200000 | 750.000000 | 1028.200000 | 1055.600000 | 1028.200000 | 0.000000 |
4 | 0.000000 | 231.900000 | 750.000000 | 981.900000 | 721.400000 | 721.400000 | 260.500000 |
5 | 260.500000 | 200.400000 | 0.000000 | 460.900000 | 493.400000 | 460.900000 | 0.000000 |
6 | 0.000000 | 178.700000 | 0.000000 | 178.700000 | 336.800000 | 178.700000 | 0.000000 |
7 | 0.000000 | 163.900000 | 0.000000 | 163.900000 | 230.400000 | 163.900000 | 0.000000 |
8 | 0.000000 | 153.900000 | 0.000000 | 153.900000 | 157.800000 | 153.900000 | 0.000000 |
df_tax
Contractor's Income ($M) | Operating Costs ($M) | Depreciation ($M) | Taxable Income ($M) | Income Tax ($M) | |
---|---|---|---|---|---|
Year | |||||
1 | 1085.612500 | 207.600000 | 750.000000 | 128.012500 | 44.804375 |
2 | 1464.521875 | 345.900000 | 750.000000 | 368.621875 | 129.017656 |
3 | 1140.862500 | 278.200000 | 750.000000 | 112.662500 | 39.431875 |
4 | 789.031250 | 231.900000 | 750.000000 | -192.868750 | -67.504062 |
5 | 523.406250 | 200.400000 | 0.000000 | 323.006250 | 113.052188 |
6 | 289.325000 | 178.700000 | 0.000000 | 110.625000 | 38.718750 |
7 | 218.750000 | 163.900000 | 0.000000 | 54.850000 | 19.197500 |
8 | 170.643750 | 153.900000 | 0.000000 | 16.743750 | 5.860313 |
Total | 5682.153125 | 1760.500000 | 3000.000000 | 921.653125 | 322.578594 |
Utilizing the Plotly plotting library, we can create interactive plots. However, plotly works best with data in long-form, not wide-form (as is a typical cash flow table.
Thus, we should first reformat our data to long-form by using the Pandas function pd.melt().
# Remove the "Total" row from dataframe
df1 = df_ncf.iloc[:len(df_ncf)-1].reset_index()
# Make investments show as a negative cash flow for plotting purposes
df1['Investment ($M)'] = -1 * df1['Investment ($M)']
# Convert wide-form data to long-form data
df_ncf = pd.melt(df1, id_vars=['Year'])
df_ncf
Year | variable | value | |
---|---|---|---|
0 | 0 | Gross Revenue ($M) | NaN |
1 | 1 | Gross Revenue ($M) | 1277.500000 |
2 | 2 | Gross Revenue ($M) | 1929.625000 |
3 | 3 | Gross Revenue ($M) | 1319.500000 |
4 | 4 | Gross Revenue ($M) | 901.750000 |
... | ... | ... | ... |
94 | 4 | Net Cash Flow ($M) | 624.635313 |
95 | 5 | Net Cash Flow ($M) | 209.954063 |
96 | 6 | Net Cash Flow ($M) | 71.906250 |
97 | 7 | Net Cash Flow ($M) | 35.652500 |
98 | 8 | Net Cash Flow ($M) | 10.883438 |
99 rows × 3 columns
# Shows that all the same column/information is here in this new format (long vs. wide formatting)
df_ncf.variable.unique()
array(['Gross Revenue ($M)', 'Royalty ($M)', 'Cost Recovery ($M)', 'Total Equity ($M)', "Contractor's Equity ($M)", "Contractor's Income ($M)", 'Operating Costs ($M)', 'Income Tax ($M)', 'Contractor Net Operating Income ($M)', 'Investment ($M)', 'Net Cash Flow ($M)'], dtype=object)
The basic equation for NCF in this case is:
$NCF = NOI - Investment$
Because no additional investments are made after the first year, we see that NOI equals NCF for years 2-8.
# Slice full NCF table to just the columns we want to visualize
col1 = ['Contractor\'s Income ($M)', 'Operating Costs ($M)', 'Income Tax ($M)', 'Contractor Net Operating Income ($M)',
'Investment ($M)', 'Net Cash Flow ($M)']
df_plot1 = df_ncf[df_ncf['variable'].isin(col1[-3:])]
fig = px.line(df_plot1, x='Year', y='value', color='variable',
title='Illustration of how (NOI - Investments) yields NCF',
labels={
'x': 'Year',
'value': 'US Dollars ($M)',
'variable': 'Series Name'
})
fig.update_traces(mode='lines+markers')
fig.show()
While our net cash flow table shows us the yearly received cash after paying all expenses, the cumulative sum of this cash stream is very helpful in idenitifying if the project is valuable over its entire life.
In a more realistic case, the cash flows of future years would be discounted in order to account for the time value of money. However, in this naive case, a direct cumulative sum suffices.
Although a positive net cash flow is returned in Year 2 on the annual basis, the cumulative net cash flow does not become positive until Year 4.
Thus, the time to payout is 4 years.
# Create figure for cash flow generation throughout project life
fig = go.Figure()
# Create the sliced dataframe that we are going to use for plotting
df_plot2 = df_plot1[df_plot1['variable'] == 'Net Cash Flow ($M)']
# Add the line plot first
fig.add_trace(
go.Scatter(x=df_plot2['Year'], y=df_plot2['value'].cumsum(), name='Cumulative Net Cash Flow ($M)')
)
# Append the bar chart to figure
fig.add_trace(
go.Bar(x=df_plot2['Year'], y=df_plot2['value'], name='Annual Net Cash Flow ($M)',
text=df_plot2['value'], textposition='outside',
textfont={
'size': 13
})
)
# Style and label figure
fig.update_traces(texttemplate='%{text: .2s}')
fig.update_layout(title='Cash Flow Generation Throughout Project Life',
xaxis_title='Year', yaxis_title='US Dollars ($M)',
legend={
'orientation': 'h',
'yanchor': 'bottom',
'xanchor': 'right',
'x': 1,
'y': 1.02
})
fig.show()