import panel as pn
import param
import pandas as pd
from bokeh.palettes import Spectral10
from bokeh.plotting import figure
from bokeh.transform import cumsum
class MortgageCalculator(param.Parameterized):
total_available_for_closing = param.Number(default=20000)
home_purchase_price = param.Number(default=200000)
home_appraised_value = param.Number(default=180000)
mortgage_duration_years = param.Number(default=30)
interest_rate_percent = param.Number(default=6.5)
hoa = param.Number(default=100)
monthly_insurance = param.Number(default=65)
property_tax_rate_percent = param.Number(default=1.4)
fixed_closing_costs = param.Number(default=15000)
@param.depends("interest_rate_percent")
def monthly_interest_rate(self):
return self.interest_rate_percent / 12 / 100.0
@param.depends("home_purchase_price", "monthly_property_tax")
def prepaid_closing_costs(self):
prepaid_interest_for_12_days = 12 * self.interest_rate_percent / 365 / 100.0
return (
15 * self.monthly_insurance
+ 4 * self.monthly_property_tax()
+ prepaid_interest_for_12_days
)
@param.depends("prepaid_closing_costs", "fixed_closing_costs")
def estimated_closing_costs(self):
return round(self.prepaid_closing_costs() + self.fixed_closing_costs)
@param.depends(
"home_purchase_price",
"home_appraised_value",
"total_available_for_closing",
"estimated_closing_costs",
)
def down_payment(self):
diff_between_purchase_price_and_appraisal = (
self.home_purchase_price - self.home_appraised_value
)
return (
self.total_available_for_closing
- diff_between_purchase_price_and_appraisal
- self.estimated_closing_costs()
)
@param.depends(
"home_appraised_value",
"down_payment",
)
def loan_amount(self):
down_payment = self.down_payment()
return round(self.home_appraised_value - down_payment)
@param.depends("loan_amount", "mortgage_duration_years", "monthly_interest_rate")
def mortgage(self):
"""
M = P [ i(1 + i)^n ] / [ (1 + i)^n – 1]
P = principal loan amount
i = monthly interest rate
n = number of months required to repay the loan
"""
# Assume monthly payments
number_of_payments = self.mortgage_duration_years * 12
monthly_interest_rate = self.monthly_interest_rate()
monthly_mortgage_payment = self.loan_amount() * (
monthly_interest_rate
* (1 + monthly_interest_rate) ** number_of_payments
/ (((1 + monthly_interest_rate) ** number_of_payments) - 1)
)
return round(monthly_mortgage_payment)
@param.depends("property_tax_rate_percent", "home_appraised_value")
def monthly_property_tax(self):
return round(
self.property_tax_rate_percent * self.home_appraised_value / 12 / 100.0
)
@param.depends("mortgage", "hoa", "monthly_insurance", "monthly_property_tax")
def monthly_estimated_payments(self):
return round(
self.mortgage()
+ self.hoa
+ self.monthly_insurance
+ self.monthly_property_tax()
)
@param.depends("mortgage")
def amortization_schedule(self):
number_of_payments = self.mortgage_duration_years * 12
monthly_interest_rate = self.monthly_interest_rate()
months = range(1, number_of_payments + 1)
mortgage = self.mortgage()
principal_left = self.loan_amount()
amortization = []
for month in months:
interest_paid_in_month = principal_left * monthly_interest_rate
principal_paid_in_month = (
min(mortgage, principal_left + interest_paid_in_month)
- interest_paid_in_month
)
principal_left -= principal_paid_in_month
amortization.append(
{
"Month": month,
"Principal Left": round(principal_left),
"Interest Paid": round(interest_paid_in_month),
"Principal Paid": round(principal_paid_in_month),
}
)
return pn.pane.DataFrame(
pd.DataFrame.from_records(amortization), width=600, index=False
)
@param.depends("monthly_estimated_payments")
def monthly_payments_pie_chart(self):
payments = {
"Mortgage": self.mortgage(),
"Property Tax": self.monthly_property_tax(),
"Home Insurance": self.monthly_insurance,
"HOA": self.hoa,
}
return make_pie_chart(
payments, f"Monthly Costs: ${self.monthly_estimated_payments()}"
)
@param.depends("estimated_closing_costs", "down_payment")
def cost_at_closing_pie_chart(self):
payments = {
"Fixed Closing Costs": self.fixed_closing_costs,
"Prepaid Closing Costs": self.prepaid_closing_costs(),
"Down Payment": self.down_payment(),
"Purchase Cost not Covered by Loan": self.home_purchase_price
- self.home_appraised_value,
}
return make_pie_chart(
payments, f"Costs at Closing: ${self.total_available_for_closing}"
)
def make_pie_chart(key_values, title):
data = (
pd.Series(key_values)
.reset_index(name="value")
.rename(columns={"index": "Costs"})
)
data["angle"] = data["value"] / data["value"].sum() * 2 * 3.14
data["color"] = Spectral10[: len(key_values)]
p = figure(
height=300,
title=title,
toolbar_location=None,
tools="hover",
tooltips="@Costs: @value{1}",
x_range=(-0.5, 1.0),
)
p.wedge(
x=0,
y=1,
radius=0.3,
start_angle=cumsum("angle", include_zero=True),
end_angle=cumsum("angle"),
line_color="white",
fill_color="color",
legend_field="Costs",
source=data,
)
p.axis.axis_label = None
p.axis.visible = False
p.grid.grid_line_color = None
return p
explorer = MortgageCalculator(name="Mortgage Calculator")
pn.Row(
explorer.param,
pn.Column(
pn.Row(
pn.Column(
explorer.cost_at_closing_pie_chart,
explorer.monthly_payments_pie_chart,
),
),
pn.pane.HTML("<h2>Amortization Schedule</h2>"),
explorer.amortization_schedule,
)
).servable()
Click Run or press shift + ENTER to run code