Mortgage Calculator

This example demonstrates how to create a mortgage calculator using Panel.

Python
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()