I was recently playing around with some loan data and only happened to have the term (or length, or duration) of the loan, the amount of the recurring payment (in this case monthly) and the remaining principal owed on the loan. I figured there was an easy way to get at the interest rate, but wasn't sure how. After some badgering from my coworker +Paul, I searched the web and found a tool from CALCAmo (a site just for calculating amortizations).
Problem solved, right? Wrong. I wanted to know why; I had to go deeper. So I did a bit of math and a bit of programming and I was where I needed to be. I'll break the following down into parts before going on full steam.
- Break down the amortization schedule in terms of the variables we have and the one we want
- Determine a function we want to find zeros of
- Write some code to implement the Newton-Raphson method
- Utilize the Newton-Raphson code to find an interest rate
- Bonus: Analyze the function to make sure we are right
Step I: Break Down the Amortization Schedule
We can do this using the series of principal owed, which varies over time and will go to zero once paid off. In this series, is the principal owed currently and is the principal owed after payments have been made. (Assuming monthly payments, this will be after months.) If the term is periods, then we have .
We have already introduced the term ; we also need the value of the recurring (again, usually monthly) payment the interest rate and the initial principal owed .
Time-Relationship between Principal Values
If after periods, is owed, then after one period has elapsed, we will owe where is some multiplier based on the length of the term. For example if each period is one month, then we divide our rate by for the interest and add to note that we are adding to existing principal:
In addition to the interest, we will have paid off hence
Formula for
Using this, we can actually determine strictly in terms of and . First, note that
since . We can show inductively that
We already have the base case by definition. Assuming it holds for we see that
and our induction is complete. (We bump the index since we are multiplying each by .) Each term in the series is related to the previous one (except since time can't be negative in this case).
Step II: Determine a Function we want to find Zeros of
Since we know and we actually have a polynomial in place that will let us solve for and in so doing, solve for .
To make our lives a tad easier, we'll do some rearranging. First, note that
We calculate this sum of a geometric series here, but I'll just refer you to the Wikipedia page instead. With this reduction we want to solve
With that, we have accomplished Step II, we have found a function (parameterized by and which we can use zeros from to find our interest rate:
Step III: Write some code to implement the Newton-Raphson method
We use the Newton-Raphson method to get super-duper-close to a zero of the function.For in-depth coverage, see the Wikipedia page on the Newton-Raphson method, but I'll give some cursory coverage below. The methods used to show that a fixed point is found are not necessary for the intuition behind the method.
Intuition behind the method
For the intuition, assume we know (and can compute) a function its derivative at a value . Assume there is some zero nearby . Since they are close, we can approximate the slope of the line between the points and with the derivative nearby. Since we know we use and intuit that
But, since we know that is a zero, hence
Using this method, one can start with a given value and compute better and better approximations of a zero via the iteration above that determines . We use a sequence to do so:
and stop calculating the either after is below a preset threshold or after the fineness of the approximation goes below a (likely different) preset threshold. Again, there is much that can be said about these approximations, but we are trying to accomplish things today, not theorize.
Programming Newton-Raphson
To perform Newton-Raphson, we'll implement a Python function that takes the initial guess and the functions and . We'll also (arbitrarily) stop after the value drops below in absolute value.
def newton_raphson_method(guess, f, f_prime):
def next_value(value):
return value - f(value)*1.0/f_prime(value)
current = guess
while abs(f(current)) > 10**(-8):
current = next_value(current)
return current
As you can see, once we have f
and f_prime
, everything else is easy
because all the work in calculating the next value (via next_value
)
is done by the functions.
Step IV: Utilize the Newton-Raphson code to find an Interest Rate
We first need to implement and in Python. Before doing so, we do a simple derivative calculation:
With these formulae in
hand, we write a function which will spit out the corresponding f
and f_prime
given the parameters (principal
),
(term
) and (payment
):
def generate_polynomials(principal, term, payment):
def f(m):
return (principal*(m**(term + 1)) - (principal + payment)*(m**term) +
payment)
def f_prime(m):
return (principal*(term + 1)*(m**term) -
(principal + payment)*term*(m**(term - 1)))
return (f, f_prime)
Note that these functions only take a single argument (m
), but we are able
to use the other parameters from the parent scope beyond the life of the call
to generate_polynomials
due to
closure in Python.
In order to solve, we need an initial guess
, but we need to know the
relationship between and before
we can determine what sort ofguess
makes sense. In addition, once a value for
is returned from Newton-Raphson, we need to be able to
turn it into an value so functions m
and m_inverse
should be implemented. For our dummy case here, we'll assume monthly
payments (and compounding):
def m(r):
return 1 + r/12.0
def m_inverse(m_value):
return 12.0*(m_value - 1)
Using these, and assuming that an interest rate of 10% is a good guess, we can put all the pieces together:
def solve_for_interest_rate(principal, term, payment, m, m_inverse):
f, f_prime = generate_polynomials(principal, term, payment)
guess_m = m(0.10) # ten percent as a decimal
m_value = newton_raphson_method(guess_m, f, f_prime)
return m_inverse(m_value)
To check that this makes sense, let's plug in some values. Using the bankrate.com loan calculator, if we have a 30-year loan (with months of payments) of $100,000 with an interest rate of 7%, the monthly payment would be $665.30. Plugging this into our pipeline:
>>> principal = 100000
>>> term = 360
>>> payment = 665.30
>>> solve_for_interest_rate(principal, term, payment, m, m_inverse)
0.0699996284703
And we see the rate of 7% is approximated quite well!
Bonus: Analyze the function to make sure we are right
Coming soon. We will analyze the derivative and concavity to make sure that our guess yield the correct (and unique) zero.