Skip to content

Commit 3f67c27

Browse files
authored
Merge pull request #122 from Eugenia-Mazur/irr_2d_array
Altered IRR function to accept 2D-array
2 parents 5c66fb0 + a00ab5f commit 3f67c27

File tree

1 file changed

+62
-47
lines changed

1 file changed

+62
-47
lines changed

numpy_financial/_financial.py

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,27 @@ def rate(
709709
return rn
710710

711711

712-
def irr(values, *, raise_exceptions=False):
712+
def _irr_default_selection(eirr):
713+
""" default selection logic for IRR function when there are > 1 real solutions """
714+
# check sign of all IRR solutions
715+
same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0)
716+
717+
# if the signs of IRR solutions are not the same, first filter potential IRR
718+
# by comparing the total positive and negative cash flows.
719+
if not same_sign:
720+
pos = sum(eirr[eirr > 0])
721+
neg = sum(eirr[eirr < 0])
722+
if pos >= neg:
723+
eirr = eirr[eirr >= 0]
724+
else:
725+
eirr = eirr[eirr < 0]
726+
727+
# pick the smallest one in magnitude and return
728+
abs_eirr = np.abs(eirr)
729+
return eirr[np.argmin(abs_eirr)]
730+
731+
732+
def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selection):
713733
r"""Return the Internal Rate of Return (IRR).
714734
715735
This is the "average" periodically compounded rate of return
@@ -731,6 +751,12 @@ def irr(values, *, raise_exceptions=False):
731751
having reached the maximum number of iterations (IterationsExceededException).
732752
Set to False as default, thus returning NaNs in the two previous
733753
cases.
754+
selection_logic: function, optional
755+
Function for selection logic when more than 1 real solutions is found.
756+
User may insert their own customised function for selection
757+
of IRR values.The function should accept a one-dimensional array
758+
of numbers and return a number.
759+
734760
735761
Returns
736762
-------
@@ -775,20 +801,24 @@ def irr(values, *, raise_exceptions=False):
775801
0.06206
776802
>>> round(npf.irr([-5, 10.5, 1, -8, 1]), 5)
777803
0.0886
778-
804+
>>> npf.irr([[-100, 0, 0, 74], [-100, 100, 0, 7]]).round(5)
805+
array([-0.0955 , 0.06206])
806+
779807
"""
780-
values = np.atleast_1d(values)
781-
if values.ndim != 1:
782-
raise ValueError("Cashflows must be a rank-1 array")
783-
784-
# If all values are of the same sign no solution exists
785-
# we don't perform any further calculations and exit early
786-
same_sign = np.all(values > 0) if values[0] > 0 else np.all(values < 0)
787-
if same_sign:
788-
if raise_exceptions:
789-
raise NoRealSolutionError('No real solution exists for IRR since all '
790-
'cashflows are of the same sign.')
791-
return np.nan
808+
values = np.atleast_2d(values)
809+
if values.ndim != 2:
810+
raise ValueError("Cashflows must be a 2D array")
811+
812+
irr_results = np.empty(values.shape[0])
813+
for i, row in enumerate(values):
814+
# If all values are of the same sign, no solution exists
815+
# We don't perform any further calculations and exit early
816+
same_sign = np.all(row > 0) if row[0] > 0 else np.all(row < 0)
817+
if same_sign:
818+
if raise_exceptions:
819+
raise NoRealSolutionError('No real solution exists for IRR since all '
820+
'cashflows are of the same sign.')
821+
irr_results[i] = np.nan
792822

793823
# We aim to solve eirr such that NPV is exactly zero. This can be framed as
794824
# simply finding the closest root of a polynomial to a given initial guess
@@ -807,40 +837,25 @@ def irr(values, *, raise_exceptions=False):
807837
#
808838
# which we solve using Newton-Raphson and then reverse out the solution
809839
# as eirr = g - 1 (if we are close enough to a solution)
810-
811-
g = np.roots(values)
812-
eirr = np.real(g[np.isreal(g)]) - 1
813-
814-
# realistic IRR
815-
eirr = eirr[eirr>=-1]
816-
817-
# if no real solution
818-
if len(eirr) == 0:
819-
if raise_exceptions:
820-
raise NoRealSolutionError("No real solution is found for IRR.")
821-
return np.nan
822-
823-
# if only one real solution
824-
if len(eirr) == 1:
825-
return eirr[0]
826-
827-
# below is for the situation when there are more than 2 real solutions.
828-
# check sign of all IRR solutions
829-
same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0)
830-
831-
# if the signs of IRR solutions are not the same, first filter potential IRR
832-
# by comparing the total positive and negative cash flows.
833-
if not same_sign:
834-
pos = sum(values[values>0])
835-
neg = sum(values[values<0])
836-
if pos >= neg:
837-
eirr = eirr[eirr>=0]
838840
else:
839-
eirr = eirr[eirr<0]
840-
841-
# pick the smallest one in magnitude and return
842-
abs_eirr = np.abs(eirr)
843-
return eirr[np.argmin(abs_eirr)]
841+
g = np.roots(row)
842+
eirr = np.real(g[np.isreal(g)]) - 1
843+
844+
# Realistic IRR
845+
eirr = eirr[eirr >= -1]
846+
847+
# If no real solution
848+
if len(eirr) == 0:
849+
if raise_exceptions:
850+
raise NoRealSolutionError("No real solution is found for IRR.")
851+
irr_results[i] = np.nan
852+
# If only one real solution
853+
elif len(eirr) == 1:
854+
irr_results[i] = eirr[0]
855+
else:
856+
irr_results[i] = selection_logic(eirr)
857+
858+
return _ufunc_like(irr_results)
844859

845860

846861
def npv(rate, values):

0 commit comments

Comments
 (0)