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
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.
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_prime, everything else is easy
because all the work in calculating the next value (via
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_prime given the parameters (
term) and (
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
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 of
guessmakes 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
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.