Skip to content

MAINT: Tidy up environment.yml #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 6, 2024
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ jobs:
with:
auto-update-conda: true
python-version: ${{ matrix.python-version }}
activate-environment: numpy-financial-dev
activate-environment: npf-dev
environment-file: environment.yml
auto-activate-base: false
- name: Conda metadata
7 changes: 3 additions & 4 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# To use:
# $ conda env create -f environment.yml # `mamba` works too for this command
# $ conda activate numpy-financial-dev
# $ conda activate npf-dev
#
name: numpy-financial-dev
name: npf-dev
channels:
- conda-forge
dependencies:
# Runtime dependencies
- python
- numpy
- numpy>=2.0.0
# Build
- cython>=3.0.9
- compilers
@@ -20,7 +20,6 @@ dependencies:
# Tests
- pytest
- pytest-xdist
- asv>=0.6.0
- hypothesis
# Docs
- myst-parser
77 changes: 51 additions & 26 deletions numpy_financial/_financial.py
Original file line number Diff line number Diff line change
@@ -245,7 +245,7 @@ def pmt(rate, nper, pv, fv=0, when='end'):
years at an annual interest rate of 7.5%?
>>> npf.pmt(0.075/12, 12*15, 200000)
-1854.0247200054619
np.float64(-1854.0247200054619)
In order to pay-off (i.e., have a future-value of 0) the $200,000 obtained
today, a monthly payment of $1,854.02 would be required. Note that this
@@ -424,7 +424,7 @@ def ipmt(rate, per, nper, pv, fv=0, when='end'):
>>> interestpd = np.sum(ipmt)
>>> np.round(interestpd, 2)
-112.98
np.float64(-112.98)
"""
when = _convert_when(when)
@@ -562,7 +562,7 @@ def pv(rate, nper, pmt, fv=0, when='end'):
interest rate is 5% (annually) compounded monthly.
>>> npf.pv(0.05/12, 10*12, -100, 15692.93)
-100.00067131625819
np.float64(-100.00067131625819)
By convention, the negative sign represents cash flow out
(i.e., money not available today). Thus, to end up with
@@ -913,7 +913,7 @@ def npv(rate, values):
>>> rate, cashflows = 0.08, [-40_000, 5_000, 8_000, 12_000, 30_000]
>>> np.round(npf.npv(rate, cashflows), 5)
3065.22267
np.float64(3065.22267)
It may be preferable to split the projected cashflow into an initial
investment and expected future cashflows. In this case, the value of
@@ -923,7 +923,7 @@ def npv(rate, values):
>>> initial_cashflow = cashflows[0]
>>> cashflows[0] = 0
>>> np.round(npf.npv(rate, cashflows) + initial_cashflow, 5)
3065.22267
np.float64(3065.22267)
The NPV calculation may be applied to several ``rates`` and ``cashflows``
simulatneously. This produces an array of shape ``(len(rates), len(cashflows))``.
@@ -963,12 +963,12 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
Parameters
----------
values : array_like
values : array_like, 1D or 2D
Cash flows, where the first value is considered a sunk cost at time zero.
It must contain at least one positive and one negative value.
finance_rate : scalar
finance_rate : scalar or 1D array
Interest rate paid on the cash flows.
reinvest_rate : scalar
reinvest_rate : scalar or D array
Interest rate received on the cash flows upon reinvestment.
raise_exceptions: bool, optional
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):
Returns
-------
out : float
out : float or 2D array
Modified internal rate of return
Notes
@@ -1007,6 +1007,22 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
>>> npf.mirr([-100, 50, -60, 70], 0.10, 0.12)
-0.03909366594356467
It is also possible to supply multiple cashflows or pairs of
finance and reinvstment rates, note that in this case the number of elements
in each of the rates arrays must match.
>>> values = [
... [-4500, -800, 800, 800, 600],
... [-120000, 39000, 30000, 21000, 37000],
... [100, 200, -50, 300, -200],
... ]
>>> finance_rate = [0.05, 0.08, 0.10]
>>> reinvestment_rate = [0.08, 0.10, 0.12]
>>> npf.mirr(values, finance_rate, reinvestment_rate)
array([[-0.1784449 , -0.17328716, -0.1684366 ],
[ 0.04627293, 0.05437856, 0.06252201],
[ 0.35712458, 0.40628857, 0.44435295]])
Now, let's consider the scenario where all cash flows are negative.
>>> npf.mirr([-100, -50, -60, -70], 0.10, 0.12)
@@ -1025,22 +1041,31 @@ def mirr(values, finance_rate, reinvest_rate, *, raise_exceptions=False):
numpy_financial._financial.NoRealSolutionError:
No real solution exists for MIRR since all cashflows are of the same sign.
"""
values = np.asarray(values)
n = values.size

# Without this explicit cast the 1/(n - 1) computation below
# becomes a float, which causes TypeError when using Decimal
# values.
if isinstance(finance_rate, Decimal):
n = Decimal(n)

pos = values > 0
neg = values < 0
if not (pos.any() and neg.any()):
values_inner = np.atleast_2d(values).astype(np.float64)
finance_rate_inner = np.atleast_1d(finance_rate).astype(np.float64)
reinvest_rate_inner = np.atleast_1d(reinvest_rate).astype(np.float64)
n = values_inner.shape[1]

if finance_rate_inner.size != reinvest_rate_inner.size:
if raise_exceptions:
raise NoRealSolutionError('No real solution exists for MIRR since'
' all cashflows are of the same sign.')
raise ValueError("finance_rate and reinvest_rate must have the same size")
return np.nan
numer = np.abs(npv(reinvest_rate, values * pos))
denom = np.abs(npv(finance_rate, values * neg))
return (numer / denom) ** (1 / (n - 1)) * (1 + reinvest_rate) - 1

out_shape = _get_output_array_shape(values_inner, finance_rate_inner)
out = np.empty(out_shape)

for i, v in enumerate(values_inner):
for j, (rr, fr) in enumerate(zip(reinvest_rate_inner, finance_rate_inner)):
pos = v > 0
neg = v < 0

if not (pos.any() and neg.any()):
if raise_exceptions:
raise NoRealSolutionError("No real solution exists for MIRR since"
" all cashflows are of the same sign.")
out[i, j] = np.nan
else:
numer = np.abs(npv(rr, v * pos))
denom = np.abs(npv(fr, v * neg))
out[i, j] = (numer / denom) ** (1 / (n - 1)) * (1 + rr) - 1
return _ufunc_like(out)
34 changes: 34 additions & 0 deletions numpy_financial/tests/strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import numpy as np
from hypothesis import strategies as st
from hypothesis.extra import numpy as npst

real_scalar_dtypes = st.one_of(
npst.floating_dtypes(),
npst.integer_dtypes(),
npst.unsigned_integer_dtypes()
)
nicely_behaved_doubles = npst.from_dtype(
np.dtype("f8"),
allow_nan=False,
allow_infinity=False,
allow_subnormal=False,
)
cashflow_array_strategy = npst.arrays(
dtype=npst.floating_dtypes(sizes=64),
shape=npst.array_shapes(min_dims=1, max_dims=2, min_side=0, max_side=25),
elements=nicely_behaved_doubles,
)
cashflow_list_strategy = cashflow_array_strategy.map(lambda x: x.tolist())
cashflow_array_like_strategy = st.one_of(
cashflow_array_strategy,
cashflow_list_strategy,
)
short_nicely_behaved_doubles = npst.arrays(
dtype=npst.floating_dtypes(sizes=64),
shape=npst.array_shapes(min_dims=0, max_dims=1, min_side=0, max_side=5),
elements=nicely_behaved_doubles,
)

when_strategy = st.sampled_from(
['end', 'begin', 'e', 'b', 0, 1, 'beginning', 'start', 'finish']
)
98 changes: 53 additions & 45 deletions numpy_financial/tests/test_financial.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import math
import warnings
from decimal import Decimal

import hypothesis.extra.numpy as npst
import hypothesis.strategies as st

# Don't use 'import numpy as np', to avoid accidentally testing
# the versions in numpy instead of numpy_financial.
import numpy
import pytest
from hypothesis import given, settings
from hypothesis import assume, given
from numpy.testing import (
assert_,
assert_allclose,
@@ -17,42 +15,11 @@
)

import numpy_financial as npf


def float_dtype():
return npst.floating_dtypes(sizes=[32, 64], endianness="<")


def int_dtype():
return npst.integer_dtypes(sizes=[32, 64], endianness="<")


def uint_dtype():
return npst.unsigned_integer_dtypes(sizes=[32, 64], endianness="<")


real_scalar_dtypes = st.one_of(float_dtype(), int_dtype(), uint_dtype())


cashflow_array_strategy = npst.arrays(
dtype=real_scalar_dtypes,
shape=npst.array_shapes(min_dims=1, max_dims=2, min_side=0, max_side=25),
)
cashflow_list_strategy = cashflow_array_strategy.map(lambda x: x.tolist())

cashflow_array_like_strategy = st.one_of(
from numpy_financial.tests.strategies import (
cashflow_array_like_strategy,
cashflow_array_strategy,
cashflow_list_strategy,
)

short_scalar_array_strategy = npst.arrays(
dtype=real_scalar_dtypes,
shape=npst.array_shapes(min_dims=0, max_dims=1, min_side=0, max_side=5),
)


when_strategy = st.sampled_from(
['end', 'begin', 'e', 'b', 0, 1, 'beginning', 'start', 'finish']
short_nicely_behaved_doubles,
when_strategy,
)


@@ -285,8 +252,7 @@ def test_npv(self):
rtol=1e-2,
)

@given(rates=short_scalar_array_strategy, values=cashflow_array_strategy)
@settings(deadline=None)
@given(rates=short_nicely_behaved_doubles, values=cashflow_array_strategy)
def test_fuzz(self, rates, values):
npf.npv(rates, values)

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

def test_mirr_broadcast(self):
values = [
[-4500, -800, 800, 800, 600],
[-120000, 39000, 30000, 21000, 37000],
[100, 200, -50, 300, -200],
]
finance_rate = [0.05, 0.08, 0.10]
reinvestment_rate = [0.08, 0.10, 0.12]
# Found using Google sheets
expected = numpy.array([
[-0.1784449, -0.17328716, -0.1684366],
[0.04627293, 0.05437856, 0.06252201],
[0.35712458, 0.40628857, 0.44435295]
])
actual = npf.mirr(values, finance_rate, reinvestment_rate)
assert_allclose(actual, expected)

def test_mirr_no_real_solution_exception(self):
# Test that if there is no solution because all the cashflows
# have the same sign, then npf.mirr returns NoRealSolutionException
@@ -402,6 +385,31 @@ def test_mirr_no_real_solution_exception(self):
with pytest.raises(npf.NoRealSolutionError):
npf.mirr(val, 0.10, 0.12, raise_exceptions=True)

@given(
values=cashflow_array_like_strategy,
finance_rate=short_nicely_behaved_doubles,
reinvestment_rate=short_nicely_behaved_doubles,
)
def test_fuzz(self, values, finance_rate, reinvestment_rate):
assume(finance_rate.size == reinvestment_rate.size)

# NumPy warns us of arithmetic overflow/underflow
# this only occurs when hypothesis generates extremely large values
# that are unlikely to ever occur in the real world.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
npf.mirr(values, finance_rate, reinvestment_rate)

@given(
values=cashflow_array_like_strategy,
finance_rate=short_nicely_behaved_doubles,
reinvestment_rate=short_nicely_behaved_doubles,
)
def test_mismatching_rates_raise(self, values, finance_rate, reinvestment_rate):
assume(finance_rate.size != reinvestment_rate.size)
with pytest.raises(ValueError):
npf.mirr(values, finance_rate, reinvestment_rate, raise_exceptions=True)


class TestNper:
def test_basic_values(self):
@@ -432,10 +440,10 @@ def test_broadcast(self):
)

@given(
rates=short_scalar_array_strategy,
payments=short_scalar_array_strategy,
present_values=short_scalar_array_strategy,
future_values=short_scalar_array_strategy,
rates=short_nicely_behaved_doubles,
payments=short_nicely_behaved_doubles,
present_values=short_nicely_behaved_doubles,
future_values=short_nicely_behaved_doubles,
whens=when_strategy,
)
def test_fuzz(self, rates, payments, present_values, future_values, whens):