Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
259 lines (195 sloc) 11.4 KB
---
title: Reverse Calculating An Interest Rate
date: 2012-05-15
author: Danny Hermes (dhermes@bossylobster.com)
tags: Finance, Interest Rate, Mortgage, Newton-Raphson Method, Numerical Analysis, Python
slug: reverse-calculating-interest-rate
comments: true
github_slug: templated_content/2012-05-15-reverse-calculating-interest-rate.template
---
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](https://plus.google.com/104679465567407024302), I searched the
web and found a
[tool](http://www.calcamo.net/loancalculator/quickcalculations/loan-rate.php5)
from [CALCAmo](http://www.calcamo.net/) (a site just for calculating
amortizations).
Problem solved, right? Wrong. I wanted to know why; I had to
[go deeper](http://knowyourmeme.com/memes/we-need-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][amort-sched]
-----------------------------------------------------------
We can do this using the series {{ get_katex("\\left\\{P_i\\right\\}_i") }} of
principal owed, which varies over time and will go to zero once paid
off. In this series, {{ get_katex("P_0") }} is the principal owed currently and
{{ get_katex("P_i") }} is the principal owed after {{ get_katex("i") }}
payments have been made. (Assuming monthly payments, this will be after
{{ get_katex("i") }} months.) If the term is {{ get_katex("T") }} periods,
then we have {{ get_katex("P_T = 0") }}.
We have already introduced the term {{ get_katex("T") }}; we also need the
value of the recurring (again, usually monthly) payment {{ get_katex("R,") }}
the interest rate {{ get_katex("r") }} and the initial principal owed
{{ get_katex("P_0 = P") }}.
#### Time-Relationship between Principal Values
If after {{ get_katex("i") }} periods, {{ get_katex("P_i") }} is owed, then
after one period has elapsed, we will owe {{ get_katex("P_i \\cdot m") }}
where {{ get_katex("m = m(r)") }} is some multiplier based on the length of the
term. For example if each period is one month, then we divide our rate by
{{ get_katex("12") }} for the interest and add {{ get_katex("1") }} to note
that we are adding to existing principal:
{{ get_katex("m(r) = 1 + \\frac{r}{12}.", blockquote=True) }}
In addition to the interest, we will have paid off {{ get_katex("R") }} hence
{{ get_katex("P_{i + 1} = P_i \\cdot m - R.", blockquote=True) }}
#### Formula for {{ get_katex("P_i") }}
Using this, we can actually determine {{ get_katex("P_i") }} strictly in terms
of {{ get_katex("m, R") }} and {{ get_katex("P") }}. First, note that
{{ get_katex("\\begin{aligned} P_2 = P_1 \\cdot m - R &= (P_0 \\cdot m - R) \\cdot m - R \\\\ &= P \\cdot m^2 - R(m + 1) \\end{aligned}", blockquote=True) }}
since {{ get_katex("P_0 = P") }}. We can show inductively that
{{ get_katex("\\displaystyle P_i = P \\cdot m^i - R \\cdot \\sum_{j = 0}^{i - 1} m^j.", blockquote=True) }}
We already have the base case {{ get_katex("i = 1,") }} by definition.
Assuming it holds for {{ get_katex("i,") }} we see that
{{ get_katex("\\begin{aligned} P_{i + 1} = P_i \\cdot m - R &= m \\cdot \\left(P \\cdot m^i - R \\cdot \\sum_{j = 0}^{i - 1} m^j\\right) - R \\\\ &= P \\cdot m^{i + 1} - R \\cdot \\sum_{j = 1}^{i} m^j - R, \\end{aligned}", blockquote=True) }}
and our induction is complete. (We bump the index {{ get_katex("j") }} since
we are multiplying each {{ get_katex("m^j") }} by {{ get_katex("m") }}.)
Each term in the series is related to the previous one (except
{{ get_katex("P_0,") }} since time can't be negative in this case).
Step II: Determine a Function we want to find Zeros of
------------------------------------------------------
Since we know {{ get_katex("P_T = 0") }} and
{{ get_katex("\\displaystyle P_T = P \\cdot m^T - R \\cdot \\sum_{j = 0}^{T - 1} m^j,") }}
we actually have a polynomial in place that will let us solve for
{{ get_katex("m") }} and in so doing, solve for {{ get_katex("r") }}.
To make our lives a tad easier, we'll do some rearranging. First, note
that
{{ get_katex("\\displaystyle \\sum_{j = 0}^{T - 1} m^j =m^{T - 1} + \\cdots + m + 1 = \\frac{m^T - 1}{m - 1}.", blockquote=True) }}
We calculate this sum of a geometric series here, but I'll just refer you to the
[Wikipedia page](http://en.wikipedia.org/wiki/Geometric_series) instead. With
this reduction we want to solve
{{ get_katex("\\begin{aligned} & 0 = P \\cdot m^T - R \\cdot \\frac{m^T - 1}{m - 1} \\\\ & \\Longleftrightarrow P \\cdot m^T \\cdot (m - 1) =R \\cdot(m^T - 1). \\end{aligned}", blockquote=True) }}
With that, we have accomplished Step II, we have found a function (parameterized
by {{ get_katex("P, T") }} and {{ get_katex("R") }} which we can
use zeros from to find our interest rate:
{{ get_katex("\\begin{aligned} f_{P, T, R}(m) &= P \\cdot m^T \\cdot (m - 1) -R \\cdot(m^T - 1) \\\\ &= P \\cdot m^{T + 1} - (P + R) \\cdot m^T + R. \\end{aligned}", blockquote=True) }}
Step III: Write some code to implement the [Newton-Raphson method][newton-raph]
---------------------------------------------------------------------
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
{{ get_katex("f,") }} its derivative {{ get_katex("f'") }} at a value
{{ get_katex("x") }}. Assume there is some zero {{ get_katex("y") }} nearby
{{ get_katex("x") }}. Since they are close, we can approximate the
slope of the line between the points {{ get_katex("(x, f(x))") }} and
{{ get_katex("(y, f(y))") }} with the derivative nearby. Since we know
{{ get_katex("x,") }} we use {{ get_katex("f'(x)") }} and intuit that
{{ get_katex("f'(x) = \\text{slope} = \\frac{f(y) - f(x)}{y - x} \\Rightarrow y - x = \\frac{f(y) - f(x)}{f'(x)}.", blockquote=True) }}
But, since we know that {{ get_katex("y") }} is a zero,
{{ get_katex("f(y) - f(x) = -f(x)") }} hence
{{ get_katex("y - x = \\frac{-f(x)}{f'(x)} \\Rightarrow y = x - \\frac{f(x)}{f'(x)}.", blockquote=True) }}
Using this method, one can start with a given value {{ get_katex("x_0 = x") }}
and compute better and better approximations of a zero via the iteration above
that determines {{ get_katex("y") }}. We use a sequence to do so:
{{ get_katex("x_{i + 1} = x_i - \\frac{f(x_i)}{f'(x_i)}", blockquote=True) }}
and stop calculating the {{ get_katex("x_i") }} either after
{{ get_katex("f(x_i)") }} is below a preset threshold or after the fineness of
the approximation {{ get_katex("\\left|x_i - x_{i + 1}\\right|") }} 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 {{ get_katex("x_0") }} and the functions
{{ get_katex("f") }} and {{ get_katex("f'") }}. We'll also (arbitrarily) stop
after the value {{ get_katex("f(x_i)") }} drops below
{{ get_katex("10^{-8}") }} in absolute value.
```python
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
{{ get_katex("f_{P, T, R}(m) = P \\cdot m^{T + 1} - (P + R) \\cdot m^T + R") }}
and {{ get_katex("f'_{P, T, R}") }} in Python. Before doing so, we do a simple
derivative calculation:
{{ get_katex("f_{P, T, R}'(m) = P \\cdot (T + 1) \\cdot m^T - (P + R) \\cdot T \\cdot m^{T - 1}.", blockquote=True) }}
With these [formulae](http://dictionary.reference.com/browse/formulae) in
hand, we write a function which will spit out the corresponding `f`
and `f_prime` given the parameters {{ get_katex("P") }} (`principal`),
{{ get_katex("T") }} (`term`) and {{ get_katex("R") }} (`payment`):
```python
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](http://en.wikipedia.org/wiki/Closure_(computer_science)) in Python.
In order to solve, we need an initial `guess`, but we need to know the
relationship between {{ get_katex("m") }} and {{ get_katex("r") }} before
we can determine what sort of`guess`makes sense. In addition, once a value for
{{ get_katex("m") }} is returned from Newton-Raphson, we need to be able to
turn it into an {{ get_katex("r") }} value so functions `m` and `m_inverse`
should be implemented. For our dummy case here, we'll assume monthly
payments (and compounding):
```python
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:
```python
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](http://www.bankrate.com/calculators/mortgages/mortgage-calculator.aspx),
if we have a 30-year loan (with {{ get_katex("12 \\cdot 30 = 360") }} months of
payments) of $100,000 with an interest rate of 7%, the monthly payment
would be $665.30. Plugging this into our pipeline:
```python
>>> 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.
[amort-sched]: http://en.wikipedia.org/wiki/Amortization_schedule
[newton-raph]: http://en.wikipedia.org/wiki/Newton's_method