test_isotonic_regression.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import numpy as np
  2. from numpy.testing import assert_allclose, assert_equal
  3. import pytest
  4. from scipy.optimize._pava_pybind import pava
  5. from scipy.optimize import isotonic_regression
  6. class TestIsotonicRegression:
  7. @pytest.mark.parametrize(
  8. ("y", "w", "msg"),
  9. [
  10. ([[0, 1]], None,
  11. "array has incorrect number of dimensions: 2; expected 1"),
  12. ([0, 1], [[1, 2]],
  13. "Input arrays y and w must have one dimension of equal length"),
  14. ([0, 1], [1],
  15. "Input arrays y and w must have one dimension of equal length"),
  16. (1, [1, 2],
  17. "Input arrays y and w must have one dimension of equal length"),
  18. ([1, 2], 1,
  19. "Input arrays y and w must have one dimension of equal length"),
  20. ([0, 1], [0, 1],
  21. "Weights w must be strictly positive"),
  22. ]
  23. )
  24. def test_raise_error(self, y, w, msg):
  25. with pytest.raises(ValueError, match=msg):
  26. isotonic_regression(y=y, weights=w)
  27. def test_simple_pava(self):
  28. # Test case of Busing 2020
  29. # https://doi.org/10.18637/jss.v102.c01
  30. y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64)
  31. w = np.ones_like(y)
  32. r = np.full(shape=y.shape[0] + 1, fill_value=-1, dtype=np.intp)
  33. pava(y, w, r)
  34. assert_allclose(y, [4, 4, 4, 4, 4, 4, 8])
  35. # Only first 2 elements of w are changed.
  36. assert_allclose(w, [6, 1, 1, 1, 1, 1, 1])
  37. # Only first 3 elements of r are changed.
  38. assert_allclose(r, [0, 6, 7, -1, -1, -1, -1, -1])
  39. @pytest.mark.parametrize("y_dtype", [np.float64, np.float32, np.int64, np.int32])
  40. @pytest.mark.parametrize("w_dtype", [np.float64, np.float32, np.int64, np.int32])
  41. @pytest.mark.parametrize("w", [None, "ones"])
  42. def test_simple_isotonic_regression(self, w, w_dtype, y_dtype):
  43. # Test case of Busing 2020
  44. # https://doi.org/10.18637/jss.v102.c01
  45. y = np.array([8, 4, 8, 2, 2, 0, 8], dtype=y_dtype)
  46. if w is not None:
  47. w = np.ones_like(y, dtype=w_dtype)
  48. res = isotonic_regression(y, weights=w)
  49. assert res.x.dtype == np.float64
  50. assert res.weights.dtype == np.float64
  51. assert_allclose(res.x, [4, 4, 4, 4, 4, 4, 8])
  52. assert_allclose(res.weights, [6, 1])
  53. assert_allclose(res.blocks, [0, 6, 7])
  54. # Assert that y was not overwritten
  55. assert_equal(y, np.array([8, 4, 8, 2, 2, 0, 8], dtype=np.float64))
  56. @pytest.mark.parametrize("increasing", [True, False])
  57. def test_linspace(self, increasing):
  58. n = 10
  59. y = np.linspace(0, 1, n) if increasing else np.linspace(1, 0, n)
  60. res = isotonic_regression(y, increasing=increasing)
  61. assert_allclose(res.x, y)
  62. assert_allclose(res.blocks, np.arange(n + 1))
  63. def test_weights(self):
  64. w = np.array([1, 2, 5, 0.5, 0.5, 0.5, 1, 3])
  65. y = np.array([3, 2, 1, 10, 9, 8, 20, 10])
  66. res = isotonic_regression(y, weights=w)
  67. assert_allclose(res.x, [12/8, 12/8, 12/8, 9, 9, 9, 50/4, 50/4])
  68. assert_allclose(res.weights, [8, 1.5, 4])
  69. assert_allclose(res.blocks, [0, 3, 6, 8])
  70. # weights are like repeated observations, we repeat the 3rd element 5
  71. # times.
  72. w2 = np.array([1, 2, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 1, 3])
  73. y2 = np.array([3, 2, 1, 1, 1, 1, 1, 10, 9, 8, 20, 10])
  74. res2 = isotonic_regression(y2, weights=w2)
  75. assert_allclose(np.diff(res2.x[0:7]), 0)
  76. assert_allclose(res2.x[4:], res.x)
  77. assert_allclose(res2.weights, res.weights)
  78. assert_allclose(res2.blocks[1:] - 4, res.blocks[1:])
  79. def test_against_R_monotone(self):
  80. y = [0, 6, 8, 3, 5, 2, 1, 7, 9, 4]
  81. res = isotonic_regression(y)
  82. # R code
  83. # library(monotone)
  84. # options(digits=8)
  85. # monotone(c(0, 6, 8, 3, 5, 2, 1, 7, 9, 4))
  86. x_R = [
  87. 0, 4.1666667, 4.1666667, 4.1666667, 4.1666667, 4.1666667,
  88. 4.1666667, 6.6666667, 6.6666667, 6.6666667,
  89. ]
  90. assert_allclose(res.x, x_R)
  91. assert_equal(res.blocks, [0, 1, 7, 10])
  92. n = 100
  93. y = np.linspace(0, 1, num=n, endpoint=False)
  94. y = 5 * y + np.sin(10 * y)
  95. res = isotonic_regression(y)
  96. # R code
  97. # library(monotone)
  98. # n <- 100
  99. # y <- 5 * ((1:n)-1)/n + sin(10 * ((1:n)-1)/n)
  100. # options(digits=8)
  101. # monotone(y)
  102. x_R = [
  103. 0.00000000, 0.14983342, 0.29866933, 0.44552021, 0.58941834, 0.72942554,
  104. 0.86464247, 0.99421769, 1.11735609, 1.23332691, 1.34147098, 1.44120736,
  105. 1.53203909, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  106. 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  107. 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  108. 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  109. 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  110. 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100, 1.57081100,
  111. 1.57081100, 1.57081100, 1.57081100, 1.62418532, 1.71654534, 1.81773256,
  112. 1.92723551, 2.04445967, 2.16873336, 2.29931446, 2.43539782, 2.57612334,
  113. 2.72058450, 2.86783750, 3.01691060, 3.16681390, 3.31654920, 3.46511999,
  114. 3.61154136, 3.75484992, 3.89411335, 4.02843976, 4.15698660, 4.27896904,
  115. 4.39366786, 4.50043662, 4.59870810, 4.68799998, 4.76791967, 4.83816823,
  116. 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
  117. 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
  118. 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130, 4.86564130,
  119. 4.86564130, 4.86564130, 4.86564130, 4.86564130,
  120. ]
  121. assert_allclose(res.x, x_R)
  122. # Test increasing
  123. assert np.all(np.diff(res.x) >= 0)
  124. # Test balance property: sum(y) == sum(x)
  125. assert_allclose(np.sum(res.x), np.sum(y))
  126. # Reverse order
  127. res_inv = isotonic_regression(-y, increasing=False)
  128. assert_allclose(-res_inv.x, res.x)
  129. assert_equal(res_inv.blocks, res.blocks)
  130. def test_readonly(self):
  131. x = np.arange(3, dtype=float)
  132. w = np.ones(3, dtype=float)
  133. x.flags.writeable = False
  134. w.flags.writeable = False
  135. res = isotonic_regression(x, weights=w)
  136. assert np.all(np.isfinite(res.x))
  137. assert np.all(np.isfinite(res.weights))
  138. assert np.all(np.isfinite(res.blocks))
  139. def test_non_contiguous_arrays(self):
  140. x = np.arange(10, dtype=float)[::3]
  141. w = np.ones(10, dtype=float)[::3]
  142. assert not x.flags.c_contiguous
  143. assert not x.flags.f_contiguous
  144. assert not w.flags.c_contiguous
  145. assert not w.flags.f_contiguous
  146. res = isotonic_regression(x, weights=w)
  147. assert np.all(np.isfinite(res.x))
  148. assert np.all(np.isfinite(res.weights))
  149. assert np.all(np.isfinite(res.blocks))