Skip to content

Commit ab706a0

Browse files
authored
Merge pull request #3535 from ales-erjavec/oweditdomain-ordered
[ENH] Edit Domain: Add support for ordered categorical variables
2 parents 87696a7 + 2518044 commit ab706a0

File tree

2 files changed

+92
-14
lines changed

2 files changed

+92
-14
lines changed

Orange/widgets/data/oweditdomain.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,
2121
QToolButton, QLineEdit, QAction, QActionGroup, QStackedWidget, QGroupBox,
2222
QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip,
23-
QDialogButtonBox, QPushButton
23+
QDialogButtonBox, QPushButton, QCheckBox
2424
)
2525
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon
2626
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
@@ -51,6 +51,10 @@ class Categorical(
5151
])): pass
5252

5353

54+
class Ordered(Categorical):
55+
pass
56+
57+
5458
class Real(
5559
NamedTuple("Real", [
5660
("name", str),
@@ -130,8 +134,14 @@ def __call__(self, var):
130134
return var._replace(annotations=self.annotations)
131135

132136

133-
Transform = Union[Rename, CategoriesMapping, Annotate]
134-
TransformTypes = (Rename, CategoriesMapping, Annotate)
137+
class ChangeOrdered(NamedTuple("ChangeOrdered", [("ordered", bool)])):
138+
"""
139+
Change Categorical <-> Ordered
140+
"""
141+
142+
143+
Transform = Union[Rename, CategoriesMapping, Annotate, ChangeOrdered]
144+
TransformTypes = (Rename, CategoriesMapping, Annotate, ChangeOrdered)
135145

136146

137147
def deconstruct(obj):
@@ -465,7 +475,10 @@ def __init__(self, *args, **kwargs):
465475
super().__init__(*args, **kwargs)
466476
form = self.layout().itemAt(0)
467477
assert isinstance(form, QFormLayout)
468-
478+
self.ordered_cb = QCheckBox(
479+
"Ordered", self, toolTip="Is this an ordered categorical."
480+
)
481+
self.ordered_cb.toggled.connect(self._set_ordered)
469482
#: A list model of discrete variable's values.
470483
self.values_model = itemmodels.PyListModel(
471484
flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
@@ -553,9 +566,12 @@ def __init__(self, *args, **kwargs):
553566
hlayout.addStretch(10)
554567
vlayout.addLayout(hlayout)
555568

556-
form.insertRow(1, "Values:", vlayout)
569+
form.insertRow(1, "", self.ordered_cb)
570+
form.insertRow(2, "Values:", vlayout)
571+
572+
QWidget.setTabOrder(self.name_edit, self.ordered_cb)
573+
QWidget.setTabOrder(self.ordered_cb, self.values_edit)
557574

558-
QWidget.setTabOrder(self.name_edit, self.values_edit)
559575
QWidget.setTabOrder(self.values_edit, button1)
560576
QWidget.setTabOrder(button1, button2)
561577
QWidget.setTabOrder(button2, button3)
@@ -566,11 +582,15 @@ def set_data(self, var, transform=()):
566582
"""
567583
Set the variable to edit.
568584
"""
585+
# pylint: disable=too-many-branches
569586
super().set_data(var, transform)
570587
tr = None # type: Optional[CategoriesMapping]
588+
ordered = None # type: Optional[ChangeOrdered]
571589
for tr_ in transform:
572590
if isinstance(tr_, CategoriesMapping):
573591
tr = tr_
592+
if isinstance(tr_, ChangeOrdered):
593+
ordered = tr_
574594

575595
items = []
576596
if tr is not None:
@@ -622,6 +642,10 @@ def set_data(self, var, transform=()):
622642
self.values_model.index(i, 0),
623643
item
624644
)
645+
if ordered is not None:
646+
self.ordered_cb.setChecked(ordered.ordered)
647+
elif var is not None:
648+
self.ordered_cb.setChecked(isinstance(var, Ordered))
625649
self.add_new_item.actionGroup().setEnabled(var is not None)
626650

627651
def __categories_mapping(self):
@@ -657,6 +681,9 @@ def get_data(self):
657681
if any(_1 != _2 or _2 != _3
658682
for (_1, _2), _3 in zip_longest(mapping, var.categories)):
659683
tr.append(CategoriesMapping(mapping))
684+
ordered = self.ordered_cb.isChecked()
685+
if ordered != isinstance(var, Ordered):
686+
tr.append(ChangeOrdered(ordered))
660687
return var, tr
661688

662689
def clear(self):
@@ -753,6 +780,10 @@ def _add_category(self):
753780
view.edit(index)
754781
self.on_values_changed()
755782

783+
def _set_ordered(self, ordered):
784+
self.ordered_cb.setChecked(ordered)
785+
self.variable_changed.emit()
786+
756787

757788
class ContinuousVariableEditor(VariableEditor):
758789
# TODO: enable editing of display format...
@@ -766,7 +797,7 @@ class TimeVariableEditor(VariableEditor):
766797

767798
def variable_icon(var):
768799
# type: (Variable) -> QIcon
769-
if isinstance(var, Categorical):
800+
if isinstance(var, (Categorical, Ordered)):
770801
return gui.attributeIconDict[1]
771802
elif isinstance(var, Real):
772803
return gui.attributeIconDict[2]
@@ -835,7 +866,7 @@ class OWEditDomain(widget.OWWidget):
835866
description = "Rename variables, edit categories and variable annotations."
836867
icon = "icons/EditDomain.svg"
837868
priority = 3125
838-
keywords = []
869+
keywords = ["rename", "drop", "reorder", "order"]
839870

840871
class Inputs:
841872
data = Input("Data", Orange.data.Table)
@@ -1037,7 +1068,7 @@ def open_editor(self, index):
10371068
tr = []
10381069

10391070
editors = {
1040-
Categorical: 0,
1071+
Categorical: 0, Ordered: 0,
10411072
Real: 1,
10421073
Time: 2,
10431074
String: 3
@@ -1248,7 +1279,7 @@ def i(text):
12481279
def text(text):
12491280
return "<span>{}</span>".format(escape(text))
12501281
assert trs
1251-
rename = annotate = catmap = None
1282+
rename = annotate = catmap = ordered = None
12521283

12531284
for tr in trs:
12541285
if isinstance(tr, Rename):
@@ -1257,10 +1288,17 @@ def text(text):
12571288
annotate = tr
12581289
elif isinstance(tr, CategoriesMapping):
12591290
catmap = tr
1291+
elif isinstance(tr, ChangeOrdered):
1292+
ordered = tr
1293+
12601294
if rename is not None:
12611295
header = "{} → {}".format(var.name, rename.name)
12621296
else:
12631297
header = var.name
1298+
if ordered is not None and ordered.ordered != isinstance(var, Ordered):
1299+
assert isinstance(var, (Categorical, Ordered))
1300+
header += " (changed to {})".format(
1301+
"ordered" if ordered.ordered else "unordered")
12641302
values_section = None
12651303
if catmap is not None:
12661304
values_section = ("Values", [])
@@ -1328,7 +1366,10 @@ def abstract(var):
13281366
if isinstance(var, Orange.data.DiscreteVariable):
13291367
values, base = var.values, var.base_value
13301368
base = values[base] if base >= 0 else None
1331-
return Categorical(var.name, tuple(values), base, annotations)
1369+
if var.ordered:
1370+
return Ordered(var.name, tuple(values), base, annotations)
1371+
else:
1372+
return Categorical(var.name, tuple(values), base, annotations)
13321373
elif isinstance(var, Orange.data.TimeVariable):
13331374
return Time(var.name, annotations)
13341375
elif isinstance(var, Orange.data.ContinuousVariable):
@@ -1360,16 +1401,20 @@ def apply_transform(var, trs):
13601401
@apply_transform.register(Orange.data.DiscreteVariable)
13611402
def apply_transform_discete(var, trs):
13621403
# type: (Orange.data.DiscreteVariable, ...) -> ...
1404+
# pylint: disable=too-many-branches
13631405
name, annotations = var.name, var.attributes
13641406
base_value = var.base_value
13651407
mapping = None
1408+
ordered = var.ordered
13661409
for tr in trs:
13671410
if isinstance(tr, Rename):
13681411
name = tr.name
13691412
elif isinstance(tr, CategoriesMapping):
13701413
mapping = tr.mapping
13711414
elif isinstance(tr, Annotate):
13721415
annotations = _parse_attributes(tr.annotations)
1416+
elif isinstance(tr, ChangeOrdered):
1417+
ordered = tr.ordered
13731418

13741419
source_values = var.values
13751420
if mapping is not None:
@@ -1399,7 +1444,8 @@ def positions(values):
13991444
else:
14001445
lookup = Identity(var)
14011446
variable = Orange.data.DiscreteVariable(
1402-
name, values=dest_values, base_value=base_value, compute_value=lookup
1447+
name, values=dest_values, base_value=base_value, compute_value=lookup,
1448+
ordered=ordered,
14031449
)
14041450
variable.attributes.update(annotations)
14051451
return variable

Orange/widgets/data/tests/test_oweditdomain.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
OWEditDomain,
1818
ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,
1919
TimeVariableEditor, Categorical, Real, Time, String,
20-
Rename, Annotate, CategoriesMapping, report_transform,
21-
apply_transform
20+
Rename, Annotate, CategoriesMapping, ChangeOrdered, report_transform,
21+
apply_transform,
2222
)
2323
from Orange.widgets.data.owcolor import OWColor, ColorRole
2424
from Orange.widgets.tests.base import WidgetTest, GuiTest
@@ -53,6 +53,12 @@ def test_categories_mapping(self):
5353
self.assertIn("b", r)
5454
self.assertIn("<s>", r)
5555

56+
def test_change_ordered(self):
57+
var = Categorical("C", ("a", "b"), None, ())
58+
tr = ChangeOrdered(True)
59+
r = report_transform(var, [tr])
60+
self.assertIn("ordered", r)
61+
5662

5763
class TestOWEditDomain(WidgetTest):
5864
def setUp(self):
@@ -166,6 +172,21 @@ def test_time_variable_preservation(self):
166172
output = self.get_output(self.widget.Outputs.data)
167173
self.assertEqual(str(table[0, 4]), str(output[0, 4]))
168174

175+
def test_change_ordered(self):
176+
"""Test categorical ordered flag change"""
177+
table = Table(Domain(
178+
[DiscreteVariable("A", values=["a", "b"], ordered=True)]))
179+
self.send_signal(self.widget.Inputs.data, table)
180+
output = self.get_output(self.widget.Outputs.data)
181+
self.assertTrue(output.domain[0].ordered)
182+
183+
editor = self.widget.findChild(DiscreteVariableEditor)
184+
assert isinstance(editor, DiscreteVariableEditor)
185+
editor.ordered_cb.setChecked(False)
186+
self.widget.commit()
187+
output = self.get_output(self.widget.Outputs.data)
188+
self.assertFalse(output.domain[0].ordered)
189+
169190

170191
class TestEditors(GuiTest):
171192
def test_variable_editor(self):
@@ -217,6 +238,7 @@ def test_discrete_editor(self):
217238
w.set_data(v)
218239

219240
self.assertEqual(w.name_edit.text(), v.name)
241+
self.assertFalse(w.ordered_cb.isChecked())
220242
self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))
221243
self.assertEqual(w.get_data(), (v, []))
222244
w.set_data(None)
@@ -233,6 +255,11 @@ def test_discrete_editor(self):
233255
w.grab() # run delegate paint method
234256
self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))
235257

258+
w.set_data(v, [CategoriesMapping(mapping), ChangeOrdered(True)])
259+
self.assertTrue(w.ordered_cb.isChecked())
260+
self.assertEqual(
261+
w.get_data()[1], [CategoriesMapping(mapping), ChangeOrdered(True)]
262+
)
236263
# test selection/deselection in the view
237264
w.set_data(v)
238265
view = w.values_edit
@@ -299,6 +326,11 @@ def test_discrete_reorder(self):
299326
DD.compute_value, Lookup(D, np.array([2, 3, 1, 0]))
300327
)
301328

329+
def test_ordered_change(self):
330+
D = DiscreteVariable("D", values=("a", "b"), ordered=True)
331+
Do = apply_transform(D, [ChangeOrdered(False)])
332+
self.assertFalse(Do.ordered)
333+
302334
def test_discrete_add_drop(self):
303335
D = DiscreteVariable("D", values=("2", "3", "1", "0"), base_value=1)
304336
mapping = (

0 commit comments

Comments
 (0)