Skip to content

Commit d41878e

Browse files
committed
Added a (LLM-Generated) test for distribution-fittint
1 parent 406ab78 commit d41878e

1 file changed

Lines changed: 165 additions & 26 deletions

File tree

tests/unit/test_utils.py

Lines changed: 165 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,171 @@
88
# the Free Software Foundation, either version 3 of the License, or
99
# (at your option) any later version.
1010

11-
import json
12-
import pathlib
11+
import numpy as np
1312
import pytest
14-
import wfcommons.utils
1513

16-
from typing import Dict, List, Tuple
14+
import wfcommons.utils
1715

1816

1917
class TestUtils:
20-
18+
2119
@pytest.mark.unit
22-
@pytest.mark.parametrize(
23-
"data,distribution",
24-
[
25-
pytest.param([1, 2, 3], ("rdist", (1.5504806356651624, 0.0013236200991527764, 0.0013236200991527767))),
26-
pytest.param([1, 1, 1], ("pareto", (2.25803497307119, -7.535551383120264e-19, 4.806600020282434e-19))),
27-
],
28-
)
29-
def test_best_fit_distribution(self, data: List[float], distribution: Tuple) -> None:
30-
assert(wfcommons.utils.best_fit_distribution(data) == distribution)
20+
def test_best_fit_distribution_uses_normalized_samples_and_density_histogram(
21+
self,
22+
monkeypatch: pytest.MonkeyPatch,
23+
) -> None:
24+
# Twenty observations produce two bins under the current
25+
# ceil(len(data) / 10) rule.
26+
data = list(range(20))
27+
28+
expected_normalized = np.asarray(data, dtype=float)
29+
expected_normalized = (
30+
(expected_normalized - expected_normalized.min())
31+
/ (expected_normalized.max() - expected_normalized.min())
32+
)
33+
34+
histogram_call = {}
35+
fit_calls = []
36+
pdf_calls = []
37+
38+
def fake_histogram(values, bins, density=False):
39+
histogram_call["values"] = np.asarray(
40+
values,
41+
dtype=float,
42+
).copy()
43+
histogram_call["bins"] = bins
44+
histogram_call["density"] = density
45+
46+
# A valid two-bin density:
47+
# 0.5 * 0.5 + 1.5 * 0.5 == 1.0
48+
return (
49+
np.array([0.5, 1.5], dtype=float),
50+
np.array([0.0, 0.5, 1.0], dtype=float),
51+
)
52+
53+
class FakeDistribution:
54+
55+
def __init__(self, index: int) -> None:
56+
self.index = index
57+
58+
def fit(self, values):
59+
fit_calls.append(
60+
np.asarray(values, dtype=float).copy()
61+
)
62+
return 0.0, 1.0
63+
64+
def pdf(self, x, *args, loc, scale):
65+
pdf_calls.append(
66+
np.asarray(x, dtype=float).copy()
67+
)
68+
69+
if self.index == 0:
70+
# Closest to the density histogram [0.5, 1.5].
71+
return np.array([0.55, 1.45], dtype=float)
72+
73+
if self.index == 1:
74+
# Closest to the old min-max-scaled histogram
75+
# [0.0, 1.0]. This candidate would win if that
76+
# obsolete transformation were restored.
77+
return np.array([0.05, 0.95], dtype=float)
78+
79+
return np.array([10.0, 10.0], dtype=float)
80+
81+
class FakeStats:
82+
83+
def __init__(self) -> None:
84+
self.requested_names = []
85+
86+
def __getattr__(self, name):
87+
index = len(self.requested_names)
88+
self.requested_names.append(name)
89+
return FakeDistribution(index)
90+
91+
fake_stats = FakeStats()
92+
93+
monkeypatch.setattr(
94+
wfcommons.utils.np,
95+
"histogram",
96+
fake_histogram,
97+
)
98+
monkeypatch.setattr(
99+
wfcommons.utils.scipy,
100+
"stats",
101+
fake_stats,
102+
)
103+
104+
distribution_name, params = (
105+
wfcommons.utils.best_fit_distribution(data)
106+
)
107+
108+
assert histogram_call["bins"] == 2
109+
assert histogram_call["density"] is True
110+
np.testing.assert_allclose(
111+
histogram_call["values"],
112+
expected_normalized,
113+
)
114+
115+
# Every candidate must be fitted to the normalized observations,
116+
# not to the histogram heights.
117+
assert fit_calls
118+
for fit_data in fit_calls:
119+
np.testing.assert_allclose(
120+
fit_data,
121+
expected_normalized,
122+
)
123+
124+
# The PDF must be evaluated at the centers of [0.0, 0.5]
125+
# and [0.5, 1.0].
126+
expected_centers = np.array([0.25, 0.75], dtype=float)
127+
128+
assert pdf_calls
129+
for pdf_x in pdf_calls:
130+
np.testing.assert_allclose(
131+
pdf_x,
132+
expected_centers,
133+
)
134+
135+
# The first fake distribution is closest to the density
136+
# histogram. The second would win under the old transformation.
137+
assert distribution_name == fake_stats.requested_names[0]
138+
assert params == (0.0, 1.0)
31139

32140
@pytest.mark.unit
33-
@pytest.mark.parametrize(
34-
"distribution,min_value,max_value",
35-
[
36-
pytest.param(None, 10, 100),
37-
pytest.param({"name": "norm", "params": [0.08688656476267097, 0.2572832376513094]}, 10, 100),
38-
],
39-
)
40-
def test_generate_rvs(self, distribution: Dict, min_value: float, max_value: float) -> None:
41-
assert(min_value <= wfcommons.utils.generate_rvs(distribution, min_value, max_value) <= max_value )
141+
def test_best_fit_distribution_returns_usable_fit(self) -> None:
142+
rng = np.random.default_rng(12345)
143+
data = rng.lognormal(
144+
mean=0.0,
145+
sigma=0.5,
146+
size=30,
147+
).tolist()
148+
149+
distribution_name, params = (
150+
wfcommons.utils.best_fit_distribution(data)
151+
)
152+
153+
assert isinstance(distribution_name, str)
154+
assert distribution_name
155+
assert hasattr(
156+
wfcommons.utils.scipy.stats,
157+
distribution_name,
158+
)
159+
160+
params_array = np.asarray(params, dtype=float)
161+
162+
# SciPy continuous-distribution fits end with loc and scale.
163+
assert params_array.size >= 2
164+
assert np.all(np.isfinite(params_array))
165+
assert params_array[-1] > 0
166+
167+
@pytest.mark.unit
168+
def test_generate_rvs_without_distribution_returns_minimum(
169+
self,
170+
) -> None:
171+
assert wfcommons.utils.generate_rvs(
172+
None,
173+
min_value=10,
174+
max_value=100,
175+
) == 10
42176

43177
@pytest.mark.unit
44178
@pytest.mark.parametrize(
@@ -48,6 +182,11 @@ def test_generate_rvs(self, distribution: Dict, min_value: float, max_value: flo
48182
pytest.param(10, 2, 45),
49183
pytest.param(10, 3, 120),
50184
],
51-
)
52-
def test_ncr(self, n: int, r: int, combinations: int) -> None:
53-
assert(wfcommons.utils.ncr(n, r) == combinations)
185+
)
186+
def test_ncr(
187+
self,
188+
n: int,
189+
r: int,
190+
combinations: int,
191+
) -> None:
192+
assert wfcommons.utils.ncr(n, r) == combinations

0 commit comments

Comments
 (0)