Repository: biolab/orange3

Issue:
Transpose cannot be cancelled
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


- [ ] **What's wrong?**
<!-- Be specific, clear, and concise. Include screenshots if relevant. -->
<!-- If you're getting an error message, copy it, and enclose it with three backticks (```). -->
For large data sets, Transpose gets slow. It is impossible to cancel the computation (remove the widget).




- [ ] **How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
<!-- Describe the steps (open this widget, click there, then add this...) -->
Use a large data set (adult?) and transpose it.




- [ ] **What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: OSX
- Orange version: 3.27.1
- How you installed Orange: installer



Discussion:

Resolving patch:
diff --git a/Orange/data/table.py b/Orange/data/table.py
--- a/Orange/data/table.py
+++ b/Orange/data/table.py
@@ -17,8 +17,6 @@
 from scipy.sparse import issparse, csc_matrix
 
 import Orange.data  # import for io.py
-from Orange.misc.collections import frozendict
-from Orange.util import OrangeDeprecationWarning
 from Orange.data import (
     _contingency, _valuecount,
     Domain, Variable, Storage, StringVariable, Unknown, Value, Instance,
@@ -27,9 +25,11 @@
 from Orange.data.util import SharedComputeValue, \
     assure_array_dense, assure_array_sparse, \
     assure_column_dense, assure_column_sparse, get_unique_names_duplicates
+from Orange.misc.collections import frozendict
 from Orange.statistics.util import bincount, countnans, contingency, \
     stats as fast_stats, sparse_has_implicit_zeros, sparse_count_implicit_zeros, \
     sparse_implicit_zero_weights
+from Orange.util import OrangeDeprecationWarning, dummy_callback
 
 __all__ = ["dataset_dirs", "get_sample_datasets_dir", "RowInstance", "Table"]
 
@@ -1704,7 +1704,8 @@ def _compute_contingency(self, col_vars=None, row_var=None):
 
     @classmethod
     def transpose(cls, table, feature_names_column="",
-                  meta_attr_name="Feature name", feature_name="Feature"):
+                  meta_attr_name="Feature name", feature_name="Feature",
+                  remove_redundant_inst=False, progress_callback=None):
         """
         Transpose the table.
 
@@ -1714,25 +1715,49 @@ def transpose(cls, table, feature_names_column="",
         :param meta_attr_name: str - name of new meta attribute into which
             feature names are mapped
         :param feature_name: str - default feature name prefix
+        :param remove_redundant_inst: bool - remove instance that
+            represents feature_names_column
+        :param progress_callback: callable - to report the progress
         :return: Table - transposed table
         """
+        if progress_callback is None:
+            progress_callback = dummy_callback
+        progress_callback(0, "Transposing...")
+
+        if isinstance(feature_names_column, Variable):
+            feature_names_column = feature_names_column.name
 
         self = cls()
         n_cols, self.n_rows = table.X.shape
         old_domain = table.attributes.get("old_domain")
+        table_domain_attributes = list(table.domain.attributes)
+        attr_index = None
+        if remove_redundant_inst:
+            attr_names = [a.name for a in table_domain_attributes]
+            if feature_names_column and feature_names_column in attr_names:
+                attr_index = attr_names.index(feature_names_column)
+                self.n_rows = self.n_rows - 1
+                table_domain_attributes.remove(
+                    table_domain_attributes[attr_index])
 
         # attributes
         # - classes and metas to attributes of attributes
         # - arbitrary meta column to feature names
         self.X = table.X.T
+        if attr_index is not None:
+            self.X = np.delete(self.X, attr_index, 0)
         if feature_names_column:
             names = [str(row[feature_names_column]) for row in table]
+            progress_callback(0.1)
             names = get_unique_names_duplicates(names)
+            progress_callback(0.3)
             attributes = [ContinuousVariable(name) for name in names]
         else:
             places = int(np.ceil(np.log10(n_cols))) if n_cols else 1
             attributes = [ContinuousVariable(f"{feature_name} {i:0{places}}")
                           for i in range(1, n_cols + 1)]
+        progress_callback(0.4)
+
         if old_domain is not None and feature_names_column:
             for i, _ in enumerate(attributes):
                 if attributes[i].name in old_domain:
@@ -1755,6 +1780,7 @@ def set_attributes_of_attributes(_vars, _table):
                         attributes[j].attributes[variable.name] = value
 
         set_attributes_of_attributes(table.domain.class_vars, table.Y)
+        progress_callback(0.5)
         set_attributes_of_attributes(table.domain.metas, table.metas)
 
         # weights
@@ -1762,7 +1788,7 @@ def set_attributes_of_attributes(_vars, _table):
 
         def get_table_from_attributes_of_attributes(_vars, _dtype=float):
             T = np.empty((self.n_rows, len(_vars)), dtype=_dtype)
-            for i, _attr in enumerate(table.domain.attributes):
+            for i, _attr in enumerate(table_domain_attributes):
                 for j, _var in enumerate(_vars):
                     val = str(_attr.attributes.get(_var.name, ""))
                     if not _var.is_string:
@@ -1782,14 +1808,15 @@ def get_table_from_attributes_of_attributes(_vars, _dtype=float):
         # - feature names and attributes of attributes to metas
         self.metas, metas = np.empty((self.n_rows, 0), dtype=object), []
         if meta_attr_name not in [m.name for m in table.domain.metas] and \
-                table.domain.attributes:
-            self.metas = np.array([[a.name] for a in table.domain.attributes],
+                table_domain_attributes:
+            self.metas = np.array([[a.name] for a in table_domain_attributes],
                                   dtype=object)
             metas.append(StringVariable(meta_attr_name))
 
         names = chain.from_iterable(list(attr.attributes)
-                                    for attr in table.domain.attributes)
+                                    for attr in table_domain_attributes)
         names = sorted(set(names) - {var.name for var in class_vars})
+        progress_callback(0.6)
 
         def guessed_var(i, var_name):
             orig_vals = M[:, i]
@@ -1803,6 +1830,7 @@ def guessed_var(i, var_name):
         if old_domain is not None:
             _metas = [m for m in old_domain.metas if m.name != meta_attr_name]
         M = get_table_from_attributes_of_attributes(_metas, _dtype=object)
+        progress_callback(0.7)
         if old_domain is None:
             _metas = [guessed_var(i, m.name) for i, m in enumerate(_metas)]
         if _metas:
@@ -1810,9 +1838,11 @@ def guessed_var(i, var_name):
             metas.extend(_metas)
 
         self.domain = Domain(attributes, class_vars, metas)
+        progress_callback(0.9)
         cls._init_ids(self)
         self.attributes = table.attributes.copy()
         self.attributes["old_domain"] = table.domain
+        progress_callback(1)
         return self
 
     def to_sparse(self, sparse_attributes=True, sparse_class=False,
diff --git a/Orange/widgets/data/owtranspose.py b/Orange/widgets/data/owtranspose.py
--- a/Orange/widgets/data/owtranspose.py
+++ b/Orange/widgets/data/owtranspose.py
@@ -1,6 +1,9 @@
-from Orange.data import Table, ContinuousVariable, StringVariable
-from Orange.widgets.settings import (Setting, ContextSetting,
-                                     DomainContextHandler)
+from typing import Optional, Union
+
+from Orange.data import Table, ContinuousVariable, StringVariable, Variable
+from Orange.widgets.settings import Setting, ContextSetting, \
+    DomainContextHandler
+from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
 from Orange.widgets.utils.itemmodels import DomainModel
 from Orange.widgets.utils.widgetpreview import WidgetPreview
 from Orange.widgets.utils.state_summary import format_summary_details
@@ -9,7 +12,28 @@
 from Orange.widgets.widget import Input, Output
 
 
-class OWTranspose(OWWidget):
+def run(data: Table,
+        variable: Optional[Union[Variable, bool]],
+        feature_name: str,
+        remove_redundant_inst: bool,
+        state: TaskState
+        ) -> Table:
+    if not data:
+        return None
+
+    def callback(i: float, status=""):
+        state.set_progress_value(i * 100)
+        if status:
+            state.set_status(status)
+        if state.is_interruption_requested():
+            raise Exception
+
+    return Table.transpose(data, variable, feature_name=feature_name,
+                           remove_redundant_inst=remove_redundant_inst,
+                           progress_callback=callback)
+
+
+class OWTranspose(OWWidget, ConcurrentWidgetMixin):
     name = "Transpose"
     description = "Transpose data table."
     icon = "icons/Transpose.svg"
@@ -33,6 +57,7 @@ class Outputs:
     feature_type = ContextSetting(GENERIC)
     feature_name = ContextSetting("")
     feature_names_column = ContextSetting(None)
+    remove_redundant_inst = ContextSetting(False)
     auto_apply = Setting(True)
 
     class Warning(OWWidget.Warning):
@@ -45,7 +70,8 @@ class Error(OWWidget.Error):
         value_error = Msg("{}")
 
     def __init__(self):
-        super().__init__()
+        OWWidget.__init__(self)
+        ConcurrentWidgetMixin.__init__(self)
         self.data = None
 
         # self.apply is changed later, pylint: disable=unnecessary-lambda
@@ -69,6 +95,11 @@ def __init__(self):
             "feature_names_column", contentsLength=12, searchable=True,
             callback=self._feature_combo_changed, model=self.feature_model)
 
+        self.remove_check = gui.checkBox(
+            gui.indentedBox(box, gui.checkButtonOffsetHint(button)), self,
+            "remove_redundant_inst", "Remove redundant instance",
+            callback=lambda: self.apply())
+
         gui.auto_apply(self.buttonsArea, self, commit=self.apply)
 
         self.info.set_input_summary(self.info.NoInput)
@@ -112,27 +143,38 @@ def set_controls(self):
 
     def apply(self):
         self.clear_messages()
-        transposed = None
-        if self.data:
-            try:
-                variable = self.feature_type == self.FROM_VAR and \
-                           self.feature_names_column
-                transposed = Table.transpose(
-                    self.data, variable,
-                    feature_name=self.feature_name or self.DEFAULT_PREFIX)
-                if variable:
-                    names = self.data.get_column_view(variable)[0]
-                    if len(names) != len(set(names)):
-                        self.Warning.duplicate_names(variable)
-                if self.data.domain.has_discrete_attributes():
-                    self.Warning.discrete_attrs()
-                self.info.set_output_summary(len(transposed),
-                                             format_summary_details(transposed))
-            except ValueError as e:
-                self.Error.value_error(e)
+        variable = self.feature_type == self.FROM_VAR and \
+            self.feature_names_column
+        if variable and self.data:
+            names = self.data.get_column_view(variable)[0]
+            if len(names) != len(set(names)):
+                self.Warning.duplicate_names(variable)
+        if self.data and self.data.domain.has_discrete_attributes():
+            self.Warning.discrete_attrs()
+        feature_name = self.feature_name or self.DEFAULT_PREFIX
+        self.start(run, self.data, variable,
+                   feature_name, self.remove_redundant_inst)
+
+    def on_partial_result(self, _):
+        pass
+
+    def on_done(self, transposed: Optional[Table]):
+        self.Outputs.data.send(transposed)
+        if transposed:
+            self.info.set_output_summary(len(transposed),
+                                         format_summary_details(transposed))
         else:
             self.info.set_output_summary(self.info.NoOutput)
-        self.Outputs.data.send(transposed)
+
+    def on_exception(self, ex: Exception):
+        if isinstance(ex, ValueError):
+            self.Error.value_error(ex)
+        else:
+            raise ex
+
+    def onDeleteWidget(self):
+        self.shutdown()
+        super().onDeleteWidget()
 
     def send_report(self):
         if self.feature_type == self.GENERIC:


Test patch:
diff --git a/Orange/tests/test_table.py b/Orange/tests/test_table.py
--- a/Orange/tests/test_table.py
+++ b/Orange/tests/test_table.py
@@ -2197,7 +2197,7 @@ def test_sparse_assignment(self):
 class TestTableTranspose(unittest.TestCase):
     def test_transpose_no_class(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
-        data = Table(Domain(attrs), np.arange(8).reshape((4, 2)))
+        table = Table(Domain(attrs), np.arange(8).reshape((4, 2)))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2206,19 +2206,19 @@ def test_transpose_no_class(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose is original
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_discrete_class(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
         domain = Domain(attrs, [DiscreteVariable("cls", values=("a", "b"))])
-        data = Table(domain, np.arange(8).reshape((4, 2)),
+        table = Table(domain, np.arange(8).reshape((4, 2)),
                      np.array([1, 1, 0, 0]))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
@@ -2232,19 +2232,19 @@ def test_transpose_discrete_class(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose is original
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_continuous_class(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
         domain = Domain(attrs, [ContinuousVariable("cls")])
-        data = Table(domain, np.arange(8).reshape((4, 2)), np.arange(4, 0, -1))
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(4, 0, -1))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2257,19 +2257,19 @@ def test_transpose_continuous_class(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_missing_class(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
         domain = Domain(attrs, [ContinuousVariable("cls")])
-        data = Table(domain, np.arange(8).reshape((4, 2)),
+        table = Table(domain, np.arange(8).reshape((4, 2)),
                      np.array([np.nan, 3, 2, 1]))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
@@ -2282,21 +2282,21 @@ def test_transpose_missing_class(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_multiple_class(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
         class_vars = [ContinuousVariable("cls1"), ContinuousVariable("cls2")]
         domain = Domain(attrs, class_vars)
-        data = Table(domain, np.arange(8).reshape((4, 2)),
-                     np.arange(8).reshape((4, 2)))
+        table = Table(domain, np.arange(8).reshape((4, 2)),
+                      np.arange(8).reshape((4, 2)))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2309,14 +2309,14 @@ def test_transpose_multiple_class(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2324,7 +2324,7 @@ def test_transpose_metas(self):
         domain = Domain(attrs, metas=metas)
         X = np.arange(8).reshape((4, 2))
         M = np.array(["aa", "bb", "cc", "dd"])[:, None]
-        data = Table(domain, X, metas=M)
+        table = Table(domain, X, metas=M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2337,14 +2337,14 @@ def test_transpose_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_discrete_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2352,7 +2352,7 @@ def test_transpose_discrete_metas(self):
         domain = Domain(attrs, metas=metas)
         X = np.arange(8).reshape((4, 2))
         M = np.array([0, 1, 0, 1])[:, None]
-        data = Table(domain, X, metas=M)
+        table = Table(domain, X, metas=M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2365,14 +2365,14 @@ def test_transpose_discrete_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose is original
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_continuous_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2380,7 +2380,7 @@ def test_transpose_continuous_metas(self):
         domain = Domain(attrs, metas=metas)
         X = np.arange(8).reshape((4, 2))
         M = np.array([0.0, 1.0, 0.0, 1.0])[:, None]
-        data = Table(domain, X, metas=M)
+        table = Table(domain, X, metas=M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2393,14 +2393,14 @@ def test_transpose_continuous_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose is original
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_missing_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2408,7 +2408,7 @@ def test_transpose_missing_metas(self):
         domain = Domain(attrs, metas=metas)
         X = np.arange(8).reshape((4, 2))
         M = np.array(["aa", "bb", "", "dd"])[:, None]
-        data = Table(domain, X, metas=M)
+        table = Table(domain, X, metas=M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2420,14 +2420,14 @@ def test_transpose_missing_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_multiple_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2436,7 +2436,7 @@ def test_transpose_multiple_metas(self):
         X = np.arange(8).reshape((4, 2))
         M = np.array([["aa", "aaa"], ["bb", "bbb"],
                       ["cc", "ccc"], ["dd", "ddd"]])
-        data = Table(domain, X, metas=M)
+        table = Table(domain, X, metas=M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2449,14 +2449,14 @@ def test_transpose_multiple_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_class_and_metas(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
@@ -2464,7 +2464,7 @@ def test_transpose_class_and_metas(self):
         domain = Domain(attrs, [ContinuousVariable("cls")], metas)
         M = np.array([["aa", "aaa"], ["bb", "bbb"],
                       ["cc", "ccc"], ["dd", "ddd"]])
-        data = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2477,21 +2477,21 @@ def test_transpose_class_and_metas(self):
                        metas=np.array(["c1", "c2"])[:, None])
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes, {})
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
 
     def test_transpose_attributes_of_attributes_discrete(self):
         attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
         attrs[0].attributes = {"attr1": "a", "attr2": "aa"}
         attrs[1].attributes = {"attr1": "b", "attr2": "bb"}
         domain = Domain(attrs)
-        data = Table(domain, np.arange(8).reshape((4, 2)))
+        table = Table(domain, np.arange(8).reshape((4, 2)))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2503,14 +2503,14 @@ def test_transpose_attributes_of_attributes_discrete(self):
         result = Table(domain, np.arange(8).reshape((4, 2)).T, metas=M)
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes,
+        self.assertDictEqual(table.domain.attributes[0].attributes,
                              {"attr1": "a", "attr2": "aa"})
 
     def test_transpose_attributes_of_attributes_continuous(self):
@@ -2518,7 +2518,7 @@ def test_transpose_attributes_of_attributes_continuous(self):
         attrs[0].attributes = {"attr1": "1.1", "attr2": "1.3"}
         attrs[1].attributes = {"attr1": "2.2", "attr2": "2.3"}
         domain = Domain(attrs)
-        data = Table(domain, np.arange(8).reshape((4, 2)))
+        table = Table(domain, np.arange(8).reshape((4, 2)))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2530,14 +2530,14 @@ def test_transpose_attributes_of_attributes_continuous(self):
                                        ["c2", 2.2, 2.3]], dtype=object))
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes,
+        self.assertDictEqual(table.domain.attributes[0].attributes,
                              {"attr1": "1.1", "attr2": "1.3"})
 
     def test_transpose_attributes_of_attributes_missings(self):
@@ -2545,7 +2545,7 @@ def test_transpose_attributes_of_attributes_missings(self):
         attrs[0].attributes = {"attr1": "a", "attr2": "aa"}
         attrs[1].attributes = {"attr1": "b"}
         domain = Domain(attrs)
-        data = Table(domain, np.arange(8).reshape((4, 2)))
+        table = Table(domain, np.arange(8).reshape((4, 2)))
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2557,14 +2557,14 @@ def test_transpose_attributes_of_attributes_missings(self):
         result = Table(domain, np.arange(8).reshape((4, 2)).T, metas=M)
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes,
+        self.assertDictEqual(table.domain.attributes[0].attributes,
                              {"attr1": "a", "attr2": "aa"})
 
     def test_transpose_class_metas_attributes(self):
@@ -2575,7 +2575,7 @@ def test_transpose_class_metas_attributes(self):
         domain = Domain(attrs, [ContinuousVariable("cls")], metas)
         M = np.array([["aa", "aaa"], ["bb", "bbb"],
                       ["cc", "ccc"], ["dd", "ddd"]])
-        data = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
 
         att = [ContinuousVariable("Feature 1"), ContinuousVariable("Feature 2"),
                ContinuousVariable("Feature 3"), ContinuousVariable("Feature 4")]
@@ -2591,14 +2591,14 @@ def test_transpose_class_metas_attributes(self):
         result = Table(domain, np.arange(8).reshape((4, 2)).T, metas=M)
 
         # transpose and compare
-        self._compare_tables(result, Table.transpose(data))
+        self._compare_tables(result, Table.transpose(table))
 
         # transpose of transpose
-        t = Table.transpose(Table.transpose(data), "Feature name")
-        self._compare_tables(data, t)
+        t = Table.transpose(Table.transpose(table), "Feature name")
+        self._compare_tables(table, t)
 
         # original should not change
-        self.assertDictEqual(data.domain.attributes[0].attributes,
+        self.assertDictEqual(table.domain.attributes[0].attributes,
                              {"attr1": "a1", "attr2": "aa1"})
 
     def test_transpose_duplicate_feature_names(self):
@@ -2618,6 +2618,431 @@ def test_transpose(self):
         self._compare_tables(zoo, t2)
         self._compare_tables(t1, t3)
 
+    def test_transpose_callback(self):
+        zoo = Table("zoo")
+        cb = Mock()
+        Table.transpose(zoo, progress_callback=cb)
+        cb.assert_called()
+
+    def test_transpose_no_class_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        table = Table(Domain(attrs), np.arange(8).reshape((4, 2)))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_discrete_class_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        domain = Domain(attrs, [DiscreteVariable("cls", values=("a", "b"))])
+        table = Table(domain, np.arange(8).reshape((4, 2)),
+                     np.array([1, 1, 0, 0]))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"cls": "b"}
+        att[1].attributes = {"cls": "b"}
+        att[2].attributes = {"cls": "a"}
+        att[3].attributes = {"cls": "a"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_continuous_class_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        domain = Domain(attrs, [ContinuousVariable("cls")])
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(4, 0, -1))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"cls": "4"}
+        att[1].attributes = {"cls": "3"}
+        att[2].attributes = {"cls": "2"}
+        att[3].attributes = {"cls": "1"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_missing_class_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        domain = Domain(attrs, [ContinuousVariable("cls")])
+        table = Table(domain, np.arange(8).reshape((4, 2)),
+                     np.array([np.nan, 3, 2, 1]))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[1].attributes = {"cls": "3"}
+        att[2].attributes = {"cls": "2"}
+        att[3].attributes = {"cls": "1"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_multiple_class_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        class_vars = [ContinuousVariable("cls1"), ContinuousVariable("cls2")]
+        domain = Domain(attrs, class_vars)
+        table = Table(domain, np.arange(8).reshape((4, 2)),
+                     np.arange(8).reshape((4, 2)))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"cls1": "0", "cls2": "1"}
+        att[1].attributes = {"cls1": "2", "cls2": "3"}
+        att[2].attributes = {"cls1": "4", "cls2": "5"}
+        att[3].attributes = {"cls1": "6", "cls2": "7"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [StringVariable("m1")]
+        domain = Domain(attrs, metas=metas)
+        X = np.arange(8).reshape((4, 2))
+        M = np.array(["aa", "bb", "cc", "dd"])[:, None]
+        table = Table(domain, X, metas=M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"m1": "aa"}
+        att[1].attributes = {"m1": "bb"}
+        att[2].attributes = {"m1": "cc"}
+        att[3].attributes = {"m1": "dd"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_discrete_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [DiscreteVariable("m1", values=("aa", "bb"))]
+        domain = Domain(attrs, metas=metas)
+        X = np.arange(8).reshape((4, 2))
+        M = np.array([0, 1, 0, 1])[:, None]
+        table = Table(domain, X, metas=M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"m1": "aa"}
+        att[1].attributes = {"m1": "bb"}
+        att[2].attributes = {"m1": "aa"}
+        att[3].attributes = {"m1": "bb"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_continuous_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [ContinuousVariable("m1")]
+        domain = Domain(attrs, metas=metas)
+        X = np.arange(8).reshape((4, 2))
+        M = np.array([0.0, 1.0, 0.0, 1.0])[:, None]
+        table = Table(domain, X, metas=M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"m1": "0"}
+        att[1].attributes = {"m1": "1"}
+        att[2].attributes = {"m1": "0"}
+        att[3].attributes = {"m1": "1"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_missing_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [StringVariable("m1")]
+        domain = Domain(attrs, metas=metas)
+        X = np.arange(8).reshape((4, 2))
+        M = np.array(["aa", "bb", "", "dd"])[:, None]
+        table = Table(domain, X, metas=M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"m1": "aa"}
+        att[1].attributes = {"m1": "bb"}
+        att[3].attributes = {"m1": "dd"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_multiple_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [StringVariable("m1"), StringVariable("m2")]
+        domain = Domain(attrs, metas=metas)
+        X = np.arange(8).reshape((4, 2))
+        M = np.array([["aa", "aaa"], ["bb", "bbb"],
+                      ["cc", "ccc"], ["dd", "ddd"]])
+        table = Table(domain, X, metas=M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"m1": "aa", "m2": "aaa"}
+        att[1].attributes = {"m1": "bb", "m2": "bbb"}
+        att[2].attributes = {"m1": "cc", "m2": "ccc"}
+        att[3].attributes = {"m1": "dd", "m2": "ddd"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_class_and_metas_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        metas = [StringVariable("m1"), StringVariable("m2")]
+        domain = Domain(attrs, [ContinuousVariable("cls")], metas)
+        M = np.array([["aa", "aaa"], ["bb", "bbb"],
+                      ["cc", "ccc"], ["dd", "ddd"]])
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"cls": "1", "m1": "aa", "m2": "aaa"}
+        att[1].attributes = {"cls": "2", "m1": "bb", "m2": "bbb"}
+        att[2].attributes = {"cls": "3", "m1": "cc", "m2": "ccc"}
+        att[3].attributes = {"cls": "4", "m1": "dd", "m2": "ddd"}
+        domain = Domain(att, metas=[StringVariable("Feature name")])
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1"]]))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes, {})
+
+    def test_transpose_attributes_of_attributes_discrete_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        attrs[0].attributes = {"attr1": "a", "attr2": "aa"}
+        attrs[1].attributes = {"attr1": "b", "attr2": "bb"}
+        domain = Domain(attrs)
+        table = Table(domain, np.arange(8).reshape((4, 2)))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        metas = [StringVariable("Feature name"),
+                 DiscreteVariable("attr1", values=("a", "b")),
+                 DiscreteVariable("attr2", values=("aa", "bb"))]
+        domain = Domain(att, metas=metas)
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1", 0.0, 0.0]], dtype=object))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes,
+                             {"attr1": "a", "attr2": "aa"})
+
+    def test_transpose_attributes_of_attributes_continuous_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        attrs[0].attributes = {"attr1": "1.1", "attr2": "1.3"}
+        attrs[1].attributes = {"attr1": "2.2", "attr2": "2.3"}
+        domain = Domain(attrs)
+        table = Table(domain, np.arange(8).reshape((4, 2)))
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        metas = [StringVariable("Feature name"), ContinuousVariable("attr1"),
+                 ContinuousVariable("attr2")]
+        domain = Domain(att, metas=metas)
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1", 1.1, 1.3]], dtype=object))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes,
+                             {"attr1": "1.1", "attr2": "1.3"})
+
+    def test_transpose_attributes_of_attributes_missings_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        attrs[0].attributes = {"attr1": "a", "attr2": "aa"}
+        attrs[1].attributes = {"attr1": "b"}
+        domain = Domain(attrs)
+        table = Table(domain, np.arange(8).reshape((4, 2)))
+
+        att = [ContinuousVariable("0"), ContinuousVariable("2"),
+               ContinuousVariable("4"), ContinuousVariable("6")]
+        metas = [StringVariable("Feature name"),
+                 DiscreteVariable("attr1", values=("b",))]
+        domain = Domain(att, metas=metas)
+        result = Table(domain, np.array([[1, 3, 5, 7]]),
+                       metas=np.array([["c2", 0.0]], dtype=object))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c1", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "2", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes,
+                             {"attr1": "a", "attr2": "aa"})
+
+    def test_transpose_class_metas_attributes_remove_inst(self):
+        attrs = [ContinuousVariable("c1"), ContinuousVariable("c2")]
+        attrs[0].attributes = {"attr1": "a1", "attr2": "aa1"}
+        attrs[1].attributes = {"attr1": "b1", "attr2": "bb1"}
+        metas = [StringVariable("m1"), StringVariable("m2")]
+        domain = Domain(attrs, [ContinuousVariable("cls")], metas)
+        M = np.array([["aa", "aaa"], ["bb", "bbb"],
+                      ["cc", "ccc"], ["dd", "ddd"]])
+        table = Table(domain, np.arange(8).reshape((4, 2)), np.arange(1, 5), M)
+
+        att = [ContinuousVariable("1"), ContinuousVariable("3"),
+               ContinuousVariable("5"), ContinuousVariable("7")]
+        att[0].attributes = {"cls": "1", "m1": "aa", "m2": "aaa"}
+        att[1].attributes = {"cls": "2", "m1": "bb", "m2": "bbb"}
+        att[2].attributes = {"cls": "3", "m1": "cc", "m2": "ccc"}
+        att[3].attributes = {"cls": "4", "m1": "dd", "m2": "ddd"}
+        metas = [StringVariable("Feature name"),
+                 DiscreteVariable("attr1", values=("a1", "b1")),
+                 DiscreteVariable("attr2", values=("aa1", "bb1"))]
+        domain = Domain(att, metas=metas)
+        result = Table(domain, np.array([[0, 2, 4, 6]]),
+                       metas=np.array([["c1", 0.0, 0.0]], dtype=object))
+
+        # transpose and compare
+        transposed = Table.transpose(table, "c2", remove_redundant_inst=True)
+        self._compare_tables(result, transposed)
+
+        # transpose of transpose is not equal to the original
+        Table.transpose(transposed, "Feature name")
+        Table.transpose(transposed, "3", remove_redundant_inst=True)
+
+        # original should not change
+        self.assertDictEqual(table.domain.attributes[0].attributes,
+                             {"attr1": "a1", "attr2": "aa1"})
+
     def _compare_tables(self, table1, table2):
         self.assertEqual(table1.n_rows, table2.n_rows)
         np.testing.assert_array_equal(table1.X, table2.X)
diff --git a/Orange/widgets/data/tests/test_owtranspose.py b/Orange/widgets/data/tests/test_owtranspose.py
--- a/Orange/widgets/data/tests/test_owtranspose.py
+++ b/Orange/widgets/data/tests/test_owtranspose.py
@@ -9,12 +9,38 @@
 from orangewidget.widget import StateInfo
 
 from Orange.data import Table
-from Orange.widgets.data.owtranspose import OWTranspose
+from Orange.widgets.data.owtranspose import OWTranspose, run
 from Orange.widgets.tests.base import WidgetTest
+from Orange.widgets.tests.utils import simulate
 from Orange.widgets.utils.state_summary import format_summary_details
 from Orange.tests import test_filename
 
 
+class TestRunner(WidgetTest):
+    def setUp(self):
+        self.zoo = Table("zoo")
+        self.state = Mock()
+        self.state.is_interruption_requested = Mock(return_value=False)
+
+    def test_run(self):
+        result = run(self.zoo, "", "Feature", False, self.state)
+        self.assert_table_equal(Table.transpose(self.zoo), result)
+
+    def test_run_var(self):
+        result = run(self.zoo, "name", "Feature", False, self.state)
+        self.assert_table_equal(Table.transpose(self.zoo, "name"), result)
+
+    def test_run_name(self):
+        result1 = run(self.zoo, "", "Foo", False, self.state)
+        result2 = Table.transpose(self.zoo, feature_name="Foo")
+        self.assert_table_equal(result1, result2)
+
+    def test_run_callback(self):
+        self.state.set_progress_value = Mock()
+        run(self.zoo, "", "Feature", False, self.state)
+        self.state.set_progress_value.assert_called()
+
+
 class TestOWTranspose(WidgetTest):
     def setUp(self):
         self.widget = self.create_widget(OWTranspose)
@@ -81,6 +107,23 @@ def test_feature_type(self):
             all(x.name.startswith(widget.DEFAULT_PREFIX)
                 for x in output.domain.attributes))
 
+    def test_remove_redundant_instance(self):
+        cb = self.widget.feature_combo
+        data = Table("iris")
+
+        self.send_signal(self.widget.Inputs.data, data)
+        simulate.combobox_activate_item(cb, "petal length")
+        self.widget.controls.remove_redundant_inst.setChecked(True)
+        self.wait_until_finished()
+
+        output = self.get_output(self.widget.Outputs.data)
+        self.assertEqual(len(output), 3)
+        self.assertNotIn("petal length", output.metas)
+
+        self.widget.controls.remove_redundant_inst.setChecked(False)
+        self.wait_until_finished()
+        self.assertEqual(len(self.get_output(self.widget.Outputs.data)), 4)
+
     def test_send_report(self):
         widget = self.widget
         widget.feature_type = widget.FROM_VAR
@@ -150,8 +193,10 @@ def test_error(self):
         with unittest.mock.patch("Orange.data.Table.transpose",
                                  side_effect=ValueError("foo")):
             self.send_signal(widget.Inputs.data, self.zoo)
+            self.wait_until_finished()
             self.assertTrue(widget.Error.value_error.is_shown())
         self.send_signal(widget.Inputs.data, self.zoo)
+        self.wait_until_finished()
         self.assertFalse(widget.Error.value_error.is_shown())
 
     def test_feature_names_from_cont_vars(self):
@@ -182,6 +227,7 @@ def test_summary(self):
         data = Table("iris")
         self.send_signal(self.widget.Inputs.data, data)
         input_sum.assert_called_with(len(data), format_summary_details(data))
+        self.wait_until_finished()
         output = self.get_output(self.widget.Outputs.data)
         output_sum.assert_called_with(len(output),
                                       format_summary_details(output))
@@ -191,8 +237,10 @@ def test_summary(self):
         self.send_signal(self.widget.Inputs.data, None)
         input_sum.assert_called_once()
         self.assertIsInstance(input_sum.call_args[0][0], StateInfo.Empty)
+        self.wait_until_finished()
         output_sum.assert_called_once()
         self.assertIsInstance(output_sum.call_args[0][0], StateInfo.Empty)
 
+
 if __name__ == "__main__":
     unittest.main()