Logo

Brooks Walsh
(Website Home)



LinkedIn
Resume
GitHub

# just spend an hour fixing a problem that didn't exist. All because Jan 1 2033 is a weekend... love bdays
#
# INFO:
#
# All the bank loan amort calcs I found rounded the interest payment amount, and then calculate the total payment 
#  based on the total interest + the principal
#
# TO FIX:
#
# Final x-axis tic set to total payment period amount
#
# Arguably the bonus payment type: seems standard (see below in TO ADD section)
#
# TO ADD:
# A pie graph that shows total amount paid in interest vs pricipal
#
# add a folder for the imported functions and change the import path
#
# add bar graph monthly payment distribution
#
# add bonus payment type (default=by_periods) (options: yearly, one-time)
#  for yearly: add optional bonus_date parameter (default=start_date)
#    bonus_date would act double as the one-time payment date parameter
#

import os
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import pandas as pd
import calendar
from statistics import mean
import seaborn as sns
import matplotlib.pyplot as plt

# functions created for this project (should create links if I ever share this)
from Required_Functions.validate_date import validate_date
from Required_Functions.business_days import business_days
from Required_Functions.cumulative_values import cumulative_values


def loan_amortization(principal, interest_rate,  term_years, start_date, periodtype="M-30", bonus=0, PLOT=False):
    
    """ 
    Args:
        principal (int or float): The principal amount of the loan.
        interest_rate (int or float): The annual interest rate, expressed as a percent (e.g. 5% = 5).
        term_years (int or float): Loan/Borrowing term in years.
        start_date (str or datetime): The start date of the loan as a string in 'YYYY-MM-DD' format or as a datetime object.
        periodtype (str, default="M-30"): The type of period to use for the loan payments, which can be one of the following:
            'D' (daily)
            'bdays' (daily, only includes business days)
            'W' (weekly)
            'BW' (biweekly)
            'M-30' (months where there is 30 days per month and 360 days per year (30/360))
            'M-Actual' (months where months' lengths are accurate, and there are 360 days per year (Actual/360))
            'Q' (quarterly)
            'S' (semi-annual)
            'Y' (Annual)
        bonus= (int or float, default=0): Optional, additional principal paid per period.
        PLOT= (Bool, default=False): With PLOT set to True, the function will create a folder in the cwd
                                    and download the loan amortization graph as a .png file.
                                - The .png file will have the following naming structure:
                                    - /Loan_Graphs/'Principal_Rate_TermYears_StartDate_PeriodType_bonus.png'
    Returns:
        pandas.DataFrame: A DataFrame containing the amortization schedule for the loan
    """
    # input validation for start_date
    if validate_date(start_date) is False:
        raise TypeError("start_date must be a string in 'YYYY-MM-DD' format or a datetime object")

    # if the date is in the string format, convert it
    if not isinstance(start_date, datetime):
        start_date = datetime.strptime(start_date, '%Y-%m-%d')

    # input type checking for principal, interest_rate, term_years, and bonus
    if not isinstance(principal, (int, float)):
        raise TypeError("Principal amount should be numeric (int or float)")
    if not isinstance(interest_rate, (int, float)):
        raise TypeError("Interest rate should be numeric and in % (int or float)")
    if not isinstance(term_years, (int, float)):
        raise TypeError("term_years should be numeric (int or float)")
    if not isinstance(bonus, (int, float)):
        raise TypeError("bonus should be numeric (int or float)")
    if bonus < 0:
        raise TypeError("bonus should be a positive integer")

    # shift the day forward one when using Daily to assume no payment is made today
    if periodtype == "D":
        start_date = start_date + timedelta(days=1)

    # create end date of term using term_years
    end_date = start_date + relativedelta(years=term_years)

    # create a list of business days for the bday index
    bdays_dates = business_days(start_date, end_date)

    # create a list of weeks for weekly and biweekly index, starting at second week
    week_range = pd.date_range(start=start_date + pd.Timedelta(weeks=1) + pd.Timedelta(days=1), periods=52*term_years, freq='W')
    week_list = [f'{date.week}-{date.year}' for date in week_range]

    # force start and end date to first day of month for month indexing (luv u feb)
    start_date_first = datetime(start_date.year, start_date.month, 1)
    end_date_first = datetime(end_date.year, end_date.month, 1)

    # shift start_date_first forward one month
    if start_date_first.month == 12:
        # handle special case where the month is December
        new_month = 1
        new_year = start_date_first.year + 1
    else:
        new_month = start_date_first.month + 1
        new_year = start_date_first.year
    start_date_p1 = start_date_first.replace(year=new_year, month=new_month)
    # shift end_date_first forward one month
    if end_date_first.month == 12:
        # handle special case where the month is December
        new_month = 1
        new_year = end_date_first.year + 1
    else:
        new_month = end_date_first.month + 1
        new_year = end_date_first.year
    end_date_first_p1 = end_date_first.replace(year=new_year, month=new_month)

    # create month_dates index
    month_dates = [start_date_p1.strftime("%m""-""%Y")]
    month_dates_4D = [start_date_p1]
    current_date = start_date_p1
    while current_date < end_date_first_p1:
        current_date += relativedelta(months=1)
        month_dates_4D.append(current_date)
        month_dates.append(current_date.strftime("%m""-""%Y"))
    # remove last month because 1 is start_date
    month_dates.pop()
    month_dates_4D.pop()

    # create list of days in the month of each date
    days_in_month = [calendar.monthrange(date.year, date.month)[1] for date in month_dates_4D]

    # period-type definition
    if periodtype == 'D':
        periods = int((end_date - start_date).days)
        adjusted_rate = interest_rate / 36525
    elif periodtype == 'bdays':
        periods = len(bdays_dates)
        adjusted_rate = interest_rate / 26100
    elif periodtype == 'W':
        periods = int(52 * term_years)
        adjusted_rate = interest_rate / 5200
    elif periodtype == 'BW':
        periods = int((52 * term_years) / 2)
        adjusted_rate = interest_rate / 2600
    elif periodtype == 'M-30':
        periods = int(12 * term_years)
        adjusted_rate = interest_rate / 1200
    elif periodtype == 'M-Actual':
        periods = int(12 * term_years)
        monthly_rate = [interest_rate / 36000 * days_in_month[i] for i in range(len(month_dates))]
        adjusted_rate = mean(monthly_rate)
    elif periodtype == 'Y':
        periods = term_years
        adjusted_rate = interest_rate / 100
    elif periodtype == 'S':
        periods = int(len(month_dates[1::6]))
        adjusted_rate = interest_rate / 200
    elif periodtype == 'Q':
        periods = int(len(month_dates[1::3]))
        adjusted_rate = interest_rate / 400
    else:
        raise TypeError("periodtype should be one of the following: 'D', 'W', 'BW', 'bdays', 'M-30', 'M-Actual', 'Q', 'S', 'Y'")

    # find payment amount
    monthly_payment = (principal * adjusted_rate / (1 - (1 + adjusted_rate) ** (-periods)))
    monthly_payment_fmt = "{:,.2f}".format(monthly_payment)
    monthly_for_plot = f"{monthly_payment_fmt} + {bonus}"
    actual_payment = (principal * adjusted_rate / (1 - (1 + adjusted_rate) ** (-periods))) + bonus

    # create a list of dates for each payment
    if periodtype == 'M-Actual' or periodtype == 'M-30':
        payment_dates = month_dates
    elif periodtype == 'Y':
        payment_dates = [(start_date + relativedelta(years=1 * i)).year for i in range(periods)]
    elif periodtype == 'bdays':
        payment_dates = bdays_dates
    elif periodtype == 'W':
        payment_dates = week_list
    elif periodtype == 'BW':
        payment_dates = week_list[::2]
    elif periodtype == 'S':
        month_dates.insert(0, start_date.strftime("%m""-""%Y"))
        payment_dates = month_dates[:-6:6]
    elif periodtype == 'Q':
        payment_dates = month_dates[1::3]
    else:
        payment_dates_nfmt = [start_date + relativedelta(days=(i)) for i in range(periods)]
        payment_dates = []
        for elem in payment_dates_nfmt:
            dates_formatted = elem.strftime('%Y-%m-%d')
            payment_dates.append(dates_formatted)

    # lists for the payment number, payment amount, interest, principal, and balance
    payment_number = list(range(1, periods + 1))
    payment_amount = [actual_payment] * periods
    interest = []
    principal_paid = []
    beg_balance = [principal]
    end_balance = []
    pct_interest = []
    pct_principal = []
    bonus_list = [bonus] * periods

    # interest, principal, and balance for each payment period (exlcuding M-actual)
    if not periodtype == "M-Actual":
        for i in range(periods):
            interest.append(beg_balance[i] * adjusted_rate)
            principal_paid.append((monthly_payment) - interest[i])
            beg_balance.append(beg_balance[i] - principal_paid[i] - bonus)
            end_balance.append(beg_balance[i] - principal_paid[i] - bonus)
            pct_interest.append((interest[i] / payment_amount[i]) * 100)
            pct_principal.append(((principal_paid[i] + bonus) / payment_amount[i]) * 100)
    elif periodtype == "M-Actual":
        for i in range(periods):
            interest.append((beg_balance[i] * monthly_rate[i]))
            principal_paid.append((monthly_payment) - interest[i])
            beg_balance.append(beg_balance[i] - principal_paid[i] - bonus)
            end_balance.append(beg_balance[i] - principal_paid[i] - bonus)
            pct_interest.append((interest[i] / payment_amount[i]) * 100)
            pct_principal.append(((principal_paid[i] + bonus) / payment_amount[i]) * 100)
        principal_paid[-1] = beg_balance[-2]
        payment_amount[-1] = principal_paid[-1] + interest[-1]
        end_balance[-1] = 0

    # if bonus > 0: do fake amortization without bonus for calc of interest saved
    if bonus > 0:
        interest2 = []
        principal_paid2 = []
        beg_balance2 = [principal]
        end_balance2 = []
        if not periodtype == "M-Actual":
            for i in range(periods):
                interest2.append(beg_balance2[i] * adjusted_rate)
                principal_paid2.append((monthly_payment) - interest2[i])
                beg_balance2.append(beg_balance2[i] - principal_paid2[i])
                end_balance2.append(beg_balance2[i] - principal_paid2[i])
        elif periodtype == "M-Actual":
            for i in range(periods):
                interest2.append((beg_balance2[i] * monthly_rate[i]))
                principal_paid2.append((monthly_payment) - interest2[i])
                beg_balance2.append(beg_balance2[i] - principal_paid2[i])
                end_balance2.append(beg_balance2[i] - principal_paid2[i])

    # make the amortization-schedule dataframe
    data = {
        'Payment Number': payment_number,
        'Payment Date': payment_dates,
        'Beginning Balance': beg_balance[:-1],
        'Payment Amount': payment_amount,
        'Bonus': bonus_list,
        'Interest Paid': interest,
        'Principal Paid': principal_paid,
        'Ending Balance': end_balance,
        '% Paid In Interest': pct_interest,
        '% Paid To Principal': pct_principal
    }
    # dataframe creation
    df = pd.DataFrame(data)

    # truncate df with bonus
    if bonus > 0:
        index_balance = (df['Ending Balance'] <= 0).idxmax()
        df = df.iloc[:index_balance + 1]
        df["Principal Paid"].iloc[-1] = df["Beginning Balance"].iloc[-1]
        df["Payment Amount"].iloc[-1] = df["Principal Paid"].iloc[-1] + df["Interest Paid"].iloc[-1]
        df["Bonus"].iloc[-1] = 0
        df["Ending Balance"].iloc[-1] = 0
        periods_b4save = periods
        periods = int(len(df.index))
        # find amount saved by extra payment
        amount_saved_nfmt = sum(interest2) - df["Interest Paid"].sum()
        amount_saved = "{:,.2f}".format(amount_saved_nfmt)
        # find periods saved
        periods_saved = periods_b4save - periods

    # create stats for plot
    # create total interest ****
    total_interest_nfmt = df["Interest Paid"].sum()
    total_interest = "{:,.2f}".format(total_interest_nfmt)

    # create total payment
    total_payment_nfmt = total_interest_nfmt + principal
    total_payment = "{:,.2f}".format(total_payment_nfmt)

    # format data for graph
    if bonus > 0:
        start_value = 0
        loan_balance_list = df["Ending Balance"].tolist()
        loan_balance = loan_balance_list.copy()
        loan_balance.insert(0, principal)
        interest_list = df["Interest Paid"].tolist()
        cumulative_interest_list = cumulative_values(interest_list)
        cumulative_interest = cumulative_interest_list.copy()
        cumulative_interest.insert(0, start_value)
        principal_paid_list = df["Principal Paid"].tolist()
        principal_paid_plot = [x + bonus if i < len(principal_paid_list)-1 else x for i, x in enumerate(principal_paid_list)] 
        cumulative_principal_list = cumulative_values(principal_paid_plot)
        cumulative_principal = cumulative_principal_list.copy()
        cumulative_principal.insert(0, start_value)
    else:
        start_value = 0
        loan_balance = end_balance.copy()
        loan_balance.insert(0, principal)
        cumulative_interest_list = cumulative_values(interest)
        cumulative_interest = cumulative_interest_list.copy()
        cumulative_interest.insert(0, start_value)
        cumulative_principal_list = cumulative_values(principal_paid)
        cumulative_principal = cumulative_principal_list.copy()
        cumulative_principal.insert(0, start_value)

    # set index to dates
    df.set_index('Payment Date', inplace=True)
    if periodtype == "M-Actual" or periodtype == "M-30" or periodtype == "Q" or periodtype == "S":
        df.index.name = "Payment Month"
    elif periodtype == "W" or periodtype == "BW":
        df.index.name = "Payment Week"
    elif periodtype == "Y":
        df.index.name = "Payment Year"
    else:
        df.index.name = 'Payment Date'

    # apply formating for dollar signs and two decimals (new df to retain old format)
    df['Payment Amount'] = df['Payment Amount'].apply(lambda x: '${:,.2f}'.format(x))
    df['Interest Paid'] = df['Interest Paid'].apply(lambda x: '${:,.2f}'.format(x))
    df['Principal Paid'] = df['Principal Paid'].apply(lambda x: '${:,.2f}'.format(x))
    df['Beginning Balance'] = df['Beginning Balance'].apply(lambda x: '${:,.2f}'.format(x))
    df['Ending Balance'] = df['Ending Balance'].apply(lambda x: '${:,.2f}'.format(x))
    df['% Paid In Interest'] = df['% Paid In Interest'].apply(lambda x: '{:,.3f}%'.format(x))
    df['% Paid To Principal'] = df['% Paid To Principal'].apply(lambda x: '{:,.3f}%'.format(x))

    plot_data = {
        'Loan Balance': loan_balance,
        'Cumulative Interest': cumulative_interest,
        'Principal Paid': cumulative_principal
    }

    # make plot
    plot = sns.lineplot(data=plot_data)
    
    # find period amount
    df_length = len(df["Payment Amount"])
    index_name = df.index.name

    # tweak visual aspects of plot
    plot.set_title(f"Loan Amortization Graph (${principal:,}|{interest_rate}%|{term_years} years|{periodtype})")
    plot.set_xlabel(f"Payment Number (Total Payments: {df_length})\n(Initial {index_name}: {df.index[0]} | Final {index_name}: {df.index[-1]})")
    plot.set_ylabel("Amount (in Dollars)")
    plt.grid(True)
    plot.set_xlim(0, periods)
    plot.set_ylim(0)

    # change line color
    lines = plot.lines
    lines[0].set(color='blue', linestyle='-')
    lines[1].set(color='red', linestyle='-')
    lines[2].set(color='green', linestyle='-')

    # make sure legend matches line color
    ax = plot.axes
    handles, labels = ax.get_legend_handles_labels()
    handles[0].set(color='blue', linestyle='-')
    handles[1].set(color='red', linestyle='-')
    handles[2].set(color='green', linestyle='-')
    ax.legend(handles=handles, labels=labels, loc='center left')

    # add the total stats as annotations
    plt.annotate(f'Total Cost of Loan: ${total_payment}', xy=((periods * .3), (principal)), xytext=((periods * .279), (ax.get_ylim()[1] - (ax.get_ylim()[1] * .045))), bbox=dict(facecolor='white', boxstyle='round'))
    plt.annotate(f'Total Interest Paid: ${total_interest}', xy=((periods * .3), (principal)), xytext=((periods * .2815), (ax.get_ylim()[1] - (ax.get_ylim()[1] * .11))), bbox=dict(facecolor='white', boxstyle='round'))
    plt.annotate(f'Payment: ${monthly_for_plot}', xy=((periods * .3), (principal)), xytext=((periods * .31), (ax.get_ylim()[1] - (ax.get_ylim()[1] * .175))), bbox=dict(facecolor='white', boxstyle='round'))

    # add addition annotations if there is bonus
    if bonus > 0:
        plt.annotate(f'Interest Saved w/ Bonus: ${amount_saved}', xy=((periods * .3), (principal)), xytext=(0 - (periods * .015) , (ax.get_ylim()[1] * 1.095)), bbox=dict(facecolor='white', boxstyle='round'))
        plt.annotate(f'Periods Saved w/ Bonus: {periods_saved}', xy=((periods * .3), (principal)), xytext=(periods - (periods * .365) , (ax.get_ylim()[1] * 1.095)), bbox=dict(facecolor='white', boxstyle='round'))

    # tighen layout of plot for saving
    plt.tight_layout()

    # save plot to graphs folder if PLOT=True
    if PLOT is True:
        if not os.path.exists('loan_graphs'):
            os.makedirs('loan_graphs')
        # save plot with filename based on input parameters
        plot_filename = f"loan_graphs/{principal}_{interest_rate}_{term_years}_{start_date.date()}_{periodtype}_bonus{bonus}.png"
        if not os.path.isfile(plot_filename):  # ensure plot doesn't attmept to save twice
            plt.savefig(plot_filename)

    return df
schedule = loan_amortization(200000, 3.5, 30, "2023-1-1", PLOT=True)

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
02-2023 1 $200,000.00 $898.09 0 $583.33 $314.76 $199,685.24 64.953% 35.047%
03-2023 2 $199,685.24 $898.09 0 $582.42 $315.67 $199,369.57 64.850% 35.150%
04-2023 3 $199,369.57 $898.09 0 $581.49 $316.59 $199,052.98 64.748% 35.252%
05-2023 4 $199,052.98 $898.09 0 $580.57 $317.52 $198,735.46 64.645% 35.355%
06-2023 5 $198,735.46 $898.09 0 $579.65 $318.44 $198,417.01 64.542% 35.458%
... ... ... ... ... ... ... ... ... ...
09-2052 356 $4,451.42 $898.09 0 $12.98 $885.11 $3,566.32 1.446% 98.554%
10-2052 357 $3,566.32 $898.09 0 $10.40 $887.69 $2,678.63 1.158% 98.842%
11-2052 358 $2,678.63 $898.09 0 $7.81 $890.28 $1,788.35 0.870% 99.130%
12-2052 359 $1,788.35 $898.09 0 $5.22 $892.87 $895.48 0.581% 99.419%
01-2053 360 $895.48 $898.09 0 $2.61 $895.48 $0.00 0.291% 99.709%

360 rows × 9 columns

png

schedule = loan_amortization(200000, 3.5, 30, "2023-1-1", bonus=200, PLOT=True)

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
02-2023 1 $200,000.00 $1,098.09 200 $583.33 $314.76 $199,485.24 53.123% 46.877%
03-2023 2 $199,485.24 $1,098.09 200 $581.83 $316.26 $198,968.99 52.986% 47.014%
04-2023 3 $198,968.99 $1,098.09 200 $580.33 $317.76 $198,451.22 52.849% 47.151%
05-2023 4 $198,451.22 $1,098.09 200 $578.82 $319.27 $197,931.95 52.711% 47.289%
06-2023 5 $197,931.95 $1,098.09 200 $577.30 $320.79 $197,411.16 52.573% 47.427%
... ... ... ... ... ... ... ... ... ...
06-2044 257 $4,510.96 $1,098.09 200 $13.16 $884.93 $3,426.03 1.198% 98.802%
07-2044 258 $3,426.03 $1,098.09 200 $9.99 $888.10 $2,337.93 0.910% 99.090%
08-2044 259 $2,337.93 $1,098.09 200 $6.82 $891.27 $1,246.66 0.621% 99.379%
09-2044 260 $1,246.66 $1,098.09 200 $3.64 $894.45 $152.20 0.331% 99.669%
10-2044 261 $152.20 $152.65 0 $0.44 $152.20 $0.00 0.040% 99.960%

261 rows × 9 columns

png

schedule = loan_amortization(200000, 3.5, 30, "2023-1-1", 'M-Actual')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
02-2023 1 $200,000.00 $903.82 0 $544.44 $359.37 $199,640.63 60.238% 39.762%
03-2023 2 $199,640.63 $903.82 0 $601.69 $302.12 $199,338.51 66.573% 33.427%
04-2023 3 $199,338.51 $903.82 0 $581.40 $322.41 $199,016.10 64.328% 35.672%
05-2023 4 $199,016.10 $903.82 0 $599.81 $304.00 $198,712.09 66.364% 33.636%
06-2023 5 $198,712.09 $903.82 0 $579.58 $324.24 $198,387.85 64.126% 35.874%
... ... ... ... ... ... ... ... ... ...
09-2052 356 $4,404.16 $903.82 0 $12.85 $890.97 $3,513.19 1.421% 98.579%
10-2052 357 $3,513.19 $903.82 0 $10.59 $893.23 $2,619.96 1.172% 98.828%
11-2052 358 $2,619.96 $903.82 0 $7.64 $896.17 $1,723.79 0.845% 99.155%
12-2052 359 $1,723.79 $903.82 0 $5.20 $898.62 $825.17 0.575% 99.425%
01-2053 360 $825.17 $827.65 0 $2.49 $825.17 $0.00 0.275% 99.725%

360 rows × 9 columns

png

schedule = loan_amortization(200000, 3.5, 30, "2023-1-1", 'M-Actual', bonus=200)

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
02-2023 1 $200,000.00 $1,103.82 200 $544.44 $359.37 $199,440.63 49.324% 50.676%
03-2023 2 $199,440.63 $1,103.82 200 $601.09 $302.72 $198,937.91 54.456% 45.544%
04-2023 3 $198,937.91 $1,103.82 200 $580.24 $323.58 $198,414.33 52.566% 47.434%
05-2023 4 $198,414.33 $1,103.82 200 $598.00 $305.82 $197,908.51 54.176% 45.824%
06-2023 5 $197,908.51 $1,103.82 200 $577.23 $326.58 $197,381.93 52.294% 47.706%
... ... ... ... ... ... ... ... ... ...
05-2044 256 $5,420.72 $1,103.82 200 $16.34 $887.48 $4,333.24 1.480% 98.520%
06-2044 257 $4,333.24 $1,103.82 200 $12.64 $891.18 $3,242.06 1.145% 98.855%
07-2044 258 $3,242.06 $1,103.82 200 $9.77 $894.04 $2,148.02 0.885% 99.115%
08-2044 259 $2,148.02 $1,103.82 200 $6.47 $897.34 $1,050.68 0.587% 99.413%
09-2044 260 $1,050.68 $1,053.74 0 $3.06 $1,050.68 $0.00 0.278% 99.722%

260 rows × 9 columns

png

schedule = loan_amortization(200000, 6, 2, "2023-1-1", 'D')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Date
2023-01-02 1 $200,000.00 $290.38 0 $32.85 $257.52 $199,742.48 11.314% 88.686%
2023-01-03 2 $199,742.48 $290.38 0 $32.81 $257.56 $199,484.91 11.300% 88.700%
2023-01-04 3 $199,484.91 $290.38 0 $32.77 $257.61 $199,227.31 11.285% 88.715%
2023-01-05 4 $199,227.31 $290.38 0 $32.73 $257.65 $198,969.66 11.271% 88.729%
2023-01-06 5 $198,969.66 $290.38 0 $32.68 $257.69 $198,711.97 11.256% 88.744%
... ... ... ... ... ... ... ... ... ...
2024-12-28 727 $1,451.17 $290.38 0 $0.24 $290.14 $1,161.03 0.082% 99.918%
2024-12-29 728 $1,161.03 $290.38 0 $0.19 $290.19 $870.84 0.066% 99.934%
2024-12-30 729 $870.84 $290.38 0 $0.14 $290.23 $580.61 0.049% 99.951%
2024-12-31 730 $580.61 $290.38 0 $0.10 $290.28 $290.33 0.033% 99.967%
2025-01-01 731 $290.33 $290.38 0 $0.05 $290.33 $-0.00 0.016% 99.984%

731 rows × 9 columns

png

schedule = loan_amortization(200000, 6, 2, "2023-1-1", 'bdays')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Date
2023-01-02 1 $200,000.00 $405.90 0 $45.98 $359.93 $199,640.07 11.327% 88.673%
2023-01-03 2 $199,640.07 $405.90 0 $45.89 $360.01 $199,280.07 11.307% 88.693%
2023-01-04 3 $199,280.07 $405.90 0 $45.81 $360.09 $198,919.98 11.286% 88.714%
2023-01-05 4 $198,919.98 $405.90 0 $45.73 $360.17 $198,559.80 11.266% 88.734%
2023-01-06 5 $198,559.80 $405.90 0 $45.65 $360.26 $198,199.55 11.246% 88.754%
... ... ... ... ... ... ... ... ... ...
2024-12-26 519 $2,028.11 $405.90 0 $0.47 $405.44 $1,622.68 0.115% 99.885%
2024-12-27 520 $1,622.68 $405.90 0 $0.37 $405.53 $1,217.15 0.092% 99.908%
2024-12-30 521 $1,217.15 $405.90 0 $0.28 $405.62 $811.52 0.069% 99.931%
2024-12-31 522 $811.52 $405.90 0 $0.19 $405.72 $405.81 0.046% 99.954%
2025-01-01 523 $405.81 $405.90 0 $0.09 $405.81 $0.00 0.023% 99.977%

523 rows × 9 columns

png

schedule = loan_amortization(200000, 5, 10, "2023-1-1", 'W')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Week
2-2023 1 $200,000.00 $488.93 0 $192.31 $296.62 $199,703.38 39.332% 60.668%
3-2023 2 $199,703.38 $488.93 0 $192.02 $296.91 $199,406.47 39.274% 60.726%
4-2023 3 $199,406.47 $488.93 0 $191.74 $297.19 $199,109.28 39.216% 60.784%
5-2023 4 $199,109.28 $488.93 0 $191.45 $297.48 $198,811.80 39.157% 60.843%
6-2023 5 $198,811.80 $488.93 0 $191.17 $297.76 $198,514.03 39.099% 60.901%
... ... ... ... ... ... ... ... ... ...
48-2032 516 $2,437.61 $488.93 0 $2.34 $486.59 $1,951.03 0.479% 99.521%
49-2032 517 $1,951.03 $488.93 0 $1.88 $487.05 $1,463.97 0.384% 99.616%
50-2032 518 $1,463.97 $488.93 0 $1.41 $487.52 $976.45 0.288% 99.712%
51-2032 519 $976.45 $488.93 0 $0.94 $487.99 $488.46 0.192% 99.808%
52-2032 520 $488.46 $488.93 0 $0.47 $488.46 $-0.00 0.096% 99.904%

520 rows × 9 columns

png

schedule = loan_amortization(200000, 5, 10, "2023-1-1", 'BW')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Week
2-2023 1 $200,000.00 $978.22 0 $384.62 $593.61 $199,406.39 39.318% 60.682%
4-2023 2 $199,406.39 $978.22 0 $383.47 $594.75 $198,811.65 39.201% 60.799%
6-2023 3 $198,811.65 $978.22 0 $382.33 $595.89 $198,215.75 39.084% 60.916%
8-2023 4 $198,215.75 $978.22 0 $381.18 $597.04 $197,618.72 38.967% 61.033%
10-2023 5 $197,618.72 $978.22 0 $380.04 $598.19 $197,020.53 38.850% 61.150%
... ... ... ... ... ... ... ... ... ...
43-2032 256 $4,863.02 $978.22 0 $9.35 $968.87 $3,894.15 0.956% 99.044%
45-2032 257 $3,894.15 $978.22 0 $7.49 $970.73 $2,923.41 0.766% 99.234%
47-2032 258 $2,923.41 $978.22 0 $5.62 $972.60 $1,950.81 0.575% 99.425%
49-2032 259 $1,950.81 $978.22 0 $3.75 $974.47 $976.34 0.384% 99.616%
51-2032 260 $976.34 $978.22 0 $1.88 $976.34 $-0.00 0.192% 99.808%

260 rows × 9 columns

png

schedule = loan_amortization(200000, 4, 10, "2023-1-1", 'Q', bonus=300)

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
03-2023 1 $200,000.00 $6,391.12 300 $2,000.00 $4,091.12 $195,608.88 31.293% 68.707%
06-2023 2 $195,608.88 $6,391.12 300 $1,956.09 $4,135.03 $191,173.85 30.606% 69.394%
09-2023 3 $191,173.85 $6,391.12 300 $1,911.74 $4,179.38 $186,694.47 29.912% 70.088%
12-2023 4 $186,694.47 $6,391.12 300 $1,866.94 $4,224.17 $182,170.29 29.212% 70.788%
03-2024 5 $182,170.29 $6,391.12 300 $1,821.70 $4,269.42 $177,600.88 28.504% 71.496%
06-2024 6 $177,600.88 $6,391.12 300 $1,776.01 $4,315.11 $172,985.77 27.789% 72.211%
09-2024 7 $172,985.77 $6,391.12 300 $1,729.86 $4,361.26 $168,324.50 27.067% 72.933%
12-2024 8 $168,324.50 $6,391.12 300 $1,683.25 $4,407.87 $163,616.63 26.337% 73.663%
03-2025 9 $163,616.63 $6,391.12 300 $1,636.17 $4,454.95 $158,861.68 25.601% 74.399%
06-2025 10 $158,861.68 $6,391.12 300 $1,588.62 $4,502.50 $154,059.17 24.857% 75.143%
09-2025 11 $154,059.17 $6,391.12 300 $1,540.59 $4,550.53 $149,208.65 24.105% 75.895%
12-2025 12 $149,208.65 $6,391.12 300 $1,492.09 $4,599.03 $144,309.61 23.346% 76.654%
03-2026 13 $144,309.61 $6,391.12 300 $1,443.10 $4,648.02 $139,361.59 22.580% 77.420%
06-2026 14 $139,361.59 $6,391.12 300 $1,393.62 $4,697.50 $134,364.09 21.806% 78.194%
09-2026 15 $134,364.09 $6,391.12 300 $1,343.64 $4,747.48 $129,316.61 21.024% 78.976%
12-2026 16 $129,316.61 $6,391.12 300 $1,293.17 $4,797.95 $124,218.65 20.234% 79.766%
03-2027 17 $124,218.65 $6,391.12 300 $1,242.19 $4,848.93 $119,069.72 19.436% 80.564%
06-2027 18 $119,069.72 $6,391.12 300 $1,190.70 $4,900.42 $113,869.30 18.630% 81.370%
09-2027 19 $113,869.30 $6,391.12 300 $1,138.69 $4,952.43 $108,616.87 17.817% 82.183%
12-2027 20 $108,616.87 $6,391.12 300 $1,086.17 $5,004.95 $103,311.92 16.995% 83.005%
03-2028 21 $103,311.92 $6,391.12 300 $1,033.12 $5,058.00 $97,953.92 16.165% 83.835%
06-2028 22 $97,953.92 $6,391.12 300 $979.54 $5,111.58 $92,542.34 15.327% 84.673%
09-2028 23 $92,542.34 $6,391.12 300 $925.42 $5,165.70 $87,076.64 14.480% 85.520%
12-2028 24 $87,076.64 $6,391.12 300 $870.77 $5,220.35 $81,556.29 13.625% 86.375%
03-2029 25 $81,556.29 $6,391.12 300 $815.56 $5,275.56 $75,980.73 12.761% 87.239%
06-2029 26 $75,980.73 $6,391.12 300 $759.81 $5,331.31 $70,349.42 11.888% 88.112%
09-2029 27 $70,349.42 $6,391.12 300 $703.49 $5,387.63 $64,661.80 11.007% 88.993%
12-2029 28 $64,661.80 $6,391.12 300 $646.62 $5,444.50 $58,917.29 10.117% 89.883%
03-2030 29 $58,917.29 $6,391.12 300 $589.17 $5,501.95 $53,115.35 9.219% 90.781%
06-2030 30 $53,115.35 $6,391.12 300 $531.15 $5,559.97 $47,255.38 8.311% 91.689%
09-2030 31 $47,255.38 $6,391.12 300 $472.55 $5,618.57 $41,336.82 7.394% 92.606%
12-2030 32 $41,336.82 $6,391.12 300 $413.37 $5,677.75 $35,359.06 6.468% 93.532%
03-2031 33 $35,359.06 $6,391.12 300 $353.59 $5,737.53 $29,321.53 5.533% 94.467%
06-2031 34 $29,321.53 $6,391.12 300 $293.22 $5,797.90 $23,223.63 4.588% 95.412%
09-2031 35 $23,223.63 $6,391.12 300 $232.24 $5,858.88 $17,064.75 3.634% 96.366%
12-2031 36 $17,064.75 $6,391.12 300 $170.65 $5,920.47 $10,844.28 2.670% 97.330%
03-2032 37 $10,844.28 $6,391.12 300 $108.44 $5,982.68 $4,561.60 1.697% 98.303%
06-2032 38 $4,561.60 $4,607.21 0 $45.62 $4,561.60 $0.00 0.714% 99.286%

png

schedule = loan_amortization(200000, 4, 10, "2023-1-1", 'S')

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Month
01-2023 1 $200,000.00 $12,231.34 0 $4,000.00 $8,231.34 $191,768.66 32.703% 67.297%
07-2023 2 $191,768.66 $12,231.34 0 $3,835.37 $8,395.97 $183,372.69 31.357% 68.643%
01-2024 3 $183,372.69 $12,231.34 0 $3,667.45 $8,563.89 $174,808.80 29.984% 70.016%
07-2024 4 $174,808.80 $12,231.34 0 $3,496.18 $8,735.17 $166,073.63 28.584% 71.416%
01-2025 5 $166,073.63 $12,231.34 0 $3,321.47 $8,909.87 $157,163.76 27.155% 72.845%
07-2025 6 $157,163.76 $12,231.34 0 $3,143.28 $9,088.07 $148,075.69 25.699% 74.301%
01-2026 7 $148,075.69 $12,231.34 0 $2,961.51 $9,269.83 $138,805.86 24.212% 75.788%
07-2026 8 $138,805.86 $12,231.34 0 $2,776.12 $9,455.23 $129,350.63 22.697% 77.303%
01-2027 9 $129,350.63 $12,231.34 0 $2,587.01 $9,644.33 $119,706.30 21.151% 78.849%
07-2027 10 $119,706.30 $12,231.34 0 $2,394.13 $9,837.22 $109,869.08 19.574% 80.426%
01-2028 11 $109,869.08 $12,231.34 0 $2,197.38 $10,033.96 $99,835.12 17.965% 82.035%
07-2028 12 $99,835.12 $12,231.34 0 $1,996.70 $10,234.64 $89,600.48 16.324% 83.676%
01-2029 13 $89,600.48 $12,231.34 0 $1,792.01 $10,439.33 $79,161.15 14.651% 85.349%
07-2029 14 $79,161.15 $12,231.34 0 $1,583.22 $10,648.12 $68,513.03 12.944% 87.056%
01-2030 15 $68,513.03 $12,231.34 0 $1,370.26 $10,861.08 $57,651.94 11.203% 88.797%
07-2030 16 $57,651.94 $12,231.34 0 $1,153.04 $11,078.30 $46,573.64 9.427% 90.573%
01-2031 17 $46,573.64 $12,231.34 0 $931.47 $11,299.87 $35,273.77 7.615% 92.385%
07-2031 18 $35,273.77 $12,231.34 0 $705.48 $11,525.87 $23,747.90 5.768% 94.232%
01-2032 19 $23,747.90 $12,231.34 0 $474.96 $11,756.39 $11,991.51 3.883% 96.117%
07-2032 20 $11,991.51 $12,231.34 0 $239.83 $11,991.51 $0.00 1.961% 98.039%

png

schedule = loan_amortization(200000, 4, 10, "2023-1-1", 'Y', bonus=300)

schedule
Payment Number Beginning Balance Payment Amount Bonus Interest Paid Principal Paid Ending Balance % Paid In Interest % Paid To Principal
Payment Year
2023 1 $200,000.00 $24,958.19 300 $8,000.00 $16,658.19 $183,041.81 32.054% 67.946%
2024 2 $183,041.81 $24,958.19 300 $7,321.67 $17,336.52 $165,405.29 29.336% 70.664%
2025 3 $165,405.29 $24,958.19 300 $6,616.21 $18,041.98 $147,063.32 26.509% 73.491%
2026 4 $147,063.32 $24,958.19 300 $5,882.53 $18,775.66 $127,987.66 23.570% 76.430%
2027 5 $127,987.66 $24,958.19 300 $5,119.51 $19,538.68 $108,148.98 20.512% 79.488%
2028 6 $108,148.98 $24,958.19 300 $4,325.96 $20,332.23 $87,516.75 17.333% 82.667%
2029 7 $87,516.75 $24,958.19 300 $3,500.67 $21,157.52 $66,059.23 14.026% 85.974%
2030 8 $66,059.23 $24,958.19 300 $2,642.37 $22,015.82 $43,743.41 10.587% 89.413%
2031 9 $43,743.41 $24,958.19 300 $1,749.74 $22,908.45 $20,534.96 7.011% 92.989%
2032 10 $20,534.96 $21,356.36 0 $821.40 $20,534.96 $0.00 3.291% 96.709%

png