Repository: biolab/orange3

Issue:
PCA: retain all data (Unification of embedding methods )
**Issue:** PCA discards the original attributes and replaces them with PCs. Often (e.g. in scOrange), it is desirable to use the original features: annotate PCA plots,  compute differentially-occurring features in different PCA regions, etc. 

**Proposed solution**: Place PCs in meta attributes and rename output to *Data*. 


This would be consistent to *t-SNE* and *MDS*. The user could use *Select columns* to restore current behaviour. This is substantially easier (number of PCs is typically small) than merging original attributes with principal components, as it's possible in the current version. 
 
Such an improvement would also play nice with the new widget *Apply domain*, which enables projecting data to existing PCs and/or attributes. 



Discussion:

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owpca.py b/Orange/widgets/unsupervised/owpca.py
--- a/Orange/widgets/unsupervised/owpca.py
+++ b/Orange/widgets/unsupervised/owpca.py
@@ -31,6 +31,7 @@ class Inputs:
 
     class Outputs:
         transformed_data = Output("Transformed Data", Table, replaces=["Transformed data"])
+        data = Output("Data", Table, default=True)
         components = Output("Components", Table)
         pca = Output("PCA", PCA, dynamic=False)
 
@@ -180,6 +181,7 @@ def clear(self):
 
     def clear_outputs(self):
         self.Outputs.transformed_data.send(None)
+        self.Outputs.data.send(None)
         self.Outputs.components.send(None)
         self.Outputs.pca.send(self._pca_projector)
 
@@ -286,7 +288,7 @@ def _update_axis(self):
         axis.setTicks([[(i, str(i)) for i in range(1, p + 1, d)]])
 
     def commit(self):
-        transformed = components = None
+        transformed = data = components = None
         if self._pca is not None:
             if self._transformed is None:
                 # Compute the full transform (MAX_COMPONENTS components) once.
@@ -311,9 +313,18 @@ def commit(self):
                                metas=metas)
             components.name = 'components'
 
+            data_dom = Domain(
+                self.data.domain.attributes,
+                self.data.domain.class_vars,
+                self.data.domain.metas + domain.attributes)
+            data = Table.from_numpy(
+                data_dom, self.data.X, self.data.Y,
+                numpy.hstack((self.data.metas, transformed.X)))
+
         self._pca_projector.component = self.ncomponents
         self.Outputs.transformed_data.send(transformed)
         self.Outputs.components.send(components)
+        self.Outputs.data.send(data)
         self.Outputs.pca.send(self._pca_projector)
 
     def send_report(self):


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owpca.py b/Orange/widgets/unsupervised/tests/test_owpca.py
--- a/Orange/widgets/unsupervised/tests/test_owpca.py
+++ b/Orange/widgets/unsupervised/tests/test_owpca.py
@@ -220,6 +220,28 @@ def test_on_cut_changed(self):
         invalidate.assert_not_called()
         self.assertEqual(widget.ncomponents, 0)
 
+    def test_output_data(self):
+        widget = self.widget
+        widget.ncomponents = 2
+        domain = Domain(self.iris.domain.attributes[:3],
+                        self.iris.domain.class_var,
+                        self.iris.domain.attributes[3:])
+        iris = self.iris.transform(domain)
+        self.send_signal(widget.Inputs.data, iris)
+        output = self.get_output(widget.Outputs.data)
+        outdom = output.domain
+        self.assertEqual(domain.attributes, outdom.attributes)
+        self.assertEqual(domain.class_var, outdom.class_var)
+        self.assertEqual(domain.metas, outdom.metas[:1])
+        self.assertEqual(len(outdom.metas), 3)
+        np.testing.assert_equal(iris.X, output.X)
+        np.testing.assert_equal(iris.Y, output.Y)
+        np.testing.assert_equal(iris.metas[:, 0], output.metas[:, 0])
+
+        trans = self.get_output(widget.Outputs.transformed_data)
+        self.assertEqual(trans.domain.attributes, outdom.metas[1:])
+        np.testing.assert_equal(trans.X, output.metas[:, 1:])
+
 
 if __name__ == "__main__":
     unittest.main()