Monte Carlo Simulation for Loan Amortization in Python

Monte Carlo Simulation for Loan Amortization in Python

This article introduces you to Monte Carlo simulations and walks you through this statistical technique to analyze loan payoff period probabilities in a loan amortization schedule. We’ll show you how to build the simulation in Python, and provide you with all the working code so that you can replicate this yourself.

Table of Contents

The Monte Carlo Method: A Primer

Monte Carlo Simulation, named after the famed Monte Carlo Casino in Monaco, is a mathematical technique that allows you to account for risk in quantitative analysis and decision making. The method provides a range of possible outcomes for any scenario that can be quantified probabilistically. Running simulations using random values within the defined constraints, allows us to explore different scenarios and understand their effects on our outcomes

 

Deciphering Loan Amortization

Before we dive into simulations, let's first understand what loan amortization is. Loan amortization is the process of paying off a debt (a loan) over time through regular payments. A portion of each payment is for interest while the remaining amount is applied towards the principal balance. The percentage of interest versus principal in each payment is determined in an amortization schedule.

Empirical rule

A substantial amount of financial data often exhibits characteristics of a normal distribution, a pattern that appears bell-shaped and symmetrical. This distribution allows for the utilization of the Empirical Rule. The Empirical Rule, specifically applicable to a normal distribution, provides estimates for where most data values fall - primarily within one, two, or three standard deviations of the mean. This rule is incredibly beneficial when analyzing financial data, as it assists in understanding the data's spread and the likelihood of certain outcomes.

The empirical rule, also known as the 68-95-99.7 rule, is a guideline for interpreting data when it follows a normal distribution (often referred to as a bell curve). This rule is called empirical because it is based on empirical, or observed, evidence rather than on strict mathematical proof.

Here's a breakdown of what the empirical rule states:

68% Rule: About 68% of data falls within one standard deviation of the mean. If you imagine a bell curve, this would be the data within the range [Mean - 1 Standard Deviation, Mean + 1 Standard Deviation].

95% Rule: Approximately 95% of data falls within two standard deviations of the mean. On the bell curve, this is the data within the range [Mean - 2 Standard Deviations, Mean + 2 Standard Deviations].

99.7% Rule: Almost all (about 99.7%) of the data falls within three standard deviations of the mean. This would be the data within the range [Mean - 3 Standard Deviations, Mean + 3 Standard Deviations].

 

Implementing Monte Carlo Simulations for Loan Amortization

Now that we understand the basics of both Monte Carlo simulations and loan amortization, let's examine how we can use these simulations to estimate the timeline for loan payoff.

For the purpose of this demonstration, let's consider a loan scenario with the following parameters:

Principal loan amount: $200,000

Annual interest rate: 5%

Loan term: 30 years

Probability of making an extra payment: 5%

Extra payment amount: $500

To run our simulations, we will use Python, a flexible and powerful programming language renowned for its use in data analysis and scientific computing. We will also use the numpy library for numerical calculations and the matplotlib library for creating visualizations.

We will start with installing the numpy-financial package, which provides a collection of basic financial functions that are commonly used in financial or business applications. These functions, like computing the present and future values, the number of periods, the payment against loan principal plus interest, etc., can help with various financial calculations.

 


!pip install numpy-financial

 

We will also be installing the pandas,  matplotlib.plot, and numpy_financial libraries.

numpy: Numpy (Numerical Python) is a library for the Python programming language which adds support for large multi-dimensional arrays and matrices, along with a large collection of high-level mathematical functions to operate on these arrays. On the other hand numpy-financial provides a collection of basic financial functions that are commonly used in financial or business applications.

pandas: Pandas is a software library written for the Python programming language for data manipulation and analysis. It provides data structures and functions needed to manipulate structured data.

matplotlib: Matplotlib is a plotting library for the Python programming language and its numerical mathematics extension NumPy. It provides an object-oriented API for embedding plots into applications using general-purpose GUI toolkits like Tkinter, wxPython, Qt, or GTK.

 


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import numpy_financial as npf

 

 

Now we will set-up our parameters and create a placeholder for our simulation results:

 


# Loan parameters
principal = 200_000  # Principal loan amount
annual_interest_rate = 0.05  # Annual interest rate
monthly_interest_rate = annual_interest_rate / 12  # Monthly interest rate
term_years = 30  # Loan term in years
term_months = term_years * 12  # Loan term in months
monthly_payment = principal * (monthly_interest_rate / (1 - (1 + monthly_interest_rate)**(-term_months)))  # Monthly payments


# Monte Carlo parameters
n_simulations = 1000  # Number of simulations
extra_payment_prob = 0.05  # Probability of making an extra payment
extra_payment_amount = 500  # Extra payment amount


# Simulation
loan_lifetime = np.zeros(n_simulations)

 

We initialize our loan parameters: principal, annual interest rate, monthly interest rate, term in years and months, and monthly payment. The Monte Carlo parameters specify the number of simulations we run and the probability and amount of an extra payment. We then set up an empty array to store the results of our simulations.

Next, we run the simulation. For each simulation, we calculate payments until the balance is paid off.

 


for i in range(n_simulations):
    balance = principal
    n_payments = 0
    while balance > 0:
        n_payments += 1
        interest_payment = balance * monthly_interest_rate
        principal_payment = monthly_payment - interest_payment
        balance -= principal_payment


        # Possible extra payment
        if np.random.rand() < extra_payment_prob:
            extra_payment = min(balance, extra_payment_amount)
            balance -= extra_payment


    loan_lifetime[i] = n_payments / 12  # Convert to years

 

In this block of code, we start by resetting the balance to the principal amount and the number of payments to zero for each simulation. We then enter a while loop that continues until the balance is completely paid off. Inside the loop, we calculate the interest and principal portions of each payment, subtract the principal payment from the balance, and increment the payment counter. We also include the possibility of an extra payment. If a randomly generated number is less than our specified probability of an extra payment, we subtract the extra payment from the balance.

Finally, we record the loan's lifetime (total number of payments divided by 12 to convert to years) in our results array.

 

Visualizing the Results

 

With our simulations complete, we can now visualize our results. A histogram is a great way to view the distribution of data. Here, we use the matplotlib library to generate a histogram representing the distribution of loan lifetimes across all of our simulations:

 


# Plotting
plt.hist(loan_lifetime, bins='auto', alpha=0.7, color='b', edgecolor='black')
plt.title('Loan lifetime distribution')
plt.xlabel('Years')
plt.ylabel('Frequency')
plt.show()

 

 

In this plot, the x-axis represents the loan lifetime in years, and the y-axis represents the frequency of each duration. Each bar in the histogram reflects the number of simulations (out of 1000) that resulted in a particular payoff period.

 

Influence of Extra Payments

Now, let's represent how the probability of making extra payments affects the loan payoff period. We'll run the simulation with different probabilities of extra payments and observe how this impacts the loan payoff period.

Defining extra payment probabilities: extra_payment_probs is a list of probabilities that a given payment will be higher than the normal amount.

Initializing placeholders for results: Two numpy arrays are created. loan_lifetime_probs will store the simulated loan lifetimes, and empirical_rules will store the calculated empirical rules (also known as the 68-95-99.7 rule) for each extra payment probability.

Running the simulation: The script then runs the simulation for each extra payment probability. For each simulation, it starts with a loan balance equal to principal (which needs to be defined before the script runs) and pays off the loan in monthly increments, with the possibility of an extra payment.

Calculating extra payments: In each month, there is an extra_payment_prob chance that the payment will be larger than the normal amount by extra_payment_amount (which needs to be defined before the script runs). If an extra payment is made, it is subtracted from the balance.

Storing and analyzing results: After each simulation, the total loan lifetime is stored in loan_lifetime_probs. Then, for each extra payment probability, the mean and standard deviation of the loan lifetimes are calculated, and the empirical rules (68-95-99.7 rule) are stored in empirical_rules.

Visualizing the results: The script plots a histogram of the loan lifetimes for each extra payment probability using plt.hist and displays it using plt.show().

Printing the empirical rule values: Finally, the empirical rule values for each extra payment probability are printed.

For this script to run, several variables must be defined beforehand, such as principal (the initial loan balance), monthly_interest_rate, monthly_payment (the amount paid each month without the extra payment), extra_payment_amount (the extra payment made when one occurs), and n_simulations (the number of times the simulation is run for each extra payment probability).

 


# Adding necessary import
from scipy.stats import norm


# Different probabilities for extra payment
extra_payment_probs = [0.01, 0.05, 0.1, 0.2]


# Placeholder for results
loan_lifetime_probs = np.zeros((len(extra_payment_probs), n_simulations))


# Placeholder for empirical rule results
empirical_rules = {}


for i, extra_payment_prob in enumerate(extra_payment_probs):
    for j in range(n_simulations):
        balance = principal
        n_payments = 0
        while balance > 0:
            n_payments += 1
            interest_payment = balance * monthly_interest_rate
            principal_payment = monthly_payment - interest_payment
            balance -= principal_payment


            # Possible extra payment
            if np.random.rand() < extra_payment_prob:
                extra_payment = min(balance, extra_payment_amount)
                balance -= extra_payment


        loan_lifetime_probs[i, j] = n_payments / 12  # Convert to years


    # Calculate empirical rule values for each extra_payment_prob
    mean = np.mean(loan_lifetime_probs[i])
    std = np.std(loan_lifetime_probs[i])


    empirical_rules[extra_payment_prob] = {
        '68%': [mean - std, mean + std],
        '95%': [mean - 2*std, mean + 2*std],
        '99.7%': [mean - 3*std, mean + 3*std],
    }


# Plotting
for i, extra_payment_prob in enumerate(extra_payment_probs):
    plt.hist(loan_lifetime_probs[i], bins='auto', alpha=0.7, label=f'Extra Payment Probability: {extra_payment_prob}')
plt.title('Loan lifetime distribution for different extra payment probabilities')
plt.xlabel('Years')
plt.ylabel('Frequency')
plt.legend(loc='upper right')
plt.show()


# Print empirical rule values
for prob, rules in empirical_rules.items():
    print(f'\nExtra Payment Probability: {prob}')
    for rule, interval in rules.items():
        print(f'{rule}: {interval}')

 

 

 

This graph visualizes how different probabilities for extra payments affect the distribution of loan lifetime. It makes it clear that increasing the chance of extra payments can significantly reduce the loan payoff period.

 


Extra Payment Probability: 0.01 
68%: [29.539680792159167, 29.902485874507498] 
95%: [29.358278250985002, 30.083888415681663] 
99.7%: [29.17687570981084, 30.265290956855825] 

Extra Payment Probability: 0.05 
68%: [28.18725513022733, 28.887078203105997] 
95%: [27.837343593788002, 29.236989739545326] 
99.7%: [27.48743205734867, 29.58690127598466] 

Extra Payment Probability: 0.1 
68%: [26.698044676907816, 27.630621989758847] 
95%: [26.231756020482305, 28.09691064618436] 
99.7%: [25.76546736405679, 28.563199302609874] 

Extra Payment Probability: 0.2
68%: [24.343158874690875, 25.38234112530912] 
95%: [23.823567749381752, 25.901932250618245] 
99.7%: [23.303976624072632, 26.421523375927364]

 

This table represents the loan pay-off periods at different confidence intervals (68%, 95%, and 99.7%) for various extra payment probabilities

 

Influence of Late Payments

This scenario examines different possibilities of late payments and computes the duration required to completely pay off the loan, presented in years. Furthermore, it performs an analysis of the distribution of these estimated loan lifetimes. Here's what the code is doing, step by step:

Defining late payment probabilities: The variable late_payment_probs is a list of probabilities that a given payment will be late.

Initializing placeholders for results: Two numpy arrays are created. loan_lifetime_probs will store the simulated loan lifetimes, and empirical_rules will store the calculated empirical rules (also known as the 68-95-99.7 rule) for each late payment probability.

Running the simulation: The script then runs the simulation for each late payment probability. For each simulation, it starts with a loan balance equal to principal (which needs to be defined before the script runs) and pays off the loan in monthly increments, with the possibility of a late payment.

Calculating late payments: In each month, there's a late_payment_prob chance that the payment will be late. If a payment is late, an additional month's interest is added to the loan balance.

Storing and analyzing results: After each simulation, the total loan lifetime is stored in loan_lifetime_probs. Then, for each late payment probability, the mean and standard deviation of the loan lifetimes are calculated, and the empirical rules (68-95-99.7 rule) are stored in empirical_rules.

Visualizing the results: The script plots a histogram of the loan lifetimes for each late payment probability and displays it using plt.show().

Printing the empirical rule values: Finally, the empirical rule values for each late payment probability are printed.

 


# Different probabilities for late payments
late_payment_probs = [0.01, 0.05, 0.1]


# Placeholder for results
loan_lifetime_probs = np.zeros((len(late_payment_probs), n_simulations))


# Placeholder for empirical rule results
empirical_rules = {}


for i, late_payment_prob in enumerate(late_payment_probs):
    for j in range(n_simulations):
        balance = principal
        n_payments = 0
        while balance > 0:
            n_payments += 1
            interest_payment = balance * monthly_interest_rate
            principal_payment = monthly_payment - interest_payment
            balance -= principal_payment


            # Possible late payment
            if np.random.rand() < late_payment_prob:
                balance += balance * monthly_interest_rate  # Additional month's interest


        loan_lifetime_probs[i, j] = n_payments / 12  # Convert to years


    # Calculate empirical rule values for each late_payment_prob
    mean = np.mean(loan_lifetime_probs[i])
    std = np.std(loan_lifetime_probs[i])


    empirical_rules[late_payment_prob] = {
        '68%': [mean - std, mean + std],
        '95%': [mean - 2*std, mean + 2*std],
        '99.7%': [mean - 3*std, mean + 3*std],
    }


# Plotting
for i, late_payment_prob in enumerate(late_payment_probs):
    plt.hist(loan_lifetime_probs[i], bins='auto', alpha=0.7, label=f'Late Payment Probability: {late_payment_prob}')
plt.title('Loan lifetime distribution for different late payment probabilities')
plt.xlabel('Years')
plt.ylabel('Frequency')
plt.legend(loc='upper right')
plt.show()


# Print empirical rule values
for prob, rules in empirical_rules.items():
    print(f'\nLate Payment Probability: {prob}')
    for rule, interval in rules.items():
        print(f'{rule}: {interval}')

 


Late Payment Probability: 0.01 
68%: [30.174912667609977, 30.72192066572335] 
95%: [29.90140866855329, 30.995424664780035] 
99.7%: [29.627904669496605, 31.26892866383672] 

Late Payment Probability: 0.05 
68%: [31.56710221254861, 32.95639778745139] 
95%: [30.87245442509722, 33.65104557490278] 
99.7%: [30.17780663764583, 34.34569336235417]

Late Payment Probability: 0.1 
68%: [33.982884322546944, 36.389115677453056] 
95%: [32.77976864509388, 37.59223135490612] 
99.7%: [31.576652967640822, 38.795347032359174]

 

68%: For each late payment probability, this represents the range within which you would expect the loan repayment time to fall 68% of the time. In other words, for a late payment probability of 0.01, about 68% of the time it would take between 30.17 and 30.72 years to repay the loan.

95%: This represents the range within which you would expect the loan repayment time to fall 95% of the time. For a late payment probability of 0.01, about 95% of the time it would take between 29.90 and 30.99 years to repay the loan.

99.7%: This represents the range within which you would expect the loan repayment time to fall 99.7% of the time. For a late payment probability of 0.01, virtually all (99.7%) of the time it would take between 29.63 and 31.27 years to repay the loan.

The results show that as the probability of late payment increases, the time it takes to repay the loan also increases on average. This makes sense, as late payments would slow down the repayment process. The ranges also widen with higher late payment probabilities, suggesting increased uncertainty about the repayment time. Below is a graphical representation of the above outputs.

 

 

The Value of Monte Carlo Simulations in Financial Planning

Monte Carlo simulations offer an enhanced perspective for financial planning. By considering various factors and uncertainties that may influence the loan payoff period, these simulations can provide more realistic predictions. Although this method involves its share of complexities, the value it brings to financial planning by providing insights into potential variability is undeniable. By using this approach, we can simulate thousands of possible outcomes and base our decisions on a comprehensive understanding of potential scenarios, making it a powerful tool for managing and understanding loans.

 

For access to the full working code visit our Github Repo

 

Written by Numan Yaqoob, PHD candidate

Back to blog