[ENH] OWTranspose: Add a new widget#1738
Conversation
Current coverage is 88.92% (diff: 100%)@@ 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
|
759671a to
8c2c6e9
Compare
janezd
left a comment
There was a problem hiding this comment.
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.
Orange/data/table.py
Outdated
| else variable.repr_val(row[i]) | ||
|
|
||
| if value not in MISSING_VALUES: | ||
| attributes[j].attributes.update({variable.name: value}) |
There was a problem hiding this comment.
Isn't this the same as attributes[j].attributes[variable.name] = value?
Orange/data/table.py
Outdated
| 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)] |
There was a problem hiding this comment.
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).
Orange/data/table.py
Outdated
| return contingencies, unknown_rows | ||
|
|
||
| @classmethod | ||
| def transpose(cls, table, feature_names_column="", meta_attr_name="Meta1", |
There was a problem hiding this comment.
"Meta1" is rather uninformative name for a meta attribute. :) Should we call it "Feature name" instead?
Orange/data/table.py
Outdated
| dtype=object) | ||
| metas.append(StringVariable(meta_attr_name)) | ||
|
|
||
| names = chain.from_iterable(list(attr.attributes.keys()) |
There was a problem hiding this comment.
Isn't list(attr.attributes.keys()) the same as list(attr.attributes)? (I don't like calling keys(), but it may be just me.)
Orange/data/table.py
Outdated
|
|
||
| # 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: |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.)
Orange/widgets/data/owtranspose.py
Outdated
| 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))) |
There was a problem hiding this comment.
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.
Orange/widgets/data/owtranspose.py
Outdated
| (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 |
There was a problem hiding this comment.
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__.
Orange/widgets/data/owtranspose.py
Outdated
| 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" |
There was a problem hiding this comment.
If class_type argument was type, not str, you could use options["class_type"] = type(class_var) here.
Orange/widgets/data/owtranspose.py
Outdated
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| from PyQt4.QtGui import QApplication |
4e4a14a to
9b71ba4
Compare
|
Two comments:
Big thumbs up for the widget! 👍 |
Orange/widgets/data/owtranspose.py
Outdated
| 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) |
There was a problem hiding this comment.
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)
Orange/widgets/data/owtranspose.py
Outdated
| options = dict() | ||
| if self.feature_type: | ||
| options["feature_names_column"] = self.feature_names_column | ||
|
|
There was a problem hiding this comment.
Feel free to remove this empty line.
Orange/widgets/data/owtranspose.py
Outdated
| resizing_enabled = False | ||
| want_main_area = False | ||
|
|
||
| settingsHandler = PerfectDomainContextHandler(metas_in_res=True) |
There was a problem hiding this comment.
Why PerfectDomainContextHandler? I prefer DomainContextHandler when there is no need to match all attributes, because it matches more domains.
Orange/widgets/data/owtranspose.py
Outdated
|
|
||
| settingsHandler = PerfectDomainContextHandler(metas_in_res=True) | ||
| feature_type = ContextSetting(0) | ||
| feature_names_column = ContextSetting("") |
There was a problem hiding this comment.
This is Variable, not str. You should use None as default.
Orange/widgets/data/owtranspose.py
Outdated
| 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"]) |
There was a problem hiding this comment.
"From meta attribute: " (singular, colon)
janezd
left a comment
There was a problem hiding this comment.
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.
Orange/widgets/data/owtranspose.py
Outdated
| 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, |
There was a problem hiding this comment.
sendSelectedValue is inapplicable when the combo uses a model --- which always returns the object from the model.
Orange/widgets/data/owtranspose.py
Outdated
| 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) |
There was a problem hiding this comment.
You don't need this since there is no label.
|
|
||
| def set_data(self, data): | ||
| self.closeContext() | ||
| # Skip the context if the combo is empty: a context with |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This could be the reason I used it in a first place.
Orange/widgets/data/owtranspose.py
Outdated
| self.feature_model.set_domain(None) | ||
| if self.data: | ||
| self.feature_model.set_domain(self.data.domain) | ||
| if len(self.feature_model): |
There was a problem hiding this comment.
PyListModel has __bool__ defined.
Orange/widgets/data/owtranspose.py
Outdated
| 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] |
There was a problem hiding this comment.
The beauty of the domain model is that you don't need to do this. Besides, it should use m, not m.name.
Orange/widgets/data/owtranspose.py
Outdated
| 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)) |
Orange/widgets/data/owtranspose.py
Outdated
| try: | ||
| transposed = Table.transpose(self.data, **options) | ||
| transposed = Table.transpose( | ||
| self.data, feature_names_column=self.feature_names_column) |
There was a problem hiding this comment.
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.
|
|
||
| app = QApplication([]) | ||
| ow = OWTranspose() | ||
| d = Table("iris") |
There was a problem hiding this comment.
This is the case where it doesn't work. (To see this, add print(self.feature_names_column) after self.openContext().)
|
The comments above are meaningless unless you read them in a diff, here https://github.com/biolab/orange3/pull/1738/files/8576ec1dd4675a04bcff0e1d4e1dda48116aff1b..99b80e860952d38d2ad2c9e8f4840adbc3e145cc. |
Orange/widgets/data/owtranspose.py
Outdated
| # GUI | ||
| box = gui.vBox(self.controlArea, "Feature names") | ||
| self.feature_radio = gui.radioButtonsInBox( | ||
| box, self, "feature_type", callback=self.apply, |
There was a problem hiding this comment.
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.)
|
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. |
|
I added a commit with a fix for auto apply, and took the liberty to change the icon. |
Issue
Transpose widget
Description of changes
Includes