Skip to content

Commit b20caa0

Browse files
committed
ENH: mirr: Mimic broadcasting
1 parent df5af39 commit b20caa0

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
@@ -963,12 +963,12 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
963963
964964
Parameters
965965
----------
966-
values : array_like
966+
values : array_like, 1D or 2D
967967
Cash flows, where the first value is considered a sunk cost at time zero.
968968
It must contain at least one positive and one negative value.
969-
finance_rate : scalar
969+
finance_rate : scalar or 1D array
970970
Interest rate paid on the cash flows.
971-
reinvest_rate : scalar
971+
reinvest_rate : scalar or D array
972972
Interest rate received on the cash flows upon reinvestment.
973973
raise_exceptions: bool, optional
974974
Flag to raise an exception when the MIRR cannot be computed due to
@@ -977,7 +977,7 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
977977
978978
Returns
979979
-------
980-
out : float
980+
out : float or 2D array
981981
Modified internal rate of return
982982
983983
Notes
@@ -1007,6 +1007,22 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
10071007
>>> npf.mirr([-100, 50, -60, 70], 0.10, 0.12)
10081008
np.float64(-0.03909366594356467)
10091009
1010+
It is also possible to supply multiple cashflows or pairs of
1011+
finance and reinvstment rates, note that in this case the number of elements
1012+
in each of the rates arrays must match.
1013+
1014+
>>> values = [
1015+
... [-4500, -800, 800, 800, 600],
1016+
... [-120000, 39000, 30000, 21000, 37000],
1017+
... [100, 200, -50, 300, -200],
1018+
... ]
1019+
>>> finance_rate = [0.05, 0.08, 0.10]
1020+
>>> reinvestment_rate = [0.08, 0.10, 0.12]
1021+
>>> npf.mirr(values, finance_rate, reinvestment_rate)
1022+
array([[-0.1784449 , -0.17328716, -0.1684366 ],
1023+
[ 0.04627293, 0.05437856, 0.06252201],
1024+
[ 0.35712458, 0.40628857, 0.44435295]])
1025+
10101026
Now, let's consider the scenario where all cash flows are negative.
10111027
10121028
>>> npf.mirr([-100, -50, -60, -70], 0.10, 0.12)
@@ -1025,22 +1041,31 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
10251041
numpy_financial._financial.NoRealSolutionError:
10261042
No real solution exists for MIRR since all cashflows are of the same sign.
10271043
"""
1028-
values = np.asarray(values)
1029-
n = values.size
1030-
1031-
# Without this explicit cast the 1/(n - 1) computation below
1032-
# becomes a float, which causes TypeError when using Decimal
1033-
# values.
1034-
if isinstance(finance_rate, Decimal):
1035-
n = Decimal(n)
1036-
1037-
pos = values > 0
1038-
neg = values < 0
1039-
if not (pos.any() and neg.any()):
1044+
values_inner = np.atleast_2d(values).astype(np.float64)
1045+
finance_rate_inner = np.atleast_1d(finance_rate).astype(np.float64)
1046+
reinvest_rate_inner = np.atleast_1d(reinvest_rate).astype(np.float64)
1047+
n = values_inner.shape[1]
1048+
1049+
if finance_rate_inner.size != reinvest_rate_inner.size:
10401050
if raise_exceptions:
1041-
raise NoRealSolutionError('No real solution exists for MIRR since'
1042-
' all cashflows are of the same sign.')
1051+
raise ValueError("finance_rate and reinvest_rate must have the same size")
10431052
return np.nan
1044-
numer = np.abs(npv(reinvest_rate, values * pos))
1045-
denom = np.abs(npv(finance_rate, values * neg))
1046-
return (numer / denom) ** (1 / (n - 1)) * (1 + reinvest_rate) - 1
1053+
1054+
out_shape = _get_output_array_shape(values_inner, finance_rate_inner)
1055+
out = np.empty(out_shape)
1056+
1057+
for i, v in enumerate(values_inner):
1058+
for j, (rr, fr) in enumerate(zip(reinvest_rate_inner, finance_rate_inner)):
1059+
pos = v > 0
1060+
neg = v < 0
1061+
1062+
if not (pos.any() and neg.any()):
1063+
if raise_exceptions:
1064+
raise NoRealSolutionError("No real solution exists for MIRR since"
1065+
" all cashflows are of the same sign.")
1066+
out[i, j] = np.nan
1067+
else:
1068+
numer = np.abs(npv(rr, v * pos))
1069+
denom = np.abs(npv(fr, v * neg))
1070+
out[i, j] = (numer / denom) ** (1 / (n - 1)) * (1 + rr) - 1
1071+
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)