@@ -675,36 +675,7 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100):
675
675
return rn
676
676
677
677
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 ):
708
679
"""
709
680
Return the Internal Rate of Return (IRR).
710
681
@@ -721,6 +692,13 @@ def irr(values):
721
692
are negative and net "withdrawals" are positive. Thus, for
722
693
example, at least the first element of `values`, which represents
723
694
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.
724
702
725
703
Returns
726
704
-------
@@ -730,7 +708,7 @@ def irr(values):
730
708
Notes
731
709
-----
732
710
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
734
712
units and then makes the following withdrawals at regular (fixed)
735
713
intervals: 39, 59, 55, 20. Assuming the ending value is 0, one's 100
736
714
unit investment yields 173 units; however, due to the combination of
@@ -771,28 +749,36 @@ def irr(values):
771
749
if values .ndim != 1 :
772
750
raise ValueError ("Cashflows must be a rank-1 array" )
773
751
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 :
783
756
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
796
782
797
783
798
784
def npv (rate , values ):
0 commit comments