diff --git a/Orange/base.py b/Orange/base.py index 07500b96a5b..2331c758d60 100644 --- a/Orange/base.py +++ b/Orange/base.py @@ -19,6 +19,7 @@ from Orange.util import Reprable, OrangeDeprecationWarning, wrap_callback, \ dummy_callback + __all__ = ["Learner", "Model", "SklLearner", "SklModel", "ReprableWithPreprocessors"] @@ -596,7 +597,15 @@ def fit(self, X, Y, W=None): def supports_weights(self): """Indicates whether this learner supports weighted instances. """ - return 'sample_weight' in self.__wraps__.fit.__code__.co_varnames + warnings.warn('SklLearner.supports_weights property is deprecated. All ' + 'subclasses should redefine the supports_weights attribute. ' + 'The property will be removed in 3.39.', + OrangeDeprecationWarning) + varnames = self.__wraps__.fit.__code__.co_varnames + # scikit-learn often uses decorators on fit() + if hasattr(self.__wraps__.fit, "__wrapped__"): + varnames = varnames + self.__wraps__.fit.__wrapped__.__code__.co_varnames + return 'sample_weight' in varnames def __getattr__(self, item): try: diff --git a/Orange/classification/gb.py b/Orange/classification/gb.py index b0027650a0a..1cd57511d6f 100644 --- a/Orange/classification/gb.py +++ b/Orange/classification/gb.py @@ -23,6 +23,7 @@ def score(self, data: Table) -> Tuple[np.ndarray, Tuple[Variable]]: class GBClassifier(SklLearner, _FeatureScorerMixin): __wraps__ = skl_ensemble.GradientBoostingClassifier __returns__ = SklModel + supports_weights = True def __init__(self, loss="log_loss", diff --git a/Orange/classification/knn.py b/Orange/classification/knn.py index 53346f87eb0..decdb354ead 100644 --- a/Orange/classification/knn.py +++ b/Orange/classification/knn.py @@ -7,3 +7,4 @@ class KNNLearner(KNNBase, SklLearner): __wraps__ = skl_neighbors.KNeighborsClassifier + supports_weights = False diff --git a/Orange/classification/logistic_regression.py b/Orange/classification/logistic_regression.py index aeb4fbfc1cb..5aeda451662 100644 --- a/Orange/classification/logistic_regression.py +++ b/Orange/classification/logistic_regression.py @@ -33,6 +33,7 @@ class LogisticRegressionLearner(SklLearner, _FeatureScorerMixin): __wraps__ = skl_linear_model.LogisticRegression __returns__ = LogisticRegressionClassifier preprocessors = SklLearner.preprocessors + supports_weights = True def __init__(self, penalty="l2", dual=False, tol=0.0001, C=1.0, fit_intercept=True, intercept_scaling=1, class_weight=None, diff --git a/Orange/classification/neural_network.py b/Orange/classification/neural_network.py index 53dff79bed4..78d4cb374fa 100644 --- a/Orange/classification/neural_network.py +++ b/Orange/classification/neural_network.py @@ -25,6 +25,7 @@ class MLPClassifierWCallback(skl_nn.MLPClassifier, NIterCallbackMixin): class NNClassificationLearner(NNBase, SklLearner): __wraps__ = MLPClassifierWCallback + supports_weights = False def _initialize_wrapped(self): clf = SklLearner._initialize_wrapped(self) diff --git a/Orange/classification/outlier_detection.py b/Orange/classification/outlier_detection.py index 52f56253464..7dad7e9ce14 100644 --- a/Orange/classification/outlier_detection.py +++ b/Orange/classification/outlier_detection.py @@ -89,6 +89,7 @@ class OneClassSVMLearner(_OutlierLearner): name = "One class SVM" __wraps__ = OneClassSVM preprocessors = SklLearner.preprocessors + [AdaptiveNormalize()] + supports_weights = True def __init__(self, kernel='rbf', degree=3, gamma="auto", coef0=0.0, tol=0.001, nu=0.5, shrinking=True, cache_size=200, @@ -100,6 +101,7 @@ def __init__(self, kernel='rbf', degree=3, gamma="auto", coef0=0.0, class LocalOutlierFactorLearner(_OutlierLearner): __wraps__ = LocalOutlierFactor name = "Local Outlier Factor" + supports_weights = False def __init__(self, n_neighbors=20, algorithm="auto", leaf_size=30, metric="minkowski", p=2, metric_params=None, @@ -112,6 +114,7 @@ def __init__(self, n_neighbors=20, algorithm="auto", leaf_size=30, class IsolationForestLearner(_OutlierLearner): __wraps__ = IsolationForest name = "Isolation Forest" + supports_weights = True def __init__(self, n_estimators=100, max_samples='auto', contamination='auto', max_features=1.0, bootstrap=False, @@ -156,6 +159,7 @@ class EllipticEnvelopeLearner(_OutlierLearner): __wraps__ = EllipticEnvelope __returns__ = EllipticEnvelopeClassifier name = "Covariance Estimator" + supports_weights = False def __init__(self, store_precision=True, assume_centered=False, support_fraction=None, contamination=0.1, diff --git a/Orange/classification/random_forest.py b/Orange/classification/random_forest.py index e5d9b8be465..f77b6f2c898 100644 --- a/Orange/classification/random_forest.py +++ b/Orange/classification/random_forest.py @@ -38,6 +38,7 @@ def wrap(tree, i): class RandomForestLearner(SklLearner, _FeatureScorerMixin): __wraps__ = skl_ensemble.RandomForestClassifier __returns__ = RandomForestClassifier + supports_weights = True def __init__(self, n_estimators=10, diff --git a/Orange/classification/sgd.py b/Orange/classification/sgd.py index 3bb617d61c1..ba0d9494757 100644 --- a/Orange/classification/sgd.py +++ b/Orange/classification/sgd.py @@ -12,6 +12,7 @@ class SGDClassificationLearner(SklLearner): __wraps__ = SGDClassifier __returns__ = LinearModel preprocessors = SklLearner.preprocessors + [Normalize()] + supports_weights = True def __init__(self, loss='hinge', penalty='l2', alpha=0.0001, l1_ratio=0.15, fit_intercept=True, max_iter=5, diff --git a/Orange/classification/tree.py b/Orange/classification/tree.py index 764e801f53d..eaef8641576 100644 --- a/Orange/classification/tree.py +++ b/Orange/classification/tree.py @@ -233,6 +233,7 @@ class SklTreeLearner(SklLearner): __wraps__ = skl_tree.DecisionTreeClassifier __returns__ = SklTreeClassifier name = 'tree' + supports_weights = True def __init__(self, criterion="gini", splitter="best", max_depth=None, min_samples_split=2, min_samples_leaf=1, diff --git a/Orange/classification/xgb.py b/Orange/classification/xgb.py index da476ce5a78..4f102a07537 100644 --- a/Orange/classification/xgb.py +++ b/Orange/classification/xgb.py @@ -25,6 +25,7 @@ def score(self, data: Table) -> Tuple[np.ndarray, Tuple[Variable]]: class XGBClassifier(XGBBase, Learner, _FeatureScorerMixin): __wraps__ = xgboost.XGBClassifier __returns__ = SklModel + supports_weights = True def __init__(self, max_depth=None, @@ -88,6 +89,7 @@ def __init__(self, class XGBRFClassifier(XGBBase, Learner, _FeatureScorerMixin): __wraps__ = xgboost.XGBRFClassifier __returns__ = SklModel + supports_weights = True def __init__(self, max_depth=None, diff --git a/Orange/ensembles/ada_boost.py b/Orange/ensembles/ada_boost.py index 9abd8980015..f0a5ef6ebf6 100644 --- a/Orange/ensembles/ada_boost.py +++ b/Orange/ensembles/ada_boost.py @@ -18,6 +18,7 @@ class SklAdaBoostClassifier(SklModelClassification): class SklAdaBoostClassificationLearner(SklLearnerClassification): __wraps__ = skl_ensemble.AdaBoostClassifier __returns__ = SklAdaBoostClassifier + supports_weights = True def __init__(self, base_estimator=None, n_estimators=50, learning_rate=1., algorithm='SAMME.R', random_state=None, preprocessors=None): @@ -40,6 +41,7 @@ class SklAdaBoostRegressor(SklModelRegression): class SklAdaBoostRegressionLearner(SklLearnerRegression): __wraps__ = skl_ensemble.AdaBoostRegressor __returns__ = SklAdaBoostRegressor + supports_weights = True def __init__(self, base_estimator=None, n_estimators=50, learning_rate=1., loss='linear', random_state=None, preprocessors=None): diff --git a/Orange/modelling/tests/test_catgb.py b/Orange/modelling/tests/test_catgb.py index f9c79863f01..d77cb4f3998 100644 --- a/Orange/modelling/tests/test_catgb.py +++ b/Orange/modelling/tests/test_catgb.py @@ -42,6 +42,9 @@ def test_scorer(self): booster.score(self.iris) booster.score(self.housing) + def test_supports_weights(self): + self.assertTrue(CatGBLearner().supports_weights) + if __name__ == "__main__": unittest.main() diff --git a/Orange/modelling/tests/test_gb.py b/Orange/modelling/tests/test_gb.py index b200b199490..141779f8764 100644 --- a/Orange/modelling/tests/test_gb.py +++ b/Orange/modelling/tests/test_gb.py @@ -37,6 +37,9 @@ def test_scorer(self): booster.score(self.iris) booster.score(self.housing) + def test_supports_weights(self): + self.assertTrue(GBLearner().supports_weights) + if __name__ == "__main__": unittest.main() diff --git a/Orange/modelling/tests/test_xgb.py b/Orange/modelling/tests/test_xgb.py index 761dc961775..c70cc463898 100644 --- a/Orange/modelling/tests/test_xgb.py +++ b/Orange/modelling/tests/test_xgb.py @@ -55,6 +55,10 @@ def test_scorer(self, learner_class: Union[XGBLearner, XGBRFLearner]): booster.score(self.iris) booster.score(self.housing) + @test_learners + def test_supports_weights(self, learner_class: Union[XGBLearner, XGBRFLearner]): + self.assertTrue(learner_class().supports_weights) + if __name__ == "__main__": unittest.main() diff --git a/Orange/regression/gb.py b/Orange/regression/gb.py index 9f37044d169..95c8e7eeb2e 100644 --- a/Orange/regression/gb.py +++ b/Orange/regression/gb.py @@ -23,6 +23,7 @@ def score(self, data: Table) -> Tuple[np.ndarray, Tuple[Variable]]: class GBRegressor(SklLearner, _FeatureScorerMixin): __wraps__ = skl_ensemble.GradientBoostingRegressor __returns__ = SklModel + supports_weights = True def __init__(self, loss="squared_error", diff --git a/Orange/regression/knn.py b/Orange/regression/knn.py index 2b80f1697d8..03991d09d91 100644 --- a/Orange/regression/knn.py +++ b/Orange/regression/knn.py @@ -7,3 +7,4 @@ class KNNRegressionLearner(KNNBase, SklLearner): __wraps__ = skl_neighbors.KNeighborsRegressor + supports_weights = False diff --git a/Orange/regression/linear.py b/Orange/regression/linear.py index 7342d3a3150..5eebacdf03e 100644 --- a/Orange/regression/linear.py +++ b/Orange/regression/linear.py @@ -27,6 +27,7 @@ def score(self, data): class LinearRegressionLearner(SklLearner, _FeatureScorerMixin): __wraps__ = skl_linear_model.LinearRegression + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, preprocessors=None, fit_intercept=True): @@ -40,6 +41,7 @@ def fit(self, X, Y, W=None): class RidgeRegressionLearner(LinearRegressionLearner): __wraps__ = skl_linear_model.Ridge + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, alpha=1.0, fit_intercept=True, copy_X=True, @@ -50,6 +52,7 @@ def __init__(self, alpha=1.0, fit_intercept=True, copy_X=True, class LassoRegressionLearner(LinearRegressionLearner): __wraps__ = skl_linear_model.Lasso + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, alpha=1.0, fit_intercept=True, precompute=False, @@ -61,6 +64,7 @@ def __init__(self, alpha=1.0, fit_intercept=True, precompute=False, class ElasticNetLearner(LinearRegressionLearner): __wraps__ = skl_linear_model.ElasticNet + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, alpha=1.0, l1_ratio=0.5, fit_intercept=True, @@ -72,6 +76,7 @@ def __init__(self, alpha=1.0, l1_ratio=0.5, fit_intercept=True, class ElasticNetCVLearner(LinearRegressionLearner): __wraps__ = skl_linear_model.ElasticNetCV + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, l1_ratio=0.5, eps=0.001, n_alphas=100, alphas=None, @@ -85,6 +90,7 @@ def __init__(self, l1_ratio=0.5, eps=0.001, n_alphas=100, alphas=None, class SGDRegressionLearner(LinearRegressionLearner): __wraps__ = skl_linear_model.SGDRegressor preprocessors = SklLearner.preprocessors + [Normalize()] + supports_weights = True # Arguments are needed for signatures, pylint: disable=unused-argument def __init__(self, loss='squared_error', penalty='l2', alpha=0.0001, diff --git a/Orange/regression/neural_network.py b/Orange/regression/neural_network.py index 7a8b553756d..85339ebb660 100644 --- a/Orange/regression/neural_network.py +++ b/Orange/regression/neural_network.py @@ -12,6 +12,7 @@ class MLPRegressorWCallback(skl_nn.MLPRegressor, NIterCallbackMixin): class NNRegressionLearner(NNBase, SklLearner): __wraps__ = MLPRegressorWCallback + supports_weights = False def _initialize_wrapped(self): clf = SklLearner._initialize_wrapped(self) diff --git a/Orange/regression/random_forest.py b/Orange/regression/random_forest.py index 055a4052812..4b37888ae27 100644 --- a/Orange/regression/random_forest.py +++ b/Orange/regression/random_forest.py @@ -38,6 +38,7 @@ def wrap(tree, i): class RandomForestRegressionLearner(SklLearner, _FeatureScorerMixin): __wraps__ = skl_ensemble.RandomForestRegressor __returns__ = RandomForestRegressor + supports_weights = True def __init__(self, n_estimators=10, diff --git a/Orange/regression/tree.py b/Orange/regression/tree.py index 79846a73d04..9e49e930736 100644 --- a/Orange/regression/tree.py +++ b/Orange/regression/tree.py @@ -184,6 +184,7 @@ class SklTreeRegressionLearner(SklLearner): __wraps__ = skl_tree.DecisionTreeRegressor __returns__ = SklTreeRegressor name = 'regression tree' + supports_weights = True def __init__(self, criterion="squared_error", splitter="best", max_depth=None, min_samples_split=2, min_samples_leaf=1, diff --git a/Orange/regression/xgb.py b/Orange/regression/xgb.py index 4c8f4b7d362..be66ba8604a 100644 --- a/Orange/regression/xgb.py +++ b/Orange/regression/xgb.py @@ -23,6 +23,7 @@ def score(self, data: Table) -> Tuple[np.ndarray, Tuple[Variable]]: class XGBRegressor(XGBBase, Learner, _FeatureScorerMixin): __wraps__ = xgboost.XGBRegressor + supports_weights = True def __init__(self, max_depth=None, @@ -75,6 +76,7 @@ def __init__(self, class XGBRFRegressor(XGBBase, Learner, _FeatureScorerMixin): __wraps__ = xgboost.XGBRFRegressor + supports_weights = True def __init__(self, max_depth=None, diff --git a/Orange/tests/test_base.py b/Orange/tests/test_base.py index 6217fb7529d..4615e113f2b 100644 --- a/Orange/tests/test_base.py +++ b/Orange/tests/test_base.py @@ -2,11 +2,15 @@ # pylint: disable=missing-docstring import pickle import unittest +from distutils.version import LooseVersion + +import Orange from Orange.base import SklLearner, Learner, Model from Orange.data import Domain, Table from Orange.preprocess import Discretize, Randomize, Continuize from Orange.regression import LinearRegressionLearner +from Orange.util import OrangeDeprecationWarning class DummyLearner(Learner): @@ -102,7 +106,8 @@ def fit(self, X, y, sample_weight=None): class DummyLearner(SklLearner): __wraps__ = DummySklLearner - self.assertTrue(DummyLearner().supports_weights) + with self.assertWarns(OrangeDeprecationWarning): + self.assertTrue(DummyLearner().supports_weights) class DummySklLearner: def fit(self, X, y): @@ -111,7 +116,8 @@ def fit(self, X, y): class DummyLearner(SklLearner): __wraps__ = DummySklLearner - self.assertFalse(DummyLearner().supports_weights) + with self.assertWarns(OrangeDeprecationWarning): + self.assertFalse(DummyLearner().supports_weights) def test_linreg(self): self.assertTrue( @@ -128,6 +134,15 @@ def test_callback(self): self.assertEqual(max(args), 1) self.assertListEqual(args, sorted(args)) + def test_supports_weights_property(self): + """This test is to be included in the 3.37 release and will fail in + version 3.39. This serves as a reminder.""" + if LooseVersion(Orange.__version__) >= LooseVersion("3.39"): + self.fail( + "`SklLearner.supports_weights` as a property that parses fit() " + "was deprecated in 3.37. Replace it with `supports_weights = False`" + ) + class TestModel(unittest.TestCase): def test_pickle(self): diff --git a/Orange/tests/test_knn.py b/Orange/tests/test_knn.py index 124fc9a0544..36355897181 100644 --- a/Orange/tests/test_knn.py +++ b/Orange/tests/test_knn.py @@ -83,3 +83,7 @@ def test_KNN_regression(self): results = cv(self.housing, learners) mse = MSE(results) self.assertLess(mse[1], mse[0]) + + def test_supports_weights(self): + self.assertFalse(KNNLearner().supports_weights) + self.assertFalse(KNNRegressionLearner().supports_weights) diff --git a/Orange/tests/test_linear_regression.py b/Orange/tests/test_linear_regression.py index 97c28b23fae..5ed13f94614 100644 --- a/Orange/tests/test_linear_regression.py +++ b/Orange/tests/test_linear_regression.py @@ -118,3 +118,6 @@ def test_linear_regression_repr(self): learner2 = eval(repr_text) self.assertIsInstance(learner2, LinearRegressionLearner) + + def test_supports_weights(self): + self.assertTrue(LinearRegressionLearner().supports_weights) diff --git a/Orange/tests/test_logistic_regression.py b/Orange/tests/test_logistic_regression.py index 4ab3cdc3a68..88b6e1f1073 100644 --- a/Orange/tests/test_logistic_regression.py +++ b/Orange/tests/test_logistic_regression.py @@ -151,3 +151,6 @@ def test_auto_solver(self): skl_clf = lr._initialize_wrapped() self.assertEqual(skl_clf.solver, "liblinear") self.assertEqual(skl_clf.penalty, "l1") + + def test_supports_weights(self): + self.assertTrue(LogisticRegressionLearner().supports_weights) diff --git a/Orange/tests/test_majority.py b/Orange/tests/test_majority.py index 013a3c35adc..0aa12333cc7 100644 --- a/Orange/tests/test_majority.py +++ b/Orange/tests/test_majority.py @@ -86,3 +86,6 @@ def test_returns_random_class(self): break else: self.fail("Majority always returns the same value.") + + def test_supports_weights(self): + self.assertFalse(MajorityLearner().supports_weights) diff --git a/Orange/tests/test_naive_bayes.py b/Orange/tests/test_naive_bayes.py index 88b6fe05a74..1ccf32ce7fc 100644 --- a/Orange/tests/test_naive_bayes.py +++ b/Orange/tests/test_naive_bayes.py @@ -276,6 +276,9 @@ def test_no_targets(self): data = Table.from_numpy(domain, x, y) self.assertRaises(ValueError, self.learner, data) + def test_supports_weights(self): + self.assertFalse(NaiveBayesLearner().supports_weights) + if __name__ == "__main__": unittest.main() diff --git a/Orange/tests/test_neural_network.py b/Orange/tests/test_neural_network.py index e16750e6eab..6515aa246b7 100644 --- a/Orange/tests/test_neural_network.py +++ b/Orange/tests/test_neural_network.py @@ -63,3 +63,7 @@ def test_NN_regression_predict_single_instance(self): clf = lrn(self.housing) for ins in self.housing[::20]: clf(ins) + + def test_supports_weights(self): + self.assertFalse(NNRegressionLearner().supports_weights) + self.assertFalse(NNClassificationLearner().supports_weights) diff --git a/Orange/tests/test_random_forest.py b/Orange/tests/test_random_forest.py index b3221a7ec81..7bca3ea2039 100644 --- a/Orange/tests/test_random_forest.py +++ b/Orange/tests/test_random_forest.py @@ -133,6 +133,10 @@ def test_max_features_reg(self): diff = np.sum(np.abs(model_1(data[:1]) - model_2(data[:1]))) self.assertGreater(diff, 1.2) + def test_supports_weights(self): + self.assertTrue(RandomForestRegressionLearner().supports_weights) + self.assertTrue(RandomForestLearner().supports_weights) + if __name__ == "__main__": unittest.main() diff --git a/Orange/tests/test_sgd.py b/Orange/tests/test_sgd.py index dbeb9d7c746..7197575c5fe 100644 --- a/Orange/tests/test_sgd.py +++ b/Orange/tests/test_sgd.py @@ -34,6 +34,9 @@ def test_coefficients(self): mod = lrn(Table("housing")) self.assertEqual(len(mod.coefficients), len(mod.domain.attributes)) + def test_supports_weights(self): + self.assertTrue(SGDRegressionLearner().supports_weights) + class TestSGDClassificationLearner(unittest.TestCase): @classmethod @@ -72,3 +75,6 @@ def test_predictions_shapes(self): mod = lrn(self.iris) self.assertTupleEqual((50, 3), mod(self.iris[:50], mod.Probs).shape) self.assertTupleEqual((50,), mod(self.iris[:50], mod.Value).shape) + + def test_supports_weights(self): + self.assertTrue(SGDClassificationLearner().supports_weights) diff --git a/Orange/tests/test_tree.py b/Orange/tests/test_tree.py index b3342cda68e..a274d79014a 100644 --- a/Orange/tests/test_tree.py +++ b/Orange/tests/test_tree.py @@ -28,6 +28,10 @@ def test_regression(self): pred = model(table) self.assertTrue(np.all(table.Y.flatten() == pred)) + def test_supports_weights(self): + self.assertTrue(SklTreeRegressionLearner().supports_weights) + self.assertTrue(SklTreeLearner().supports_weights) + class TestTreeLearner(unittest.TestCase): def test_uses_preprocessors(self): @@ -38,6 +42,9 @@ def test_uses_preprocessors(self): tree(iris) mock_preprocessor.assert_called_with(iris) + def test_supports_weights(self): + self.assertFalse(TreeLearner().supports_weights) + class TestDecisionTreeClassifier(unittest.TestCase): @classmethod diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 29779fa7a92..7b96182d71f 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -64,7 +64,7 @@ requirements: - python-louvain >=0.13 - pyyaml - requests - - scikit-learn >=1.1.0,<1.2.0 + - scikit-learn >=1.1.0,!=1.2.*,<1.4 # ignoring 1.2.*: scikit-learn/issues/26241 - scipy >=1.9 - serverfiles - setuptools >=51.0.0 diff --git a/requirements-core.txt b/requirements-core.txt index 02c2f3ffbe7..8abe068a6e8 100644 --- a/requirements-core.txt +++ b/requirements-core.txt @@ -17,7 +17,7 @@ pip>=18.0 python-louvain>=0.13 pyyaml requests -scikit-learn>=1.1.0,<1.2.0 +scikit-learn>=1.1.0,!=1.2.*,<1.4 # ignoring 1.2.*: scikit-learn/issues/26241 scipy>=1.9 serverfiles # for Data Sets synchronization setuptools>=51.0.0