Skip to content

Commit 66ff8d8

Browse files
committed
PLS: Add 'Inside Ellipsis' column to output data
1 parent 0144062 commit 66ff8d8

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

Orange/widgets/model/owpls.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from AnyQt.QtCore import Qt
2+
import numpy as np
3+
import scipy.stats as ss
24
import scipy.sparse as sp
35

4-
from Orange.data import Table, Domain
6+
from Orange.data import Table, Domain, DiscreteVariable
7+
from Orange.data.util import get_unique_names
58
from Orange.regression import PLSRegressionLearner
69
from Orange.widgets import gui
710
from Orange.widgets.settings import Setting
@@ -62,11 +65,32 @@ def update_model(self):
6265

6366
def _create_output_data(self) -> Table:
6467
projection = self.model.project(self.data)
68+
inside = np.full(len(self.data), np.nan)
69+
if self.n_components > 1:
70+
inside = self._inside_ellipsis(projection.X[:, :2])
6571
data_domain = self.data.domain
6672
proj_domain = projection.domain
6773
metas = proj_domain.metas + proj_domain.attributes
6874
domain = Domain(data_domain.attributes, data_domain.class_vars, metas)
69-
return self.data.transform(domain)
75+
data: Table = self.data.transform(domain)
76+
inside_name = get_unique_names(domain, "Inside Ellipsis")
77+
inside_var = DiscreteVariable(inside_name, values=("No", "Yes"))
78+
return data.add_column(inside_var, inside, to_metas=True)
79+
80+
@staticmethod
81+
def _inside_ellipsis(scores: np.ndarray) -> np.ndarray:
82+
# https://stats.stackexchange.com/questions/577628/trying-to-understand-how-to-calculate-a-hotellings-t2-confidence-ellipse
83+
# https://stackoverflow.com/questions/66179256/how-to-check-if-a-point-is-in-an-ellipse-in-python
84+
assert scores.shape[1] > 1
85+
86+
f = ss.f.ppf(0.95, len(scores[0]), len(scores) - len(scores[0]))
87+
m = [np.pi * x / 100 for x in range(201)]
88+
cx = np.cos(m) * np.std(scores[:, 0]) * f
89+
cy = np.sin(m) * np.std(scores[:, 1]) * f
90+
91+
a = ((scores[:, 0] - np.mean(cx)) ** 2) / (np.min(cx) ** 2)
92+
b = ((np.mean(cy) - scores[:, 1]) ** 2) / (np.min(cy) ** 2)
93+
return a + b <= 1
7094

7195
@OWBaseLearner.Inputs.data
7296
def set_data(self, data):

Orange/widgets/model/tests/test_owpls.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def test_output_data(self):
4848
output = self.get_output(self.widget.Outputs.data)
4949
self.assertEqual(output.X.shape, (506, 13))
5050
self.assertEqual(output.Y.shape, (506,))
51-
self.assertEqual(output.metas.shape, (506, 5))
51+
self.assertEqual(output.metas.shape, (506, 6))
5252

5353
def test_output_data_multi_target(self):
5454
self.send_signal(self.widget.Inputs.data, self._data_multi_target)
5555
output = self.get_output(self.widget.Outputs.data)
5656
self.assertEqual(output.X.shape, (506, 12))
5757
self.assertEqual(output.Y.shape, (506, 2))
58-
self.assertEqual(output.metas.shape, (506, 5))
58+
self.assertEqual(output.metas.shape, (506, 6))
5959

6060
def test_output_components(self):
6161
self.send_signal(self.widget.Inputs.data, self._data)
@@ -71,6 +71,15 @@ def test_output_components_multi_target(self):
7171
self.assertEqual(components.Y.shape, (2, 2))
7272
self.assertEqual(components.metas.shape, (2, 1))
7373

74+
def test_output_data_inside_ellipsis(self):
75+
self.send_signal(self.widget.Inputs.data, self._data)
76+
output: Table = self.get_output(self.widget.Outputs.data)
77+
inside_col = list(output.get_column("Inside Ellipsis"))
78+
inside = [1] * len(self._data)
79+
for i in [162, 163, 257, 262, 364, 370, 374, 414]:
80+
inside[i] = 0
81+
self.assertEqual(inside, inside_col)
82+
7483

7584
if __name__ == "__main__":
7685
unittest.main()

0 commit comments

Comments
 (0)