Edisonymy commited on
Commit
a600ee3
·
1 Parent(s): 0713b71
.streamlit/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [theme]
2
+ base="light"
README.md CHANGED
@@ -1,13 +1,8 @@
1
- ---
2
- title: Buy Or Rent
3
- emoji: 📚
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: streamlit
7
- sdk_version: 1.26.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
+ # Opensource Buy or Rent Calculator
2
+ https://buy-or-rent.streamlit.app/
 
 
 
 
 
 
 
 
 
3
 
4
+ tl;dr: I was frustrated by online buy vs rent calculators because they 1. tend to ignore the time value of money, and 2. Don't reflect risk and uncertainty. So I made my own open-source calculator.
5
+
6
+ To give an example, this calculator (https://smartmoneytools.co.uk/tools/rent-vs-buy/) simply adds up all the costs and returns regardless of time. This means that £100 today is treated the same as £100 in 10 years. This is highly inaccurate because it fails to take into account the time value of money. This way of calculating the result underestimates the impact of periodic payments like rent and mortgage as well as initial payments like stamp duty, compared to the future property value.
7
+
8
+ Moreover, the uncertainty involved in comparing buying vs renting is really why I wanted to make this. Online calculators allow you to set a single number for parameters like the mortgage rate, but no one can really be certain what these numbers will be. Whether you choose 2% or 3% is mostly arbitrary, so I wanted to see what happens if you allow the input to be a probability density function.
dev.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ streamlit==1.26.0
2
+ pandas
3
+ numpy
4
+ numpy_financial
5
+ scipy
6
+ seaborn
7
+ matplotlib
src/__pycache__/main.cpython-310.pyc ADDED
Binary file (12.5 kB). View file
 
src/app.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ from main import Buy_or_Rent_Model, generate_combinations_and_calculate_npv, graph_kde_plots
6
+ from utils.general import get_param_distribution
7
+
8
+
9
+ # Streamlit app title
10
+ st.title('Open Source UK Buy or Rent Simulation Model')
11
+ st.write("---")
12
+ st.write("Not sure whether it is financially better to buy a property or rent and invest? We use simulations to show possible returns for buying a property or renting given your assumptions. All parameters are assumed to be uncorrelated.")
13
+
14
+ st.markdown("***This app is currently in Beta. It assumes that you are in England, UK. Different countries may have different tax rules. No data is collected by this app. The source code can be found at: https://github.com/edisonymy/buy_or_rent***")
15
+ st.write("---")
16
+
17
+ n_samples = 500
18
+ n_bins = 30
19
+
20
+ np.random.seed(123)
21
+
22
+ # User-enterable parameters for data generation
23
+ st.subheader('Your Assumptions')
24
+ with st.expander("Expand", expanded=True):
25
+ # st.sidebar.subheader('Fixed Model Parameters:')
26
+ house_price = st.number_input("Property Price", value=300000, step = 5000)
27
+ # rental_yield = st.sidebar.number_input("Rental Yield (used to calculate monthly rent for equivalent property):", value=0.043, step = 0.001, format="%.3f") #st.sidebar.slider('Rental Yield (used to calculate monthly rent for equivalent property):', min_value=0.01, max_value=1.0, value=0.044, format="%.3f")
28
+ # implied_monthly_rent = house_price * rental_yield / 12
29
+ # st.sidebar.write(f"Monthly Rent: {implied_monthly_rent:.0f}")
30
+ monthly_rent = st.number_input("Monthly Rent:", value=int(house_price*0.043/12), step = 100)
31
+ rental_yield = (12 * monthly_rent)/ house_price
32
+ st.markdown(f"<span style='font-size: 12px;'>Implied Rental Yield: {rental_yield:.3f}</span>", unsafe_allow_html=True)
33
+ text = 'Checkout a rental yield map here: https://www.home.co.uk/company/press/rental_yield_heat_map_london_postcodes.pdf'
34
+ st.markdown(f"<span style='font-size: 11px;'>{text}</span>", unsafe_allow_html=True)
35
+
36
+ deposit = st.slider('Deposit:', min_value=0, max_value=house_price, value=int(0.4*house_price), step = 1000)
37
+ deposit_mult = deposit/house_price
38
+ # deposit_mult = st.sidebar.slider('Deposit percentage:', min_value=0.01, max_value=1.0, value=0.4)
39
+ # st.sidebar.markdown(f"<span style='font-size: 12px;'>Deposit Amount: {deposit:,.0f}</span>", unsafe_allow_html=True)
40
+ mortgage_length = st.slider('Mortgage Length:', min_value=15, max_value=35, value=30, step = 1)
41
+
42
+ st.markdown("***For advanced options, please use the left sidebar (mobile users please press the top left button).***")
43
+
44
+ # sidebar
45
+ st.write("---")
46
+ st.sidebar.subheader('Additional Parameters:')
47
+ stamp_duty_bol = st.sidebar.checkbox('I pay Stamp Duty.', value = False, help="Stamp Duty (UK): A tax paid by the buyer when purchasing property in the United Kingdom. The amount of Stamp Duty depends on the property's purchase price and may include additional rates for second homes and buy-to-let properties.")
48
+ cgt_bol = st.sidebar.checkbox('I pay capital gains tax on the property.', value = False, help = "Capital Gains Tax (CGT): A tax levied on the profit (capital gain) made from the sale or disposal of assets such as property, investments, or valuable possessions. The amount of CGT owed is typically calculated based on the difference between the purchase price and the selling price of the asset. Different rates may apply to individuals and businesses, and there are often exemptions and allowances that can reduce the taxable amount.")
49
+ cgt_investment_bol = st.sidebar.checkbox('I pay capital gains tax on the investment.', value = False, help = "Capital Gains Tax (CGT): A tax levied on the profit (capital gain) made from the sale or disposal of assets such as property, investments, or valuable possessions. The amount of CGT owed is typically calculated based on the difference between the purchase price and the selling price of the asset. Different rates may apply to individuals and businesses, and there are often exemptions and allowances that can reduce the taxable amount.")
50
+ ongoing_cost = st.sidebar.number_input("Annual combined maintenance and service charge:", value=int(house_price*0.006), step = 100, help="The total annual cost associated with maintaining and servicing a property, including expenses such as property management fees, maintenance fees, and other related charges.")
51
+ buying_cost = st.sidebar.number_input("Buying cost (excluding stamp duty):", value=3000, step = 100, help="This includes laywer's fee, mortgage lender fees, surveyor's fee, valuation fees and other fees you expect to pay at the time of purchase.")
52
+ annual_income = st.sidebar.number_input("Annual Salary", value=20000, step = 100, help="This should be set to the expected salary at time of sale, required to calculate capital gains tax.")
53
+ selling_cost_no_cgt = st.sidebar.number_input("Selling cost (excluding Capital Gains Tax):", value=int(house_price*0.02), step = 100, help= "Total expense incurred when selling a property. This typically include real estate agent commissions, legal fees, advertising expenses, and any necessary repairs or renovations to prepare the property for sale.")
54
+ inflation = st.sidebar.number_input('Inflation:', min_value=0.0, max_value=1.0, value=0.02, step = 0.001, format="%.3f", help="Used for inflation adjustment only. It does not affect the model.")
55
+ # uncertain parameters
56
+ st.sidebar.subheader('Advanced Model Parameters:')
57
+ st.sidebar.write("It's hard to predict the future, so this section allows the simulations to reflect your uncertainty. The more uncertain you are about a paramter, the higher the standard deviation (sd) you should assume.")
58
+ mortgage_interest_annual_mean = st.sidebar.slider('Mortgage Interest Rate Mean:', min_value=0.01, max_value=0.1, value=0.055, step = 0.001, format="%.3f")
59
+ mortgage_interest_annual_std = st.sidebar.slider('Mortgage Interest Rate sd:', min_value=0.0, max_value=0.1, value=0.012, step = 0.001, format="%.3f")
60
+ text = 'Check out historical mortgage rates here: https://tradingeconomics.com/united-kingdom/mortgage-rate'
61
+ st.sidebar.markdown(f"<span style='font-size: 11px;'>{text}</span>", unsafe_allow_html=True)
62
+ mortgage_interest_annual_list = get_param_distribution(mortgage_interest_annual_mean, mortgage_interest_annual_std, n_samples, n_bins, title ='Assumed Distribution for Average Mortgage Interest Rate')
63
+ st.sidebar.write("---")
64
+ property_price_growth_annual_mean = st.sidebar.slider('Property Price Growth Mean:', min_value=0.01, max_value=0.1, value=0.03, step = 0.001, format="%.3f")
65
+ property_price_growth_annual_std = st.sidebar.slider('Property Price Growth sd:', min_value=0.0, max_value=0.05, value=0.01, step = 0.001, format="%.3f")
66
+ text = 'Check out historical property price growth here: https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/housepriceindex/june2023'
67
+ st.sidebar.markdown(f"<span style='font-size: 11px;'>{text}</span>", unsafe_allow_html=True)
68
+ property_price_growth_annual_list = get_param_distribution(property_price_growth_annual_mean, property_price_growth_annual_std, n_samples, n_bins, title ='Assumed Distribution for Annual Property Value Growth')
69
+ st.sidebar.write("---")
70
+ rent_increase_mean = st.sidebar.slider('Rent Increase Mean:', min_value=0.01, max_value=0.1, value=0.02, step = 0.001, format="%.3f")
71
+ rent_increase_std = st.sidebar.slider('Rent Increase sd:', min_value=0.0, max_value=0.05, value=0.01, step = 0.001, format="%.3f")
72
+ text = 'Checkout historical rent increases here: https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/indexofprivatehousingrentalprices/july2023'
73
+ st.sidebar.markdown(f"<span style='font-size: 11px;'>{text}</span>", unsafe_allow_html=True)
74
+ rent_increase_list = get_param_distribution(rent_increase_mean, rent_increase_std, n_samples, n_bins, title ='Assumed Distribution for Average Annual Rent Increase')
75
+ st.sidebar.write("---")
76
+ investment_return_annual_mean = st.sidebar.slider('Investment Return Mean:', min_value=0.01, max_value=0.2, value=0.06, step = 0.001, format="%.3f")
77
+ investment_return_annual_std = st.sidebar.slider('Investment Return sd:', min_value=0.0, max_value=0.05, value=0.02, step = 0.001, format="%.3f")
78
+ text = 'Check out historical stock market returns here: https://www.investopedia.com/ask/answers/042415/what-average-annual-return-sp-500.asp'
79
+ st.sidebar.markdown(f"<span style='font-size: 11px;'>{text}</span>", unsafe_allow_html=True)
80
+ investment_return_annual_list = get_param_distribution(investment_return_annual_mean, investment_return_annual_std, n_samples, n_bins, title ='Assumed Distribution for Average Investment Rate of Return')
81
+ st.sidebar.write("---")
82
+ years_until_sell_mean = st.sidebar.slider('Years Until Sell Mean:', min_value=0, max_value=100, value=15)
83
+ years_until_sell_std = st.sidebar.slider('Years Until Sell sd:', min_value=0, max_value=10, value=5)
84
+ years_until_sell_list = get_param_distribution(years_until_sell_mean, years_until_sell_std, n_samples, n_bins, as_int=True, title ='Assumed Distribution for Years Until Property Is Sold')
85
+ st.sidebar.write("---")
86
+ # n_samples = st.sidebar.slider('Number of Samples:', min_value=100, max_value=50000, value=10000)
87
+ # n_bins = st.sidebar.slider('Number of Bins:', min_value=10, max_value=100, value=30)
88
+ st.sidebar.subheader('Simulation Settings:')
89
+ n_samples_simulation = st.sidebar.slider('Number of Simulation Samples:', min_value=100, max_value=10000, value=500)
90
+
91
+ # Initialize the model
92
+ model = Buy_or_Rent_Model()
93
+ model.HOUSE_PRICE = house_price
94
+ model.DEPOSIT_MULT = deposit_mult
95
+ model.RENTAL_YIELD = rental_yield
96
+ model.MORTGAGE_LENGTH = mortgage_length
97
+ model.ANNUAL_SALARY = annual_income
98
+ model.ONGOING_COST_MULT = ongoing_cost/house_price
99
+ model.BUYING_COST_FLAT = buying_cost
100
+ model.STAMP_DUTY_BOL = stamp_duty_bol
101
+ model.CGT_BOL = cgt_bol
102
+ model.CGT_INVESTMENT_BOL = cgt_investment_bol
103
+ model.inflation = inflation
104
+
105
+ # Generate combinations and calculate NPV
106
+ percentiles_df, results_df = generate_combinations_and_calculate_npv(
107
+ n_samples_simulation,
108
+ model,
109
+ mortgage_interest_annual_list=mortgage_interest_annual_list,
110
+ property_price_growth_annual_list=property_price_growth_annual_list,
111
+ rent_increase_list=rent_increase_list,
112
+ investment_return_annual_list=investment_return_annual_list,
113
+ years_until_sell_list=years_until_sell_list
114
+ )
115
+
116
+ # Display the results DataFrame
117
+ # st.subheader('Correlations Between Parameters and Buying NPV')
118
+ with st.expander('Correlations Between Parameters and Buying NPV', expanded=False):
119
+ st.write(results_df.corr().iloc[0,1:])
120
+
121
+ st.write('---')
122
+ st.write("If you found this useful and want to support me, consider buying me a coffee here:")
123
+ # Embed the "Buy Me a Coffee" button script with custom CSS
124
+ buy_me_a_coffee_html = """
125
+ <script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js"
126
+ data-name="bmc-button" data-slug="edisonymyt" data-color="#FFDD00" data-emoji="☕" data-font="Cookie"
127
+ data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#000000"
128
+ data-coffee-color="#ffffff" ></script>
129
+ """
130
+
131
+ # Display the custom CSS and the button in the Streamlit app
132
+ st.components.v1.html(buy_me_a_coffee_html)
133
+ # plot_button = st.button("Plot Additional Graphs")
134
+
135
+ # Check if the button is clicked
136
+ # if plot_button:
137
+ # FEATURES = ['mortgage_interest_annual', 'property_price_growth_annual', 'rent_increase', 'investment_return_annual', 'years_until_sell']
138
+ # graph_kde_plots(results_df,FEATURES)
src/main.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import numpy_financial as npf
5
+ from scipy.stats import norm, skew
6
+ import matplotlib
7
+ import matplotlib.pyplot as plt
8
+ import matplotlib.ticker as mticker
9
+ import seaborn as sns
10
+ from utils.general import calculate_percentiles, bin_continuous_features, get_param_distribution
11
+ from utils.finance import get_stamp_duty_next_home, annuity_pv, annuity_fv, annuity_payment, pv_future_payment, fv_present_payment
12
+
13
+ import warnings
14
+ warnings.simplefilter(action='ignore', category=FutureWarning)
15
+ matplotlib.rcParams["axes.formatter.limits"] = (-99, 99)
16
+
17
+ def format_with_commas(x, pos):
18
+ return '{:,.0f}'.format(x)
19
+
20
+ class Buy_or_Rent_Model():
21
+ def __init__(self) -> None:
22
+ # PARAMS
23
+ # Fixed (in expectation)
24
+ self.HOUSE_PRICE = 800000 #including upfront repairs and renovations
25
+ self.RENTAL_YIELD = 0.043 # assumed rent as a proportion of house price https://www.home.co.uk/company/press/rental_yield_heat_map_london_postcodes.pdf
26
+ self.DEPOSIT_MULT = 0.5
27
+ self.MORTGAGE_LENGTH = 30
28
+ self.BUYING_COST_FLAT = 3000 #https://www.movingcostscalculator.co.uk/calculator/
29
+ self.SELLING_COST_MULT = 0.02 #https://www.movingcostscalculator.co.uk/calculator/
30
+ self.ONGOING_COST_MULT = 0.006 # service charge + repairs, council tax and bills are omitted since they are the same whether buying or renting
31
+ self.ANNUAL_SALARY = 55000
32
+ self.CGT_ALLOWANCE = 6000
33
+ self.PERSONAL_ALLOWANCE = 12570
34
+ self.CGT_BOL = True
35
+ self.CGT_INVESTMENT_BOL = False
36
+ self.STAMP_DUTY_BOL = True
37
+ # Probability distribution
38
+ self.rent_increase = 0.01325 # historical: https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/indexofprivatehousingrentalprices/april2023
39
+ self.property_price_growth_annual = 0.025 # historical average = 0.034 over the last 8 year, adjusted down due to end to abnormally low interest rates; source for historical data: https://www.statista.com/statistics/620414/monthly-house-price-index-in-london-england-uk/
40
+ self.mortgage_interest_annual = 0.05
41
+ self.investment_return_annual = 0.06
42
+ self.years_until_sell = 20
43
+ # financial modelling params
44
+ self.inflation = 0.02 #on ongoing costs, also for converting fv
45
+ self.adjust_for_inflation = 0
46
+
47
+ def get_capital_gains_tax_property(self):
48
+ cgt = 0
49
+ if self.CGT_BOL:
50
+ taxable_gains = self.future_house_price - self.HOUSE_PRICE
51
+ if self.ANNUAL_SALARY > 50271:
52
+ cgt = taxable_gains * 0.28
53
+ else:
54
+ taxable_income = self.ANNUAL_SALARY - self.PERSONAL_ALLOWANCE
55
+ if taxable_gains - self.CGT_ALLOWANCE + taxable_income <= 50270:
56
+ cgt = (taxable_gains - self.CGT_ALLOWANCE) * 0.18
57
+ else:
58
+ cgt += (50271 - taxable_income) * 0.18
59
+ cgt += (taxable_gains - self.CGT_ALLOWANCE - 50271) * 0.28
60
+ return cgt
61
+
62
+ def get_capital_gains_tax_investment(self):
63
+ cgt = 0
64
+ if self.CGT_INVESTMENT_BOL:
65
+ taxable_gains = self.total_investment_fv - self.total_investment
66
+ if self.ANNUAL_SALARY > 50271:
67
+ cgt = taxable_gains * 0.2
68
+ else:
69
+ taxable_income = self.ANNUAL_SALARY - self.PERSONAL_ALLOWANCE
70
+ if taxable_gains - self.CGT_ALLOWANCE + taxable_income <= 50270:
71
+ cgt = (taxable_gains - self.CGT_ALLOWANCE) * 0.1
72
+ else:
73
+ cgt += (50271 - taxable_income) * 0.1
74
+ cgt += (taxable_gains - self.CGT_ALLOWANCE - 50271) * 0.2
75
+ return cgt
76
+
77
+ def run_calculations(self, adjust_for_inflation_bool = False):
78
+ self.future_house_price = self.HOUSE_PRICE * (1+self.property_price_growth_annual)**self.years_until_sell
79
+ self.CGT = self.get_capital_gains_tax_property()
80
+ self.SELLING_COST = self.future_house_price * self.SELLING_COST_MULT + self.CGT
81
+ if adjust_for_inflation_bool:
82
+ self.adjust_for_inflation = self.inflation
83
+ # self.SELLING_COST = self.SELLING_COST / float(1+self.adjust_for_inflation)**(self.years_until_sell)
84
+ # self.future_house_price = self.future_house_price / float(1+self.adjust_for_inflation)**(self.years_until_sell)
85
+ self.monthly_rent = self.HOUSE_PRICE * self.RENTAL_YIELD /12
86
+ if self.STAMP_DUTY_BOL:
87
+ self.STAMP_DUTY = get_stamp_duty_next_home(self.HOUSE_PRICE)
88
+ else:
89
+ self.STAMP_DUTY = 0
90
+ self.discount_rate = self.investment_return_annual
91
+ self.DEPOSIT = self.HOUSE_PRICE * self.DEPOSIT_MULT
92
+ self.mortgage_calculations()
93
+ self.get_house_buying_npv()
94
+ self.get_house_buying_fv()
95
+ self.get_renting_fv()
96
+
97
+ def mortgage_calculations(self):
98
+ self.mortgage_amount = self.HOUSE_PRICE * (1 - self.DEPOSIT_MULT)
99
+ self.annual_mortgage_payment = annuity_payment(self.mortgage_amount, self.mortgage_interest_annual,self.MORTGAGE_LENGTH,0)
100
+ self.pv_mortage_payments = annuity_pv(self.annual_mortgage_payment, self.discount_rate, self.MORTGAGE_LENGTH, 0)
101
+ self.fv_mortgage_payments = pv_future_payment(annuity_fv(self.annual_mortgage_payment, self.discount_rate, self.MORTGAGE_LENGTH, 0), self.discount_rate, self.MORTGAGE_LENGTH - self.years_until_sell)/ float(1+self.adjust_for_inflation)**(self.years_until_sell)#annuity_fv(self.annual_mortgage_payment, self.discount_rate, self.MORTGAGE_LENGTH, 0)
102
+
103
+ def get_house_buying_npv(self):
104
+ self.pv_of_future_house_price = pv_future_payment(self.future_house_price, self.discount_rate, self.years_until_sell)
105
+ self.pv_of_selling_cost = pv_future_payment(self.SELLING_COST, self.discount_rate, self.years_until_sell)
106
+ self.pv_ongoing_cost = annuity_pv(self.HOUSE_PRICE * self.ONGOING_COST_MULT,self.discount_rate, self.years_until_sell, self.inflation)
107
+ # rent saved
108
+ self.pv_rent_saved = annuity_pv(self.HOUSE_PRICE*self.RENTAL_YIELD, self.discount_rate, self.years_until_sell, self.rent_increase)
109
+ # sum it up
110
+ self.buying_npv = self.pv_of_future_house_price + self.pv_rent_saved - self.pv_mortage_payments- self.pv_ongoing_cost - self.DEPOSIT - self.BUYING_COST_FLAT - self.STAMP_DUTY - self.pv_of_selling_cost
111
+
112
+ def get_house_buying_fv(self): # not accounting for deposit, immediate costs, and rent saved. ongoing costs and mortgage are rolled up and deducted from fv
113
+ if self.adjust_for_inflation > 0:
114
+ self.SELLING_COST = self.SELLING_COST / float(1+self.adjust_for_inflation)**(self.years_until_sell)
115
+ self.future_house_price = self.future_house_price / float(1+self.adjust_for_inflation)**(self.years_until_sell)
116
+ self.fv_ongoing_cost = annuity_fv(self.HOUSE_PRICE * self.ONGOING_COST_MULT,self.discount_rate, self.years_until_sell, self.inflation, adjust_for_inflation = self.adjust_for_inflation)
117
+ self.rent_fv = annuity_fv(self.HOUSE_PRICE*self.RENTAL_YIELD, self.discount_rate, self.years_until_sell, self.rent_increase, adjust_for_inflation = self.adjust_for_inflation)
118
+ self.buying_fv = self.future_house_price + self.rent_fv - self.SELLING_COST - self.fv_ongoing_cost - self.fv_mortgage_payments
119
+ # self.buying_pv = self.pv_of_future_house_price + self.pv_rent_saved - self.pv_of_selling_cost - self.pv_ongoing_cost - self.pv_mortage_payments
120
+ # self.buying_fv_inflation_adjusted = pv_future_payment(self.buying_fv, self.inflation, self.years_until_sell)
121
+
122
+ def get_renting_fv(self): # assumes that buying costs and stamp duty are invested, rent is rolled up and deducted
123
+ self.total_investment = self.BUYING_COST_FLAT + self.STAMP_DUTY + self.DEPOSIT
124
+ self.total_investment_fv = fv_present_payment(self.total_investment, self.discount_rate, self.years_until_sell, adjust_for_inflation = self.adjust_for_inflation)
125
+ # fv_buying_cost = fv_present_payment(self.BUYING_COST_FLAT, self.discount_rate, self.years_until_sell, adjust_for_inflation = self.adjust_for_inflation)
126
+ # fv_STAMP_DUTY = fv_present_payment(self.STAMP_DUTY, self.discount_rate, self.years_until_sell, adjust_for_inflation = self.adjust_for_inflation)
127
+ # deposit_fv = fv_present_payment(self.DEPOSIT, self.discount_rate, self.years_until_sell, adjust_for_inflation = self.adjust_for_inflation)
128
+ self.cgt_investment = self.get_capital_gains_tax_investment()
129
+ self.renting_fv = self.total_investment_fv - self.cgt_investment
130
+ # self.renting_pv = self.total_investment
131
+ # self.renting_fv_inflation_adjusted = pv_future_payment(self.renting_fv, self.inflation, self.years_until_sell)
132
+
133
+ def plot_kde_from_list(arrays, st, figsize=(7, 2), main_colors = ['green'], secondary_color = 'red', legends = None, title = 'Net Present Value Probability Distribution', xlabel = 'Net Present Value For Property Purchase'):
134
+ fig, ax = plt.subplots(figsize=figsize)
135
+ x_lower = []
136
+ x_higher = []
137
+ for num, array in enumerate(arrays):
138
+ # Plot the entire KDE plot in one color
139
+ sns.kdeplot(data=array, ax=ax, color=main_colors[num-1], fill=True, bw_adjust = 2, label='Entire KDE', clip=(0, None))
140
+
141
+ # Plot the shaded area to the left of 0 in a different color
142
+ sns.kdeplot(data=array, ax=ax, color=secondary_color, fill=True, bw_adjust = 2, label='Shaded Area', clip=(None, 0))
143
+ # x_low_percentile = np.percentile(array, 0.001)
144
+ # x_high_percentile = np.percentile(array, 99)
145
+ x_mean = np.mean(array)
146
+ x_std = np.std(array)
147
+ x_lower.append(x_mean-3*x_std)
148
+ x_higher.append(x_mean+3*x_std)
149
+
150
+ # Set the axis limits based on the 95th percentile
151
+ ax.xaxis.set_major_formatter(mticker.FuncFormatter(format_with_commas))
152
+ ax.set_xlim(np.min(x_lower), np.max(x_higher))
153
+ ax.set_xlabel(xlabel)
154
+ ax.set_xticklabels(ax.get_xticklabels(), rotation=45)
155
+ ax.set_title(title)
156
+ if legends:
157
+ plt.legend(labels=legends)
158
+ st.pyplot(fig)
159
+
160
+
161
+ def generate_combinations_and_calculate_npv(
162
+ n_combinations,
163
+ model,
164
+ mortgage_interest_annual_list=[0.05],
165
+ property_price_growth_annual_list=[0.026],
166
+ rent_increase_list=[0.01325],
167
+ investment_return_annual_list=[0.06],
168
+ years_until_sell_list=[20]
169
+ ):
170
+ buying_npv_list = []
171
+ buying_fv_list = []
172
+ renting_fv_list = []
173
+ mortgage_interest_annual_list_chosen=[]
174
+ property_price_growth_annual_list_chosen=[]
175
+ rent_increase_list_chosen=[]
176
+ investment_return_annual_list_chosen=[]
177
+ years_until_sell_list_chosen=[]
178
+ adjust_for_inflation_bool = st.toggle('Adjust for inflation (2% a year)')
179
+ # use_present_value = st.toggle('Use present value instead of future value')
180
+
181
+ for n in range(n_combinations):
182
+
183
+ model.rent_increase = np.random.choice(rent_increase_list)
184
+ model.property_price_growth_annual = np.random.choice(property_price_growth_annual_list)
185
+ model.mortgage_interest_annual = np.random.choice(mortgage_interest_annual_list)
186
+ model.investment_return_annual = np.random.choice(investment_return_annual_list)
187
+ model.years_until_sell = np.random.choice(years_until_sell_list)
188
+
189
+ model.run_calculations(adjust_for_inflation_bool = adjust_for_inflation_bool)
190
+ buying_npv_list.append(model.buying_npv)
191
+ buying_fv_list.append(model.buying_fv)
192
+ renting_fv_list.append(model.renting_fv)
193
+ mortgage_interest_annual_list_chosen.append(model.mortgage_interest_annual)
194
+ property_price_growth_annual_list_chosen.append(model.property_price_growth_annual)
195
+ rent_increase_list_chosen.append(model.rent_increase)
196
+ investment_return_annual_list_chosen.append(model.investment_return_annual)
197
+ years_until_sell_list_chosen.append(model.years_until_sell)
198
+ model.rent_increase = np.median(rent_increase_list)
199
+ model.property_price_growth_annual = np.median(property_price_growth_annual_list)
200
+ model.mortgage_interest_annual = np.median(mortgage_interest_annual_list)
201
+ model.investment_return_annual = np.median(investment_return_annual_list)
202
+ model.years_until_sell = int(np.median(years_until_sell_list))
203
+ model.run_calculations(adjust_for_inflation_bool = adjust_for_inflation_bool)
204
+
205
+ results_dict = {'buying_npv':buying_npv_list,
206
+ 'mortgage_interest_annual':mortgage_interest_annual_list_chosen,
207
+ 'property_price_growth_annual':property_price_growth_annual_list_chosen,
208
+ 'rent_increase':rent_increase_list_chosen,
209
+ 'investment_return_annual':investment_return_annual_list_chosen,
210
+ 'years_until_sell':years_until_sell_list_chosen}
211
+ results_df = pd.DataFrame(results_dict)
212
+ percentiles_df = calculate_percentiles(buying_npv_list,model.DEPOSIT)
213
+ # st.write(f'Capital Invested: £{model.DEPOSIT:.2f}')
214
+ # st.write(f'Assumed Monthly Rent: £{model.monthly_rent:.2f}')
215
+ # st.write(f'NPV mean: £{np.mean(buying_npv_list):.2f}')
216
+ # st.write(f'NPV mean (as % of invested capital): {np.mean(buying_npv_list)/model.DEPOSIT*100:.2f}%')
217
+ # st.write(f'NPV std: £{np.std(buying_npv_list):.2f}')
218
+ # st.write(f'NPV std (as % of invested capital): {np.std(buying_npv_list)/model.DEPOSIT*100:.2f}%')
219
+ # st.write(f'NPV skew: {skew(buying_npv_list):.2f}')
220
+ if model.buying_fv > model.renting_fv:
221
+ text="Under current assumptions, return is typically higher if you <strong>buy</strong>."
222
+ else:
223
+ text="Under current assumptions, return is typically higher if you <strong>rent and invest the deposit</strong>."
224
+ st.markdown(f'<p style="background-color:#F0F2F6;font-size:32px;border-radius:0%; font-style: italic;">{text}</p>', unsafe_allow_html=True)
225
+ # st.markdown(f'**<span style="font-size: 32px; font-style: italic;">{text}</span>**', unsafe_allow_html=True)
226
+ left_column, right_column = st.columns(2)
227
+ with right_column:
228
+ st.write(f"### Buy - Asset future value after {model.years_until_sell} years")
229
+ st.markdown(f"**Typical Total Asset Value: £{model.buying_fv:,.0f}**", help="All components are converted to future value at the time of sale.")
230
+ # right_column.markdown(f"***Breakdown:***")
231
+ with st.expander("***Breakdown***:", expanded=False):
232
+ st.markdown(f" - Capital Invested (deposit): £{model.DEPOSIT:,.0f}")
233
+ st.markdown(f" - Capital Invested (buying cost + stamp duty, if any): £{model.BUYING_COST_FLAT + model.STAMP_DUTY:,.0f}")
234
+ st.markdown(f" - Property Price at Sale: :green[£{model.future_house_price:,.0f}]", help="Calculated using the property price growth rate set in the left sidebar.")
235
+ st.markdown(f" - Selling cost (including Capital Gains Tax): :red[ -£{model.SELLING_COST:,.0f}]",help= "Total expenses incurred when selling a property. These costs typically include real estate agent commissions, legal fees, advertising expenses, and any necessary repairs or renovations to prepare the property for sale.")
236
+ st.markdown(f" - Total maintenance and service costs: :red[ -£{model.fv_ongoing_cost:,.0f}]",help="Future value at the time of sale for the total cost associated with maintaining and servicing a property, including expenses such as property management fees, maintenance fees, and other related charges. Assumed to grow at inflation rate. Future value is determined by the discount rate, which is assumed to be equal to the investment return.")
237
+ st.markdown(f" - Total Mortgage Payments: :red[ -£{model.fv_mortgage_payments:,.0f}]", help="This is higher than the sum of all mortgage payments since the payments are converted to their future value at the time of sale. Future value is determined by the discount rate, which is assumed to be equal to the investment return.")
238
+ st.markdown(f" - Total Rent Saved (future value at time of sale): :green[£{model.rent_fv:,.0f}]", help="This is higher than the sum of all rent payments that would have been paid since the payments are converted to their future value at the time of sale. Future value is determined by the discount rate, which is assumed to be equal to the investment return.")
239
+
240
+ with left_column:
241
+ st.write(f"### Rent and invest - Asset future value after {model.years_until_sell} years")
242
+ # plot_kde_from_list(renting_fv_list, left_column, figsize=(5, 2), title = 'Asset Value Probability Distribution', xlabel = 'Asset Value')
243
+ st.markdown(f"**Typical Total Asset Value: £{model.renting_fv:,.0f}**", help="All components are converted to future value at the time of sale.")
244
+ with st.expander("***Breakdown***:", expanded=False):
245
+ st.markdown(f" - Capital Invested (deposit): £{model.DEPOSIT:,.0f}")
246
+ st.markdown(f" - Capital Invested (buying cost + stamp duty, if any): £{model.BUYING_COST_FLAT + model.STAMP_DUTY:,.0f}")
247
+ st.markdown(f" - Capital Gains Tax: :red[-£{model.cgt_investment:,.0f}]", help='Your tax rate is determined by the annual salary set in the left sidebar.')
248
+ if model.renting_fv - (model.DEPOSIT + model.BUYING_COST_FLAT + model.STAMP_DUTY) >= 0:
249
+ st.markdown(f" - Assumed Typical Capital Growth: :green[£{model.renting_fv - (model.DEPOSIT + model.BUYING_COST_FLAT + model.STAMP_DUTY):,.0f}]", help="Calculated with the investment return rated provided in the left sidebar.")
250
+ else:
251
+ st.markdown(f" - Assumed Typical Capital Growth: :red[£{model.renting_fv - (model.DEPOSIT + model.BUYING_COST_FLAT + model.STAMP_DUTY):,.0f}]")
252
+ st.write('---')
253
+ plot_kde_from_list([buying_fv_list,renting_fv_list], st, figsize=(7, 2), legends = ['Buying', 'Renting'],main_colors = ['orange', 'blue'], title = 'Future Asset Value Probability Distribution', xlabel = 'Asset Value')
254
+ plot_kde_from_list([buying_npv_list], st, legends = ['Buying is better', 'Renting is better'],main_colors = ['blue'], secondary_color = 'orange', title = 'Net Present Value Probability Distribution', xlabel = 'Net Present Value For Property Purchase')
255
+ st.markdown("<span style='font-size: 14px; font-style: italic;'>Net Present Value represents the net gain/loss that result in purchasing the property in present value. If it is positive, then it is financially better to buy a property. Present value is calculated using a future discount rate equal to your assumed investment return. This is equivalent to assuming that any amount you save on rent or mortgage will be invested. </span>", unsafe_allow_html=True)
256
+ # st.write("### Net Present Value Statistics")
257
+ with st.expander("### Net Present Value Statistics", expanded=False):
258
+ st.write(f'- Buying is better {100-percentiles_df.loc[5,"Percentile"]:.0f}% of the time')
259
+ st.write(f"- Mean: £{np.mean(buying_npv_list):,.0f}")
260
+ st.write(f"- Mean (as % of deposit): {np.mean(buying_npv_list)/model.DEPOSIT*100:.0f}%")
261
+ st.write(f"- Standard Deviation: £{np.std(buying_npv_list):,.0f}")
262
+ st.write(f"- Standard Deviation (as % of deposit): {np.std(buying_npv_list)/model.DEPOSIT*100:.0f}%")
263
+ st.write(f"- Skew: {skew(buying_npv_list):.2f}")
264
+ return percentiles_df, results_df
265
+
266
+ def graph_kde_plots(results_df, FEATURES, num_cols = 2):
267
+
268
+ # Calculate the number of rows and columns needed for subplots
269
+ num_features = len(FEATURES)
270
+
271
+ num_rows = (num_features + num_cols - 1) // num_cols
272
+
273
+ # Create a figure and axis for subplots
274
+ fig, axes = plt.subplots(num_rows, num_cols, figsize=(15, 5 * num_rows))
275
+
276
+ # Flatten the axes if necessary (in case there's only one row)
277
+ if num_rows == 1:
278
+ axes = axes.reshape(1, -1)
279
+
280
+ # Loop through each feature and plot it
281
+ for i, feature in enumerate(FEATURES):
282
+ row = i // num_cols
283
+ col = i % num_cols
284
+ ax = axes[row, col]
285
+
286
+ sns.kdeplot(data=results_df, x=feature, y="buying_npv", bw_adjust = 2, ax=ax, fill=True)
287
+ ax.set_title(f"{feature} vs. buying_npv")
288
+ ax.set_ylabel("buying_npv")
289
+ ax.set_xlabel(feature)
290
+ # Calculate the 95th percentile for x and y axes
291
+ x_low_percentile = np.percentile(results_df[feature], 0.1)
292
+ y_low_percentile = np.percentile(results_df['buying_npv'], 0.1)
293
+ x_high_percentile = np.percentile(results_df[feature], 99.9)
294
+ y_high_percentile = np.percentile(results_df['buying_npv'], 99.9)
295
+
296
+ # Set the axis limits based on the 95th percentile
297
+ ax.set_xlim(x_low_percentile, x_high_percentile)
298
+ ax.set_ylim(y_low_percentile, y_high_percentile)
299
+ ax.yaxis.set_major_formatter(mticker.FuncFormatter(format_with_commas))
300
+
301
+ # ax.set_xticklabels(ax.get_xticklabels(), rotation=45) # Adjust the rotation angle as needed
302
+
303
+ # Remove any empty subplots
304
+ for i in range(len(FEATURES), num_rows * num_cols):
305
+ fig.delaxes(axes.flatten()[i])
306
+
307
+ # Adjust spacing between subplots
308
+ plt.tight_layout()
309
+
310
+ # Show the plots
311
+ plt.show()
312
+ st.pyplot(fig)
src/utils/__init__.py ADDED
File without changes
src/utils/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (160 Bytes). View file
 
src/utils/__pycache__/finance.cpython-310.pyc ADDED
Binary file (1.34 kB). View file
 
src/utils/__pycache__/general.cpython-310.pyc ADDED
Binary file (2.7 kB). View file
 
src/utils/finance.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def get_stamp_duty_next_home(HOUSE_PRICE):
2
+ if HOUSE_PRICE <=250000:
3
+ return 0
4
+ elif HOUSE_PRICE <=925000:
5
+ return (HOUSE_PRICE-250000) * 0.05
6
+ elif HOUSE_PRICE <=1500000:
7
+ return (HOUSE_PRICE-925000) * 0.10 + (925000-250000) * 0.05
8
+ else:
9
+ return (HOUSE_PRICE-1500000) * 0.12 + (925000-250000) * 0.05 + (1500000-925000) * 0.10
10
+
11
+ def annuity_pv(payment, discount_rate, n_periods, growth_rate):
12
+ pv = payment * (1- (1+growth_rate)**n_periods*(1+discount_rate)**(-1*n_periods)) / (discount_rate-growth_rate)
13
+ return pv
14
+
15
+ def annuity_fv(payment, discount_rate, n_periods, growth_rate, adjust_for_inflation = 0):
16
+ fv = payment * ((1+discount_rate)**n_periods - (1+growth_rate)**n_periods) / (discount_rate-growth_rate)
17
+ return fv / float(1+adjust_for_inflation)**(n_periods)
18
+
19
+ def annuity_payment(pv, discount_rate, n_periods, growth_rate):
20
+ return pv* (discount_rate - growth_rate) / (1- (1+growth_rate)**n_periods * (1+discount_rate)**(-1*n_periods))
21
+
22
+
23
+ def pv_future_payment(payment, discount_rate, n_periods):
24
+ return payment/(1+discount_rate)**(n_periods)
25
+
26
+ def fv_present_payment(payment, discount_rate, n_periods, adjust_for_inflation = 0):
27
+ return payment*(1+discount_rate)**(n_periods) / float(1+adjust_for_inflation)**(n_periods)
src/utils/general.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import seaborn as sns
4
+ import pandas as pddef
5
+ import matplotlib.pyplot as plt
6
+ import streamlit as st
7
+ def calculate_percentiles(arr, capital_invested):
8
+ """
9
+ Calculate the 10th, 25th, 50th (median), 75th, 90th, and the percentile of the value closest to 0 in an array.
10
+ Also, add a column that represents the values as a percentage of capital invested.
11
+
12
+ Args:
13
+ arr (list or numpy.ndarray): Input array.
14
+ capital_invested (float): The amount of capital invested.
15
+
16
+ Returns:
17
+ pandas.DataFrame: A DataFrame with percentiles and value as a percentage of capital invested.
18
+ """
19
+ if not isinstance(arr, (list, np.ndarray)):
20
+ raise ValueError("Input must be a list or numpy.ndarray")
21
+
22
+ percentiles = [10, 25, 50, 75, 90]
23
+ percentile_values = np.percentile(arr, percentiles)
24
+
25
+ # Find the value closest to 0
26
+ closest_value = min(arr, key=lambda x: abs(x - 0))
27
+
28
+ # Calculate the percentile of the closest value
29
+ sorted_arr = np.sort(arr)
30
+ index_of_closest = np.where(sorted_arr == closest_value)[0][0]
31
+ closest_percentile = (index_of_closest / (len(sorted_arr) - 1)) * 100
32
+
33
+ # Create the DataFrame with the "Value as % of Capital" column
34
+ data = {
35
+ 'Percentile': percentiles + [closest_percentile],
36
+ 'NPV': np.append(percentile_values, closest_value)
37
+ }
38
+
39
+ df = pd.DataFrame(data)
40
+ df['% return'] = (df['NPV'] / capital_invested) * 100
41
+
42
+ return df
43
+
44
+ def bin_continuous_features(df, bin_config):
45
+ """
46
+ Encode continuous features into bins and add them as new columns to the DataFrame.
47
+
48
+ Parameters:
49
+ - df: pandas DataFrame
50
+ The DataFrame containing the continuous features.
51
+ - bin_config: dict
52
+ A dictionary specifying the binning configuration for each feature.
53
+ Example: {'feature1': [0, 10, 20, 30], 'feature2': [0, 5, 10]}
54
+
55
+ Returns:
56
+ - df: pandas DataFrame
57
+ The DataFrame with binned features added as new columns.
58
+ """
59
+
60
+ for feature, bins in bin_config.items():
61
+ # Create a new column with the binned values
62
+ df[f'{feature}_bin'] = pd.cut(df[feature], bins=bins, labels=False)
63
+
64
+ return df
65
+
66
+ def get_param_distribution(mean, std, samples, bins,plot=True, as_int = False, title =''):
67
+ if std <=0:
68
+ return [mean]
69
+ s = np.random.normal(mean, std, samples)
70
+ if plot:
71
+ fig, ax = plt.subplots(figsize=(4, 1.7))
72
+ # plt.hist(s, bins, density=False)
73
+ sns.kdeplot(s,bw_adjust=5)
74
+ plt.title(title)
75
+ st.sidebar.pyplot(fig)
76
+ if as_int:
77
+ s=s.astype(int)
78
+ return s