Skip to content

Commit ef789ee

Browse files
authored
Merge pull request #5105 from PrimozGodec/select-columns-new-data
Select columns: option to unselect new columns
2 parents 4dbbdde + fd7972b commit ef789ee

File tree

2 files changed

+135
-10
lines changed

2 files changed

+135
-10
lines changed

Orange/widgets/data/owselectcolumns.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from functools import partial
2-
from typing import Optional
2+
from typing import Optional, Dict, Tuple
33

44
from AnyQt.QtWidgets import QWidget, QGridLayout
55
from AnyQt.QtWidgets import QListView
@@ -8,6 +8,7 @@
88
QMimeData, QAbstractItemModel
99
)
1010

11+
from Orange.data import Domain, Variable
1112
from Orange.widgets import gui, widget
1213
from Orange.widgets.settings import (
1314
ContextSetting, Setting, DomainContextHandler
@@ -167,6 +168,7 @@ class Outputs:
167168
settingsHandler = SelectAttributesDomainContextHandler(first_match=False)
168169
domain_role_hints = ContextSetting({})
169170
use_input_features = Setting(False)
171+
select_new_features = Setting(True)
170172
auto_commit = Setting(True)
171173

172174
class Warning(widget.OWWidget.Warning):
@@ -286,7 +288,7 @@ def dropcompleted(action):
286288
self.down_class_button = gui.button(bbox, self, "Down",
287289
callback=partial(self.move_down, self.class_attrs_view))
288290

289-
bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0)
291+
bbox = gui.vBox(self.controlArea, addToLayout=False)
290292
layout.addWidget(bbox, 2, 1, 1, 1)
291293
self.up_meta_button = gui.button(bbox, self, "Up",
292294
callback=partial(self.move_up, self.meta_attrs_view))
@@ -297,8 +299,14 @@ def dropcompleted(action):
297299
self.down_meta_button = gui.button(bbox, self, "Down",
298300
callback=partial(self.move_down, self.meta_attrs_view))
299301

302+
bbox = gui.vBox(self.controlArea, "Additional settings", addToLayout=False)
303+
gui.checkBox(
304+
bbox, self, "select_new_features", "Automatically select additional/new features"
305+
)
306+
layout.addWidget(bbox, 3, 0, 1, 3)
307+
300308
autobox = gui.auto_send(None, self, "auto_commit")
301-
layout.addWidget(autobox, 3, 0, 1, 3)
309+
layout.addWidget(autobox, 4, 0, 1, 3)
302310
reset = gui.button(None, self, "Reset", callback=self.reset, width=120)
303311
autobox.layout().insertWidget(0, reset)
304312
autobox.layout().insertStretch(1, 20)
@@ -370,19 +378,50 @@ def attrs_for_role(role):
370378
]
371379
return sorted(selected_attrs, key=lambda attr: domain_hints[attr][1])
372380

373-
domain = data.domain
374-
domain_hints = {}
375-
domain_hints.update(self._hints_from_seq("attribute", domain.attributes))
376-
domain_hints.update(self._hints_from_seq("meta", domain.metas))
377-
domain_hints.update(self._hints_from_seq("class", domain.class_vars))
378-
domain_hints.update(self.domain_role_hints)
379-
381+
domain_hints = self.restore_hints(data.domain)
380382
self.used_attrs[:] = attrs_for_role("attribute")
381383
self.class_attrs[:] = attrs_for_role("class")
382384
self.meta_attrs[:] = attrs_for_role("meta")
383385
self.available_attrs[:] = attrs_for_role("available")
384386
self.info.set_input_summary(len(data), format_summary_details(data))
385387

388+
def restore_hints(self, domain: Domain) -> Dict[Variable, Tuple[str, int]]:
389+
"""
390+
Define hints for selected/unselected features.
391+
Rules:
392+
- if context available, restore new features based on checked/unchecked
393+
select_new_features, context hint should be took into account
394+
- in no context, restore features based on the domain (as selected)
395+
396+
Parameters
397+
----------
398+
domain
399+
Data domain
400+
401+
Returns
402+
-------
403+
Dictionary with hints about order and model in which each feature
404+
should appear
405+
"""
406+
domain_hints = {}
407+
if self.select_new_features or len(self.domain_role_hints) == 0:
408+
# select_new_features selected or no context - restore based on domain
409+
domain_hints.update(
410+
self._hints_from_seq("attribute", domain.attributes)
411+
)
412+
domain_hints.update(self._hints_from_seq("meta", domain.metas))
413+
domain_hints.update(
414+
self._hints_from_seq("class", domain.class_vars)
415+
)
416+
else:
417+
# if context restored and select_new_features unselected - restore
418+
# new features as available
419+
d = domain.attributes + domain.metas + domain.class_vars
420+
domain_hints.update(self._hints_from_seq("available", d))
421+
422+
domain_hints.update(self.domain_role_hints)
423+
return domain_hints
424+
386425
def update_domain_role_hints(self):
387426
""" Update the domain hints to be stored in the widgets settings.
388427
"""

Orange/widgets/data/tests/test_owselectcolumns.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# pylint: disable=unsubscriptable-object
2+
import unittest
23
from unittest import TestCase
34
from unittest.mock import Mock
45

@@ -411,3 +412,88 @@ def test_domain_new_feature(self):
411412
data.Y
412413
)
413414
self.send_signal(self.widget.Inputs.data, data1)
415+
416+
def test_select_new_features(self):
417+
"""
418+
When select_new_features checked new attributes must appear in one of
419+
selected columns. Test with fist make context remember attributes of
420+
reduced domain and then testing with full domain. Features in missing
421+
in reduced domain must appears as seleceted.
422+
"""
423+
data = Table("iris")
424+
domain = data.domain
425+
426+
# data with one feature missing
427+
new_domain = Domain(
428+
domain.attributes[:-1], domain.class_var, domain.metas
429+
)
430+
new_data = Table.from_table(new_domain, data)
431+
432+
# make context remember features in reduced domain
433+
self.send_signal(self.widget.Inputs.data, new_data)
434+
output = self.get_output(self.widget.Outputs.data)
435+
436+
self.assertTupleEqual(
437+
new_data.domain.attributes, output.domain.attributes
438+
)
439+
self.assertTupleEqual(new_data.domain.metas, output.domain.metas)
440+
self.assertEqual(new_data.domain.class_var, output.domain.class_var)
441+
442+
# send full domain
443+
self.send_signal(self.widget.Inputs.data, data)
444+
output = self.get_output(self.widget.Outputs.data)
445+
446+
# if select_new_features checked all new features goes in the selected
447+
# features columns - domain equal original
448+
self.assertTrue(self.widget.select_new_features)
449+
self.assertTupleEqual(data.domain.attributes, output.domain.attributes)
450+
self.assertTupleEqual(data.domain.metas, output.domain.metas)
451+
self.assertEqual(data.domain.class_var, output.domain.class_var)
452+
453+
def test_unselect_new_features(self):
454+
"""
455+
When select_new_features not checked new attributes must appear in one
456+
available attributes column. Test with fist make context remember
457+
attributes of reduced domain and then testing with full domain.
458+
Features in missing in reduced domain must appears as not seleceted.
459+
"""
460+
data = Table("iris")
461+
domain = data.domain
462+
463+
# data with one feature missing
464+
new_domain = Domain(
465+
domain.attributes[:-1], domain.class_var, domain.metas
466+
)
467+
new_data = Table.from_table(new_domain, data)
468+
469+
# make context remember features in reduced domain
470+
self.send_signal(self.widget.Inputs.data, new_data)
471+
# unselect select_new_features
472+
self.widget.controls.select_new_features.click()
473+
self.assertFalse(self.widget.select_new_features)
474+
output = self.get_output(self.widget.Outputs.data)
475+
476+
self.assertTupleEqual(
477+
new_data.domain.attributes, output.domain.attributes
478+
)
479+
self.assertTupleEqual(new_data.domain.metas, output.domain.metas)
480+
self.assertEqual(new_data.domain.class_var, output.domain.class_var)
481+
482+
# send full domain
483+
self.send_signal(self.widget.Inputs.data, data)
484+
output = self.get_output(self.widget.Outputs.data)
485+
486+
# if select_new_features not checked all new features goes in the
487+
# available attributes column
488+
self.assertFalse(self.widget.select_new_features)
489+
self.assertTupleEqual(new_domain.attributes, output.domain.attributes)
490+
self.assertTupleEqual(new_domain.metas, output.domain.metas)
491+
self.assertEqual(new_domain.class_var, output.domain.class_var)
492+
# test if new attribute was added to unselected attributes
493+
self.assertEqual(
494+
domain.attributes[-1], list(self.widget.available_attrs)[0]
495+
)
496+
497+
498+
if __name__ == "__main__":
499+
unittest.main()

0 commit comments

Comments
 (0)