Skip to content

Commit b9e1588

Browse files
committed
ENH: mirr: Mimic broadcasting
1 parent 5c66fb0 commit b9e1588

File tree

2 files changed

+83
-23
lines changed

2 files changed

+83
-23
lines changed

numpy_financial/_financial.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -948,12 +948,12 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
948948
949949
Parameters
950950
----------
951-
values : array_like
951+
values : array_like, 1D or 2D
952952
Cash flows, where the first value is considered a sunk cost at time zero.
953953
It must contain at least one positive and one negative value.
954-
finance_rate : scalar
954+
finance_rate : scalar or 1D array
955955
Interest rate paid on the cash flows.
956-
reinvest_rate : scalar
956+
reinvest_rate : scalar or D array
957957
Interest rate received on the cash flows upon reinvestment.
958958
raise_exceptions: bool, optional
959959
Flag to raise an exception when the MIRR cannot be computed due to
@@ -962,7 +962,7 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
962962
963963
Returns
964964
-------
965-
out : float
965+
out : float or 2D array
966966
Modified internal rate of return
967967
968968
Notes
@@ -992,6 +992,22 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
992992
>>> npf.mirr([-100, 50, -60, 70], 0.10, 0.12)
993993
-0.03909366594356467
994994
995+
It is also possible to supply multiple cashflows or pairs of
996+
finance and reinvstment rates, note that in this case the number of elements
997+
in each of the rates arrays must match.
998+
999+
>>> values = [
1000+
... [-4500, -800, 800, 800, 600],
1001+
... [-120000, 39000, 30000, 21000, 37000],
1002+
... [100, 200, -50, 300, -200],
1003+
... ]
1004+
>>> finance_rate = [0.05, 0.08, 0.10]
1005+
>>> reinvestment_rate = [0.08, 0.10, 0.12]
1006+
>>> npf.mirr(values, finance_rate, reinvestment_rate)
1007+
array([[-0.1784449 , -0.17328716, -0.1684366 ],
1008+
[ 0.04627293, 0.05437856, 0.06252201],
1009+
[ 0.35712458, 0.40628857, 0.44435295]])
1010+
9951011
Now, let's consider the scenario where all cash flows are negative.
9961012
9971013
>>> npf.mirr([-100, -50, -60, -70], 0.10, 0.12)
@@ -1010,22 +1026,31 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
10101026
numpy_financial._financial.NoRealSolutionError:
10111027
No real solution exists for MIRR since all cashflows are of the same sign.
10121028
"""
1013-
values = np.asarray(values)
1014-
n = values.size
1015-
1016-
# Without this explicit cast the 1/(n - 1) computation below
1017-
# becomes a float, which causes TypeError when using Decimal
1018-
# values.
1019-
if isinstance(finance_rate, Decimal):
1020-
n = Decimal(n)
1021-
1022-
pos = values > 0
1023-
neg = values < 0
1024-
if not (pos.any() and neg.any()):
1029+
values_inner = np.atleast_2d(values).astype(np.float64)
1030+
finance_rate_inner = np.atleast_1d(finance_rate).astype(np.float64)
1031+
reinvest_rate_inner = np.atleast_1d(reinvest_rate).astype(np.float64)
1032+
n = values_inner.shape[1]
1033+
1034+
if finance_rate_inner.size != reinvest_rate_inner.size:
10251035
if raise_exceptions:
1026-
raise NoRealSolutionError('No real solution exists for MIRR since'
1027-
' all cashflows are of the same sign.')
1036+
raise ValueError("finance_rate and reinvest_rate must have the same size")
10281037
return np.nan
1029-
numer = np.abs(npv(reinvest_rate, values * pos))
1030-
denom = np.abs(npv(finance_rate, values * neg))
1031-
return (numer / denom) ** (1 / (n - 1)) * (1 + reinvest_rate) - 1
1038+
1039+
out_shape = _get_output_array_shape(values_inner, finance_rate_inner)
1040+
out = np.empty(out_shape)
1041+
1042+
for i, v in enumerate(values_inner):
1043+
for j, (rr, fr) in enumerate(zip(reinvest_rate_inner, finance_rate_inner)):
1044+
pos = v > 0
1045+
neg = v < 0
1046+
1047+
if not (pos.any() and neg.any()):
1048+
if raise_exceptions:
1049+
raise NoRealSolutionError("No real solution exists for MIRR since"
1050+
" all cashflows are of the same sign.")
1051+
out[i, j] = np.nan
1052+
else:
1053+
numer = np.abs(npv(rr, v * pos))
1054+
denom = np.abs(npv(fr, v * neg))
1055+
out[i, j] = (numer / denom) ** (1 / (n - 1)) * (1 + rr) - 1
1056+
return _ufunc_like(out)

numpy_financial/tests/test_financial.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# the versions in numpy instead of numpy_financial.
99
import numpy
1010
import pytest
11-
from hypothesis import given, settings
11+
from hypothesis import given, settings, assume
1212
from numpy.testing import (
1313
assert_,
1414
assert_allclose,
@@ -18,7 +18,6 @@
1818

1919
import numpy_financial as npf
2020

21-
2221
def float_dtype():
2322
return npst.floating_dtypes(sizes=[32, 64], endianness="<")
2423

@@ -393,6 +392,23 @@ def test_mirr(self, values, finance_rate, reinvest_rate, expected):
393392
else:
394393
assert_(numpy.isnan(result))
395394

395+
def test_mirr_broadcast(self):
396+
values = [
397+
[-4500, -800, 800, 800, 600],
398+
[-120000, 39000, 30000, 21000, 37000],
399+
[100, 200, -50, 300, -200],
400+
]
401+
finance_rate = [0.05, 0.08, 0.10]
402+
reinvestment_rate = [0.08, 0.10, 0.12]
403+
# Found using Google sheets
404+
expected = numpy.array([
405+
[-0.1784449, -0.17328716, -0.1684366],
406+
[0.04627293, 0.05437856, 0.06252201],
407+
[0.35712458, 0.40628857, 0.44435295]
408+
])
409+
actual = npf.mirr(values, finance_rate, reinvestment_rate)
410+
assert_allclose(actual, expected)
411+
396412
def test_mirr_no_real_solution_exception(self):
397413
# Test that if there is no solution because all the cashflows
398414
# have the same sign, then npf.mirr returns NoRealSolutionException
@@ -402,6 +418,25 @@ def test_mirr_no_real_solution_exception(self):
402418
with pytest.raises(npf.NoRealSolutionError):
403419
npf.mirr(val, 0.10, 0.12, raise_exceptions=True)
404420

421+
@given(
422+
values=cashflow_array_like_strategy,
423+
finance_rate=short_scalar_array_strategy,
424+
reinvestment_rate=short_scalar_array_strategy,
425+
)
426+
def test_fuzz(self, values, finance_rate, reinvestment_rate):
427+
assume(finance_rate.size == reinvestment_rate.size)
428+
npf.mirr(values, finance_rate, reinvestment_rate)
429+
430+
@given(
431+
values=cashflow_array_like_strategy,
432+
finance_rate=short_scalar_array_strategy,
433+
reinvestment_rate=short_scalar_array_strategy,
434+
)
435+
def test_fuzz(self, values, finance_rate, reinvestment_rate):
436+
assume(finance_rate.size != reinvestment_rate.size)
437+
with pytest.raises(ValueError):
438+
npf.mirr(values, finance_rate, reinvestment_rate, raise_exceptions=True)
439+
405440

406441
class TestNper:
407442
def test_basic_values(self):

0 commit comments

Comments
 (0)