Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] OWTranspose: Add a new widget #1738

Merged
merged 5 commits into from Nov 28, 2016
Merged

[ENH] OWTranspose: Add a new widget #1738

merged 5 commits into from Nov 28, 2016

Conversation

VesnaT
Copy link
Contributor

@VesnaT VesnaT commented Nov 10, 2016

Issue

Transpose widget

Description of changes
Includes
  • Code changes
  • Tests
  • Documentation

@codecov-io
Copy link

codecov-io commented Nov 10, 2016

Current coverage is 88.92% (diff: 100%)

Merging #1738 into master will increase coverage by 0.06%

@@             master      #1738   diff @@
==========================================
  Files            82         82          
  Lines          8896       8952    +56   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           7905       7961    +56   
  Misses          991        991          
  Partials          0          0          

Sunburst

Powered by Codecov. Last update 1cf3e90...bad6ed1

@VesnaT VesnaT force-pushed the transpose branch 2 times, most recently from 759671a to 8c2c6e9 Compare November 10, 2016 11:33
Copy link
Contributor

@janezd janezd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few minor comments and suggestion (DomainModel would really simplify the widget, I guess), and one related to GUI itself.

As always, I appreciate the effort you put in tests - especially since I know how annoying it is to write them.

else variable.repr_val(row[i])

if value not in MISSING_VALUES:
attributes[j].attributes.update({variable.name: value})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the same as attributes[j].attributes[variable.name] = value?

self.X = table.X.T
attributes = [ContinuousVariable(str(row[feature_names_column]))
for row in table] if feature_names_column else \
[ContinuousVariable("Feature" + str(i + 1)) for i in range(n_cols)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space between Feature and number. Consider also putting the necessary number of leading zeros before the number (format to 1 + int(ceil(log(n_cols, 10))) places).

@@ -1430,6 +1431,90 @@ def _compute_contingency(self, col_vars=None, row_var=None):

return contingencies, unknown_rows

@classmethod
def transpose(cls, table, feature_names_column="", meta_attr_name="Meta1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Meta1" is rather uninformative name for a meta attribute. :) Should we call it "Feature name" instead?

dtype=object)
metas.append(StringVariable(meta_attr_name))

names = chain.from_iterable(list(attr.attributes.keys())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't list(attr.attributes.keys()) the same as list(attr.attributes)? (I don't like calling keys(), but it may be just me.)


# class_var - class_name to class_var with type of class_type
self.Y, class_vars = np.empty((self.n_rows, 0)), []
if class_name:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if class_name is not None unless you intentionally want to treat "" as false.

options = dict(callback=self.apply, orientation=Qt.Horizontal,
labelWidth=100, contentsLength=12)
self.feature_model = itemmodels.VariableListModel()
self.feature_combo = gui.comboBox(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this second combo. Until I read the code I had no clue what it actually does. I don't find the idea that you can transpose the transpose and get the original appealing and very useful. (I know it wasn't your idea.)

list(a.attributes.keys()) for a in self.data.domain.attributes)
variables = chain.from_iterable(
(DiscreteVariable(name), ContinuousVariable(name))
for name in sorted(set(names)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuck. I really don't like this transpose of transpose. This looks terrible, both in code as well as in GUI -- offering the same attribute twice, with the icon determining the type.

(DiscreteVariable(name), ContinuousVariable(name))
for name in sorted(set(names)))
self.class_model[:] += list(variables)
self.class_variable_index = min(2, len(self.class_model[:])) - 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find min(1, len(self.class_model[:]) - 1) more informative than using 2 since you're going to subtract 1. (Or have I misread it?)

Besides, you don't need [:]: VariableListModel defines __len__.

class_var = self.class_model[self.class_variable_index]
if class_var != "None":
options["class_name"] = class_var.name
options["class_type"] = "d" if class_var.is_discrete else "c"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If class_type argument was type, not str, you could use options["class_type"] = type(class_var) here.



if __name__ == "__main__":
from PyQt4.QtGui import QApplication
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnyQt?

@VesnaT VesnaT force-pushed the transpose branch 2 times, most recently from 4e4a14a to 9b71ba4 Compare November 15, 2016 14:55
@ajdapretnar
Copy link
Contributor

Two comments:

  • The widget doesn't show class variable to retrieve feature names by, but it does the retrieval. In other words, iris-setosa appears in feature name, but it doesn't appear as a drop-down in the widget.
  • Fails on sparse data. Well, the widget itself doesn't fail, Data Table does, which makes sense, but I think a warning would be nice ('Can't transpose sparse data.')

Big thumbs up for the widget! 👍

@VesnaT VesnaT changed the title [ENH] OWTranspose: Add a new widget [WIP][ENH] OWTranspose: Add a new widget Nov 24, 2016
box, self, "feature_names_column", orientation=Qt.Horizontal,
labelWidth=100, sendSelectedValue=True, callback=self.apply,
contentsLength=12, enabled=self.feature_type)
self.feature_combo.setModel(self.feature_model)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the combo like this:

        self.feature_combo = gui.comboBox(
            gui.indentedBox(
                box, gui.checkButtonOffsetHint(self.feature_radio.buttons[0])),
            self, "feature_names_column", orientation=Qt.Horizontal,
            labelWidth=100, sendSelectedValue=True, callback=self.apply,
            contentsLength=12, enabled=self.feature_type)

options = dict()
if self.feature_type:
options["feature_names_column"] = self.feature_names_column

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to remove this empty line.

resizing_enabled = False
want_main_area = False

settingsHandler = PerfectDomainContextHandler(metas_in_res=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why PerfectDomainContextHandler? I prefer DomainContextHandler when there is no need to match all attributes, because it matches more domains.


settingsHandler = PerfectDomainContextHandler(metas_in_res=True)
feature_type = ContextSetting(0)
feature_names_column = ContextSetting("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Variable, not str. You should use None as default.

box = gui.vBox(self.controlArea, "Feature names")
self.feature_radio = gui.radioButtonsInBox(
box, self, "feature_type", callback=self._feature_type_changed,
btnLabels=["Generic", "From meta attributes"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"From meta attribute: " (singular, colon)

@VesnaT VesnaT changed the title [WIP][ENH] OWTranspose: Add a new widget [ENH] OWTranspose: Add a new widget Nov 25, 2016
Copy link
Contributor

@janezd janezd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying to find the reason for the bug described below, I fixed it so I just added a commit to your PR. Please review and verify. Also, please fix my mistake at the bottom.

@@ -45,8 +43,7 @@ def __init__(self):
gui.indentedBox(
box, gui.checkButtonOffsetHint(self.feature_radio.buttons[0])),
self, "feature_names_column", callback=self._feature_combo_changed,
model=self.feature_model, sendSelectedValue=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendSelectedValue is inapplicable when the combo uses a model --- which always returns the object from the model.

@@ -45,8 +43,7 @@ def __init__(self):
gui.indentedBox(
box, gui.checkButtonOffsetHint(self.feature_radio.buttons[0])),
self, "feature_names_column", callback=self._feature_combo_changed,
model=self.feature_model, sendSelectedValue=True,
labelWidth=100, contentsLength=12, orientation=Qt.Horizontal)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this since there is no label.

@@ -57,20 +54,23 @@ def _feature_combo_changed(self):
self.apply()

def set_data(self, data):
self.closeContext()
# Skip the context if the combo is empty: a context with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent an hour hunting this down. The problem is that if you take Iris, it sets feature_names_column to None. The you take zoo. The Iris' context matches for zoo domain since it doesn't require any attribute - hence it will set feature_names_column to None. For the bug to be harder to track, the combo will not reflect this since it does not contain a None value.

This is not a bug in DomainContextHandler, but it will appear in all such cases. @astaric, what about preventing a match when no attribute were matched since no matcher were required?

PerfectDomainContextHandler would work here. :) But I still think that widgets shouldn't use PerfectDomainContextHandler unless unavoidable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be the reason I used it in a first place.

self.apply()

def update_controls(self):
self.feature_model.set_domain(None)
if self.data:
self.feature_model.set_domain(self.data.domain)
if len(self.feature_model):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyListModel has __bool__ defined.

self.apply()

def update_controls(self):
self.feature_model.set_domain(None)
if self.data:
self.feature_model.set_domain(self.data.domain)
if len(self.feature_model):
_names = [m.name for m in self.data.domain.metas if m.is_string]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The beauty of the domain model is that you don't need to do this. Besides, it should use m, not m.name.

if len(self.feature_model):
_names = [m.name for m in self.data.domain.metas if m.is_string]
self.feature_names_column = _names[0]
enabled = bool(len(self.feature_model))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, bool suffices here.

try:
transposed = Table.transpose(self.data, **options)
transposed = Table.transpose(
self.data, feature_names_column=self.feature_names_column)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit wrong, but I wouldn't like to push -f over it, and lose the comments I made above. Please change this to .... feature_names_column = self.feature_type and self.feature_names_column.

@@ -101,6 +99,8 @@ def send_report(self):

app = QApplication([])
ow = OWTranspose()
d = Table("iris")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case where it doesn't work. (To see this, add print(self.feature_names_column) after self.openContext().)

@janezd
Copy link
Contributor

janezd commented Nov 25, 2016

The comments above are meaningless unless you read them in a diff, here https://github.com/biolab/orange3/pull/1738/files/8576ec1dd4675a04bcff0e1d4e1dda48116aff1b..99b80e860952d38d2ad2c9e8f4840adbc3e145cc.

# GUI
box = gui.vBox(self.controlArea, "Feature names")
self.feature_radio = gui.radioButtonsInBox(
box, self, "feature_type", callback=self.apply,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing (sorry!): changing the radio button always applies even if the auto-apply is off. Change callback=self.apply to callback=lambda: self.apply().

(The problem is that callback is bound to the original self.apply before gui.auto_commit replaces it with the conditional apply.)

@janezd
Copy link
Contributor

janezd commented Nov 28, 2016

Icon contains invisible lines to the left, and the file is rather long. The picture contains two smaller tables; larger, less complex symbols work better, imho. Please consider the icon in the attachment.

Transpose.svg.zip

@janezd
Copy link
Contributor

janezd commented Nov 28, 2016

I added a commit with a fix for auto apply, and took the liberty to change the icon.

@janezd janezd merged commit f3f5ca0 into biolab:master Nov 28, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants