@@ -675,36 +675,7 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
675675 return rn
676676
677677
678- def _roots (p ):
679- """Modified version of NumPy's roots function.
680-
681- NumPy's roots uses the companion matrix method, which divides by
682- p[0]. This can causes overflows/underflows. Instead form a
683- modified companion matrix that is scaled by 2^c * p[0], where the
684- exponent c is chosen to balance the magnitudes of the
685- coefficients. Since scaling the matrix just scales the
686- eigenvalues, we can remove the scaling at the end.
687-
688- Scaling by a power of 2 is chosen to avoid rounding errors.
689-
690- """
691- _ , e = np .frexp (p )
692- # Balance the most extreme exponents e_max and e_min by solving
693- # the equation
694- #
695- # |c + e_max| = |c + e_min|.
696- #
697- # Round the exponent to an integer to avoid rounding errors.
698- c = int (- 0.5 * (np .max (e ) + np .min (e )))
699- p = np .ldexp (p , c )
700-
701- A = np .diag (np .full (p .size - 2 , p [0 ]), k = - 1 )
702- A [0 ,:] = - p [1 :]
703- eigenvalues = np .linalg .eigvals (A )
704- return eigenvalues / p [0 ]
705-
706-
707- def irr (values ):
678+ def irr (values , guess = 0.1 , tol = 1e-12 , maxiter = 100 ):
708679 """
709680 Return the Internal Rate of Return (IRR).
710681
@@ -721,6 +692,13 @@ def irr(values):
721692 are negative and net "withdrawals" are positive. Thus, for
722693 example, at least the first element of `values`, which represents
723694 the initial investment, will typically be negative.
695+ guess : float, optional
696+ Initial guess of the IRR for the iterative solver. If no guess is
697+ given an initial guess of 0.1 (i.e. 10%) is assumed instead.
698+ tol : float, optional
699+ Required tolerance to accept solution. Default is 1e-12.
700+ maxiter : int, optional
701+ Maximum iterations to perform in finding a solution. Default is 100.
724702
725703 Returns
726704 -------
@@ -730,7 +708,7 @@ def irr(values):
730708 Notes
731709 -----
732710 The IRR is perhaps best understood through an example (illustrated
733- using np.irr in the Examples section below). Suppose one invests 100
711+ using np.irr in the Examples section below). Suppose one invests 100
734712 units and then makes the following withdrawals at regular (fixed)
735713 intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100
736714 unit investment yields 173 units; however, due to the combination of
@@ -771,28 +749,36 @@ def irr(values):
771749 if values .ndim != 1 :
772750 raise ValueError ("Cashflows must be a rank-1 array" )
773751
774- # Strip leading and trailing zeros. Since we only care about
775- # positive roots we can neglect roots at zero.
776- non_zero = np .nonzero (np .ravel (values ))[0 ]
777- values = values [int (non_zero [0 ]):int (non_zero [- 1 ])+ 1 ]
778-
779- res = _roots (values [::- 1 ])
780-
781- mask = (res .imag == 0 ) & (res .real > 0 )
782- if not mask .any ():
752+ # If all values are of the same sign no solution exists
753+ # we don't perform any further calculations and exit early
754+ same_sign = np .all (values > 0 ) if values [0 ] > 0 else np .all (values < 0 )
755+ if same_sign :
783756 return np .nan
784- res = res [mask ].real
785- # NPV(rate) = 0 can have more than one solution so we return
786- # only the solution closest to zero.
787- rate = 1 / res - 1
788-
789- # If there are any positive solutions prefer those over negative
790- # rates.
791- if (rate > 0 ).any ():
792- rate = np .where (rate > 0 , rate , np .inf )
793-
794- rate = rate .item (np .argmin (np .abs (rate )))
795- return rate
757+
758+ # We aim to solve eirr such that NPV is exactly zero. This can be framed as
759+ # simply finding the closest root of a polynomial to a given initial guess
760+ # as follows:
761+ # V0 V1 V2 V3
762+ # NPV = ---------- + ---------- + ---------- + ---------- + ...
763+ # (1+eirr)^0 (1+eirr)^1 (1+eirr)^2 (1+eirr)^3
764+ #
765+ # by letting x = 1 / (1+eirr), we substitute to get
766+ #
767+ # NPV = V0 * x^0 + V1 * x^1 + V2 * x^2 + V3 * x^3 + ...
768+ #
769+ # which we solve using Newton-Raphson and then reverse out the solution
770+ # as eirr = 1/x - 1 (if we are close enough to a solution)
771+ npv_ = np .polynomial .Polynomial (values )
772+ d_npv = npv_ .deriv ()
773+ x = 1 / (1 + guess )
774+
775+ for _ in range (maxiter ):
776+ x_new = x - (npv_ (x ) / d_npv (x ))
777+ if abs (x_new - x ) < tol :
778+ return 1 / x_new - 1
779+ x = x_new
780+
781+ return np .nan
796782
797783
798784def npv (rate , values ):
0 commit comments