Repository: biolab/orange3

Issue:
Feature Constructor loses features when input changed
I have a FILE widget followed by a SELECT COLUMNS followed by a FEATURE CONSTRUCTOR

I have created 20 or so features in FEATURE CONSTRUCTOR..

When in the SELECT COLUMNS if I add an additional variable to the FEATURES list I lose all but 2 of my constructed features when I open the FEATURE CONSTRUCTOR widget.

**To Reproduce**
Steps to reproduce the behavior:
I am not yet sure if this is reproducible as I have a bit of work to reproduce.  I will probably create a separate flow to add more features.

**Orange version:**
Orange - About - 3.24.1

**Expected behavior**
To not lose any features in FEATURE CONSTRUCTOR when it's input is changed.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Operating system:**
Windows, OSX or Linux? Which version? Windows 10

**Additional context**
Add any other context about the problem here.




Discussion:
Do you by any chance have this schema saved? Can you upload an .ows file?

I have a hypothesis but it's difficult to check.

[train.zip](https://github.com/biolab/orange3/files/4196138/train.zip)

The error occurred when I add "YrSold" to the features in "Select Column - Scale".  
 Hi,
I updated the github issue.

Let me know if you need anything else.
Thanks,
Mark

    On Wednesday, February 12, 2020, 4:18:14 PM EST, Janez Demšar <notifications@github.com> wrote:  
 
 
Do you by any chance have this schema saved? Can you upload an .ows file?

I have a hypothesis but it's difficult to check.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub, or unsubscribe.
  
I can't replicate this based on your instructions. The FeatConst stays the same.
I would need an .ows more than the data.

Say that a widget receives some data, and you change the widget's settings somehow, e.g. select x and y. 

- Next the widget gets an empty input (perhaps the input comes from some selection in another widget). It has to reset to defaults (that is, empty).
- Next it again receives some new data sample, but apparently from the same data because it still has the same attributes x, y and color that were used previously. Of course you would expect the widget to reset to its previous settings. And it does.
- Now let this widget get some completely different data. It has to reset to some defaults.
- Now let this widget again receive the same data as before. It would be nice if it remembered the settings from the history. It does.
- Now let this widget receive similar data, but with some variables missing. Maybe you removed them in Select Columns. It can (and will) retain the settings if the attributes used for x and y are present. For the sake of example, let us say that you also change something else, for instance the color attribute.

At this point, the widget remembers three different sets of settings: one is for the initial data, one is for the completely different second data and the last one is for the last data which was compatible, but different from the first one.

Finally, the widget receives some fourth data set, which is "compatible" with the first and the third. Which settings should it retrieve? I would say that those which are the most similar to the data. Moreover, if one of the saved settings has exactly the same attributes, it takes if be default.

I believe that in your case Orange works as intended, but this behaviour sometimes seems incorrect. You probably first sent the entire data to feature constructor. You constructed no features. Next you removed some attributes. You constructed some features. When you add attributes, the data becomes more similar to the initial data, which contained no features.

We might be able to see this from .ows (but not necessarily). Or my hypothesis may be wrong altogether.

We have encountered this in the past, I believe it was related to Select Rows. Behaviour of Orange could be changed so that it grabs **the most recent** compatible context, ignoring how similar it is. I however fear this would look wrong in other scenarious.
 Hi,
I thought I attached a zip file with a data file (train.csv) and an .ows file?
One other note, after I update the 'select columns' 2 of the 20+ features do remain in the feature constructor.  Not exactly sure why these 2 remain.
it is probable that at one point I did have all of the file attributes passing thru the select columns and into the feature constructor.
I just checked github and I do see 2 files in the .zip one is the .ows.
Thanks,
Mark



    On Thursday, February 13, 2020, 4:18:38 AM EST, Janez Demšar <notifications@github.com> wrote:  
 
 
I would need an .ows more than the data.

Say that a widget receives some data, and you change the widget's settings somehow, e.g. select x and y.
   
   - Next the widget gets an empty input (perhaps the input comes from some selection in another widget). It has to reset to defaults (that is, empty).
   - Next it again receives some new data sample, but apparently from the same data because it still has the same attributes x, y and color that were used previously. Of course you would expect the widget to reset to its previous settings. And it does.
   - Now let this widget get some completely different data. It has to reset to some defaults.
   - Now let this widget again receive the same data as before. It would be nice if it remembered the settings from the history. It does.
   - Now let this widget receive similar data, but with some variables missing. Maybe you removed them in Select Columns. It can (and will) retain the settings if the attributes used for x and y are present. For the sake of example, let us say that you also change something else, for instance the color attribute.

At this point, the widget remembers three different sets of settings: one is for the initial data, one is for the completely different second data and the last one is for the last data which was compatible, but different from the first one.

Finally, the widget receives some fourth data set, which is "compatible" with the first and the third. Which settings should it retrieve? I would say that those which are the most similar to the data. Moreover, if one of the saved settings have exactly the same attributes, it takes if be default.

I believe that in your case Orange works as intended, but this behaviour sometimes seems incorrect. You probably first sent the entire data to feature constructor. You constructed no features. Next you removed some attributes. You constructed some features. When you add attributes, the data becomes more similar to the initial data, which contained no features.

We might be able to see this from .ows (but not necessarily). Or my hypothesis may be wrong altogether.

We have encountered this in the past, I believe it was related to Select Rows. Behaviour of Orange could be changed so that it grabs the most recent compatible context, ignoring how similar it is. I however fear this would look wrong in other scenarious.

—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub, or unsubscribe.
  
> I thought I attached a zip file with a data file (train.csv) and an .ows file?

You have. Sorry.

My hypothesis is unfortunatelly correct. If your data contains variables

```
{'1stFlrSF': 2, 'LotFrontage': 2, 'LotArea': 2, 'MasVnrArea': 2, 'BsmtFinSF1': 2, 'BsmtFinSF2': 2, 'BsmtUnfSF': 2, 'TotalBsmtSF': 2, '2ndFlrSF': 2, 'LowQualFinSF': 2, 'GrLivArea': 2, 'BsmtFullBath': 2, 'BsmtHalfBath': 2, 'FullBath': 2, 'HalfBath': 2, 'BedroomAbvGr': 2, 'KitchenAbvGr': 2, 'TotRmsAbvGrd': 2, 'Fireplaces': 2, 'GarageYrBlt': 2, 'GarageCars': 2, 'GarageArea': 2, 'WoodDeckSF': 2, 'OpenPorchSF': 2, 'EnclosedPorch': 2, '3SsnPorch': 2, 'ScreenPorch': 2, 'PoolArea': 2, 'MiscVal': 2, 'OverallQual': 2, 'OverallCond': 2, 'SalePrice': 2}
```

widget will contain the following expressions:

```
[ContinuousDescriptor(name='1stFlrSF_Scaled', expression='_1stFlrSF*10/4692', number_of_decimals=3), ContinuousDescriptor(name='2ndFlrSF_Scaled', expression='_2ndFlrSF*10/2065', number_of_decimals=3), ContinuousDescriptor(name='3SsnPorch_Scaled', expression='_3SsnPorch*10/508', number_of_decimals=3), ContinuousDescriptor(name='BedroomAbvGr_Scaled', expression='BedroomAbvGr*10/8', number_of_decimals=3), ContinuousDescriptor(name='BSMTFinSF1_Scalled', expression='BsmtFinSF1*10/5644', number_of_decimals=3), ContinuousDescriptor(name='BSMTFinSF2_Scalled', expression='BsmtFinSF2*10/1474', number_of_decimals=3), ContinuousDescriptor(name='BsmtFullBath_Scaled', expression='BsmtFullBath*10/3', number_of_decimals=3), ContinuousDescriptor(name='BsmtHalfBath_Scaled', expression='BsmtHalfBath*10/2', number_of_decimals=3), ContinuousDescriptor(name='BsmtUnfSF_Scaled', expression='BsmtUnfSF*10/2336', number_of_decimals=3), ContinuousDescriptor(name='EnclosedPorch_Scaled', expression='EnclosedPorch*10/552', number_of_decimals=3), ContinuousDescriptor(name='Fireplaces_Scaled', expression='Fireplaces*10/3', number_of_decimals=3), ContinuousDescriptor(name='FullBath_Scaled', expression='FullBath*10/3', number_of_decimals=3), ContinuousDescriptor(name='GarageArea_Scaled', expression='GarageArea*10/1418', number_of_decimals=3), ContinuousDescriptor(name='GarageCars_Scaled', expression='GarageCars*10/4', number_of_decimals=3), ContinuousDescriptor(name='GrLivArea_Scaled', expression='GrLivArea*10/5642', number_of_decimals=3), ContinuousDescriptor(name='HalfBath_Scaled', expression='HalfBath*10/2', number_of_decimals=3), ContinuousDescriptor(name='KitchenAbvGr_Scale', expression='KitchenAbvGr*10/3', number_of_decimals=3), ContinuousDescriptor(name='LotArea_Scaled', expression='LotArea*10/215245', number_of_decimals=3), ContinuousDescriptor(name='LotFrontage_Scaled', expression='LotFrontage*10/313', number_of_decimals=3), ContinuousDescriptor(name='LowQualFinSF_Scaled', expression='LowQualFinSF*10/572', number_of_decimals=3), ContinuousDescriptor(name='MasVnrArea_Scaled', expression='MasVnrArea*10/1600', number_of_decimals=3), ContinuousDescriptor(name='MiscVal_Scaled', expression='MiscVal*10/15500', number_of_decimals=3), ContinuousDescriptor(name='OpenPorchSF_Scaled', expression='OpenPorchSF*10/547', number_of_decimals=3), ContinuousDescriptor(name='OverallCond_Scaled', expression='OverallCond', number_of_decimals=3), ContinuousDescriptor(name='PoolArea_Scaled', expression='PoolArea*10/738', number_of_decimals=3), ContinuousDescriptor(name='ScreenPorch_Scaled', expression='ScreenPorch*10/480', number_of_decimals=3), ContinuousDescriptor(name='TotalBsmtSF_Scaled', expression='TotalBsmtSF*10/6110', number_of_decimals=3), ContinuousDescriptor(name='TotalRmsAbvGrd_Scaled', expression='TotRmsAbvGrd*10/14', number_of_decimals=3), ContinuousDescriptor(name='WoodDeckSF_Scaled', expression='WoodDeckSF*10/857', number_of_decimals=3)], '__version__': 1})
```

However, if you have

```
{'LotFrontage': 2, 'LotArea': 2, 'MasVnrArea': 2, 'BsmtFinSF1': 2, 'BsmtFinSF2': 2, 'BsmtUnfSF': 2, 'TotalBsmtSF': 2, '1stFlrSF': 2, '2ndFlrSF': 2, 'LowQualFinSF': 2, 'GrLivArea': 2, 'BsmtFullBath': 2, 'BsmtHalfBath': 2, 'FullBath': 2, 'HalfBath': 2, 'BedroomAbvGr': 2, 'KitchenAbvGr': 2, 'TotRmsAbvGrd': 2, 'Fireplaces': 2, 'GarageYrBlt': 2, 'GarageCars': 2, 'GarageArea': 2, 'WoodDeckSF': 2, 'OpenPorchSF': 2, 'EnclosedPorch': 2, '3SsnPorch': 2, 'ScreenPorch': 2, 'PoolArea': 2, 'MiscVal': 2, 'SalePrice': 2}
```

you have just

```
[ContinuousDescriptor(name='1stFlrSF_scaled', expression='_1stFlrSF*10/5095', number_of_decimals=3), ContinuousDescriptor(name='2ndFlrSF_Scaled', expression='_2ndFlrSF*10/2065', number_of_decimals=3)]
```
To replicate:
- File -> Select Columns -> Feature Constructor.
- In Feature Constructor, add expression X1 = 42.
- In Select Columns, remove the first attribute. **X1 = 42 remains in Feature constructor.**
- In Feature Constructor, add X2 = 42
- In Select Columns, add the first attribute. **X2 = 42 disappears.**

(Adding X1 = 42 is not needed, but it's good to check that it remains after removing an attribute.)

This is not strictly a bug -- Orange works as intended, but perhaps it could work differently. To be discussed.
Result of discussion: we decided to modify Orange so that it will use the latest compatible context.
 Thank you!.
Any way to avoid this, until the change?
I am a little concerned about losing extensive transformations if the input to the feature constructor changes.
I guess I could move all file attributes into the select columns from the start, and then hopefully not have to add any columns to the file.
Thanks,
Mark



    On Friday, February 14, 2020, 5:27:59 AM EST, Janez Demšar <notifications@github.com> wrote:  
 
 
Result of discussion: we decided to modify Orange so that it will use the latest compatible context.

—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub, or unsubscribe.
  
> I guess I could move all file attributes into the select columns from the start, and then hopefully not have to add any columns to the file.

Yes, this seems to be the only solution at the moment. If everything goes well, the fix will be included in next release of Orange.

Resolving patch:
diff --git a/Orange/widgets/data/owselectcolumns.py b/Orange/widgets/data/owselectcolumns.py
--- a/Orange/widgets/data/owselectcolumns.py
+++ b/Orange/widgets/data/owselectcolumns.py
@@ -119,7 +119,7 @@ class Outputs:
     want_main_area = False
     want_control_area = True
 
-    settingsHandler = SelectAttributesDomainContextHandler()
+    settingsHandler = SelectAttributesDomainContextHandler(first_match=False)
     domain_role_hints = ContextSetting({})
     use_input_features = Setting(False)
     auto_commit = Setting(True)
diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py
--- a/Orange/widgets/settings.py
+++ b/Orange/widgets/settings.py
@@ -66,9 +66,10 @@ class DomainContextHandler(ContextHandler):
 
     MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3)
 
-    def __init__(self, *, match_values=0, **kwargs):
+    def __init__(self, *, match_values=0, first_match=True, **kwargs):
         super().__init__()
         self.match_values = match_values
+        self.first_match = first_match
 
         for name in kwargs:
             warnings.warn(
@@ -81,7 +82,8 @@ def _warn_about_str_var_settings(setting):
         warnings.warn(
             "Storing variables as strings in settings is deprecated.\n"
             "Support for this will be dropped in Orange 3.26.\n"
-            f"Change {setting.name} to store an instance of `Variable`.")
+            f"Change {setting.name} to store an instance of `Variable`.",
+            stacklevel=9)
 
     def encode_domain(self, domain):
         """
@@ -208,9 +210,6 @@ def _var_exists(cls, setting, value, attributes, metas):
                 metas.get(attr_name, -1) == attr_type)
 
     def match(self, context, domain, attrs, metas):
-        if (attrs, metas) == (context.attributes, context.metas):
-            return self.PERFECT_MATCH
-
         matches = []
         try:
             for setting, data, _ in \
@@ -234,9 +233,11 @@ def match(self, context, domain, attrs, metas):
         except IncompatibleContext:
             return self.NO_MATCH
 
+        if self.first_match:
+            return 1  # Change to self.MATCH after releasing orange-widget-base
+
         matches.append((0, 0))
         matched, available = [sum(m) for m in zip(*matches)]
-
         return matched / available if available else 0.1
 
     def match_list(self, setting, value, context, attrs, metas):
diff --git a/Orange/widgets/unsupervised/owlouvainclustering.py b/Orange/widgets/unsupervised/owlouvainclustering.py
--- a/Orange/widgets/unsupervised/owlouvainclustering.py
+++ b/Orange/widgets/unsupervised/owlouvainclustering.py
@@ -21,8 +21,7 @@
 from Orange import preprocess
 from Orange.projection import PCA
 from Orange.widgets import widget, gui, report
-from Orange.widgets.settings import DomainContextHandler, ContextSetting, \
-    Setting
+from Orange.widgets.settings import Setting
 from Orange.widgets.utils.annotated_data import add_columns, \
     ANNOTATED_DATA_SIGNAL_NAME
 from Orange.widgets.utils.concurrent import FutureWatcher
@@ -51,11 +50,11 @@ class OWLouvainClustering(widget.OWWidget):
     icon = "icons/LouvainClustering.svg"
     priority = 2110
 
+    settings_version = 2
+
     want_main_area = False
     resizing_enabled = False
 
-    settingsHandler = DomainContextHandler()
-
     class Inputs:
         data = Input("Data", Table, default=True)
 
@@ -64,12 +63,12 @@ class Outputs:
         if Network is not None:
             graph = Output("Network", Network)
 
-    apply_pca = ContextSetting(True)
-    pca_components = ContextSetting(_DEFAULT_PCA_COMPONENTS)
-    normalize = ContextSetting(True)
-    metric_idx = ContextSetting(0)
-    k_neighbors = ContextSetting(_DEFAULT_K_NEIGHBORS)
-    resolution = ContextSetting(1.)
+    apply_pca = Setting(True)
+    pca_components = Setting(_DEFAULT_PCA_COMPONENTS)
+    normalize = Setting(True)
+    metric_idx = Setting(0)
+    k_neighbors = Setting(_DEFAULT_K_NEIGHBORS)
+    resolution = Setting(1.)
     auto_commit = Setting(False)
 
     class Information(widget.OWWidget.Information):
@@ -400,11 +399,9 @@ def _send_data(self):
 
     @Inputs.data
     def set_data(self, data):
-        self.closeContext()
         self.Error.clear()
 
         prev_data, self.data = self.data, data
-        self.openContext(self.data)
         # Make sure to properly enable/disable slider based on `apply_pca` setting
         self.controls.pca_components.setEnabled(self.apply_pca)
 
@@ -471,6 +468,20 @@ def send_report(self):
             ("Resolution", self.resolution),
         ))
 
+    @classmethod
+    def migrate_settings(cls, settings, version):
+        if version < 2 and "context_settings" in settings:
+            try:
+                current_context = settings["context_settings"][0]
+                for n in ['apply_pca', 'k_neighbors', 'metric_idx',
+                          'normalize', 'pca_components', 'resolution']:
+                    if n in current_context.values:
+                        settings[n] = current_context.values[n][0]
+            except:  # pylint: disable=bare-except
+                pass
+            finally:
+                del settings["context_settings"]
+
 
 class TaskState(QObject):
 


Test patch:
diff --git a/Orange/widgets/data/tests/test_owselectcolumns.py b/Orange/widgets/data/tests/test_owselectcolumns.py
--- a/Orange/widgets/data/tests/test_owselectcolumns.py
+++ b/Orange/widgets/data/tests/test_owselectcolumns.py
@@ -36,7 +36,7 @@ def setUp(self):
                       'd2': Discrete, 'd3': Discrete},
                      {'c2': Continuous, 'd4': Discrete, })
 
-        self.handler = SelectAttributesDomainContextHandler()
+        self.handler = SelectAttributesDomainContextHandler(first_match=False)
         self.handler.read_defaults = lambda: None
 
     def test_open_context(self):
@@ -67,6 +67,10 @@ def test_open_context(self):
 
     def test_open_context_with_imperfect_match(self):
         self.handler.bind(SimpleWidget)
+        context1 = Mock(values=dict(
+            domain_role_hints=({('d1', Discrete): ('attribute', 0),
+                                ('m2', Discrete): ('meta', 0)})
+        ))
         context = Mock(values=dict(
             domain_role_hints=({('d1', Discrete): ('available', 0),
                                 ('d2', Discrete): ('meta', 0),
@@ -76,7 +80,7 @@ def test_open_context_with_imperfect_match(self):
                                 ('c2', Continuous): ('class', 0)}, -2)
         ))
         self.handler.global_contexts = \
-            [Mock(values={}), context, Mock(values={})]
+            [Mock(values={}), context1, context, Mock(values={})]
 
         widget = SimpleWidget()
         self.handler.initialize(widget)
diff --git a/Orange/widgets/tests/test_domain_context_handler.py b/Orange/widgets/tests/test_domain_context_handler.py
--- a/Orange/widgets/tests/test_domain_context_handler.py
+++ b/Orange/widgets/tests/test_domain_context_handler.py
@@ -68,11 +68,6 @@ def test_encode_domain_with_match_all(self):
         self.assertEqual(encoded_metas,
                          {'c2': Continuous - 100, 'd4': list('jkl')})
 
-    def test_match_returns_2_on_perfect_match(self):
-        context = Mock(
-            attributes=self.args[1], metas=self.args[2], values={})
-        self.assertEqual(2., self.handler.match(context, *self.args))
-
     def test_match_returns_1_if_everything_matches(self):
         self.handler.bind(SimpleWidget)
 
@@ -100,11 +95,11 @@ def test_match_returns_1_if_everything_matches(self):
         ))
         self.assertEqual(1., self.handler.match(context, *self.args))
 
-    def test_match_returns_point_1_when_nothing_to_match(self):
+    def test_match_when_nothing_to_match(self):
         self.handler.bind(SimpleWidget)
 
         context = Mock(values={})
-        self.assertEqual(0.1, self.handler.match(context, *self.args))
+        self.assertEqual(1, self.handler.match(context, *self.args))
 
     def test_match_returns_zero_on_incompatible_context(self):
         self.handler.bind(SimpleWidget)
@@ -137,7 +132,7 @@ def test_open_context(self):
             with_metas=[('d1', Discrete), ('d2', Discrete)]
         ))
         self.handler.global_contexts = \
-            [Mock(values={}), context, Mock(values={})]
+            [context, Mock(values={})]
 
         widget = SimpleWidget()
         self.handler.initialize(widget)
@@ -154,6 +149,28 @@ def test_open_context(self):
                                              ('d2', Discrete)])
 
     def test_open_context_with_imperfect_match(self):
+        self.handler.bind(SimpleWidget)
+        context = self.create_context(None, dict(
+            text=('u', -2),
+            with_metas=[('d1', Discrete), ('d1', Continuous),
+                        ('c1', Continuous), ('c1', Discrete)]
+        ))
+        self.handler.global_contexts = \
+            [context, Mock(values={})]
+
+        widget = SimpleWidget()
+        self.handler.initialize(widget)
+        self.handler.open_context(widget, self.args[0])
+
+        context = widget.current_context
+        self.assertEqual(context.attributes, self.args[1])
+        self.assertEqual(context.metas, self.args[2])
+
+        self.assertEqual(widget.text, 'u')
+        self.assertEqual(widget.with_metas, [('d1', Discrete),
+                                             ('c1', Continuous)])
+
+    def test_open_context_not_first_match(self):
         self.handler.bind(SimpleWidget)
         context = self.create_context(None, dict(
             text=('u', -2),
@@ -162,6 +179,7 @@ def test_open_context_with_imperfect_match(self):
         ))
         self.handler.global_contexts = \
             [Mock(values={}), context, Mock(values={})]
+        self.handler.first_match = False
 
         widget = SimpleWidget()
         self.handler.initialize(widget)
diff --git a/Orange/widgets/unsupervised/tests/test_owlouvain.py b/Orange/widgets/unsupervised/tests/test_owlouvain.py
--- a/Orange/widgets/unsupervised/tests/test_owlouvain.py
+++ b/Orange/widgets/unsupervised/tests/test_owlouvain.py
@@ -1,13 +1,15 @@
 from unittest.mock import patch
 
 import numpy as np
+from sklearn.utils import check_random_state
+
+from orangewidget.settings import Context
 
 from Orange.data import Table, Domain, ContinuousVariable
 from Orange.preprocess import Normalize
 from Orange.widgets.tests.base import WidgetTest
-from Orange.widgets.tests.utils import table_dense_sparse, simulate
+from Orange.widgets.tests.utils import table_dense_sparse
 from Orange.widgets.unsupervised.owlouvainclustering import OWLouvainClustering
-from sklearn.utils import check_random_state
 
 # Deterministic tests
 np.random.seed(42)
@@ -235,37 +237,6 @@ def _compute_clustering(data):
         sparse_result = _compute_clustering(sparse_data)
         np.testing.assert_equal(dense_result.metas, sparse_result.metas)
 
-    def test_settings_correctly_restored(self):
-        """
-        This test checks if contextsettings are correctly restored after
-        dataset changed.
-        """
-        w = self.widget
-
-        self.send_signal(w.Inputs.data, self.iris)
-        w.controls.apply_pca.setChecked(False)
-        w.controls.pca_components.setValue(2)
-        simulate.combobox_activate_item(w.controls.metric_idx, "Manhattan")
-        w.controls.normalize.setChecked(False)
-        w.controls.k_neighbors.setValue(4)
-        w.controls.resolution.setValue(0.5)
-
-        self.send_signal(w.Inputs.data, Table("zoo"))
-        w.controls.apply_pca.setChecked(True)
-        w.controls.pca_components.setValue(3)
-        simulate.combobox_activate_item(w.controls.metric_idx, "Euclidean")
-        w.controls.normalize.setChecked(True)
-        w.controls.k_neighbors.setValue(5)
-        w.controls.resolution.setValue(1)
-
-        self.send_signal(w.Inputs.data, self.iris)
-        self.assertFalse(w.apply_pca)
-        self.assertEqual(2, w.pca_components)
-        self.assertEqual(1, w.metric_idx)
-        self.assertFalse(w.normalize)
-        self.assertEqual(4, w.k_neighbors)
-        self.assertEqual(0.5, w.resolution)
-
     def test_graph_output(self):
         w = self.widget
 
@@ -280,3 +251,22 @@ def test_graph_output(self):
         self.send_signal(w.Inputs.data, None)
         graph = self.get_output(w.Outputs.graph)
         self.assertIsNone(graph)
+
+    def test_migrate_settings(self):
+        # any context settings are removed
+        settings = {"context_settings": []}
+        self.widget.migrate_settings(settings, 1)
+        self.assertEqual(len(settings), 0)
+
+        # context settings become ordinary settings
+        settings = {"context_settings": [Context(values={'__version__': 1,
+                                                         'apply_pca': (True, -2),
+                                                         'k_neighbors': (29, -2),
+                                                         'metric_idx': (1, -2),
+                                                         'normalize': (False, -2),
+                                                         'pca_components': (10, -2),
+                                                         'resolution': (1.0, -2)})]}
+        self.widget.migrate_settings(settings, 1)
+        correct = {'apply_pca': True, 'k_neighbors': 29, 'metric_idx': 1,
+                   'normalize': False, 'pca_components': 10, 'resolution': 1.0}
+        self.assertEqual(sorted(settings.items()), sorted(correct.items()))
diff --git a/Orange/widgets/unsupervised/tests/test_owmds.py b/Orange/widgets/unsupervised/tests/test_owmds.py
--- a/Orange/widgets/unsupervised/tests/test_owmds.py
+++ b/Orange/widgets/unsupervised/tests/test_owmds.py
@@ -157,7 +157,7 @@ def test_migrate_settings_from_version_1(self):
                     time=1500000000,
                     values={'__version__': 1,
                             'color_value': ('iris', 1),
-                            'shape_value': ('iris', 2),
+                            'shape_value': ('iris', 1),
                             'size_value': ('Stress', -2),
                             'label_value': ('sepal length', 2)})]
         settings = {
diff --git a/Orange/widgets/visualize/tests/test_owlinearprojection.py b/Orange/widgets/visualize/tests/test_owlinearprojection.py
--- a/Orange/widgets/visualize/tests/test_owlinearprojection.py
+++ b/Orange/widgets/visualize/tests/test_owlinearprojection.py
@@ -178,7 +178,7 @@ def test_invalidated_model_selected(self):
 
         self.widget.setup_plot.reset_mock()
         self.send_signal(self.widget.Inputs.data, self.data)
-        self.widget.setup_plot.assert_called_once()
+        self.widget.setup_plot.assert_not_called()
 
     def test_two_classes_dataset(self):
         self.widget.radio_placement.buttons[1].click()
diff --git a/Orange/widgets/visualize/tests/test_owradviz.py b/Orange/widgets/visualize/tests/test_owradviz.py
--- a/Orange/widgets/visualize/tests/test_owradviz.py
+++ b/Orange/widgets/visualize/tests/test_owradviz.py
@@ -128,4 +128,4 @@ def test_invalidated_model_selected(self):
 
         self.widget.setup_plot.reset_mock()
         self.send_signal(self.widget.Inputs.data, self.data)
-        self.widget.setup_plot.assert_called_once()
+        self.widget.setup_plot.assert_not_called()