using python to avoid the loan calculation headache

Loans are terrifying and confusing and downright frustrating. To this day, even after navigating the treacherous waters of taking out a mortgage, I still find myself a little confused about how it all fits together. One of the tools I came across that really helped clarify how my payments actually affected my loan balance, and the total interest I will ultimately pay, was an Amortization Table.

While playing with a free amortization table creator, I thought, “Hey, I’m in a pretty good mood, why not ruin it by trying to build this in Python??”. The good news is my mood is far from ruined and I actually plan on expanding and formalizing this code a bit in the future. The bad news is 18 year-old Tim would probably pretty disillusioned by seeing his future self write code and an article about a common financial table. It’s OK though, he won’t find out for a while yet ;).

Initial Variables

We’re going to need a few pieces of information to get started; namely, the loan_amount, the apr and the loan_term, in months. Here’s the variables initialized:

loan_amount = 18000
apr = 5.29
loan_term = 60

Calculating the Monthly Payment

The first step in understanding loan mechanics is calculating the monthly payment, based on the given loan amount, APR and term. We can use the formula below to calculate the monthly payment on our loan:

monthly\_payment = \frac{loan\_amount * (mpr)}{1-(1 + mpr)^{-loan\_term}}

While we already have the loan_amount and loan_term defined, we haven’t encountered mpr yet.

APR Explained

APR stands for Annual Percentage Rate, and is used to calculate the interest accrued on a loan over the term of a year. Since we are making monthly payments, and the amount of the loan changes over the course of the year, we need to calculate the interest on a monthly basis. We’ll do this by converting the percentage rate to a decimal number, and then dividing that number by 12. This gives us our APR rate on a monthly basis, or, as I very unofficially refer to it, mpr.

apr /= 100
mpr = apr / 12

Now that we’ve got our variables correctly defined, we can go ahead and recreate the equation above in our Python code.

monthly_payment = (loan_amount * mpr)/(1-(1+mpr) ** -loan_term)
print(f'The monthly payment comes to ${monthly_payment}')
The monthly payment comes to $342.0789050954898

Not bad; a quick check against Bankrate.com confirms that our calcualtions are correct!

Now that we have the monthly payment, we need to figure out how much of that payment goes towards the balance of the loan, or principal (the money you actually borrowed) and the how much is paid in interest (the money you pay for borrowing money).

The remaining principal multiplied by the mpr gives us the amount of interest paid for a month; therefore, the paid directly to the principal is

monthly\_payment - monthly\_interest

To put this all together, and update our principal balance, we use the equation

updated\_principal = monthly\_payment - (principal * mpr)

Creating a Python Function for Monthly Payments

Now that we have the formulas figured out, we can put it all together in a function that updates the principal balance of a loan, given the loan amount, term, and monthly payment amount.

def make_payment(principal, mpr, monthly_payment):
    '''Makes a 'payment' by subtracting and updated payment amount from the 
    principal. Returns the principal remaining, and the remaining loan term
    '''

    current_interest_payment = principal * mpr
    current_principal_payment = monthly_payment - current_interest_payment

    principal -= current_principal_payment

    return round(principal, 2)

Now that we have our payment function written, we can simulate paying off the loan! If our calculations were correct, we should end up with a $0.00 balance after our 60th payment.

Using While Loop

Here’s a stress-relief piggy bank

The easiest way to implement our payment function is within a while loop. In Python a while loop will continue to execute as long as certain conditions are met; in our case, that condition is owing the bank money, or principal > 0.

Think of it this way: we’ll continue to make_payment() while our principal > 0.

Before implementing the while loop, we’ll declare two variables that will be updated as it runs. The principal variable is equal to the initial loan amount, and the term_remaining variable is set to the initial loan term. These variables will be updated in our while loop, leaving the original values untouched for future use.

Finally, we’ll add a print() statement so we can see the principal balance being updated in real time!

term_remaining = loan_term
principal = loan_amount

while principal > 0 and term_remaining > 0:
    principal = make_payment(principal, mpr, monthly_payment)
    term_remaining -= 1
    print(f'Principal remaining: ${principal}')
Principal remaining: $17737.27
Principal remaining: $17473.38
Principal remaining: $17208.33
Principal remaining: $16942.11
Principal remaining: $16674.72
...
Principal remaining: $1353.36
Principal remaining: $1017.25
Principal remaining: $679.66
Principal remaining: $340.58
Principal remaining: $0.0

It appears everything is working as it should be; after the final payment we ended up with a $0 balance, and the loan fully paid off.

Time to start keeping track of our payments; before starting the while loop we’ll initialize a list to store he record of payments, and the balance remaining at each subsequent month.

term_remaining = loan_term
principal = loan_amount

# Iinitialize the payments list with the starting balance and term
payments = [[(loan_term - term_remaining), principal]]
while principal > 0 and term_remaining > 0:
    principal = make_payment(principal, mpr, monthly_payment)
    term_remaining -= 1
    payments.append([(loan_term - term_remaining), principal])

payments
[[0, 18000],
 [1, 17737.27],
 [2, 17473.38],
 [3, 17208.33],
 [4, 16942.11],
...
 [56, 1353.36],
 [57, 1017.25],
 [58, 679.66],
 [59, 340.58],
 [60, 0.0]]

Formatting with Pandas DataFrames

This, in essence, is the amortization table. However it’s not that difficult to add some features to it to make it both more legible and more useful. For instance, we can format the table itself using the DataFrame object pandas library, as well as tracking the principal, interest, and total interest paid over the course of the loan.

import pandas as pd
amortization_table = pd.DataFrame(data=payments,
                                  columns=['Month No', 'Principal Remaining'])
amortization_table.tail()
Principal RemainingMonth no
56561353.36
57571017.25
5858679.66
5959340.58
60600.00

Our table looks a little better, but it’s not exactly bursting with financial insight at the moment. We can beef it up quite a bit by modifying our make_payment() function to return more of the information we already have calculated within th function itself.

The astute observer will also see that the Month No column is no longer necessary; the DataFrame index can track that for us!

All we have to do is return the current_interest_payment and current_principal_payment that are calculated earlier in the function. We’ll do this as a list, so we can easily index or iterate through the different elements that comprise one payment.

def make_payment(principal, mpr, monthly_payment):
    '''Makes a 'payment' by subtracting and updated payment amount from the 
    principal. Returns the principal remaining, and the remaining loan term
    '''

    current_interest_payment = principal * mpr
    current_principal_payment = monthly_payment - current_interest_payment

    principal -= current_principal_payment

    return [round(principal, 2), round(current_principal_payment, 2), round(current_interest_payment, 2)] 

Now to edit the while loop to accommodate the updated function. The payments list will be initialized with three numbers, for principal,current_principal_payment, and current_interest_payment. We also have to update the principal from the values returned from the make_payment() function. Now for a test run:

term_remaining = loan_term
principal = loan_amount
payments = [[principal, 0, 0]]

while principal > 0 and term_remaining > 0:
    payment = make_payment(principal, mpr, monthly_payment)
    principal = payment[0]
    term_remaining -= 1
    payments.append(payment)
[[18000, 0, 0],
 [17737.27, 262.73, 79.35],
 [17473.38, 263.89, 78.19],
 [17208.33, 265.05, 77.03],
 [16942.11, 266.22, 75.86],
 ... 
 [1353.36, 334.64, 7.44],
 [1017.25, 336.11, 5.97],
 [679.66, 337.59, 4.48],
 [340.58, 339.08, 3.0],
 [0.0, 340.58, 1.5]]

Looks good! We’ll put this into a DataFrame for a more legible format:

term_remaining = loan_term
principal = loan_amount

# Iinitialize the payments list with the starting balance and term
payments = [[principal, 0, 0]]
total_interest = 0
while principal > 0 and term_remaining > 0:
    payment = make_payment(principal, mpr, monthly_payment)
    principal = payment[0]
    term_remaining -= 1
    payments.append(payment)

amortization_table = pd.DataFrame(data=payments,
                                  columns=['Principal Remaining',
                                          'Current Principal Payment',
                                          'Current Interest Payment'])
amortization_table
Principal RemainingCurrent Principal PaymentCurrent Interest Payment
018000.000.000.00
117737.27262.7379.35
217473.38263.8978.19
317208.33265.0577.03
416942.11266.2275.86
561353.36334.647.44
571017.25336.115.97
58679.66337.594.48
59340.58339.083.00
600.00340.581.50

Adding the Total Interest Paid

Not too shabby, be there’s a few more tweaks to make to the table to bring out even more info; adding the total interest paid, a fairly important number when comparing different loan options. To do so, we just have to initialize a total_interest variable and update it for each iteration of the while loop.

term_remaining = loan_term
principal = loan_amount

# Iinitialize the payments list with the starting balance and term
payments = [[principal, 0, 0, 0]]
total_interest = 0
while principal > 0 and term_remaining > 0:
    payment = make_payment(principal, mpr, monthly_payment)
    principal = payment[0]
    term_remaining -= 1
    total_interest += payment[2]
    payment.append(total_interest)
    payments.append(payment)

amortization_table = pd.DataFrame(data=payments,
                                  columns=['Principal Remaining',
                                          'Current Principal Payment',
                                          'Current Interest Payment',
                                          'Total Interest Paid'])
amortization_table
Principal RemainingCurrent Principal PaymentCurrent Interest PaymentTotal Interest Paid
018000.000.000.000.00
117737.27262.7379.3579.35
217473.38263.8978.19157.54
317208.33265.0577.03234.57
416942.11266.2275.86310.43
561353.36334.647.442509.79
571017.25336.115.972515.76
58679.66337.594.482520.24
59340.58339.083.002523.24
600.00340.581.502524.74

Now that we’ve got the table looking as it should, we can wrap all of this in a function for consistency. I’ve created a Github Gist for this that you can download and run as a CLI program, but you can assemble the code in any way you see fit. For a more in depth version of amortization using python, I have a repository that implements classes and some deeper analysis functions. Stay tuned for more, or check out some other tutorials!