diff --git a/Orange/base.py b/Orange/base.py index 2ac2e64ebcb..89a2ebc073c 100644 --- a/Orange/base.py +++ b/Orange/base.py @@ -3,7 +3,7 @@ from collections.abc import Iterable import re import warnings -from typing import Callable, Optional, NamedTuple, Union, Type +from typing import Callable, Optional, NamedTuple, Type import numpy as np import scipy @@ -186,8 +186,8 @@ def active_preprocessors(self): self.preprocessors is not type(self).preprocessors): yield from type(self).preprocessors - # declared for derived classes, pylint: disable=unused-argument - def fitted_parameters(self, problem_type: Union[str, Table, Domain]) -> list: + @property + def fitted_parameters(self) -> list: return [] # pylint: disable=no-self-use diff --git a/Orange/classification/random_forest.py b/Orange/classification/random_forest.py index 3dd9f774acf..f77b6f2c898 100644 --- a/Orange/classification/random_forest.py +++ b/Orange/classification/random_forest.py @@ -1,6 +1,6 @@ import sklearn.ensemble as skl_ensemble -from Orange.base import RandomForestModel, Learner +from Orange.base import RandomForestModel from Orange.classification import SklLearner, SklModel from Orange.classification.tree import SklTreeClassifier from Orange.data import Variable, DiscreteVariable @@ -58,7 +58,3 @@ def __init__(self, preprocessors=None): super().__init__(preprocessors=preprocessors) self.params = vars() - - def fitted_parameters(self, _) -> list[Learner.FittedParameter]: - return [self.FittedParameter("n_estimators", "Number of trees", - int, 1, None)] diff --git a/Orange/evaluation/testing.py b/Orange/evaluation/testing.py index 6b4f826c331..8553dc7ae0d 100644 --- a/Orange/evaluation/testing.py +++ b/Orange/evaluation/testing.py @@ -98,6 +98,7 @@ def __init__(self, data=None, *, row_indices=None, folds=None, score_by_folds=True, learners=None, models=None, failed=None, actual=None, predicted=None, probabilities=None, + # pylint: disable=unused-argument store_data=None, store_models=None, train_time=None, test_time=None): """ diff --git a/Orange/modelling/randomforest.py b/Orange/modelling/randomforest.py index 5240656deba..ad7f8cb19d7 100644 --- a/Orange/modelling/randomforest.py +++ b/Orange/modelling/randomforest.py @@ -1,8 +1,6 @@ -from typing import Union - from Orange.base import RandomForestModel, Learner from Orange.classification import RandomForestLearner as RFClassification -from Orange.data import Variable, Domain, Table +from Orange.data import Variable from Orange.modelling import SklFitter from Orange.preprocess.score import LearnerScorer from Orange.regression import RandomForestRegressionLearner as RFRegression @@ -27,8 +25,7 @@ class RandomForestLearner(SklFitter, _FeatureScorerMixin): __returns__ = RandomForestModel - def fitted_parameters( - self, - problem_type: Union[str, Table, Domain] - ) -> list[Learner.FittedParameter]: - return self.get_learner(problem_type).fitted_parameters(problem_type) + @property + def fitted_parameters(self) -> list[Learner.FittedParameter]: + return [self.FittedParameter("n_estimators", "Number of trees", + int, 1, None)] diff --git a/Orange/regression/pls.py b/Orange/regression/pls.py index d8bd85c4c69..2830c0b05ae 100644 --- a/Orange/regression/pls.py +++ b/Orange/regression/pls.py @@ -255,7 +255,8 @@ def incompatibility_reason(self, domain): reason = "Only numeric target variables expected." return reason - def fitted_parameters(self, _) -> list[Learner.FittedParameter]: + @property + def fitted_parameters(self) -> list[Learner.FittedParameter]: return [self.FittedParameter("n_components", "Components", int, 1, None)] diff --git a/Orange/regression/random_forest.py b/Orange/regression/random_forest.py index c9706ded861..4b37888ae27 100644 --- a/Orange/regression/random_forest.py +++ b/Orange/regression/random_forest.py @@ -1,6 +1,6 @@ import sklearn.ensemble as skl_ensemble -from Orange.base import RandomForestModel, Learner +from Orange.base import RandomForestModel from Orange.data import Variable, ContinuousVariable from Orange.preprocess.score import LearnerScorer from Orange.regression import SklLearner, SklModel @@ -57,7 +57,3 @@ def __init__(self, preprocessors=None): super().__init__(preprocessors=preprocessors) self.params = vars() - - def fitted_parameters(self, _) -> list[Learner.FittedParameter]: - return [self.FittedParameter("n_estimators", "Number of trees", - int, 1, None)] diff --git a/Orange/regression/tests/test_pls.py b/Orange/regression/tests/test_pls.py index c2f7a29daaf..bc053d9cee7 100644 --- a/Orange/regression/tests/test_pls.py +++ b/Orange/regression/tests/test_pls.py @@ -22,7 +22,7 @@ def table(rows, attr, variables): class TestPLSRegressionLearner(unittest.TestCase): def test_fitted_parameters(self): - fitted_parameters = PLSRegressionLearner().fitted_parameters(None) + fitted_parameters = PLSRegressionLearner().fitted_parameters self.assertIsInstance(fitted_parameters, list) self.assertEqual(len(fitted_parameters), 1) diff --git a/Orange/widgets/evaluate/owparameterfitter.py b/Orange/widgets/evaluate/owparameterfitter.py index bd56910be66..78521b96f0b 100644 --- a/Orange/widgets/evaluate/owparameterfitter.py +++ b/Orange/widgets/evaluate/owparameterfitter.py @@ -320,12 +320,12 @@ class Inputs: DEFAULT_PARAMETER_INDEX = 0 DEFAULT_MINIMUM = 1 DEFAULT_MAXIMUM = 9 - parameter_index = Setting(DEFAULT_PARAMETER_INDEX, schema_only=True) + parameter_index = Setting(DEFAULT_PARAMETER_INDEX) FROM_RANGE, MANUAL = range(2) type: int = Setting(FROM_RANGE) - minimum: int = Setting(DEFAULT_MINIMUM, schema_only=True) - maximum: int = Setting(DEFAULT_MAXIMUM, schema_only=True) - manual_steps: str = Setting("", schema_only=True) + minimum: int = Setting(DEFAULT_MINIMUM) + maximum: int = Setting(DEFAULT_MAXIMUM) + manual_steps: str = Setting("") auto_commit = Setting(True) class Error(OWWidget.Error): @@ -345,13 +345,10 @@ def __init__(self): self._data: Optional[Table] = None self._learner: Optional[Learner] = None self.__parameters_model = QStandardItemModel() - - self.__pending_parameter_index = self.parameter_index \ - if self.parameter_index != self.DEFAULT_PARAMETER_INDEX else None - self.__pending_minimum = self.minimum \ - if self.minimum != self.DEFAULT_MINIMUM else None - self.__pending_maximum = self.maximum \ - if self.maximum != self.DEFAULT_MAXIMUM else None + self.__initialize_settings = \ + self.parameter_index == self.DEFAULT_PARAMETER_INDEX and \ + self.minimum == self.DEFAULT_MINIMUM and \ + self.maximum == self.DEFAULT_MAXIMUM self.setup_gui() VisualSettingsDialog( @@ -418,10 +415,13 @@ def _(): gui.auto_apply(self.buttonsArea, self, "auto_commit") + self._update_preview() + def __on_type_changed(self): self._settings_changed() def __on_parameter_changed(self): + self.__initialize_settings = True self._set_range_controls() self._settings_changed() @@ -439,19 +439,16 @@ def _settings_changed(self): @property def fitted_parameters(self) -> list: - if not self._learner \ - or isinstance(self._learner, Fitter) and not self._data: + if not self._learner: return [] - return self._learner.fitted_parameters(self._data) + return self._learner.fitted_parameters @property def initial_parameters(self) -> dict: if not self._learner: return {} if isinstance(self._learner, Fitter): - if not self._data: - return {} - return self._learner.get_params(self._data) + return self._learner.get_params(self._data or "classification") return self._learner.params @property @@ -495,38 +492,32 @@ def _steps_from_manual(self) -> tuple[int, ...]: @Inputs.data @check_multiple_targets_input def set_data(self, data: Optional[Table]): + self.Error.not_enough_data.clear() + self.Error.missing_target.clear() self._data = data + if self._data and len(self._data) < N_FOLD: + self.Error.not_enough_data() + self._data = None + if self._data and len(self._data.domain.class_vars) < 1: + self.Error.missing_target() + self._data = None @Inputs.learner def set_learner(self, learner: Optional[Learner]): + if self._learner: + self.__initialize_settings = \ + not isinstance(self._learner, type(learner)) self._learner = learner def handleNewSignals(self): self.Warning.clear() self.Error.unknown_err.clear() - self.Error.not_enough_data.clear() self.Error.incompatible_learner.clear() self.Error.manual_steps_error.clear() self.Error.min_max_error.clear() - self.Error.missing_target.clear() self.clear() - if self._data is None or self._learner is None: - return - - if self._data and len(self._data) < N_FOLD: - self.Error.not_enough_data() - self._data = None - return - - if self._data and len(self._data.domain.class_vars) < 1: - self.Error.missing_target() - self._data = None - return - - reason = self._learner.incompatibility_reason(self._data.domain) - if reason: - self.Error.incompatible_learner(reason) + if self._learner is None: return for param in self.fitted_parameters: @@ -535,19 +526,22 @@ def handleNewSignals(self): if not self.fitted_parameters: self.Warning.no_parameters(self._learner.name) else: - if self.__pending_parameter_index is not None: - self.parameter_index = self.__pending_parameter_index + if self.__initialize_settings: + self.parameter_index = 0 + else: self.__combo.setCurrentIndex(self.parameter_index) - self.__pending_parameter_index = None self._set_range_controls() - if self.__pending_minimum is not None: - self.minimum = self.__pending_minimum - self.__pending_minimum = None - if self.__pending_maximum is not None: - self.maximum = self.__pending_maximum - self.__pending_maximum = None self._update_preview() + + if self._data is None: + return + + reason = self._learner.incompatibility_reason(self._data.domain) + if reason: + self.Error.incompatible_learner(reason) + return + self.commit.now() def _set_range_controls(self): @@ -561,19 +555,24 @@ def _set_range_controls(self): if param.min is not None: self.__spin_min.setMinimum(param.min) self.__spin_max.setMinimum(param.min) - self.minimum = param.min + if self.__initialize_settings: + self.minimum = param.min else: self.__spin_min.setMinimum(-MIN_MAX_SPIN) self.__spin_max.setMinimum(-MIN_MAX_SPIN) - self.minimum = self.initial_parameters[param.name] + if self.__initialize_settings: + self.minimum = self.initial_parameters[param.name] if param.max is not None: self.__spin_min.setMaximum(param.max) self.__spin_max.setMaximum(param.max) - self.maximum = param.max + if self.__initialize_settings: + self.maximum = param.max else: self.__spin_min.setMaximum(MIN_MAX_SPIN) self.__spin_max.setMaximum(MIN_MAX_SPIN) - self.maximum = self.initial_parameters[param.name] + if self.__initialize_settings: + self.maximum = self.initial_parameters[param.name] + self.__initialize_settings = False tip = "Enter a list of values" if param.min is not None: diff --git a/Orange/widgets/evaluate/tests/test_owparameterfitter.py b/Orange/widgets/evaluate/tests/test_owparameterfitter.py index d012f2c4c7d..20536f9692f 100644 --- a/Orange/widgets/evaluate/tests/test_owparameterfitter.py +++ b/Orange/widgets/evaluate/tests/test_owparameterfitter.py @@ -19,7 +19,8 @@ class DummyLearner(PLSRegressionLearner): - def fitted_parameters(self, _): + @property + def fitted_parameters(self): return [ self.FittedParameter("n_components", "Foo", int, 5, None), self.FittedParameter("n_components", "Bar", int, 5, 10), @@ -42,6 +43,15 @@ def setUpClass(cls): def setUp(self): self.widget = self.create_widget(OWParameterFitter) + def test_init(self): + self.send_signal(self.widget.Inputs.learner, self._pls) + self.assertEqual(self.widget.controls.parameter_index.currentText(), + "Components") + + self.send_signal(self.widget.Inputs.learner, None) + self.assertEqual(self.widget.controls.parameter_index.currentText(), + "") + def test_input(self): self.send_signal(self.widget.Inputs.data, self._housing) self.send_signal(self.widget.Inputs.learner, self._pls) @@ -85,34 +95,30 @@ def test_random_forest(self): self.assertFalse(self.widget.Error.not_enough_data.is_shown()) self.assertFalse(self.widget.Error.incompatible_learner.is_shown()) - def test_random_forest_classless_data(self): - domain = self._heart.domain - data = self._heart.transform(Domain(domain.attributes)) - rf_widget = self.create_widget(OWRandomForest) - learner = self.get_output(rf_widget.Outputs.learner) + def test_classless_data(self): + data = self._housing + classless_data = data.transform(Domain(data.domain.attributes)) - self.send_signal(self.widget.Inputs.learner, learner) - self.send_signal(self.widget.Inputs.data, data) + self.send_signal(self.widget.Inputs.learner, self._pls) + self.send_signal(self.widget.Inputs.data, classless_data) self.wait_until_finished() self.assertTrue(self.widget.Error.missing_target.is_shown()) - self.send_signal(self.widget.Inputs.data, self._heart) + self.send_signal(self.widget.Inputs.data, data) self.wait_until_finished() self.assertFalse(self.widget.Error.missing_target.is_shown()) - def test_random_forest_multiclass_data(self): - domain = self._heart.domain - data = self._heart.transform(Domain(domain.attributes[2:], - domain.attributes[:2])) - rf_widget = self.create_widget(OWRandomForest) - learner = self.get_output(rf_widget.Outputs.learner) + def test_multiclass_data(self): + data = self._housing + multiclass_data = data.transform(Domain(data.domain.attributes[2:], + data.domain.attributes[:2])) - self.send_signal(self.widget.Inputs.learner, learner) - self.send_signal(self.widget.Inputs.data, data) + self.send_signal(self.widget.Inputs.learner, self._pls) + self.send_signal(self.widget.Inputs.data, multiclass_data) self.wait_until_finished() self.assertTrue(self.widget.Error.multiple_targets_data.is_shown()) - self.send_signal(self.widget.Inputs.data, self._heart) + self.send_signal(self.widget.Inputs.data, data) self.wait_until_finished() self.assertFalse(self.widget.Error.multiple_targets_data.is_shown()) @@ -279,6 +285,9 @@ def test_initial_parameters(self): self.wait_until_finished() self.send_signal(self.widget.Inputs.data, None) + self.assertEqual(len(self.widget.initial_parameters), 14) + + self.send_signal(self.widget.Inputs.learner, None) self.assertEqual(self.widget.initial_parameters, {}) def test_saved_workflow(self): @@ -301,6 +310,43 @@ def test_saved_workflow(self): self.assertEqual(widget.minimum, 3) self.assertEqual(widget.maximum, 6) + def test_retain_settings(self): + self.send_signal(self.widget.Inputs.learner, self._dummy) + + controls = self.widget.controls + + def _test(): + self.assertEqual(controls.parameter_index.currentText(), "Bar") + self.assertEqual(controls.minimum.value(), 6) + self.assertEqual(controls.maximum.value(), 8) + self.assertEqual(self.widget.parameter_index, 1) + self.assertEqual(self.widget.minimum, 6) + self.assertEqual(self.widget.maximum, 8) + + simulate.combobox_activate_index(controls.parameter_index, 1) + controls.minimum.setValue(6) + controls.maximum.setValue(8) + + self.send_signal(self.widget.Inputs.data, self._housing) + _test() + + self.send_signal(self.widget.Inputs.learner, + DummyLearner(n_components=6)) + _test() + + self.send_signal(self.widget.Inputs.data, None) + self.send_signal(self.widget.Inputs.data, self._housing) + _test() + + self.send_signal(self.widget.Inputs.learner, self._rf) + self.assertEqual(controls.parameter_index.currentText(), + "Number of trees") + self.assertEqual(controls.minimum.value(), 1) + self.assertEqual(controls.maximum.value(), 10) + self.assertEqual(self.widget.parameter_index, 0) + self.assertEqual(self.widget.minimum, 1) + self.assertEqual(self.widget.maximum, 10) + def test_visual_settings(self): graph = self.widget.graph