Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion deeptab/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,18 @@ def _validate_target_array(y, *, name: str, regression: bool, family: str | None
raise invalid_data_error(str(error), "supply discrete classification labels.") from error
if family is not None and numeric is not None:
family_lower = family.lower()
if family_lower in {"poisson", "negativebinom"} and (numeric < 0).any():
# Count families evaluate a Poisson/negative-binomial log-probability,
# whose support is the non-negative integers.
if family_lower in {"poisson", "negativebinom", "zip"} and (
(numeric < 0).any() or (numeric != np.floor(numeric)).any()
):
raise target_range_error(family, "non-negative integer")
if family_lower == "tweedie" and (numeric < 0).any():
raise target_range_error(family, "non-negative")
if family_lower in {"gamma", "inversegamma", "lognormal"} and (numeric <= 0).any():
raise target_range_error(family, "strictly positive")
if family_lower == "beta" and ((numeric <= 0) | (numeric >= 1)).any():
raise target_range_error(family, "unit-interval (0 < y < 1)")
return targets


Expand Down
12 changes: 10 additions & 2 deletions docs/core_concepts/training_and_evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,16 @@ save/load. Categorical output width is inferred from the training labels;
Dirichlet width is inferred from a target matrix of shape `(n_samples, K)`.
Dirichlet targets must be positive proportions whose rows sum to one.

Targets for `gamma`, `inversegamma`, and `lognormal` must be strictly positive.
Zero or negative targets raise `DataError` before preprocessing or model construction.
Targets must lie in the support of the selected family:

| Family | Valid targets |
| ------------------------------------ | ------------------------- |
| `poisson`, `negativebinom`, `zip` | Non-negative integers |
| `tweedie` | Non-negative values |
| `gamma`, `inversegamma`, `lognormal` | Strictly positive values |
| `beta` | Values strictly in (0, 1) |

Targets outside the support raise `DataError` before preprocessing or model construction.

LSS `fit()` seeds Python, NumPy, and PyTorch using the resolved `random_state`
before model construction. Equal seeds support repeatable fits on the same
Expand Down
36 changes: 36 additions & 0 deletions tests/core/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,42 @@ def test_positive_support_families_accept_positive_targets(self, family):

_validate_fit_inputs(self._X(3), np.array([0.1, 1.0, 2.0]), regression=True, family=family)

@pytest.mark.parametrize("family", ["poisson", "negativebinom", "zip"])
@pytest.mark.parametrize("invalid_target", [-1.0, 0.5, 2.25])
def test_count_families_reject_negative_or_fractional_targets(self, family, invalid_target):
from deeptab.models.base import _validate_fit_inputs

targets = np.array([0.0, invalid_target, 3.0])
with pytest.raises(DataError, match="non-negative integer"):
_validate_fit_inputs(self._X(3), targets, regression=True, family=family)

@pytest.mark.parametrize("family", ["poisson", "negativebinom", "zip"])
@pytest.mark.parametrize("targets", [np.array([0, 1, 4]), np.array([0.0, 1.0, 4.0])])
def test_count_families_accept_integer_valued_targets(self, family, targets):
from deeptab.models.base import _validate_fit_inputs

_validate_fit_inputs(self._X(3), targets, regression=True, family=family)

def test_tweedie_rejects_negative_and_accepts_zero_targets(self):
from deeptab.models.base import _validate_fit_inputs

with pytest.raises(DataError, match="tweedie"):
_validate_fit_inputs(self._X(3), np.array([0.0, -0.5, 2.0]), regression=True, family="tweedie")
_validate_fit_inputs(self._X(3), np.array([0.0, 0.5, 2.0]), regression=True, family="tweedie")

@pytest.mark.parametrize("invalid_target", [0.0, 1.0, -0.2, 1.5])
def test_beta_rejects_targets_outside_open_unit_interval(self, invalid_target):
from deeptab.models.base import _validate_fit_inputs

targets = np.array([0.2, invalid_target, 0.7])
with pytest.raises(DataError, match="unit-interval"):
_validate_fit_inputs(self._X(3), targets, regression=True, family="beta")

def test_beta_accepts_targets_inside_open_unit_interval(self):
from deeptab.models.base import _validate_fit_inputs

_validate_fit_inputs(self._X(3), np.array([0.01, 0.5, 0.99]), regression=True, family="beta")

def test_high_nan_columns_warns(self):
from deeptab.models.base import _validate_fit_inputs

Expand Down
19 changes: 19 additions & 0 deletions tests/integration/test_estimator_input_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ def test_lss_rejects_non_positive_targets_before_building(family, invalid_target
assert model._built is False


@pytest.mark.parametrize(
"family, invalid_target",
[("poisson", 1.5), ("negativebinom", 2.5), ("zip", 0.5), ("tweedie", -1.0), ("beta", 1.0)],
)
def test_lss_rejects_targets_outside_family_support_before_building(family, invalid_target):
from deeptab.core.exceptions import DataError
from deeptab.models import MLPLSS

features = np.ones((6, 2))
targets = np.array([0.0, 1.0, invalid_target, 0.0, 1.0, 0.0])
if family == "beta":
targets = np.array([0.2, 0.4, invalid_target, 0.6, 0.8, 0.5])
model = MLPLSS()
with pytest.raises(DataError, match=f"family='{family}'"):
model.fit(features, targets, family=family, max_epochs=1)
assert model._data_module is None
assert model._built is False


class TestEdgeCaseInputs:
"""fit() with input shapes that previously crashed instead of failing cleanly or working."""

Expand Down
Loading