From 4046a2cbd1e75578d278ea262a729da1bd2584e5 Mon Sep 17 00:00:00 2001 From: jlopezpena Date: Mon, 13 Feb 2023 15:31:30 +0000 Subject: [PATCH 1/3] Avoid redundant computations in IRR calculation The IRR computation adds and then subtract the previous iteration value before comparing to the tolerance. We can just compute the delta and compare that to the tolerance instead. This should also make the computation more robust if there is a large difference in magnitude between `x` and `delta` --- 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 be3fb67..c8519af 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -773,10 +773,10 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100): x = 1 / (1 + guess) for _ in range(maxiter): - x_new = x - (npv_(x) / d_npv(x)) - if abs(x_new - x) < tol: - return 1 / x_new - 1 - x = x_new + delta = npv_(x) / d_npv(x) + if abs(delta) < tol: + return 1 / (x - delta) - 1 + x -= delta return np.nan From e50c6101237576b9c127091ecd14d8514214513b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20L=C3=B3pez=20Pe=C3=B1a?= Date: Mon, 20 Feb 2023 10:51:52 +0000 Subject: [PATCH 2/3] Use heuristic for guess and reverse polynomial for stability --- numpy_financial/_financial.py | 40 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index c8519af..d41c0d0 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -10,13 +10,12 @@ Functions support the :class:`decimal.Decimal` type unless otherwise stated. """ -from __future__ import division, absolute_import, print_function +from __future__ import absolute_import, division, print_function from decimal import Decimal import numpy as np - __all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate', 'irr', 'npv', 'mirr'] @@ -675,7 +674,7 @@ def rate(nper, pmt, pv, fv, when='end', guess=None, tol=None, maxiter=100): return rn -def irr(values, guess=0.1, tol=1e-12, maxiter=100): +def irr(values, guess=None, tol=1e-12, maxiter=100): """ Return the Internal Rate of Return (IRR). @@ -694,7 +693,8 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100): the initial investment, will typically be negative. guess : float, optional Initial guess of the IRR for the iterative solver. If no guess is - given an initial guess of 0.1 (i.e. 10%) is assumed instead. + given an heuristic is used to estimate the guess through the ratio of + positive to negative cash lows tol : float, optional Required tolerance to accept solution. Default is 1e-12. maxiter : int, optional @@ -755,28 +755,38 @@ def irr(values, guess=0.1, tol=1e-12, maxiter=100): if same_sign: return np.nan + # If no value is passed for `guess`, then make a heuristic estimate + if guess is None: + inflow = sum(x for x in values if x > 0) + outflow = -sum(x for x in values if x < 0) + guess = inflow / outflow - 1 + # 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 # as follows: # V0 V1 V2 V3 - # NPV = ---------- + ---------- + ---------- + ---------- + ... + # NPV = ---------- + ---------- + ---------- + ---------- + ... = 0 # (1+eirr)^0 (1+eirr)^1 (1+eirr)^2 (1+eirr)^3 # - # by letting x = 1 / (1+eirr), we substitute to get + # by letting g = (1+eirr), we substitute to get + # + # NPV = V0 * 1/g^0 + V1 * 1/g^1 + V2 * 1/x^2 + V3 * 1/g^3 + ... = 0 + # + # Multiplying by g^N this becomes + # + # V0 * g^N + V1 * g^{N-1} + V2 * g^{N-2} + V3 * g^{N-3} + ... = 0 # - # NPV = V0 * x^0 + V1 * x^1 + V2 * x^2 + V3 * x^3 + ... - # - # which we solve using Newton-Raphson and then reverse out the solution - # as eirr = 1/x - 1 (if we are close enough to a solution) - npv_ = np.polynomial.Polynomial(values) + # which we solve using Newton-Raphson and then reverse out the solution + # as eirr = g - 1 (if we are close enough to a solution) + npv_ = np.polynomial.Polynomial(values[::-1]) d_npv = npv_.deriv() - x = 1 / (1 + guess) + g = 1 + guess for _ in range(maxiter): - delta = npv_(x) / d_npv(x) + delta = npv_(g) / d_npv(g) if abs(delta) < tol: - return 1 / (x - delta) - 1 - x -= delta + return g - 1 + g -= delta return np.nan From accc21429e26b41bd49cad41c33c82439dff4d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20L=C3=B3pez=20Pe=C3=B1a?= Date: Mon, 20 Feb 2023 10:55:45 +0000 Subject: [PATCH 3/3] Use numpy mask for inflows and outflows --- numpy_financial/_financial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/numpy_financial/_financial.py b/numpy_financial/_financial.py index d41c0d0..60fc02f 100644 --- a/numpy_financial/_financial.py +++ b/numpy_financial/_financial.py @@ -757,8 +757,9 @@ def irr(values, guess=None, tol=1e-12, maxiter=100): # If no value is passed for `guess`, then make a heuristic estimate if guess is None: - inflow = sum(x for x in values if x > 0) - outflow = -sum(x for x in values if x < 0) + positive_cashflow = values > 0 + inflow = values.sum(where=positive_cashflow) + outflow = -values.sum(where=~positive_cashflow) guess = inflow / outflow - 1 # We aim to solve eirr such that NPV is exactly zero. This can be framed as