Python: irr redux
An irr function is included in a past entry for interest entry. Here we separate it and make it more robust by allowing it to return multiple irr values (if there is more than one) on a specified interval [a, b]. Our revised irr function still uses Newton’s method, but is combined with an interval generator where a solution may be found.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | """ file irr.py author Ernesto P. Adorio UPDEPP (UP Clarkfield) revisisions 0.0.2 apr. 2, 2009 """ def irr(C, ilow = 0, ihigh = 4, npts = 1000, ztol = 1.0e-5, verbose = True): """ C - cash flow ilow, ihigh - interval to find the interest rate. npts - number of pts to try. ztol - tolerance for Newton's method verbose - True if print progress to solution. Returns an array of the interests which results in an NPV of zero. Each element in the array returns a tuple of estimated rate of return, NPV, derivative, final increment, number of iterations. The returned array may have more than one element. """ def f(guess, C): """ Evaluates the NPV function for cashflow array C using interest rate of guess. """ f = 0.0 n = len(C) factor = 1.0 for i in range(n): d = C[i]/factor f+=d factor *= (1 + guess) return f def getintervals(C, ilow, ihigh, npts = 1000): """ Returns intervals where there is a change of sign in the irr. """ di = (ihigh - ilow)/float(npts) intervals = [] interest = ilow feval = f(interest, C) sign = (feval < 0.0) if verbose: print "initial interest, function value:", interest, feval for j in range(1, npts): interest += di fnew = f(interest, C) if (fnew< 0.0) != sign: #different sign detected. intervals.append((interest - di, interest)) sign = not sign return intervals def fg(guess, C): """ Evaluates and compute the derivative of the NPV function using interest rate of guess. """ f,g = 0.0, 0.0 n = len(C) factor = 1.0 for i in range(n): d = C[i]/factor f+=d factor *= (1 + guess) g+= -i* d/(1 + guess) return f, g def newton(guess, C, maxiter=1000, ztol = 1.0e-5, verbose = True): for i in range(maxiter): f, g = fg(guess, C) dguess = f/g guess = guess - dguess if verbose: print "(i,guess, f,g)", i, guess, f, g if abs(dguess) < ztol: f, g = fg(guess, C) dguess = f/g guess = guess - dguess return guess, f, g, dguess, i+1 return guess, f, g, dguess, maxiter intervals = getintervals(C, ilow, ihigh, npts) solutions = [] for interval in intervals: if verbose: print "Testing interval:", interval guess = interval[0] +0.5 * (interval[1] - interval[0]) sol = newton(guess, C, verbose = verbose) print "a solution:(irr, f, g, delta irr,maxiter )", sol solutions.append(sol) return solutions if __name__ =="__main__": C = [-2000, 1000, -2500, 0, 5000, 3000] print "irr = ", irr(C, 0, 4, 1000) |
The getintervals function is what makes the module more reliable in finding one (or more solutions). The intervals ensure that the function whose zero or root is to be found has opposite signs in the interval ends. Here is the result of running the above program with verbose=True:
$ python irr.py
irr = initial interest, function value: 0 4500.0
Testing interval: (0.27600000000000019, 0.28000000000000019)
(i,guess, f,g) 0 0.278812461885 6.11465040097 -7526.07662803
(i,guess, f,g) 1 0.278813976123 0.0113539822869 -7498.14691018
a solution:(irr, f, g, delta irr,maxiter ) (0.27881397612830144,
3.9325300349446479e-08, -7498.0949668773683,
-5.2447055583004659e-12, 2)
[(0.27881397612830144, 3.9325300349446479e-08,
-7498.0949668773683, -5.2447055583004659e-12, 2)]
$
Next , in the same spirit as safesec, we combine both half-interval or bisection method with the Newton's method in order that is more reliable and still fast as ever to find a solution.







