diff --git a/Orange/classification/__init__.py b/Orange/classification/__init__.py index 842518fca31..c32fe8ff2d7 100644 --- a/Orange/classification/__init__.py +++ b/Orange/classification/__init__.py @@ -15,7 +15,7 @@ from .tree import * from .simple_tree import * from .simple_random_forest import * -from .elliptic_envelope import * +from .outlier_detection import * from .rules import * from .sgd import * from .neural_network import * diff --git a/Orange/classification/elliptic_envelope.py b/Orange/classification/elliptic_envelope.py deleted file mode 100644 index 60b8e5357d7..00000000000 --- a/Orange/classification/elliptic_envelope.py +++ /dev/null @@ -1,41 +0,0 @@ -import sklearn.covariance as skl_covariance - -from Orange.base import SklLearner, SklModel -from Orange.data import Table, Domain -from Orange.preprocess import Continuize, RemoveNaNColumns, SklImpute - -__all__ = ["EllipticEnvelopeLearner"] - - -class EllipticEnvelopeClassifier(SklModel): - def mahalanobis(self, observations): - """Computes squared Mahalanobis distances of given observations. - - Parameters - ---------- - observations : ndarray (n_samples, n_features) or Orange Table - - Returns - ------- - distances : ndarray (n_samples,) - Squared Mahalanobis distances given observations. - """ - if isinstance(observations, Table): - observations = observations.X - return self.skl_model.mahalanobis(observations) - - -class EllipticEnvelopeLearner(SklLearner): - __wraps__ = skl_covariance.EllipticEnvelope - __returns__ = EllipticEnvelopeClassifier - preprocessors = [Continuize(), RemoveNaNColumns(), SklImpute()] - - def __init__(self, store_precision=True, assume_centered=False, - support_fraction=None, contamination=0.1, - random_state=None, preprocessors=None): - super().__init__(preprocessors=preprocessors) - self.params = vars() - - def __call__(self, data): - classless_data = data.transform(Domain(data.domain.attributes)) - return super().__call__(classless_data) diff --git a/Orange/classification/outlier_detection.py b/Orange/classification/outlier_detection.py new file mode 100644 index 00000000000..6892a04d112 --- /dev/null +++ b/Orange/classification/outlier_detection.py @@ -0,0 +1,73 @@ +# pylint: disable=unused-argument +from sklearn.covariance import EllipticEnvelope +from sklearn.ensemble import IsolationForest +from sklearn.neighbors import LocalOutlierFactor +from Orange.base import SklLearner, SklModel +from Orange.data import Table, Domain + +__all__ = ["LocalOutlierFactorLearner", "IsolationForestLearner", + "EllipticEnvelopeLearner"] + + +class _OutlierDetector(SklLearner): + def __call__(self, data: Table): + data = data.transform(Domain(data.domain.attributes)) + return super().__call__(data) + + +class LocalOutlierFactorLearner(_OutlierDetector): + __wraps__ = LocalOutlierFactor + name = "Local Outlier Factor" + + def __init__(self, n_neighbors=20, algorithm="auto", leaf_size=30, + metric="minkowski", p=2, metric_params=None, + contamination="auto", novelty=True, n_jobs=None, + preprocessors=None): + super().__init__(preprocessors=preprocessors) + self.params = vars() + + +class IsolationForestLearner(_OutlierDetector): + __wraps__ = IsolationForest + name = "Isolation Forest" + + def __init__(self, n_estimators=100, max_samples='auto', + contamination='auto', max_features=1.0, bootstrap=False, + n_jobs=None, behaviour='deprecated', random_state=None, + verbose=0, warm_start=False, preprocessors=None): + super().__init__(preprocessors=preprocessors) + self.params = vars() + + +class EllipticEnvelopeClassifier(SklModel): + def mahalanobis(self, observations): + """Computes squared Mahalanobis distances of given observations. + + Parameters + ---------- + observations : ndarray (n_samples, n_features) or Orange Table + + Returns + ------- + distances : ndarray (n_samples,) + Squared Mahalanobis distances given observations. + """ + if isinstance(observations, Table): + observations = observations.X + return self.skl_model.mahalanobis(observations) + + +class EllipticEnvelopeLearner(_OutlierDetector): + __wraps__ = EllipticEnvelope + __returns__ = EllipticEnvelopeClassifier + name = "Covariance Estimator" + + def __init__(self, store_precision=True, assume_centered=False, + support_fraction=None, contamination=0.1, + random_state=None, preprocessors=None): + super().__init__(preprocessors=preprocessors) + self.params = vars() + + def __call__(self, data: Table): + data = data.transform(Domain(data.domain.attributes)) + return super().__call__(data) diff --git a/Orange/classification/svm.py b/Orange/classification/svm.py index 689e654f0ef..c603161320c 100644 --- a/Orange/classification/svm.py +++ b/Orange/classification/svm.py @@ -69,6 +69,7 @@ def __init__(self, nu=0.5, kernel='rbf', degree=3, gamma="auto", coef0=0.0, class OneClassSVMLearner(SklLearnerBase): + name = "One class SVM" __wraps__ = skl_svm.OneClassSVM preprocessors = svm_pps diff --git a/Orange/tests/test_elliptic_envelope.py b/Orange/classification/tests/test_outlier_detection.py similarity index 75% rename from Orange/tests/test_elliptic_envelope.py rename to Orange/classification/tests/test_outlier_detection.py index 682e47f4abc..1ac0b0c4ada 100644 --- a/Orange/tests/test_elliptic_envelope.py +++ b/Orange/classification/tests/test_outlier_detection.py @@ -5,7 +5,8 @@ import numpy as np from Orange.data import Table, Domain, ContinuousVariable -from Orange.classification import EllipticEnvelopeLearner +from Orange.classification import EllipticEnvelopeLearner, \ + IsolationForestLearner, LocalOutlierFactorLearner class TestEllipticEnvelopeLearner(unittest.TestCase): @@ -44,7 +45,7 @@ def test_mahalanobis(self): def test_EllipticEnvelope_ignores_y(self): domain = Domain((ContinuousVariable("x1"), ContinuousVariable("x2")), - class_vars=(ContinuousVariable("y1"), ContinuousVariable("y2"))) + (ContinuousVariable("y1"), ContinuousVariable("y2"))) X = np.random.random((40, 2)) Y = np.random.random((40, 2)) table = Table(domain, X, Y) @@ -60,3 +61,25 @@ def test_EllipticEnvelope_ignores_y(self): np.testing.assert_array_equal(pred1, pred2) np.testing.assert_array_equal(pred2, pred3) np.testing.assert_array_equal(pred3, pred4) + + +class TestOutlierDetection(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.iris = Table("iris") + + def test_LocalOutlierFactorDetector(self): + detector = LocalOutlierFactorLearner(contamination=0.1) + detect = detector(self.iris) + is_inlier = detect(self.iris) + self.assertEqual(len(np.where(is_inlier == -1)[0]), 14) + + def test_IsolationForestDetector(self): + detector = IsolationForestLearner(contamination=0.1) + detect = detector(self.iris) + is_inlier = detect(self.iris) + self.assertEqual(len(np.where(is_inlier == -1)[0]), 15) + + +if __name__ == "__main__": + unittest.main() diff --git a/Orange/widgets/data/owoutliers.py b/Orange/widgets/data/owoutliers.py index 8ecc8321c3d..99bc219aaae 100644 --- a/Orange/widgets/data/owoutliers.py +++ b/Orange/widgets/data/owoutliers.py @@ -1,17 +1,140 @@ +from typing import Dict, Tuple + import numpy as np -from AnyQt.QtWidgets import QLayout -from Orange.base import SklLearner -from Orange.classification import OneClassSVMLearner, EllipticEnvelopeLearner -from Orange.data import Table, Domain, ContinuousVariable -from Orange.widgets import widget, gui +from AnyQt.QtCore import Signal, Qt +from AnyQt.QtWidgets import QWidget, QVBoxLayout + +from orangewidget.settings import SettingProvider + +from Orange.base import Model +from Orange.classification import OneClassSVMLearner, EllipticEnvelopeLearner,\ + LocalOutlierFactorLearner, IsolationForestLearner +from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable +from Orange.data.util import get_unique_names +from Orange.widgets import gui from Orange.widgets.settings import Setting -from Orange.widgets.utils.widgetpreview import WidgetPreview -from Orange.widgets.widget import Msg, Input, Output from Orange.widgets.utils.sql import check_sql_input +from Orange.widgets.utils.widgetpreview import WidgetPreview +from Orange.widgets.widget import Msg, Input, Output, OWWidget + + +class ParametersEditor(QWidget, gui.OWComponent): + param_changed = Signal() + + def __init__(self, parent): + QWidget.__init__(self, parent) + gui.OWComponent.__init__(self, parent) + + self.setMinimumWidth(300) + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + self.param_box = gui.vBox(self, spacing=0) + + def parameter_changed(self): + self.param_changed.emit() + + def get_parameters(self) -> Dict: + raise NotImplementedError + + +class SVMEditor(ParametersEditor): + nu = Setting(50) + gamma = Setting(0.01) + + def __init__(self, parent): + super().__init__(parent) + + tooltip = "An upper bound on the fraction of training errors and a " \ + "lower bound of the fraction of support vectors" + gui.widgetLabel(self.param_box, "Nu:", tooltip=tooltip) + gui.hSlider(self.param_box, self, "nu", minValue=1, maxValue=100, + ticks=10, labelFormat="%d %%", tooltip=tooltip, + callback=self.parameter_changed) + gui.doubleSpin(self.param_box, self, "gamma", + label="Kernel coefficient:", step=1e-2, minv=0.01, + maxv=10, callback=self.parameter_changed) + + def get_parameters(self): + return {"nu": self.nu / 100, + "gamma": self.gamma} + + +class CovarianceEditor(ParametersEditor): + cont = Setting(10) + empirical_covariance = Setting(False) + support_fraction = Setting(1) + + def __init__(self, parent): + super().__init__(parent) + + gui.widgetLabel(self.param_box, "Contamination:") + gui.hSlider(self.param_box, self, "cont", minValue=0, + maxValue=100, ticks=10, labelFormat="%d %%", + callback=self.parameter_changed) + + ebox = gui.hBox(self.param_box) + gui.checkBox(ebox, self, "empirical_covariance", + "Support fraction:", callback=self.parameter_changed) + gui.doubleSpin(ebox, self, "support_fraction", step=1e-1, + minv=0.1, maxv=10, callback=self.parameter_changed) + + def get_parameters(self): + fraction = self.support_fraction if self.empirical_covariance else None + return {"contamination": self.cont / 100, + "support_fraction": fraction} + + +class LocalOutlierFactorEditor(ParametersEditor): + METRICS = ("euclidean", "manhattan", "cosine", "jaccard", + "hamming", "minkowski") + + n_neighbors = Setting(20) + cont = Setting(10) + metric_index = Setting(0) + + def __init__(self, parent): + super().__init__(parent) + + gui.widgetLabel(self.param_box, "Contamination:") + gui.hSlider(self.param_box, self, "cont", minValue=1, + maxValue=50, ticks=5, labelFormat="%d %%", + callback=self.parameter_changed) + gui.spin(self.param_box, self, "n_neighbors", label="Neighbors:", + minv=1, maxv=100000, callback=self.parameter_changed) + gui.comboBox(self.param_box, self, "metric_index", label="Metric:", + orientation=Qt.Horizontal, + items=[m.capitalize() for m in self.METRICS], + callback=self.parameter_changed) + + def get_parameters(self): + return {"n_neighbors": self.n_neighbors, + "contamination": self.cont / 100, + "algorithm": "brute", # works faster for big datasets + "metric": self.METRICS[self.metric_index]} + + +class IsolationForestEditor(ParametersEditor): + cont = Setting(10) + replicable = Setting(False) + + def __init__(self, parent): + super().__init__(parent) + gui.widgetLabel(self.param_box, "Contamination:") + gui.hSlider(self.param_box, self, "cont", minValue=0, + maxValue=100, ticks=10, labelFormat="%d %%", + callback=self.parameter_changed) + gui.checkBox(self.param_box, self, "replicable", + "Replicable training", callback=self.parameter_changed) -class OWOutliers(widget.OWWidget): + def get_parameters(self): + return {"contamination": self.cont / 100, + "random_state": 42 if self.replicable else None} + + +class OWOutliers(OWWidget): name = "Outliers" description = "Detect outliers." icon = "icons/Outliers.svg" @@ -25,170 +148,140 @@ class Inputs: class Outputs: inliers = Output("Inliers", Table) outliers = Output("Outliers", Table) + data = Output("Data", Table) want_main_area = False + resizing_enabled = False - OneClassSVM, Covariance = range(2) + OneClassSVM, Covariance, LOF, IsolationForest = range(4) + METHODS = (OneClassSVMLearner, EllipticEnvelopeLearner, + LocalOutlierFactorLearner, IsolationForestLearner) + svm_editor = SettingProvider(SVMEditor) + cov_editor = SettingProvider(CovarianceEditor) + lof_editor = SettingProvider(LocalOutlierFactorEditor) + isf_editor = SettingProvider(IsolationForestEditor) - outlier_method = Setting(OneClassSVM) - nu = Setting(50) - gamma = Setting(0.01) - cont = Setting(10) - empirical_covariance = Setting(False) - support_fraction = Setting(1) + settings_version = 2 + outlier_method = Setting(LOF) + auto_commit = Setting(True) - data_info_default = 'No data on input.' - in_out_info_default = ' ' + MAX_FEATURES = 1500 - class Error(widget.OWWidget.Error): + class Warning(OWWidget.Warning): + disabled_cov = Msg("Too many features for covariance estimation.") + + class Error(OWWidget.Error): singular_cov = Msg("Singular covariance matrix.") memory_error = Msg("Not enough memory") def __init__(self): super().__init__() - self.data = None - self.n_inliers = self.n_outliers = None - - box = gui.vBox(self.controlArea, "Information") - self.data_info_label = gui.widgetLabel(box, self.data_info_default) - self.in_out_info_label = gui.widgetLabel(box, - self.in_out_info_default) - - box = gui.vBox(self.controlArea, "Outlier Detection Method") - detection = gui.radioButtons(box, self, "outlier_method") + self.data = None # type: Table + self.n_inliers = None # type: int + self.n_outliers = None # type: int + self.editors = None # type: Tuple[ParametersEditor] + self.current_editor = None # type: ParametersEditor + self.method_combo = None # type: QComboBox + self.init_gui() + + def init_gui(self): + box = gui.vBox(self.controlArea, "Method") + self.method_combo = gui.comboBox(box, self, "outlier_method", + items=[m.name for m in self.METHODS], + callback=self.__method_changed) + + self._init_editors() + + gui.auto_send(self.controlArea, self, "auto_commit") + + self.info.set_input_summary(self.info.NoInput) + self.info.set_output_summary(self.info.NoOutput) + + def _init_editors(self): + self.svm_editor = SVMEditor(self) + self.cov_editor = CovarianceEditor(self) + self.lof_editor = LocalOutlierFactorEditor(self) + self.isf_editor = IsolationForestEditor(self) + + box = gui.vBox(self.controlArea, "Parameters") + self.editors = (self.svm_editor, self.cov_editor, + self.lof_editor, self.isf_editor) + for editor in self.editors: + editor.param_changed.connect(lambda: self.commit()) + box.layout().addWidget(editor) + editor.hide() + + self.set_current_editor() + + def __method_changed(self): + self.set_current_editor() + self.commit() - gui.appendRadioButton(detection, - "One class SVM with non-linear kernel (RBF)") - ibox = gui.indentedBox(detection) - tooltip = "An upper bound on the fraction of training errors and a " \ - "lower bound of the fraction of support vectors" - gui.widgetLabel(ibox, 'Nu:', tooltip=tooltip) - self.nu_slider = gui.hSlider( - ibox, self, "nu", minValue=1, maxValue=100, ticks=10, - labelFormat="%d %%", callback=self.nu_changed, tooltip=tooltip) - self.gamma_spin = gui.spin( - ibox, self, "gamma", label="Kernel coefficient:", step=1e-2, - spinType=float, minv=0.01, maxv=10, callback=self.gamma_changed) - gui.separator(detection, 12) - - self.rb_cov = gui.appendRadioButton(detection, "Covariance estimator") - ibox = gui.indentedBox(detection) - self.l_cov = gui.widgetLabel(ibox, 'Contamination:') - self.cont_slider = gui.hSlider( - ibox, self, "cont", minValue=0, maxValue=100, ticks=10, - labelFormat="%d %%", callback=self.cont_changed) - - ebox = gui.hBox(ibox) - self.cb_emp_cov = gui.checkBox( - ebox, self, "empirical_covariance", - "Support fraction:", callback=self.empirical_changed) - self.support_fraction_spin = gui.spin( - ebox, self, "support_fraction", step=1e-1, spinType=float, - minv=0.1, maxv=10, callback=self.support_fraction_changed) - - gui.separator(detection, 12) - - gui.button(self.buttonsArea, self, "Detect Outliers", - callback=self.commit) - self.layout().setSizeConstraint(QLayout.SetFixedSize) - - def nu_changed(self): - self.outlier_method = self.OneClassSVM - - def gamma_changed(self): - self.outlier_method = self.OneClassSVM - - def cont_changed(self): - self.outlier_method = self.Covariance - - def support_fraction_changed(self): - self.outlier_method = self.Covariance - - def empirical_changed(self): - self.outlier_method = self.Covariance - - def disable_covariance(self): - self.outlier_method = self.OneClassSVM - self.rb_cov.setDisabled(True) - self.l_cov.setDisabled(True) - self.cont_slider.setDisabled(True) - self.cb_emp_cov.setDisabled(True) - self.support_fraction_spin.setDisabled(True) - self.warning('Too many features for covariance estimation.') - - def enable_covariance(self): - self.rb_cov.setDisabled(False) - self.l_cov.setDisabled(False) - self.cont_slider.setDisabled(False) - self.cb_emp_cov.setDisabled(False) - self.support_fraction_spin.setDisabled(False) - self.warning() + def set_current_editor(self): + if self.current_editor: + self.current_editor.hide() + self.current_editor = self.editors[self.outlier_method] + self.current_editor.show() @Inputs.data @check_sql_input - def set_data(self, dataset): - self.data = dataset - if self.data is None: - self.data_info_label.setText(self.data_info_default) - self.in_out_info_label.setText(self.in_out_info_default) - else: - self.data_info_label.setText('%d instances' % len(self.data)) - self.in_out_info_label.setText(' ') - - self.enable_covariance() - if self.data and len(self.data.domain.attributes) > 1500: - self.disable_covariance() - - self.commit() - - def _get_outliers(self): + def set_data(self, data): + self.clear_messages() + self.data = data + self.info.set_input_summary(len(data) if data else self.info.NoOutput) + self.enable_controls() + self.unconditional_commit() + + def enable_controls(self): + self.method_combo.model().item(self.Covariance).setEnabled(True) + if self.data and len(self.data.domain.attributes) > self.MAX_FEATURES: + self.outlier_method = self.LOF + self.set_current_editor() + self.method_combo.model().item(self.Covariance).setEnabled(False) + self.Warning.disabled_cov() + + def _get_outliers(self) -> Tuple[Table, Table, Table]: + self.Error.singular_cov.clear() + self.Error.memory_error.clear() try: y_pred, amended_data = self.detect_outliers() except ValueError: self.Error.singular_cov() - self.in_out_info_label.setText(self.in_out_info_default) - return None, None + return None, None, None except MemoryError: self.Error.memory_error() - return None, None + return None, None, None else: inliers_ind = np.where(y_pred == 1)[0] outliers_ind = np.where(y_pred == -1)[0] inliers = amended_data[inliers_ind] outliers = amended_data[outliers_ind] - self.in_out_info_label.setText( - f"{len(inliers)} inliers, {len(outliers)} outliers") self.n_inliers = len(inliers) self.n_outliers = len(outliers) - - return inliers, outliers + return inliers, outliers, self.annotated_data(amended_data, y_pred) def commit(self): - self.clear_messages() - inliers = outliers = None + inliers = outliers = data = None self.n_inliers = self.n_outliers = None if self.data: - inliers, outliers = self._get_outliers() + inliers, outliers, data = self._get_outliers() + summary = len(inliers) if inliers else self.info.NoOutput + self.info.set_output_summary(summary) self.Outputs.inliers.send(inliers) self.Outputs.outliers.send(outliers) + self.Outputs.data.send(data) - def detect_outliers(self): - if self.outlier_method == self.OneClassSVM: - learner = OneClassSVMLearner( - gamma=self.gamma, nu=self.nu / 100, - preprocessors=SklLearner.preprocessors) - else: - learner = EllipticEnvelopeLearner( - support_fraction=self.support_fraction - if self.empirical_covariance else None, - contamination=self.cont / 100.) + def detect_outliers(self) -> Tuple[np.ndarray, Table]: + learner_class = self.METHODS[self.outlier_method] + kwargs = self.current_editor.get_parameters() + learner = learner_class(**kwargs) model = learner(self.data) y_pred = model(self.data) amended_data = self.amended_data(model) return np.array(y_pred), amended_data - def amended_data(self, model): + def amended_data(self, model: Model) -> Table: if self.outlier_method != self.Covariance: return self.data mahal = model.mahalanobis(self.data.X) @@ -202,6 +295,21 @@ def amended_data(self, model): amended_data.metas = np.hstack((self.data.metas, mahal)) return amended_data + @staticmethod + def annotated_data(data: Table, labels: np.ndarray) -> Table: + domain = data.domain + names = [v.name for v in domain.variables + domain.metas] + name = get_unique_names(names, "Outlier") + + outlier_var = DiscreteVariable(name, values=["Yes", "No"]) + metas = domain.metas + (outlier_var,) + domain = Domain(domain.attributes, domain.class_vars, metas) + data = data.transform(domain) + + labels[labels == -1] = 0 + data.metas[:, -1] = labels + return data + def send_report(self): if self.n_outliers is None or self.n_inliers is None: return @@ -209,19 +317,45 @@ def send_report(self): (("Input instances", len(self.data)), ("Inliers", self.n_inliers), ("Outliers", self.n_outliers))) - if self.outlier_method == 0: + + params = self.current_editor.get_parameters() + if self.outlier_method == self.OneClassSVM: self.report_items( "Detection", (("Detection method", "One class SVM with non-linear kernel (RBF)"), - ("Regularization (nu)", self.nu), - ("Kernel coefficient", self.gamma))) - else: + ("Regularization (nu)", params["nu"]), + ("Kernel coefficient", params["gamma"]))) + elif self.outlier_method == self.Covariance: self.report_items( "Detection", (("Detection method", "Covariance estimator"), - ("Contamination", self.cont), - ("Support fraction", self.support_fraction))) + ("Contamination", params["contamination"]), + ("Support fraction", params["support_fraction"]))) + elif self.outlier_method == self.LOF: + self.report_items( + "Detection", + (("Detection method", "Local Outlier Factor"), + ("Contamination", params["contamination"]), + ("Number of neighbors", params["n_neighbors"]), + ("Metric", params["metric"]))) + elif self.outlier_method == self.IsolationForest: + self.report_items( + "Detection", + (("Detection method", "Isolation Forest"), + ("Contamination", params["contamination"]))) + else: + raise NotImplementedError + + @classmethod + def migrate_settings(cls, settings: Dict, version: int): + if version is None or version < 2: + settings["svm_editor"] = {"nu": settings.get("nu", 50), + "gamma": settings.get("gamma", 0.01)} + ec, sf = "empirical_covariance", "support_fraction" + settings["cov_editor"] = {"cont": settings.get("cont", 10), + ec: settings.get(ec, False), + sf: settings.get(sf, 1)} if __name__ == "__main__": # pragma: no cover diff --git a/Orange/widgets/data/tests/test_owoutliers.py b/Orange/widgets/data/tests/test_owoutliers.py index 6bfcb2c58f0..fa06dd6a976 100644 --- a/Orange/widgets/data/tests/test_owoutliers.py +++ b/Orange/widgets/data/tests/test_owoutliers.py @@ -1,13 +1,14 @@ # Test methods with long descriptive names can omit docstrings -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, protected-access import unittest +from unittest.mock import patch, Mock import numpy as np from Orange.data import Table from Orange.widgets.data.owoutliers import OWOutliers -from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.tests.base import WidgetTest, simulate class TestOWOutliers(WidgetTest): @@ -19,11 +20,25 @@ def test_data(self): """Check widget's data and the output with data on the input""" self.send_signal(self.widget.Inputs.data, self.iris) self.assertEqual(self.widget.data, self.iris) - self.assertEqual(len(self.get_output(self.widget.Outputs.inliers)), 76) - self.assertEqual(len(self.get_output(self.widget.Outputs.outliers)), 74) + self.assertEqual(len(self.get_output(self.widget.Outputs.inliers)), 135) + self.assertEqual(len(self.get_output(self.widget.Outputs.outliers)), 15) + self.assertEqual(len(self.get_output(self.widget.Outputs.data)), 150) self.send_signal(self.widget.Inputs.data, None) self.assertEqual(self.widget.data, None) self.assertIsNone(self.get_output(self.widget.Outputs.inliers)) + self.assertIsNone(self.get_output(self.widget.Outputs.outliers)) + self.assertIsNone(self.get_output(self.widget.Outputs.data)) + + def test_methods(self): + def callback(): + self.widget.send_report() + self.assertIsNotNone(self.get_output(self.widget.Outputs.inliers)) + self.assertIsNotNone(self.get_output(self.widget.Outputs.outliers)) + self.assertIsNotNone(self.get_output(self.widget.Outputs.data)) + + self.send_signal(self.widget.Inputs.data, self.iris) + simulate.combobox_run_through_all(self.widget.method_combo, + callback=callback) def test_memory_error(self): """ @@ -46,6 +61,53 @@ def test_nans(self): self.send_signal(self.widget.Inputs.data, data) self.assertIsNot(self.get_output(self.widget.Outputs.inliers), None) + def test_in_out_summary(self): + info = self.widget.info + self.assertEqual(info._StateInfo__input_summary.brief, "") + self.assertEqual(info._StateInfo__output_summary.brief, "") + + self.send_signal(self.widget.Inputs.data, self.iris) + self.assertEqual(info._StateInfo__input_summary.brief, "150") + self.assertEqual(info._StateInfo__output_summary.brief, "135") + + self.send_signal(self.widget.Inputs.data, None) + self.assertEqual(info._StateInfo__input_summary.brief, "") + self.assertEqual(info._StateInfo__output_summary.brief, "") + + @patch("Orange.widgets.data.owoutliers.OWOutliers.MAX_FEATURES", 3) + @patch("Orange.widgets.data.owoutliers.OWOutliers.commit", Mock()) + def test_covariance_enabled(self): + cov_item = self.widget.method_combo.model().item(self.widget.Covariance) + self.send_signal(self.widget.Inputs.data, self.iris) + self.assertTrue(self.widget.Warning.disabled_cov.is_shown()) + self.assertFalse(cov_item.isEnabled()) + + self.send_signal(self.widget.Inputs.data, self.iris[:, :2]) + self.assertFalse(self.widget.Warning.disabled_cov.is_shown()) + self.assertTrue(cov_item.isEnabled()) + + self.send_signal(self.widget.Inputs.data, self.iris) + self.assertTrue(self.widget.Warning.disabled_cov.is_shown()) + self.assertFalse(cov_item.isEnabled()) + + self.send_signal(self.widget.Inputs.data, None) + self.assertFalse(self.widget.Warning.disabled_cov.is_shown()) + self.assertTrue(cov_item.isEnabled()) + + def test_migrate_settings(self): + settings = {"cont": 20, "empirical_covariance": True, + "gamma": 0.04, "nu": 30, "outlier_method": 0, + "support_fraction": 0.5, "__version__": 1} + + widget = self.create_widget(OWOutliers, stored_settings=settings) + self.send_signal(widget.Inputs.data, self.iris) + self.assertEqual(widget.svm_editor.nu, 30) + self.assertEqual(widget.svm_editor.gamma, 0.04) + + self.assertEqual(widget.cov_editor.cont, 20) + self.assertEqual(widget.cov_editor.empirical_covariance, True) + self.assertEqual(widget.cov_editor.support_fraction, 0.5) + if __name__ == "__main__": unittest.main()