diff --git a/conda/meta.yaml b/conda/meta.yaml index 6b40efbf9..78cb6c561 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -24,7 +24,7 @@ requirements: run: - python >=3.8 - numpy >=1.20.0 - - orange3 >=3.34.0 + - orange3 >=3.37.0 - orange-canvas-core >=0.1.28 - orange-widget-base >=4.19.0 - scipy >=1.9.0 diff --git a/doc/index.rst b/doc/index.rst index 0fd62172d..e56f652a3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -33,7 +33,6 @@ Widgets widgets/average widgets/interferogram-to-spectrum widgets/reshape-map - widgets/pls widgets/peakfit widgets/snr diff --git a/doc/widgets.json b/doc/widgets.json index f41ac3697..6315855fa 100644 --- a/doc/widgets.json +++ b/doc/widgets.json @@ -99,15 +99,6 @@ "background": "light-blue", "keywords": [] }, - { - "text": "PLS", - "doc": null, - "icon": "../orangecontrib/spectroscopy/widgets/icons/PLS.svg", - "background": "light-blue", - "keywords": [ - "partial least squares" - ] - }, { "text": "Reshape Map", "doc": "widgets/reshape-map.md", diff --git a/doc/widgets/images/PLS-Example.png b/doc/widgets/images/PLS-Example.png deleted file mode 100644 index d635d86f0..000000000 Binary files a/doc/widgets/images/PLS-Example.png and /dev/null differ diff --git a/doc/widgets/images/PLS-stamped.png b/doc/widgets/images/PLS-stamped.png deleted file mode 100644 index 9fe4e4c93..000000000 Binary files a/doc/widgets/images/PLS-stamped.png and /dev/null differ diff --git a/doc/widgets/pls.md b/doc/widgets/pls.md deleted file mode 100644 index 0e46d27c0..000000000 --- a/doc/widgets/pls.md +++ /dev/null @@ -1,33 +0,0 @@ -# PLS - -Partial Least Squares Regression widget for multivariate data analysis. - -**Inputs** - -- Data: input dataset -- Preprocessor: preprocessing method(s) - -**Outputs** - -- Learner: PLS regression learning algorithm -- Model: trained model -- Coefficients: PLS regression coefficients - -**PLS** (Partial Least Squares) widget acts as a regressor for data with numeric target variable. In its current implementation, it is the same as linear regression, but with a different kind of regularization. Here, regularization is performed with the choice of the components - the more components, the lesser the effect of regularization. - -PLS widget can output coefficients, just like [Linear Regression](../model/linearregression.md). One can observe the effect of each variable in a [Data Table](../data/datatable.md). - -![](images/PLS-stamped.png) - -1. The learner/predictor name -2. Parameters: - - Components: the number of components of the model, which act as regularization (the more components, the lesser the regularization) - - Iteration limit: maximum iterations for stopping the algorithm -3. Press *Apply* to commit changes. If *Apply Automatically* is ticked, changes are committed automatically. - -Example -------- - -Below, is a simple workflow with *housing* dataset. We trained **PLS** and [Linear Regression](../model/linearregression.md) and evaluated their performance in [Test & Score](../evaluate/testandscore.md). - -![](images/PLS-Example.png) diff --git a/orangecontrib/spectroscopy/models/__init__.py b/orangecontrib/spectroscopy/models/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/orangecontrib/spectroscopy/models/pls.py b/orangecontrib/spectroscopy/models/pls.py deleted file mode 100644 index 0927c7f7c..000000000 --- a/orangecontrib/spectroscopy/models/pls.py +++ /dev/null @@ -1,176 +0,0 @@ -import numpy as np -import pkg_resources -import sklearn -import sklearn.cross_decomposition as skl_pls - -from Orange.data import Table, Domain, Variable, \ - ContinuousVariable, StringVariable -from Orange.data.util import get_unique_names, SharedComputeValue -from Orange.preprocess.score import LearnerScorer -from Orange.regression import SklLearner, SklModel - - -class _FeatureScorerMixin(LearnerScorer): - feature_type = Variable - class_type = ContinuousVariable - - def score(self, data): - model = self(data) - return np.abs(model.coefficients), model.domain.attributes - - -class _PLSCommonTransform: - - def __init__(self, pls_model): - self.pls_model = pls_model - - def _transform_with_numpy_output(self, X, Y): - pls = self.pls_model.skl_model - """ - # the next command does the following - x_center = X - pls._x_mean - y_center = Y - pls._y_mean - t = x_center @ pls.x_rotations_ - u = y_center @ pls.y_rotations_ - """ - t, u = pls.transform(X, Y) - return np.hstack((t, u)) - - def __call__(self, data): - if data.domain != self.pls_model.domain: - data = data.transform(self.pls_model.domain) - if len(data.Y.shape) == 1: - Y = data.Y.reshape(-1, 1) - else: - Y = data.Y - return self._transform_with_numpy_output(data.X, Y) - - -class PLSProjector(SharedComputeValue): - def __init__(self, transform, feature): - super().__init__(transform) - self.feature = feature - - def compute(self, _, space): - return space[:, self.feature] - - -class PLSModel(SklModel): - var_prefix_X = "PLS T" - var_prefix_Y = "PLS U" - - @property - def coefficients(self): - coef = self.skl_model.coef_ - # 1.3 has transposed coef_ - if pkg_resources.parse_version(sklearn.__version__) < pkg_resources.parse_version("1.3.0"): - coef = coef.T - return coef - - def predict(self, X): - vals = self.skl_model.predict(X) - if len(self.domain.class_vars) == 1: - vals = vals.ravel() - return vals - - def __str__(self): - return 'PLSModel {}'.format(self.skl_model) - - def _get_var_names(self, n, prefix): - names = [f"{prefix}{postfix}" for postfix in range(1, n + 1)] - return get_unique_names([var.name for var in self.domain.metas], names) - - def project(self, data): - if not isinstance(data, Table): - raise RuntimeError("PLSModel can only project tables") - - transformer = _PLSCommonTransform(self) - - def trvar(i, name): - return ContinuousVariable(name, compute_value=PLSProjector(transformer, i)) - - n_components = self.skl_model.x_loadings_.shape[1] - - var_names_X = self._get_var_names(n_components, self.var_prefix_X) - var_names_Y = self._get_var_names(n_components, self.var_prefix_Y) - - domain = Domain( - [trvar(i, var_names_X[i]) for i in range(n_components)], - data.domain.class_vars, - list(data.domain.metas) + - [trvar(n_components + i, var_names_Y[i]) for i in range(n_components)] - ) - - return data.transform(domain) - - def components(self): - orig_domain = self.domain - names = [a.name for a in orig_domain.attributes + orig_domain.class_vars] - meta_name = get_unique_names(names, 'components') - - n_components = self.skl_model.x_loadings_.shape[1] - - meta_vars = [StringVariable(name=meta_name)] - metas = np.array( - [[f"Component {i + 1}" for i in range(n_components)]], dtype=object - ).T - dom = Domain( - [ContinuousVariable(a.name) for a in orig_domain.attributes], - [ContinuousVariable(a.name) for a in orig_domain.class_vars], - metas=meta_vars) - components = Table(dom, - self.skl_model.x_loadings_.T, - Y=self.skl_model.y_loadings_.T, - metas=metas) - components.name = 'components' - return components - - def coefficients_table(self): - coeffs = self.coefficients.T - domain = Domain( - [ContinuousVariable(f"coef {i}") for i in range(coeffs.shape[1])], - metas=[StringVariable("name")] - ) - waves = [[attr.name] for attr in self.domain.attributes] - coef_table = Table.from_numpy(domain, X=coeffs, metas=waves) - coef_table.name = "coefficients" - return coef_table - - -class PLSRegressionLearner(SklLearner, _FeatureScorerMixin): - __wraps__ = skl_pls.PLSRegression - __returns__ = PLSModel - supports_multiclass = True - preprocessors = SklLearner.preprocessors - - def fit(self, X, Y, W=None): - params = self.params.copy() - params["n_components"] = min(X.shape[1] - 1, - X.shape[0] - 1, - params["n_components"]) - clf = self.__wraps__(**params) - return self.__returns__(clf.fit(X, Y)) - - def __init__(self, n_components=2, scale=True, - max_iter=500, preprocessors=None): - super().__init__(preprocessors=preprocessors) - self.params = vars() - - def incompatibility_reason(self, domain): - reason = None - if not domain.class_vars: - reason = "Numeric targets expected." - else: - for cv in domain.class_vars: - if not cv.is_continuous: - reason = "Only numeric target variables expected." - return reason - -if __name__ == '__main__': - import Orange - - data = Orange.data.Table('housing') - learners = [PLSRegressionLearner(n_components=2, max_iter=100)] - res = Orange.evaluation.CrossValidation()(data, learners) - for l, ca in zip(learners, Orange.evaluation.RMSE(res)): - print("learner: {}\nRMSE: {}\n".format(l, ca)) diff --git a/orangecontrib/spectroscopy/tests/test_owpls.py b/orangecontrib/spectroscopy/tests/test_owpls.py deleted file mode 100644 index c7de52244..000000000 --- a/orangecontrib/spectroscopy/tests/test_owpls.py +++ /dev/null @@ -1,123 +0,0 @@ -from unittest import TestCase, skipIf - -import numpy as np - -import pkg_resources -import sklearn -from sklearn.cross_decomposition import PLSRegression - -from Orange.data import Table, Domain, ContinuousVariable -from Orange.widgets.tests.base import WidgetTest, WidgetLearnerTestMixin, ParameterMapping - -import Orange.version -from orangecontrib.spectroscopy.widgets.owpls import OWPLS -from orangecontrib.spectroscopy.models.pls import PLSRegressionLearner - - -def table(rows, attr, vars): - attr_vars = [ContinuousVariable(name="Feature %i" % i) for i in range(attr)] - class_vars = [ContinuousVariable(name="Class %i" % i) for i in range(vars)] - domain = Domain(attr_vars, class_vars, []) - X = np.random.RandomState(0).random((rows, attr)) - Y = np.random.RandomState(1).random((rows, vars)) - return Table.from_numpy(domain, X=X, Y=Y) - - -def coefficients(sklmodel): - coef = sklmodel.coef_ - # 1.3 has transposed coef_ - if pkg_resources.parse_version(sklearn.__version__) < pkg_resources.parse_version("1.3.0"): - coef = coef.T - return coef - - -class TestPLS(TestCase): - - def test_allow_y_dim(self): - """ The current PLS version allows only a single Y dimension. """ - learner = PLSRegressionLearner(n_components=2) - d = table(10, 5, 0) - with self.assertRaises(ValueError): - learner(d) - for n_class_vars in [1, 2, 3]: - d = table(10, 5, n_class_vars) - learner(d) # no exception - - def test_compare_to_sklearn(self): - d = table(10, 5, 1) - orange_model = PLSRegressionLearner()(d) - scikit_model = PLSRegression().fit(d.X, d.Y) - np.testing.assert_almost_equal(scikit_model.predict(d.X).ravel(), - orange_model(d)) - np.testing.assert_almost_equal(coefficients(scikit_model), - orange_model.coefficients) - - def test_compare_to_sklearn_multid(self): - d = table(10, 5, 3) - orange_model = PLSRegressionLearner()(d) - scikit_model = PLSRegression().fit(d.X, d.Y) - np.testing.assert_almost_equal(scikit_model.predict(d.X), - orange_model(d)) - np.testing.assert_almost_equal(coefficients(scikit_model), - orange_model.coefficients) - - def test_too_many_components(self): - # do not change n_components - d = table(5, 5, 1) - model = PLSRegressionLearner(n_components=4)(d) - self.assertEqual(model.skl_model.n_components, 4) - # need to use fewer components; column limited - d = table(6, 5, 1) - model = PLSRegressionLearner(n_components=6)(d) - self.assertEqual(model.skl_model.n_components, 4) - # need to use fewer components; row limited - d = table(5, 6, 1) - model = PLSRegressionLearner(n_components=6)(d) - self.assertEqual(model.skl_model.n_components, 4) - - def test_scores(self): - for d in [table(10, 5, 1), table(10, 5, 3)]: - orange_model = PLSRegressionLearner()(d) - scikit_model = PLSRegression().fit(d.X, d.Y) - scores = orange_model.project(d) - sx, sy = scikit_model.transform(d.X, d.Y) - np.testing.assert_almost_equal(sx, scores.X) - np.testing.assert_almost_equal(sy, scores.metas) - - def test_components(self): - def t2d(m): - return m.reshape(-1, 1) if len(m.shape) == 1 else m - for d in [table(10, 5, 1), table(10, 5, 3)]: - orange_model = PLSRegressionLearner()(d) - scikit_model = PLSRegression().fit(d.X, d.Y) - components = orange_model.components() - np.testing.assert_almost_equal(scikit_model.x_loadings_, components.X.T) - np.testing.assert_almost_equal(scikit_model.y_loadings_, t2d(components.Y).T) - - def test_coefficients(self): - for d in [table(10, 5, 1), table(10, 5, 3)]: - orange_model = PLSRegressionLearner()(d) - scikit_model = PLSRegression().fit(d.X, d.Y) - coef_table = orange_model.coefficients_table() - np.testing.assert_almost_equal(coefficients(scikit_model).T, coef_table.X) - - -class TestOWPLS(WidgetTest, WidgetLearnerTestMixin): - def setUp(self): - self.widget = self.create_widget(OWPLS, - stored_settings={"auto_apply": False}) - self.init() - self.parameters = [ - ParameterMapping('max_iter', self.widget.n_iters), - ParameterMapping('n_components', self.widget.ncomps_spin)] - - skip_reason = "orange-widget-base changed apply button, which resulted in " \ - "test failures (but was otherwise benign)" - - @skipIf(Orange.version.version < "3.35.0", skip_reason) - def test_output_learner(self): - super().test_output_learner() - - @skipIf(Orange.version.version < "3.35.0", skip_reason) - def test_output_learner_name(self): - super().test_output_learner_name() diff --git a/orangecontrib/spectroscopy/widgets/icons/PLS.svg b/orangecontrib/spectroscopy/widgets/icons/PLS.svg deleted file mode 100644 index 84a1f070e..000000000 --- a/orangecontrib/spectroscopy/widgets/icons/PLS.svg +++ /dev/null @@ -1,311 +0,0 @@ - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PLS diff --git a/orangecontrib/spectroscopy/widgets/owpls.py b/orangecontrib/spectroscopy/widgets/owpls.py deleted file mode 100644 index b1d44e61f..000000000 --- a/orangecontrib/spectroscopy/widgets/owpls.py +++ /dev/null @@ -1,93 +0,0 @@ -from AnyQt.QtCore import Qt -import scipy.sparse as sp - -from Orange.widgets import gui -from Orange.widgets.widget import Msg -from Orange.data import Table, Domain, ContinuousVariable, StringVariable -from Orange.widgets.settings import Setting -from Orange.widgets.utils.owlearnerwidget import OWBaseLearner -from Orange.widgets.utils.signals import Output -from Orange.widgets.utils.widgetpreview import WidgetPreview - -from orangecontrib.spectroscopy.models.pls import PLSRegressionLearner - - -class OWPLS(OWBaseLearner): - name = 'PLS' - description = "Partial Least Squares Regression widget for multivariate data analysis" - icon = "icons/PLS.svg" - keywords = ["partial least squares"] - - LEARNER = PLSRegressionLearner - - class Outputs(OWBaseLearner.Outputs): - coefsdata = Output("Coefficients", Table, explicit=True) - transformed = Output("Transformed Data", Table) - components = Output("Components", Table) - - class Warning(OWBaseLearner.Warning): - sparse_data = Msg('Sparse input data: default preprocessing is to scale it.') - - n_components = Setting(2) - max_iter = Setting(500) - - def add_main_layout(self): - - self.optimization_box = gui.vBox( - self.controlArea, "Optimization Parameters") - self.ncomps_spin = gui.spin( - self.optimization_box, self, "n_components", 1, 50, 1, - label="Components: ", - alignment=Qt.AlignRight, controlWidth=100, - callback=self.settings_changed) - self.n_iters = gui.spin( - self.optimization_box, self, "max_iter", 5, 1000000, 50, - label="Iteration limit: ", - alignment=Qt.AlignRight, controlWidth=100, - callback=self.settings_changed, - checkCallback=self.settings_changed) - - def update_model(self): - super().update_model() - coef_table = None - projection = None - components = None - if self.model is not None: - coef_table = self.model.coefficients_table() - projection = self.model.project(self.data) - components = self.model.components() - self.Outputs.coefsdata.send(coef_table) - self.Outputs.transformed.send(projection) - self.Outputs.components.send(components) - - @OWBaseLearner.Inputs.data - def set_data(self, data): - # reimplemented completely because the base learner does not - # allow multiclass - - self.Warning.sparse_data.clear() - - self.Error.data_error.clear() - self.data = data - - if data is not None and data.domain.class_var is None and not data.domain.class_vars: - self.Error.data_error( - "Data has no target variable.\n" - "Select one with the Select Columns widget.") - self.data = None - - # invalidate the model so that handleNewSignals will update it - self.model = None - - if self.data and sp.issparse(self.data.X): - self.Warning.sparse_data() - - def create_learner(self): - common_args = {'preprocessors': self.preprocessors} - return PLSRegressionLearner(n_components=self.n_components, - max_iter=self.max_iter, - **common_args) - - -if __name__ == "__main__": # pragma: no cover - WidgetPreview(OWPLS).run(Table("housing")) diff --git a/setup.py b/setup.py index 435e83a32..8ff6d2053 100644 --- a/setup.py +++ b/setup.py @@ -128,7 +128,7 @@ def include_documentation(local_dir, install_dir): 'setuptools>=36.3', # same as for Orange 3.28 'pip>=9.0', # same as for Orange 3.28 'numpy>=1.20.0', - 'Orange3>=3.34.0', + 'Orange3>=3.37.0', 'orange-canvas-core>=0.1.28', 'orange-widget-base>=4.19.0', 'scipy>=1.9.0', diff --git a/tox.ini b/tox.ini index 663cec2a1..1230826fb 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ setenv = deps = {env:PYQT_PYPI_NAME:PyQt5}=={env:PYQT_PYPI_VERSION:5.15.*} {env:WEBENGINE_PYPI_NAME:PyQtWebEngine}=={env:WEBENGINE_PYPI_VERSION:5.15.*} - oldest: orange3==3.34.0 + oldest: orange3==3.37.0 oldest: orange-canvas-core==0.1.28 oldest: orange-widget-base==4.19.0 oldest: scikit-learn~=1.0.1