Skip to content

Commit f1c2e54

Browse files
committed
ENH: NPV: Support "gufunc" like behaviour
1 parent 3ea3a6e commit f1c2e54

File tree

2 files changed

+57
-10
lines changed

2 files changed

+57
-10
lines changed

benchmarks/benchmarks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def setup(self, n_cashflows, cashflow_lengths, rates_lengths):
2424
cf_shape = (n_cashflows, cashflow_lengths)
2525
self.cashflows = rng.standard_normal(cf_shape)
2626
self.rates = rng.standard_normal(rates_lengths)
27-
self.cashflows_decimal = rng.standard_normal(cf_shape).asdtype(Decimal)
28-
self.rates_decimal = rng.standard_normal(rates_lengths).asdtype(Decimal)
27+
self.cashflows_decimal = rng.standard_normal(cf_shape).astype(Decimal)
28+
self.rates_decimal = rng.standard_normal(rates_lengths).astype(Decimal)
2929

3030
def time_broadcast(self, n_cashflows, cashflow_lengths, rates_lengths):
3131
npf.npv(self.rates, self.cashflows)

numpy_financial/_financial.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ def _convert_when(when):
4646
return [_when_to_num[x] for x in when]
4747

4848

49+
def _return_ufunc_like(array):
50+
try:
51+
# If size of array is one, return scalar
52+
return array.item()
53+
except ValueError:
54+
# Otherwise, return entire array
55+
return array
56+
57+
58+
def _is_object_array(array):
59+
return array.dtype == np.dtype("O")
60+
61+
62+
def _use_decimal_dtype(*arrays):
63+
return any(_is_object_array(array) for array in arrays)
64+
65+
4966
def fv(rate, nper, pmt, pv, when='end'):
5067
"""Compute the future value.
5168
@@ -825,6 +842,15 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
825842
return np.nan
826843

827844

845+
def _to_decimal_array_1d(array):
846+
return np.array([Decimal(x) for x in array.tolist()])
847+
848+
849+
def _to_decimal_array_2d(array):
850+
l = [Decimal(x) for row in array.tolist() for x in row]
851+
return np.array(l).reshape(array.shape)
852+
853+
828854
def npv(rate, values):
829855
r"""Return the NPV (Net Present Value) of a cash flow series.
830856
@@ -892,15 +918,36 @@ def npv(rate, values):
892918
3065.22267
893919
894920
"""
921+
rates = np.atleast_1d(rate)
895922
values = np.atleast_2d(values)
896-
timestep_array = np.arange(0, values.shape[1])
897-
npv = (values / (1 + rate) ** timestep_array).sum(axis=1)
898-
try:
899-
# If size of array is one, return scalar
900-
return npv.item()
901-
except ValueError:
902-
# Otherwise, return entire array
903-
return npv
923+
924+
if rates.ndim != 1:
925+
msg = "invalid shape for rates. Rate must be either a scalar or 1d array"
926+
raise ValueError(msg)
927+
928+
if values.ndim != 2:
929+
msg = "invalid shape for values. Values must be either a 1d or 2d array"
930+
raise ValueError(msg)
931+
932+
dtype = Decimal if _use_decimal_dtype(rates, values) else np.float64
933+
934+
if dtype == Decimal:
935+
rates = _to_decimal_array_1d(rates)
936+
values = _to_decimal_array_2d(values)
937+
zero = dtype("0.0")
938+
one = dtype("1.0")
939+
940+
shape = tuple(array.shape[0] for array in (rates, values))
941+
out = np.empty(shape=shape, dtype=dtype)
942+
943+
for i in range(rates.shape[0]):
944+
for j in range(values.shape[0]):
945+
acc = zero
946+
for t in range(values.shape[1]):
947+
acc += values[j, t] / ((one + rates[i]) ** t)
948+
out[i, j] = acc
949+
950+
return _return_ufunc_like(out)
904951

905952

906953
def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):

0 commit comments

Comments
 (0)