-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
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
There was a problem hiding this 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}) |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AnyQt?
4e4a14a
to
9b71ba4
Compare
Two comments:
Big thumbs up for the widget! 👍 |
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.
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 | ||
|
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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("") |
There was a problem hiding this comment.
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"]) |
There was a problem hiding this comment.
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)
There was a problem hiding this 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, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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()
.)
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, |
There was a problem hiding this comment.
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.)
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