Skip to content

Commit 5d7e419

Browse files
authored
Merge pull request #3299 from VesnaT/sel_cols_by_features
[ENH] OWSelectAttributes: Use input features
2 parents 5f8dc4e + c988589 commit 5d7e419

File tree

4 files changed

+295
-13
lines changed

4 files changed

+295
-13
lines changed

Orange/widgets/data/owrank.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
ContextSetting)
3434
from Orange.widgets.utils.itemmodels import PyTableModel
3535
from Orange.widgets.utils.sql import check_sql_input
36-
from Orange.widgets.widget import OWWidget, Msg, Input, Output
36+
from Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList
3737

3838

3939
log = logging.getLogger(__name__)
@@ -180,6 +180,7 @@ class Inputs:
180180
class Outputs:
181181
reduced_data = Output("Reduced Data", Table, default=True)
182182
scores = Output("Scores", Table)
183+
features = Output("Features", AttributeList, dynamic=False)
183184

184185
SelectNone, SelectAll, SelectManual, SelectNBest = range(4)
185186

@@ -526,12 +527,14 @@ def commit(self):
526527
for i in self.selected_rows]
527528
if not selected_attrs:
528529
self.Outputs.reduced_data.send(None)
530+
self.Outputs.features.send(None)
529531
self.out_domain_desc = None
530532
else:
531533
reduced_domain = Domain(
532534
selected_attrs, self.data.domain.class_var, self.data.domain.metas)
533535
data = self.data.transform(reduced_domain)
534536
self.Outputs.reduced_data.send(data)
537+
self.Outputs.features.send(AttributeList(selected_attrs))
535538
self.out_domain_desc = report.describe_domain(data.domain)
536539

537540
def create_scores_table(self, labels):

Orange/widgets/data/owselectcolumns.py

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
SelectAttributesDomainContextHandler
1515
from Orange.widgets.settings import ContextSetting, Setting
1616
from Orange.widgets.utils.listfilter import VariablesListItemView, slices, variables_filter
17-
from Orange.widgets.widget import Input, Output
17+
from Orange.widgets.widget import Input, Output, AttributeList, Msg
1818
from Orange.data.table import Table
1919
from Orange.widgets.utils import vartype
2020
from Orange.widgets.utils.itemmodels import VariableListModel
@@ -106,21 +106,29 @@ class OWSelectAttributes(widget.OWWidget):
106106
keywords = ["filter"]
107107

108108
class Inputs:
109-
data = Input("Data", Table)
109+
data = Input("Data", Table, default=True)
110+
features = Input("Features", AttributeList)
110111

111112
class Outputs:
112113
data = Output("Data", Table)
113-
features = Output("Features", widget.AttributeList, dynamic=False)
114+
features = Output("Features", AttributeList, dynamic=False)
114115

115116
want_main_area = False
116117
want_control_area = True
117118

118119
settingsHandler = SelectAttributesDomainContextHandler()
119120
domain_role_hints = ContextSetting({})
121+
use_input_features = Setting(False)
120122
auto_commit = Setting(True)
121123

124+
class Warning(widget.OWWidget.Warning):
125+
mismatching_domain = Msg("Features and data domain do not match")
126+
122127
def __init__(self):
123128
super().__init__()
129+
self.data = None
130+
self.features = None
131+
124132
# Schedule interface updates (enabled buttons) using a coalescing
125133
# single shot timer (complex interactions on selection and filtering
126134
# updates in the 'available_attrs_view')
@@ -161,6 +169,8 @@ def dropcompleted(action):
161169

162170
box = gui.vBox(self.controlArea, "Features", addToLayout=False)
163171
self.used_attrs = VariablesListItemModel()
172+
self.used_attrs.rowsInserted.connect(self.__used_attrs_changed)
173+
self.used_attrs.rowsRemoved.connect(self.__used_attrs_changed)
164174
self.used_attrs_view = VariablesListItemView(
165175
acceptedType=(Orange.data.DiscreteVariable,
166176
Orange.data.ContinuousVariable))
@@ -169,6 +179,14 @@ def dropcompleted(action):
169179
self.used_attrs_view.selectionModel().selectionChanged.connect(
170180
partial(update_on_change, self.used_attrs_view))
171181
self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted)
182+
self.use_features_box = gui.auto_commit(
183+
self.controlArea, self, "use_input_features",
184+
"Use input features", "Always use input features",
185+
box=False, commit=self.__use_features_clicked,
186+
callback=self.__use_features_changed, addToLayout=False
187+
)
188+
self.enable_use_features_box()
189+
box.layout().addWidget(self.use_features_box)
172190
box.layout().addWidget(self.used_attrs_view)
173191
layout.addWidget(box, 0, 2, 1, 1)
174192

@@ -244,11 +262,39 @@ def dropcompleted(action):
244262
layout.setHorizontalSpacing(0)
245263
self.controlArea.setLayout(layout)
246264

247-
self.data = None
248265
self.output_data = None
249266
self.original_completer_items = []
250267

251-
self.resize(500, 600)
268+
self.resize(600, 600)
269+
270+
@property
271+
def features_from_data_attributes(self):
272+
if self.data is None or self.features is None:
273+
return []
274+
domain = self.data.domain
275+
return [domain[feature.name] for feature in self.features
276+
if feature.name in domain and domain[feature.name]
277+
in domain.attributes]
278+
279+
def can_use_features(self):
280+
return bool(self.features_from_data_attributes) and \
281+
self.features_from_data_attributes != self.used_attrs[:]
282+
283+
def __use_features_changed(self): # Use input features check box
284+
# Needs a check since callback is invoked before object is created
285+
if not hasattr(self, "use_features_box"):
286+
return
287+
self.enable_used_attrs(not self.use_input_features)
288+
if self.use_input_features and self.can_use_features():
289+
self.use_features()
290+
if not self.use_input_features:
291+
self.enable_use_features_box()
292+
293+
def __use_features_clicked(self): # Use input features button
294+
self.use_features()
295+
296+
def __used_attrs_changed(self):
297+
self.enable_use_features_box()
252298

253299
@Inputs.data
254300
def set_data(self, data=None):
@@ -302,8 +348,6 @@ def set_data(self, data=None):
302348
self.meta_attrs[:] = []
303349
self.available_attrs[:] = []
304350

305-
self.unconditional_commit()
306-
307351
def update_domain_role_hints(self):
308352
""" Update the domain hints to be stored in the widgets settings.
309353
"""
@@ -316,6 +360,46 @@ def update_domain_role_hints(self):
316360
hints.update(hints_from_model("meta", self.meta_attrs))
317361
self.domain_role_hints = hints
318362

363+
@Inputs.features
364+
def set_features(self, features):
365+
self.features = features
366+
367+
def handleNewSignals(self):
368+
self.check_data()
369+
self.enable_used_attrs()
370+
self.enable_use_features_box()
371+
if self.use_input_features and len(self.features_from_data_attributes):
372+
self.enable_used_attrs(False)
373+
self.use_features()
374+
self.unconditional_commit()
375+
376+
def check_data(self):
377+
self.Warning.mismatching_domain.clear()
378+
if self.data is not None and self.features is not None and \
379+
not len(self.features_from_data_attributes):
380+
self.Warning.mismatching_domain()
381+
382+
def enable_used_attrs(self, enable=True):
383+
self.up_attr_button.setEnabled(enable)
384+
self.move_attr_button.setEnabled(enable)
385+
self.down_attr_button.setEnabled(enable)
386+
self.used_attrs_view.setEnabled(enable)
387+
self.used_attrs_view.repaint()
388+
389+
def enable_use_features_box(self):
390+
self.use_features_box.button.setEnabled(self.can_use_features())
391+
enable_checkbox = bool(self.features_from_data_attributes)
392+
self.use_features_box.setHidden(not enable_checkbox)
393+
self.use_features_box.repaint()
394+
395+
def use_features(self):
396+
attributes = self.features_from_data_attributes
397+
available, used = self.available_attrs[:], self.used_attrs[:]
398+
self.available_attrs[:] = [attr for attr in used + available
399+
if attr not in attributes]
400+
self.used_attrs[:] = attributes
401+
self.commit()
402+
319403
def selected_rows(self, view):
320404
""" Return the selected rows in the view.
321405
"""
@@ -397,8 +481,9 @@ def selected_vars(view):
397481
all_primitive = all(var.is_primitive()
398482
for var in available_types)
399483

400-
move_attr_enabled = (available_selected and all_primitive) or \
401-
attrs_selected
484+
move_attr_enabled = \
485+
((available_selected and all_primitive) or attrs_selected) and \
486+
self.used_attrs_view.isEnabled()
402487

403488
self.move_attr_button.setEnabled(bool(move_attr_enabled))
404489
if move_attr_enabled:
@@ -429,13 +514,15 @@ def commit(self):
429514
newdata = self.data.transform(domain)
430515
self.output_data = newdata
431516
self.Outputs.data.send(newdata)
432-
self.Outputs.features.send(widget.AttributeList(attributes))
517+
self.Outputs.features.send(AttributeList(attributes))
433518
else:
434519
self.output_data = None
435520
self.Outputs.data.send(None)
436521
self.Outputs.features.send(None)
437522

438523
def reset(self):
524+
self.enable_used_attrs()
525+
self.use_features_box.checkbox.setChecked(False)
439526
if self.data is not None:
440527
self.available_attrs[:] = []
441528
self.used_attrs[:] = self.data.domain.attributes
@@ -476,6 +563,8 @@ def main(argv=None): # pragma: no cover
476563
w = OWSelectAttributes()
477564
data = Orange.data.Table(filename)
478565
w.set_data(data)
566+
w.set_features(AttributeList(data.domain.attributes[:2]))
567+
w.handleNewSignals()
479568
w.show()
480569
w.raise_()
481570
rval = app.exec_()

Orange/widgets/data/tests/test_owrank.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from Orange.projection import PCA
99
from Orange.widgets.data.owrank import OWRank, ProblemType, CLS_SCORES, REG_SCORES
1010
from Orange.widgets.tests.base import WidgetTest
11+
from Orange.widgets.widget import AttributeList
1112

1213
from AnyQt.QtCore import Qt, QItemSelection
1314
from AnyQt.QtWidgets import QCheckBox
@@ -107,6 +108,13 @@ def test_output_scores_with_scorer(self):
107108
self.assertIsInstance(output, Table)
108109
self.assertEqual(output.X.shape, (len(self.iris.domain.attributes), 5))
109110

111+
def test_output_features(self):
112+
self.send_signal(self.widget.Inputs.data, self.iris)
113+
output = self.get_output(self.widget.Outputs.features)
114+
self.assertIsInstance(output, AttributeList)
115+
self.send_signal(self.widget.Inputs.data, None)
116+
self.assertIsNone(self.get_output(self.widget.Outputs.features))
117+
110118
def test_scoring_method_problem_type(self):
111119
"""Check scoring methods check boxes"""
112120
self.send_signal(self.widget.Inputs.data, self.iris)

0 commit comments

Comments
 (0)