Skip to content

Commit

Permalink
Merge pull request #3299 from VesnaT/sel_cols_by_features
Browse files Browse the repository at this point in the history
[ENH] OWSelectAttributes: Use input features
  • Loading branch information
lanzagar authored Oct 26, 2018
2 parents 5f8dc4e + c988589 commit 5d7e419
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 13 deletions.
5 changes: 4 additions & 1 deletion Orange/widgets/data/owrank.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ContextSetting)
from Orange.widgets.utils.itemmodels import PyTableModel
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -180,6 +180,7 @@ class Inputs:
class Outputs:
reduced_data = Output("Reduced Data", Table, default=True)
scores = Output("Scores", Table)
features = Output("Features", AttributeList, dynamic=False)

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

Expand Down Expand Up @@ -526,12 +527,14 @@ def commit(self):
for i in self.selected_rows]
if not selected_attrs:
self.Outputs.reduced_data.send(None)
self.Outputs.features.send(None)
self.out_domain_desc = None
else:
reduced_domain = Domain(
selected_attrs, self.data.domain.class_var, self.data.domain.metas)
data = self.data.transform(reduced_domain)
self.Outputs.reduced_data.send(data)
self.Outputs.features.send(AttributeList(selected_attrs))
self.out_domain_desc = report.describe_domain(data.domain)

def create_scores_table(self, labels):
Expand Down
109 changes: 99 additions & 10 deletions Orange/widgets/data/owselectcolumns.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
SelectAttributesDomainContextHandler
from Orange.widgets.settings import ContextSetting, Setting
from Orange.widgets.utils.listfilter import VariablesListItemView, slices, variables_filter
from Orange.widgets.widget import Input, Output
from Orange.widgets.widget import Input, Output, AttributeList, Msg
from Orange.data.table import Table
from Orange.widgets.utils import vartype
from Orange.widgets.utils.itemmodels import VariableListModel
Expand Down Expand Up @@ -106,21 +106,29 @@ class OWSelectAttributes(widget.OWWidget):
keywords = ["filter"]

class Inputs:
data = Input("Data", Table)
data = Input("Data", Table, default=True)
features = Input("Features", AttributeList)

class Outputs:
data = Output("Data", Table)
features = Output("Features", widget.AttributeList, dynamic=False)
features = Output("Features", AttributeList, dynamic=False)

want_main_area = False
want_control_area = True

settingsHandler = SelectAttributesDomainContextHandler()
domain_role_hints = ContextSetting({})
use_input_features = Setting(False)
auto_commit = Setting(True)

class Warning(widget.OWWidget.Warning):
mismatching_domain = Msg("Features and data domain do not match")

def __init__(self):
super().__init__()
self.data = None
self.features = None

# Schedule interface updates (enabled buttons) using a coalescing
# single shot timer (complex interactions on selection and filtering
# updates in the 'available_attrs_view')
Expand Down Expand Up @@ -161,6 +169,8 @@ def dropcompleted(action):

box = gui.vBox(self.controlArea, "Features", addToLayout=False)
self.used_attrs = VariablesListItemModel()
self.used_attrs.rowsInserted.connect(self.__used_attrs_changed)
self.used_attrs.rowsRemoved.connect(self.__used_attrs_changed)
self.used_attrs_view = VariablesListItemView(
acceptedType=(Orange.data.DiscreteVariable,
Orange.data.ContinuousVariable))
Expand All @@ -169,6 +179,14 @@ def dropcompleted(action):
self.used_attrs_view.selectionModel().selectionChanged.connect(
partial(update_on_change, self.used_attrs_view))
self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted)
self.use_features_box = gui.auto_commit(
self.controlArea, self, "use_input_features",
"Use input features", "Always use input features",
box=False, commit=self.__use_features_clicked,
callback=self.__use_features_changed, addToLayout=False
)
self.enable_use_features_box()
box.layout().addWidget(self.use_features_box)
box.layout().addWidget(self.used_attrs_view)
layout.addWidget(box, 0, 2, 1, 1)

Expand Down Expand Up @@ -244,11 +262,39 @@ def dropcompleted(action):
layout.setHorizontalSpacing(0)
self.controlArea.setLayout(layout)

self.data = None
self.output_data = None
self.original_completer_items = []

self.resize(500, 600)
self.resize(600, 600)

@property
def features_from_data_attributes(self):
if self.data is None or self.features is None:
return []
domain = self.data.domain
return [domain[feature.name] for feature in self.features
if feature.name in domain and domain[feature.name]
in domain.attributes]

def can_use_features(self):
return bool(self.features_from_data_attributes) and \
self.features_from_data_attributes != self.used_attrs[:]

def __use_features_changed(self): # Use input features check box
# Needs a check since callback is invoked before object is created
if not hasattr(self, "use_features_box"):
return
self.enable_used_attrs(not self.use_input_features)
if self.use_input_features and self.can_use_features():
self.use_features()
if not self.use_input_features:
self.enable_use_features_box()

def __use_features_clicked(self): # Use input features button
self.use_features()

def __used_attrs_changed(self):
self.enable_use_features_box()

@Inputs.data
def set_data(self, data=None):
Expand Down Expand Up @@ -302,8 +348,6 @@ def set_data(self, data=None):
self.meta_attrs[:] = []
self.available_attrs[:] = []

self.unconditional_commit()

def update_domain_role_hints(self):
""" Update the domain hints to be stored in the widgets settings.
"""
Expand All @@ -316,6 +360,46 @@ def update_domain_role_hints(self):
hints.update(hints_from_model("meta", self.meta_attrs))
self.domain_role_hints = hints

@Inputs.features
def set_features(self, features):
self.features = features

def handleNewSignals(self):
self.check_data()
self.enable_used_attrs()
self.enable_use_features_box()
if self.use_input_features and len(self.features_from_data_attributes):
self.enable_used_attrs(False)
self.use_features()
self.unconditional_commit()

def check_data(self):
self.Warning.mismatching_domain.clear()
if self.data is not None and self.features is not None and \
not len(self.features_from_data_attributes):
self.Warning.mismatching_domain()

def enable_used_attrs(self, enable=True):
self.up_attr_button.setEnabled(enable)
self.move_attr_button.setEnabled(enable)
self.down_attr_button.setEnabled(enable)
self.used_attrs_view.setEnabled(enable)
self.used_attrs_view.repaint()

def enable_use_features_box(self):
self.use_features_box.button.setEnabled(self.can_use_features())
enable_checkbox = bool(self.features_from_data_attributes)
self.use_features_box.setHidden(not enable_checkbox)
self.use_features_box.repaint()

def use_features(self):
attributes = self.features_from_data_attributes
available, used = self.available_attrs[:], self.used_attrs[:]
self.available_attrs[:] = [attr for attr in used + available
if attr not in attributes]
self.used_attrs[:] = attributes
self.commit()

def selected_rows(self, view):
""" Return the selected rows in the view.
"""
Expand Down Expand Up @@ -397,8 +481,9 @@ def selected_vars(view):
all_primitive = all(var.is_primitive()
for var in available_types)

move_attr_enabled = (available_selected and all_primitive) or \
attrs_selected
move_attr_enabled = \
((available_selected and all_primitive) or attrs_selected) and \
self.used_attrs_view.isEnabled()

self.move_attr_button.setEnabled(bool(move_attr_enabled))
if move_attr_enabled:
Expand Down Expand Up @@ -429,13 +514,15 @@ def commit(self):
newdata = self.data.transform(domain)
self.output_data = newdata
self.Outputs.data.send(newdata)
self.Outputs.features.send(widget.AttributeList(attributes))
self.Outputs.features.send(AttributeList(attributes))
else:
self.output_data = None
self.Outputs.data.send(None)
self.Outputs.features.send(None)

def reset(self):
self.enable_used_attrs()
self.use_features_box.checkbox.setChecked(False)
if self.data is not None:
self.available_attrs[:] = []
self.used_attrs[:] = self.data.domain.attributes
Expand Down Expand Up @@ -476,6 +563,8 @@ def main(argv=None): # pragma: no cover
w = OWSelectAttributes()
data = Orange.data.Table(filename)
w.set_data(data)
w.set_features(AttributeList(data.domain.attributes[:2]))
w.handleNewSignals()
w.show()
w.raise_()
rval = app.exec_()
Expand Down
8 changes: 8 additions & 0 deletions Orange/widgets/data/tests/test_owrank.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from Orange.projection import PCA
from Orange.widgets.data.owrank import OWRank, ProblemType, CLS_SCORES, REG_SCORES
from Orange.widgets.tests.base import WidgetTest
from Orange.widgets.widget import AttributeList

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

def test_output_features(self):
self.send_signal(self.widget.Inputs.data, self.iris)
output = self.get_output(self.widget.Outputs.features)
self.assertIsInstance(output, AttributeList)
self.send_signal(self.widget.Inputs.data, None)
self.assertIsNone(self.get_output(self.widget.Outputs.features))

def test_scoring_method_problem_type(self):
"""Check scoring methods check boxes"""
self.send_signal(self.widget.Inputs.data, self.iris)
Expand Down
Loading

0 comments on commit 5d7e419

Please sign in to comment.