Skip to content

Commit 135dab4

Browse files
authored
Merge pull request #101 from Kai-Striega/rev/remove-numba
REV: Remove numba
2 parents 0b05d53 + b72fd7b commit 135dab4

File tree

5 files changed

+11
-156
lines changed

5 files changed

+11
-156
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
matrix:
1010
os: [ubuntu-latest, macos-latest, windows-latest]
11-
python-version: ["3.9", "3.10", "3.11"]
11+
python-version: ["3.9", "3.10", "3.11", "3.12"]
1212
steps:
1313
- uses: actions/checkout@v3
1414
- name: Set up Python ${{ matrix.python-version }}

benchmarks/benchmarks.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,11 @@ def setup(self, n_cashflows, cashflow_lengths, rates_lengths):
3737
self.cashflows_decimal = _to_decimal_array_2d(self.cashflows)
3838
self.rates_decimal = _to_decimal_array_1d(self.rates)
3939

40-
def time_broadcast(self, n_cashflows, cashflow_lengths, rates_lengths):
41-
npf.npv(self.rates, self.cashflows)
42-
4340
def time_for_loop(self, n_cashflows, cashflow_lengths, rates_lengths):
4441
for rate in self.rates:
4542
for cashflow in self.cashflows:
4643
npf.npv(rate, cashflow)
4744

48-
def time_broadcast_decimal(self, n_cashflows, cashflow_lengths, rates_lengths):
49-
npf.npv(self.rates_decimal, self.cashflows_decimal)
50-
5145
def time_for_loop_decimal(self, n_cashflows, cashflow_lengths, rates_lengths):
5246
for rate in self.rates_decimal:
5347
for cashflow in self.cashflows_decimal:

numpy_financial/_financial.py

Lines changed: 8 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from decimal import Decimal
1515

16-
import numba as nb
1716
import numpy as np
1817

1918
__all__ = ['fv', 'pmt', 'nper', 'ipmt', 'ppmt', 'pv', 'rate',
@@ -47,36 +46,6 @@ def _convert_when(when):
4746
return [_when_to_num[x] for x in when]
4847

4948

50-
def _return_ufunc_like(array):
51-
try:
52-
# If size of array is one, return scalar
53-
return array.item()
54-
except ValueError:
55-
# Otherwise, return entire array
56-
return array
57-
58-
59-
def _is_object_array(array):
60-
return array.dtype == np.dtype("O")
61-
62-
63-
def _use_decimal_dtype(*arrays):
64-
return any(_is_object_array(array) for array in arrays)
65-
66-
67-
def _to_decimal_array_1d(array):
68-
return np.array([Decimal(x) for x in array.tolist()])
69-
70-
71-
def _to_decimal_array_2d(array):
72-
decimals = [Decimal(x) for row in array.tolist() for x in row]
73-
return np.array(decimals).reshape(array.shape)
74-
75-
76-
def _get_output_array_shape(*arrays):
77-
return tuple(array.shape[0] for array in arrays)
78-
79-
8049
def fv(rate, nper, pmt, pv, when='end'):
8150
"""Compute the future value.
8251
@@ -856,27 +825,6 @@ def irr(values, *, guess=None, tol=1e-12, maxiter=100, raise_exceptions=False):
856825
return np.nan
857826

858827

859-
@nb.njit(parallel=True)
860-
def _npv_native(rates, values, out):
861-
for i in nb.prange(rates.shape[0]):
862-
for j in nb.prange(values.shape[0]):
863-
acc = 0.0
864-
for t in range(values.shape[1]):
865-
acc += values[j, t] / ((1.0 + rates[i]) ** t)
866-
out[i, j] = acc
867-
868-
869-
# We require ``forceobj=True`` here to support decimal.Decimal types
870-
@nb.jit(forceobj=True)
871-
def _npv_decimal(rates, values, out):
872-
for i in range(rates.shape[0]):
873-
for j in range(values.shape[0]):
874-
acc = Decimal("0.0")
875-
for t in range(values.shape[1]):
876-
acc += values[j, t] / ((Decimal("1.0") + rates[i]) ** t)
877-
out[i, j] = acc
878-
879-
880828
def npv(rate, values):
881829
r"""Return the NPV (Net Present Value) of a cash flow series.
882830
@@ -944,58 +892,16 @@ def npv(rate, values):
944892
>>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
945893
3065.22267
946894
947-
The NPV calculation may be applied to several ``rates`` and ``cashflows``
948-
simulatneously. This produces an array of shape
949-
``(len(rates), len(cashflows))``.
950-
951-
>>> rates = [0.00, 0.05, 0.10]
952-
>>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
953-
>>> npf.npv(rates, cashflows).round(2)
954-
array([[-2700. , -3500. ],
955-
[-2798.19, -3612.24],
956-
[-2884.3 , -3710.74]])
957-
958-
The NPV calculation also supports `decimal.Decimal` types, for example
959-
if using Decimal ``rates``:
960-
961-
>>> rates = [Decimal("0.00"), Decimal("0.05"), Decimal("0.10")]
962-
>>> cashflows = [[-4_000, 500, 800], [-5_000, 600, 900]]
963-
>>> npf.npv(rates, cashflows)
964-
array([[Decimal('-2700.0'), Decimal('-3500.0')],
965-
[Decimal('-2798.185941043083900226757370'),
966-
Decimal('-3612.244897959183673469387756')],
967-
[Decimal('-2884.297520661157024793388430'),
968-
Decimal('-3710.743801652892561983471074')]], dtype=object)
969-
970-
This also works for Decimal cashflows.
971-
972895
"""
973-
rates = np.atleast_1d(rate)
974896
values = np.atleast_2d(values)
975-
976-
if rates.ndim != 1:
977-
msg = "invalid shape for rates. Rate must be either a scalar or 1d array"
978-
raise ValueError(msg)
979-
980-
if values.ndim != 2:
981-
msg = "invalid shape for values. Values must be either a 1d or 2d array"
982-
raise ValueError(msg)
983-
984-
dtype = Decimal if _use_decimal_dtype(rates, values) else np.float64
985-
986-
if dtype == Decimal:
987-
rates = _to_decimal_array_1d(rates)
988-
values = _to_decimal_array_2d(values)
989-
990-
shape = _get_output_array_shape(rates, values)
991-
out = np.empty(shape=shape, dtype=dtype)
992-
993-
if dtype == Decimal:
994-
_npv_decimal(rates, values, out)
995-
else:
996-
_npv_native(rates, values, out)
997-
998-
return _return_ufunc_like(out)
897+
timestep_array = np.arange(0, values.shape[1])
898+
npv = (values / (1 + rate) ** timestep_array).sum(axis=1)
899+
try:
900+
# If size of array is one, return scalar
901+
return npv.item()
902+
except ValueError:
903+
# Otherwise, return entire array
904+
return npv
999905

1000906

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

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ classifiers = [
2424
"Programming Language :: Python :: 3.9",
2525
"Programming Language :: Python :: 3.10",
2626
"Programming Language :: Python :: 3.11",
27+
"Programming Language :: Python :: 3.12",
2728
"Programming Language :: Python :: 3 :: Only",
2829
"Topic :: Software Development",
2930
"Topic :: Office/Business :: Financial :: Accounting",
@@ -37,10 +38,8 @@ classifiers = [
3738
packages = [{include = "numpy_financial"}]
3839

3940
[tool.poetry.dependencies]
40-
python = "^3.9,<3.12"
41+
python = "^3.9"
4142
numpy = "^1.23"
42-
numba = "^0.58.1"
43-
4443

4544
[tool.poetry.group.test.dependencies]
4645
pytest = "^7.4"

tests/test_financial.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -250,50 +250,6 @@ def test_npv_decimal(self):
250250
Decimal("122.894854950942692161628715"),
251251
)
252252

253-
def test_npv_broadcast(self):
254-
cashflows = [
255-
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
256-
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
257-
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
258-
[-15000.0, 1500.0, 2500.0, 3500.0, 4500.0, 6000.0],
259-
]
260-
expected_npvs = [[122.8948549, 122.8948549, 122.8948549, 122.8948549]]
261-
actual_npvs = npf.npv(0.05, cashflows)
262-
assert_allclose(actual_npvs, expected_npvs)
263-
264-
@pytest.mark.parametrize("dtype", [Decimal, float])
265-
def test_npv_broadcast_equals_for_loop(self, dtype):
266-
cashflows_str = [
267-
["-15000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
268-
["-25000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
269-
["-35000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
270-
["-45000.0", "1500.0", "2500.0", "3500.0", "4500.0", "6000.0"],
271-
]
272-
rates_str = ["-0.05", "0.00", "0.05", "0.10", "0.15"]
273-
274-
cashflows = numpy.array([[dtype(x) for x in cf] for cf in cashflows_str])
275-
rates = numpy.array([dtype(x) for x in rates_str])
276-
277-
expected = numpy.empty((len(rates), len(cashflows)), dtype=dtype)
278-
for i, r in enumerate(rates):
279-
for j, cf in enumerate(cashflows):
280-
expected[i, j] = npf.npv(r, cf)
281-
282-
actual = npf.npv(rates, cashflows)
283-
assert_equal(actual, expected)
284-
285-
@pytest.mark.parametrize("rates", ([[1, 2, 3]], numpy.empty(shape=(1, 1, 1))))
286-
def test_invalid_rates_shape(self, rates):
287-
cashflows = [1, 2, 3]
288-
with pytest.raises(ValueError):
289-
npf.npv(rates, cashflows)
290-
291-
@pytest.mark.parametrize("cf", ([[[1, 2, 3]]], numpy.empty(shape=(1, 1, 1))))
292-
def test_invalid_cashflows_shape(self, cf):
293-
rates = [1, 2, 3]
294-
with pytest.raises(ValueError):
295-
npf.npv(rates, cf)
296-
297253

298254
class TestPmt:
299255
def test_pmt_simple(self):

0 commit comments

Comments
 (0)