Spaces:
Running
Running
init
Browse files- .streamlit/config.toml +2 -0
- README.md +7 -12
- dev.ipynb +0 -0
- requirements.txt +7 -0
- src/__pycache__/main.cpython-310.pyc +0 -0
- src/app.py +138 -0
- src/main.py +312 -0
- src/utils/__init__.py +0 -0
- src/utils/__pycache__/__init__.cpython-310.pyc +0 -0
- src/utils/__pycache__/finance.cpython-310.pyc +0 -0
- src/utils/__pycache__/general.cpython-310.pyc +0 -0
- src/utils/finance.py +27 -0
- src/utils/general.py +78 -0
.streamlit/config.toml
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
[theme]
|
2 |
+
base="light"
|
README.md
CHANGED
@@ -1,13 +1,8 @@
|
|
1 |
-
|
2 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
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
|