From b7659486b59c9728e12c24df27b9e9889b77f3bf Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Sun, 14 Apr 2024 15:56:16 +1000 Subject: [PATCH 1/9] ENH: Refactored IRR function to include default selection logic Created function to store default selection logic to allow user to insert their own selection logic for more than 1 real solution --- numpy_financial/_financial.py | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 6128885..2c192f4 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -708,8 +708,26 @@ def rate( rn[~close] = np.nan return rn +# default selection logic for IRR function when there are > 2 real solutions +def irr_default_selection(eirr): + # check sign of all IRR solutions + same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0) + + # if the signs of IRR solutions are not the same, first filter potential IRR + # by comparing the total positive and negative cash flows. + if not same_sign: + pos = sum(eirr[eirr > 0]) + neg = sum(eirr[eirr < 0]) + if pos >= neg: + eirr = eirr[eirr >= 0] + else: + eirr = eirr[eirr < 0] + + # pick the smallest one in magnitude and return + abs_eirr = np.abs(eirr) + return eirr[np.argmin(abs_eirr)] -def irr(values, *, raise_exceptions=False): +def irr(values, *, raise_exceptions=False, selection_logic=irr_default_selection): r"""Return the Internal Rate of Return (IRR). This is the "average" periodically compounded rate of return @@ -824,23 +842,8 @@ def irr(values, *, raise_exceptions=False): if len(eirr) == 1: return eirr[0] - # below is for the situation when there are more than 2 real solutions. - # check sign of all IRR solutions - same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0) - - # if the signs of IRR solutions are not the same, first filter potential IRR - # by comparing the total positive and negative cash flows. - if not same_sign: - pos = sum(values[values>0]) - neg = sum(values[values<0]) - if pos >= neg: - eirr = eirr[eirr>=0] - else: - eirr = eirr[eirr<0] - - # pick the smallest one in magnitude and return - abs_eirr = np.abs(eirr) - return eirr[np.argmin(abs_eirr)] + eirr = selection_logic(eirr) + return eirr def npv(rate, values): From a0564a4532bce261b5fd774e5f131f4c5192abac Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Sun, 14 Apr 2024 16:08:41 +1000 Subject: [PATCH 2/9] DOC: Documented new parameter for IRR function Documented addition of IRR selection logic function. The new function contains default logic for selection of IRR Values. User may enter their own function with custom logic if required. --- numpy_financial/_financial.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 2c192f4..09a8b08 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -708,8 +708,9 @@ def rate( rn[~close] = np.nan return rn + # default selection logic for IRR function when there are > 2 real solutions -def irr_default_selection(eirr): +def _irr_default_selection(eirr): # check sign of all IRR solutions same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0) @@ -727,7 +728,8 @@ def irr_default_selection(eirr): abs_eirr = np.abs(eirr) return eirr[np.argmin(abs_eirr)] -def irr(values, *, raise_exceptions=False, selection_logic=irr_default_selection): + +def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selection): r"""Return the Internal Rate of Return (IRR). This is the "average" periodically compounded rate of return @@ -749,6 +751,11 @@ def irr(values, *, raise_exceptions=False, selection_logic=irr_default_selection having reached the maximum number of iterations (IterationsExceededException). Set to False as default, thus returning NaNs in the two previous cases. + selection_logic: function, optional + Function for selection logic when more than 1 real solutions is found. User may + insert their own customised function for selection of IRR values. + The function should accept a one-dimensional array of numbers and return a number. + Returns ------- From 048810299d1e5948156a099d074a1357d6140ae9 Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Mon, 15 Apr 2024 19:26:01 +1000 Subject: [PATCH 3/9] MAINT: Fixed E501 Line too long error When attempting to push changes to GitHub, tests returned a "757:89: E501 Line too long (90 > 88)" error. Line 757 was shortened in this update. --- numpy_financial/_financial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 09a8b08..01894c3 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -754,7 +754,8 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio selection_logic: function, optional Function for selection logic when more than 1 real solutions is found. User may insert their own customised function for selection of IRR values. - The function should accept a one-dimensional array of numbers and return a number. + The function should accept a one-dimensional array of numbers + and return a number. Returns From 63aa1b1e7724b4c505dbc977ecc11bca92480ea4 Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Mon, 15 Apr 2024 20:04:57 +1000 Subject: [PATCH 4/9] MAINT: Fixed "Line too long" error WHen attemping to commit a change, a "Line too long" error occured. Line 757 has been shortened. --- numpy_financial/_financial.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 01894c3..76da2a6 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -752,10 +752,10 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio Set to False as default, thus returning NaNs in the two previous cases. selection_logic: function, optional - Function for selection logic when more than 1 real solutions is found. User may - insert their own customised function for selection of IRR values. - The function should accept a one-dimensional array of numbers - and return a number. + Function for selection logic when more than 1 real solutions is found. + User may insert their own customised function for selection + of IRR values.The function should accept a one-dimensional array + of numbers and return a number. Returns From 370abb0d1b0d27a5930a36d315e5bedff3ce470a Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Tue, 23 Apr 2024 19:36:59 +1000 Subject: [PATCH 5/9] ENH: Altered IRR function to accept 2D-array IRR function was changed to accept 2D-arrays as input. A for-loop was included to iterate over each row of the array, generate an IRR, and append its value to a results array. --- numpy_financial/_financial.py | 64 ++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 76da2a6..32e82b8 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -803,18 +803,20 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio 0.0886 """ - values = np.atleast_1d(values) - if values.ndim != 1: - raise ValueError("Cashflows must be a rank-1 array") - - # If all values are of the same sign no solution exists - # we don't perform any further calculations and exit early - same_sign = np.all(values > 0) if values[0] > 0 else np.all(values < 0) - if same_sign: - if raise_exceptions: - raise NoRealSolutionError('No real solution exists for IRR since all ' - 'cashflows are of the same sign.') - return np.nan + values = np.atleast_2d(values) + if values.ndim not in [1, 2]: + raise ValueError("Cashflows must be a 2D array") + + irr_results = [] + for row in values: + # If all values are of the same sign, no solution exists + # We don't perform any further calculations and exit early + same_sign = np.all(row > 0) if row[0] > 0 else np.all(row < 0) + if same_sign: + if raise_exceptions: + raise NoRealSolutionError('No real solution exists for IRR since all ' + 'cashflows are of the same sign.') + irr_results.append(np.nan) # We aim to solve eirr such that NPV is exactly zero. This can be framed as # simply finding the closest root of a polynomial to a given initial guess @@ -833,25 +835,25 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio # # which we solve using Newton-Raphson and then reverse out the solution # as eirr = g - 1 (if we are close enough to a solution) - - g = np.roots(values) - eirr = np.real(g[np.isreal(g)]) - 1 - - # realistic IRR - eirr = eirr[eirr>=-1] - - # if no real solution - if len(eirr) == 0: - if raise_exceptions: - raise NoRealSolutionError("No real solution is found for IRR.") - return np.nan - - # if only one real solution - if len(eirr) == 1: - return eirr[0] - - eirr = selection_logic(eirr) - return eirr + g = np.roots(row) + eirr = np.real(g[np.isreal(g)]) - 1 + + # Realistic IRR + eirr = eirr[eirr >= -1] + + # If no real solution + if len(eirr) == 0: + if raise_exceptions: + raise NoRealSolutionError("No real solution is found for IRR.") + irr_results.append(np.nan) + # If only one real solution + if len(eirr) == 1: + irr_results.append(eirr[0]) + + eirr = selection_logic(eirr) + irr_results.append(eirr) + + return np.array(irr_results) def npv(rate, values): From a753576ca0bc9117d4e409698312015ec13e7c72 Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Wed, 24 Apr 2024 18:59:40 +1000 Subject: [PATCH 6/9] BUG: Fixed error in logic for irr Bug was found upon commit in IRR function. Logic was incorrect and returning an out of index error. Conditional statement was fixed to overcome this. --- numpy_financial/_financial.py | 39 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 32e82b8..96582c6 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -835,25 +835,26 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio # # which we solve using Newton-Raphson and then reverse out the solution # as eirr = g - 1 (if we are close enough to a solution) - g = np.roots(row) - eirr = np.real(g[np.isreal(g)]) - 1 - - # Realistic IRR - eirr = eirr[eirr >= -1] - - # If no real solution - if len(eirr) == 0: - if raise_exceptions: - raise NoRealSolutionError("No real solution is found for IRR.") - irr_results.append(np.nan) - # If only one real solution - if len(eirr) == 1: - irr_results.append(eirr[0]) - - eirr = selection_logic(eirr) - irr_results.append(eirr) - - return np.array(irr_results) + else: + g = np.roots(row) + eirr = np.real(g[np.isreal(g)]) - 1 + + # Realistic IRR + eirr = eirr[eirr >= -1] + + # If no real solution + if len(eirr) == 0: + if raise_exceptions: + raise NoRealSolutionError("No real solution is found for IRR.") + irr_results.append(np.nan) + # If only one real solution + elif len(eirr) == 1: + irr_results.append(eirr[0]) + else: + eirr = selection_logic(eirr) + irr_results.append(eirr) + + return _ufunc_like(np.array(irr_results)) def npv(rate, values): From 18d23f90d844bae5c3923ed5a3e51123b3ffd6e0 Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Wed, 24 Apr 2024 19:28:42 +1000 Subject: [PATCH 7/9] MAINT: Changed IRR result from list to np.array Altered structure of IRR function to utilise a np.array to store IRR calculated, rather than a list. Also made small fixes in indentation. --- numpy_financial/_financial.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 96582c6..f6358f2 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -807,8 +807,8 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio if values.ndim not in [1, 2]: raise ValueError("Cashflows must be a 2D array") - irr_results = [] - for row in values: + irr_results = np.empty(values.shape[0]) + for i, row in enumerate(values): # If all values are of the same sign, no solution exists # We don't perform any further calculations and exit early same_sign = np.all(row > 0) if row[0] > 0 else np.all(row < 0) @@ -816,7 +816,7 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio if raise_exceptions: raise NoRealSolutionError('No real solution exists for IRR since all ' 'cashflows are of the same sign.') - irr_results.append(np.nan) + irr_results[i] = np.nan # We aim to solve eirr such that NPV is exactly zero. This can be framed as # simply finding the closest root of a polynomial to a given initial guess @@ -844,17 +844,16 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio # If no real solution if len(eirr) == 0: - if raise_exceptions: - raise NoRealSolutionError("No real solution is found for IRR.") - irr_results.append(np.nan) + if raise_exceptions: + raise NoRealSolutionError("No real solution is found for IRR.") + irr_results[i] = np.nan # If only one real solution elif len(eirr) == 1: - irr_results.append(eirr[0]) + irr_results[i] = eirr[0] else: - eirr = selection_logic(eirr) - irr_results.append(eirr) - - return _ufunc_like(np.array(irr_results)) + irr_results[i] = selection_logic(eirr) + + return _ufunc_like(irr_results) def npv(rate, values): From cd486857df4dc85ae194135fd908dec273bab27d Mon Sep 17 00:00:00 2001 From: Eugenia Mazur Date: Wed, 24 Apr 2024 19:39:13 +1000 Subject: [PATCH 8/9] DOC: Altered comment to format as docstring Comment in Irr selection logic was altered to be formated as a docstring. An example was included in the documentation containing a 2D-array input. --- numpy_financial/_financial.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index f6358f2..792d370 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -709,8 +709,8 @@ def rate( return rn -# default selection logic for IRR function when there are > 2 real solutions def _irr_default_selection(eirr): + """ default selection logic for IRR function when there are > 1 real solutions """ # check sign of all IRR solutions same_sign = np.all(eirr > 0) if eirr[0] > 0 else np.all(eirr < 0) @@ -801,7 +801,9 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio 0.06206 >>> round(npf.irr([-5, 10.5, 1, -8, 1]), 5) 0.0886 - + >>> npf.irr([[-100, 0, 0, 74], [-100, 100, 0, 7]]).round(5) + array([-0.0955 , 0.06206]) + """ values = np.atleast_2d(values) if values.ndim not in [1, 2]: From a00ab5f0443d2f1c52875b70f19f334c73a17729 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 7 May 2024 12:55:34 +0800 Subject: [PATCH 9/9] ENH: irr: Compare dimensions only to two We only need to check the 2d case. As this will never be a 1d array as ``np.atleast_2d`` was used. --- numpy_financial/_financial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index 792d370..c5cd231 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -806,7 +806,7 @@ def irr(values, *, raise_exceptions=False, selection_logic=_irr_default_selectio """ values = np.atleast_2d(values) - if values.ndim not in [1, 2]: + if values.ndim != 2: raise ValueError("Cashflows must be a 2D array") irr_results = np.empty(values.shape[0])