166 lines
6.8 KiB
Python
166 lines
6.8 KiB
Python
|
import numpy as np
|
||
|
from numpy.testing import assert_allclose, assert_equal
|
||
|
import pytest
|
||
|
|
||
|
from scipy.optimize._pava_pybind import pava
|
||
|
from scipy.optimize import isotonic_regression
|
||
|
|
||
|
|
||
|
class TestIsotonicRegression:
|
||
|
@pytest.mark.parametrize(
|
||
|
("y", "w", "msg"),
|
||
|
[
|
||
|
([[0, 1]], None,
|
||
|
"array has incorrect number of dimensions: 2; expected 1"),
|
||
|
([0, 1], [[1, 2]],
|
||
|
"Input arrays y and w must have one dimension of equal length"),
|
||
|
([0, 1], [1],
|
||
|
"Input arrays y and w must have one dimension of equal length"),
|
||
|
(1, 2,
|
||
|
"Input arrays y and w must have one dimension of equal length"),
|
||
|
([0, 1], [0, 1],
|
||
|
"Weights w must be strictly positive"),
|
||
|
]
|
||
|
)
|
||
|
def test_raise_error(self, y, w, msg):
|
||
|
with pytest.raises(ValueError, match=msg):
|
||
|
isotonic_regression(y=y, weights=w)
|
||
|
|
||
|
def test_simple_pava(self):
|
||
|
# Test case of Busing 2020
|
||
|
# https://doi.org/10.18637/jss.v102.c01
|
||
|
y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64)
|
||
|
w = np.ones_like(y)
|
||
|
r = np.full(shape=y.shape[0] + 1, fill_value=-1, dtype=np.intp)
|
||
|
pava(y, w, r)
|
||
|
assert_allclose(y, [4, 4, 4, 4, 4, 4, 8])
|
||
|
# Only first 2 elements of w are changed.
|
||
|
assert_allclose(w, [6, 1, 1, 1, 1, 1, 1])
|
||
|
# Only first 3 elements of r are changed.
|
||
|
assert_allclose(r, [0, 6, 7, -1, -1, -1, -1, -1])
|
||
|
|
||
|
@pytest.mark.parametrize("y_dtype", [np.float64, np.float32, np.int64, np.int32])
|
||
|
@pytest.mark.parametrize("w_dtype", [np.float64, np.float32, np.int64, np.int32])
|
||
|
@pytest.mark.parametrize("w", [None, "ones"])
|
||
|
def test_simple_isotonic_regression(self, w, w_dtype, y_dtype):
|
||
|
# Test case of Busing 2020
|
||
|
# https://doi.org/10.18637/jss.v102.c01
|
||
|
y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=y_dtype)
|
||
|
if w is not None:
|
||
|
w = np.ones_like(y, dtype=w_dtype)
|
||
|
res = isotonic_regression(y, weights=w)
|
||
|
assert res.x.dtype == np.float64
|
||
|
assert res.weights.dtype == np.float64
|
||
|
assert_allclose(res.x, [4, 4, 4, 4, 4, 4, 8])
|
||
|
assert_allclose(res.weights, [6, 1])
|
||
|
assert_allclose(res.blocks, [0, 6, 7])
|
||
|
# Assert that y was not overwritten
|
||
|
assert_equal(y, np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64))
|
||
|
|
||
|
@pytest.mark.parametrize("increasing", [True, False])
|
||
|
def test_linspace(self, increasing):
|
||
|
n = 10
|
||
|
y = np.linspace(0, 1, n) if increasing else np.linspace(1, 0, n)
|
||
|
res = isotonic_regression(y, increasing=increasing)
|
||
|
assert_allclose(res.x, y)
|
||
|
assert_allclose(res.blocks, np.arange(n + 1))
|
||
|
|
||
|
def test_weights(self):
|
||
|
w = np.array([1, 2, 5, 0.5, 0.5, 0.5, 1, 3])
|
||
|
y = np.array([3, 2, 1, 10, 9, 8, 20, 10])
|
||
|
res = isotonic_regression(y, weights=w)
|
||
|
assert_allclose(res.x, [12/8, 12/8, 12/8, 9, 9, 9, 50/4, 50/4])
|
||
|
assert_allclose(res.weights, [8, 1.5, 4])
|
||
|
assert_allclose(res.blocks, [0, 3, 6, 8])
|
||
|
|
||
|
# weights are like repeated observations, we repeat the 3rd element 5
|
||
|
# times.
|
||
|
w2 = np.array([1, 2, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 1, 3])
|
||
|
y2 = np.array([3, 2, 1, 1, 1, 1, 1, 10, 9, 8, 20, 10])
|
||
|
res2 = isotonic_regression(y2, weights=w2)
|
||
|
assert_allclose(np.diff(res2.x[0:7]), 0)
|
||
|
assert_allclose(res2.x[4:], res.x)
|
||
|
assert_allclose(res2.weights, res.weights)
|
||
|
assert_allclose(res2.blocks[1:] - 4, res.blocks[1:])
|
||
|
|
||
|
def test_against_R_monotone(self):
|
||
|
y = [0, 6, 8, 3, 5, 2, 1, 7, 9, 4]
|
||
|
res = isotonic_regression(y)
|
||
|
# R code
|
||
|
# library(monotone)
|
||
|
# options(digits=8)
|
||
|
# monotone(c(0, 6, 8, 3, 5, 2, 1, 7, 9, 4))
|
||
|
x_R = [
|
||
|
0, 4.1666667, 4.1666667, 4.1666667, 4.1666667, 4.1666667,
|
||
|
4.1666667, 6.6666667, 6.6666667, 6.6666667,
|
||
|
]
|
||
|
assert_allclose(res.x, x_R)
|
||
|
assert_equal(res.blocks, [0, 1, 7, 10])
|
||
|
|
||
|
n = 100
|
||
|
y = np.linspace(0, 1, num=n, endpoint=False)
|
||
|
y = 5 * y + np.sin(10 * y)
|
||
|
res = isotonic_regression(y)
|
||
|
# R code
|
||
|
# library(monotone)
|
||
|
# n <- 100
|
||
|
# y <- 5 * ((1:n)-1)/n + sin(10 * ((1:n)-1)/n)
|
||
|
# options(digits=8)
|
||
|
# monotone(y)
|
||
|
x_R = [
|
||
|
0.00000000, 0.14983342, 0.29866933, 0.44552021, 0.58941834, 0.72942554,
|
||
|
0.86464247, 0.99421769, 1.11735609, 1.23332691, 1.34147098, 1.44120736,
|
||
|
1.53203909, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
|
||
|
1.57081100, 1.57081100, 1.57081100, 1.62418532, 1.71654534, 1.81773256,
|
||
|
1.92723551, 2.04445967, 2.16873336, 2.29931446, 2.43539782, 2.57612334,
|
||
|
2.72058450, 2.86783750, 3.01691060, 3.16681390, 3.31654920, 3.46511999,
|
||
|
3.61154136, 3.75484992, 3.89411335, 4.02843976, 4.15698660, 4.27896904,
|
||
|
4.39366786, 4.50043662, 4.59870810, 4.68799998, 4.76791967, 4.83816823,
|
||
|
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
|
||
|
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
|
||
|
4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
|
||
|
4.86564130, 4.86564130, 4.86564130, 4.86564130,
|
||
|
]
|
||
|
assert_allclose(res.x, x_R)
|
||
|
|
||
|
# Test increasing
|
||
|
assert np.all(np.diff(res.x) >= 0)
|
||
|
|
||
|
# Test balance property: sum(y) == sum(x)
|
||
|
assert_allclose(np.sum(res.x), np.sum(y))
|
||
|
|
||
|
# Reverse order
|
||
|
res_inv = isotonic_regression(-y, increasing=False)
|
||
|
assert_allclose(-res_inv.x, res.x)
|
||
|
assert_equal(res_inv.blocks, res.blocks)
|
||
|
|
||
|
def test_readonly(self):
|
||
|
x = np.arange(3, dtype=float)
|
||
|
w = np.ones(3, dtype=float)
|
||
|
|
||
|
x.flags.writeable = False
|
||
|
w.flags.writeable = False
|
||
|
|
||
|
res = isotonic_regression(x, weights=w)
|
||
|
assert np.all(np.isfinite(res.x))
|
||
|
assert np.all(np.isfinite(res.weights))
|
||
|
assert np.all(np.isfinite(res.blocks))
|
||
|
|
||
|
def test_non_contiguous_arrays(self):
|
||
|
x = np.arange(10, dtype=float)[::3]
|
||
|
w = np.ones(10, dtype=float)[::3]
|
||
|
assert not x.flags.c_contiguous
|
||
|
assert not x.flags.f_contiguous
|
||
|
assert not w.flags.c_contiguous
|
||
|
assert not w.flags.f_contiguous
|
||
|
|
||
|
res = isotonic_regression(x, weights=w)
|
||
|
assert np.all(np.isfinite(res.x))
|
||
|
assert np.all(np.isfinite(res.weights))
|
||
|
assert np.all(np.isfinite(res.blocks))
|