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 zeroes 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
We can do this using the series {Pi}i of principal owed, which varies over time and will go to zero once paid off. In this series, P0 is the principal owed currently and Pi is the principal owed after i payments have been made. (Assuming monthly payments, this will be after i months.) If the term is T periods, then we have PT=0.
We have already introduced the term (T); we also need the value of the recurring (again, usually monthly) payment R, the interest rate r and the initial principal owed P0=P.
Time-Relationship between Principal Values
If after i periods, Pi is owed, then after one period has elapsed, we will owe Pi⋅m where 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 12 for the interest and add 1 to note that we are adding to existing principal: m(r)=1+r12.
In addition to the interest, we will have paid off R hence Pi+1=Pi⋅m−R.
Formula for Pi
Using this, we can actually determine Pi strictly in terms of m,R and P. First, note that P2=P1⋅m−R=(P0⋅m−R)⋅m−R=P⋅m2−R(m+1)
since P0=P. We can show inductively that Pi=P⋅mi−R⋅i−1∑j=0mj.
We already have the base case i=1, by definition. Assuming it holds for i, we see that Pi+1=Pi⋅m−R=m⋅(P⋅mi−R⋅i−1∑j=0mj)−R=P⋅mi+1−R⋅i∑j=1mj−R,
and our induction is complete. (We bump the index j since we are multiplying each mj by m.)
Each term in the series is related to the previous one (except P0, since time can't be negative in this case).
Step II: Determine a Function we want to find Zeroes of
Since we know PT=0 and PT=P⋅mT−R⋅∑T−1j=0mj, we actually have a polynomial in place that will let us solve for m and in so doing, solve for r.
To make our lives a tad easier, we'll do some rearranging. First, note that T−1∑j=0mj=mT−1+⋯+m+1=mT−1m−1.
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 0=P⋅mT−R⋅mT−1m−1⟺P⋅mT⋅(m−1)=R⋅(mT−1).
With that, we have accomplished Step II, we have found a function (parameterized by P,T and R which we can use zeroes from to find our interest rate: fP,T,R(m)=P⋅mT⋅(m−1)−R⋅(mT−1)=P⋅mT+1−(P+R)⋅mT+R.
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 f, its derivative f′ and a value x. Assume there is some zero y nearby x. Since they are close, we can approximate the slope of the line between the points (x,f(x) and (y,f(y) with the derivative nearby. Since we know x, we use f′(x) and intuit that f′(x)=slope=f(y)−f(x)y−x⇒y−x=f(y)−f(x)f′(x).
But, since we know that y is a zero, f(y)−f(x)=−f(x) hence y−x=−f(x)f′(x)⇒y=x−f(x)f′(x).
Using this method, one can start with a given value x0=x and compute better and better approximations of a zero via the iteration above that determines y. We use a sequence to do so: xi+1=xi−f(xi)f′(xi)
and stop calculating the xi either after f(xi) is below a preset threshold or after the fineness of the approximation |xi−xi+1| 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 (x0) and the functions f and f′. We'll also (arbitrarily) stop after the value f(xi) drops below 10−8 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 currentAs 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 fP,T,R(m)=P⋅mT+1−(P+R)⋅mT+R and f′P,T,R in Python. Before doing so, we do a simple derivative calculation: f′P,T,R(m)=P⋅(T+1)⋅mT−(P+R)⋅T⋅mT−1.
With these formulae in hand, we write a function which will spit out the corresponding f and f_prime given the parameters P (principal), T (term) and R (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 m and r before we can determine what sort of guess makes sense. In addition, once a value for m is returned from Newton-Raphson, we need to be able to turn it into an r 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 12⋅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:
>>> principal = 100000 >>> term = 360 >>> payment = 665.30 >>> solve_for_interest_rate(principal, term, payment, m, m_inverse) 0.0699996284703And 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 derivate and concavity to make sure that our guess yield the correct (and unique) zero.
No comments:
New comments are not allowed.