From cbaaac9e3d48314c16874cd57f50d7835b5829b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Primo=C5=BE=20Godec?=
Date: Sun, 1 Mar 2020 11:20:12 +0100
Subject: [PATCH] OWDomain: Merge less frequent values
---
Orange/widgets/data/oweditdomain.py | 319 ++++++++++++++----
.../widgets/data/tests/test_oweditdomain.py | 136 ++++++--
2 files changed, 360 insertions(+), 95 deletions(-)
diff --git a/Orange/widgets/data/oweditdomain.py b/Orange/widgets/data/oweditdomain.py
index c13a350752c..09fe5d71646 100644
--- a/Orange/widgets/data/oweditdomain.py
+++ b/Orange/widgets/data/oweditdomain.py
@@ -11,28 +11,25 @@
from contextlib import contextmanager
from collections import namedtuple, Counter
from functools import singledispatch, partial
-
from typing import (
Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,
FrozenSet, Type, Callable, TypeVar, Mapping, Hashable
)
+
+import numpy as np
+import pandas as pd
from AnyQt.QtWidgets import (
QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,
QToolButton, QLineEdit, QAction, QActionGroup, QGroupBox,
QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy, QToolTip,
- QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QShortcut,
- QStackedLayout
-)
+ QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,
+ QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox)
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QKeySequence, QIcon
from AnyQt.QtCore import (
- Qt, QEvent, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex,
- QRect,
+ Qt, QEvent, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
-import numpy as np
-import pandas as pd
-
import Orange.data
from Orange.preprocess.transformation import Transformation, Identity, Lookup
@@ -73,6 +70,20 @@ def __ne__(self, other):
def __hash__(self):
return hash((type(self), super().__hash__()))
+ def name_type(self):
+ """
+ Returns a tuple with name and type of the variable.
+ It is used since it is forbidden to use names of variables in settings.
+ """
+ type_number = {
+ "Categorical": 0,
+ "Ordered": 1,
+ "Real": 2,
+ "Time": 3,
+ "String": 4
+ }
+ return self.name, type_number[type(self).__name__]
+
#: An ordered sequence of key, value pairs (variable annotations)
AnnotationsType = Tuple[Tuple[str, str], ...]
@@ -662,6 +673,164 @@ def on_label_selection_changed(self):
self.remove_label_action.setEnabled(bool(len(selected)))
+class GroupItemsDialog(QDialog):
+ """
+ A dialog for group less frequent values.
+ """
+ DEFAULT_LABEL = "other"
+
+ def __init__(
+ self, variable: Categorical, data: Union[np.ndarray, List],
+ selected_attributes: List[str], parent: QWidget = None,
+ flags: Qt.WindowFlags = Qt.Dialog, **kwargs
+ ) -> None:
+ super().__init__(parent, flags, **kwargs)
+ self.variable = variable
+ self.data = data
+ self.selected_attributes = selected_attributes
+
+ # grouping strategy
+ self.selected_radio = radio1 = QRadioButton("Group selected values")
+ self.frequent_abs_radio = radio2 = QRadioButton(
+ "Group values with less than"
+ )
+ self.frequent_rel_radio = radio3 = QRadioButton(
+ "Group values with less than"
+ )
+ self.n_values_radio = radio4 = QRadioButton(
+ "Group all except"
+ )
+
+ # if selected attributes available check the first radio button,
+ # othervise disable it
+ if selected_attributes:
+ radio1.setChecked(True)
+ else:
+ radio1.setEnabled(False)
+ radio2.setChecked(True)
+
+ label2 = QLabel("occurrences")
+ label3 = QLabel("occurrences")
+ label4 = QLabel("most frequent values")
+
+ self.frequent_abs_spin = spin2 = QSpinBox()
+ max_val = len(data)
+ spin2.setMinimum(1)
+ spin2.setMaximum(max_val)
+ spin2.setValue(10)
+ spin2.setMinimumWidth(
+ self.fontMetrics().width("X") * (len(str(max_val)) + 1) + 20
+ )
+ spin2.valueChanged.connect(self._frequent_abs_spin_changed)
+
+ self.frequent_rel_spin = spin3 = QDoubleSpinBox()
+ spin3.setMinimum(0)
+ spin3.setDecimals(1)
+ spin3.setSingleStep(0.1)
+ spin3.setMaximum(100)
+ spin3.setValue(10)
+ spin3.setMinimumWidth(self.fontMetrics().width("X") * (2 + 1) + 20)
+ spin3.setSuffix(" %")
+ spin3.valueChanged.connect(self._frequent_rel_spin_changed)
+
+ self.n_values_spin = spin4 = QSpinBox()
+ max_val = min(10, len(variable.categories))
+ spin4.setMinimum(0)
+ spin4.setMaximum(len(variable.categories))
+ spin4.setValue(max_val)
+ spin4.setMinimumWidth(
+ self.fontMetrics().width("X") * (len(str(max_val)) + 1) + 20
+ )
+ spin4.valueChanged.connect(self._n_values_spin_spin_changed)
+
+ grid_layout = QGridLayout()
+ # first row
+ grid_layout.addWidget(radio1, 0, 0, 1, 2)
+ # second row
+ grid_layout.addWidget(radio2, 1, 0, 1, 2)
+ grid_layout.addWidget(spin2, 1, 2)
+ grid_layout.addWidget(label2, 1, 3)
+ # third row
+ grid_layout.addWidget(radio3, 2, 0, 1, 2)
+ grid_layout.addWidget(spin3, 2, 2)
+ grid_layout.addWidget(label3, 2, 3)
+ # fourth row
+ grid_layout.addWidget(radio4, 3, 0)
+ grid_layout.addWidget(spin4, 3, 1)
+ grid_layout.addWidget(label4, 3, 2, 1, 2)
+
+ group_box = QGroupBox()
+ group_box.setLayout(grid_layout)
+
+ # grouped variable name
+ new_name_label = QLabel("New value name: ")
+ self.new_name_line_edit = n_line_edit = QLineEdit(self.DEFAULT_LABEL)
+ # it is shown gray when user removes the text and let user know that
+ # word others is default one
+ n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)
+ name_hlayout = QHBoxLayout()
+ name_hlayout.addWidget(new_name_label)
+ name_hlayout.addWidget(n_line_edit)
+
+ # confirm_button = QPushButton("Apply")
+ # cancel_button = QPushButton("Cancel")
+ buttons = QDialogButtonBox(
+ orientation=Qt.Horizontal,
+ standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),
+ objectName="dialog-button-box",
+ )
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+
+ # join components
+ self.setLayout(QVBoxLayout())
+ self.layout().addWidget(group_box)
+ self.layout().addLayout(name_hlayout)
+ self.layout().addWidget(buttons)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+
+ def _frequent_abs_spin_changed(self) -> None:
+ self.frequent_abs_radio.setChecked(True)
+
+ def _n_values_spin_spin_changed(self) -> None:
+ self.n_values_radio.setChecked(True)
+
+ def _frequent_rel_spin_changed(self) -> None:
+ self.frequent_rel_radio.setChecked(True)
+
+ def get_merge_attributes(self) -> List[str]:
+ """
+ Returns attributes that will be merged
+
+ Returns
+ -------
+ List of attributes' to be merged names
+ """
+ counts = Counter(self.data)
+ if self.selected_radio.isChecked():
+ return self.selected_attributes
+ elif self.n_values_radio.isChecked():
+ keep_values = self.n_values_spin.value()
+ values = counts.most_common()[keep_values:]
+ indices = [i for i, _ in values]
+ elif self.frequent_abs_radio.isChecked():
+ indices = [v for v, c in counts.most_common()
+ if c < self.frequent_abs_spin.value()]
+ else: # self.frequent_rel_radio.isChecked():
+ n_all = sum(counts.values())
+ indices = [v for v, c in counts.most_common()
+ if c / n_all * 100 < self.frequent_rel_spin.value()]
+ return np.array(self.variable.categories)[indices].tolist()
+
+ def get_merged_value_name(self) -> str:
+ """
+ Returns
+ -------
+ New label of merged values
+ """
+ return self.new_name_line_edit.text() or self.DEFAULT_LABEL
+
+
@contextmanager
def disconnected(signal, slot, connection_type=Qt.AutoConnection):
signal.disconnect(slot)
@@ -847,6 +1016,9 @@ class DiscreteVariableEditor(VariableEditor):
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ self._values = None
+
form = self.layout().itemAt(0)
assert isinstance(form, QFormLayout)
self.ordered_cb = QCheckBox(
@@ -916,8 +1088,7 @@ def __init__(self, *args, **kwargs):
objectName="action-merge-item",
toolTip="Merge selected items.",
shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
- shortcutContext=Qt.WidgetShortcut,
- enabled=False,
+ shortcutContext=Qt.WidgetShortcut
)
self.add_new_item.triggered.connect(self._add_category)
@@ -945,8 +1116,7 @@ def __init__(self, *args, **kwargs):
accessibleName="Merge",
)
self.values_edit.addActions([self.move_value_up, self.move_value_down,
- self.add_new_item, self.remove_item,
- self.merge_items])
+ self.add_new_item, self.remove_item])
hlayout.addWidget(button1)
hlayout.addWidget(button2)
hlayout.addSpacing(3)
@@ -969,12 +1139,18 @@ def __init__(self, *args, **kwargs):
QWidget.setTabOrder(button3, button4)
def set_data(self, var, transform=()):
- # type: (Optional[Categorical], Sequence[Transform]) -> None
+ raise NotImplementedError
+
+ def set_data_categorical(self, var, values, transform=()):
+ # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None
"""
Set the variable to edit.
+
+ `values` is needed for categorical features to perform grouping.
"""
# pylint: disable=too-many-branches
- super().set_data(var, transform)
+ super().set_data(var, transform=transform)
+ self._values = values
tr = None # type: Optional[CategoriesMapping]
ordered = None # type: Optional[ChangeOrdered]
for tr_ in transform:
@@ -1118,7 +1294,6 @@ def on_value_selection_changed(self):
len(rows) > 1 and \
not any(index.data(EditStateRole) != ItemEditState.NoState
for index in rows)
- self.merge_items.setEnabled(enable_merge)
if len(rows) == 1:
i = rows[0].row()
@@ -1182,65 +1357,63 @@ def _add_category(self):
view.edit(index)
self.on_values_changed()
- def _merge_categories(self):
+ def _reset_name_merge(self) -> None:
+ """
+ This function resets renamed and merged variables in the model.
"""
- Merge selected categories into one.
+ view = self.values_edit
+ model = view.model() # type: QAbstractItemModel
+ prows = [
+ QPersistentModelIndex(model.index(i, 0))
+ for i in range(model.rowCount())
+ ]
+ with disconnected(model.dataChanged, self.on_values_changed):
+ for prow in prows:
+ if prow.isValid():
+ model.setData(
+ QModelIndex(prow), prow.data(SourceNameRole),
+ Qt.EditRole
+ )
+ self.variable_changed.emit()
- Popup an editable combo box for selection/edit of a new value.
+ def _merge_categories(self) -> None:
+ """
+ Merge less common categories into one with the dialog for merge
+ selection.
"""
view = self.values_edit
model = view.model() # type: QAbstractItemModel
- rows = view.selectedIndexes() # type: List[QModelIndex]
- if not len(rows) >= 2:
- return # pragma: no cover
- first_row = rows[0]
- def mapRectTo(widget, parent, rect):
- # type: (QWidget, QWidget, QRect) -> QRect
- return QRect(
- widget.mapTo(parent, rect.topLeft()),
- rect.size(),
- )
+ selected_attributes = [ind.data() for ind in view.selectedIndexes()]
- def mapRectToGlobal(widget, rect):
- # type: (QWidget, QRect) -> QRect
- return QRect(
- widget.mapToGlobal(rect.topLeft()),
- rect.size(),
- )
- view.scrollTo(first_row)
- vport = view.viewport()
- vrect = view.visualRect(first_row)
- vrect = mapRectTo(vport, view, vrect)
- vrect = vrect.intersected(vport.geometry())
- vrect = mapRectToGlobal(vport, vrect)
-
- cb = QComboBox(editable=True, insertPolicy=QComboBox.InsertAtBottom)
- cb.setAttribute(Qt.WA_DeleteOnClose)
- sh = QShortcut(QKeySequence(QKeySequence.Cancel), cb)
- sh.activated.connect(cb.close)
- cb.setParent(self, Qt.Popup)
- cb.move(vrect.topLeft())
-
- cb.addItems(
- list(unique(str(row.data(Qt.EditRole)) for row in rows)))
- prows = [QPersistentModelIndex(row) for row in rows]
-
- def complete_merge(text):
+ dlg = GroupItemsDialog(
+ self.var, self._values, selected_attributes, self,
+ windowTitle="Import Options",
+ sizeGripEnabled=True,
+ )
+ dlg.setWindowModality(Qt.WindowModal)
+ status = dlg.exec_()
+ dlg.deleteLater()
+
+ prows = [
+ QPersistentModelIndex(model.index(i, 0))
+ for i in range(model.rowCount())
+ ]
+
+ def complete_merge(text, merge_attributes):
# write the new text for edit role in all rows
+ self._reset_name_merge()
with disconnected(model.dataChanged, self.on_values_changed):
for prow in prows:
- if prow.isValid():
+ if (prow.isValid()
+ and prow.data(SourceNameRole) in merge_attributes):
model.setData(QModelIndex(prow), text, Qt.EditRole)
- cb.close()
self.variable_changed.emit()
- cb.activated[str].connect(complete_merge)
- size = cb.sizeHint().expandedTo(vrect.size())
- cb.resize(size)
- cb.show()
- cb.raise_()
- cb.setFocus(Qt.PopupFocusReason)
+ if status == QDialog.Accepted:
+ complete_merge(
+ dlg.get_merged_value_name(), dlg.get_merge_attributes()
+ )
def _set_ordered(self, ordered):
self.ordered_cb.setChecked(ordered)
@@ -1438,7 +1611,10 @@ def set_data(self, data, transform=()): # pylint: disable=arguments-differ
if index != -1:
w = self.layout().currentWidget()
assert isinstance(w, VariableEditor)
- w.set_data(var, transform)
+ if isinstance(var, Categorical):
+ w.set_data_categorical(var, data.data(), transform=transform)
+ else:
+ w.set_data(var, transform=transform)
self.__history[var] = tuple(transform)
cb = w.findChild(QComboBox, "type-combo")
cb.setCurrentIndex(index)
@@ -1494,6 +1670,7 @@ def __reinterpret_activated(self, index):
var = self.var
self.__transform = transform
+ data = None
if transform is not None and self.__data is not None:
data = transform(self.__data)
var = data.vtype
@@ -1510,7 +1687,11 @@ def __reinterpret_activated(self, index):
w.variable_changed, self.variable_changed,
Qt.UniqueConnection
):
- w.set_data(var, tr)
+ if isinstance(w, DiscreteVariableEditor):
+ data = data or self.__data
+ w.set_data_categorical(var, data.data(), transform=tr)
+ else:
+ w.set_data(var, transform=tr)
self.variable_changed.emit()
@@ -1534,7 +1715,7 @@ class Error(widget.OWWidget.Error):
settings_version = 2
_domain_change_store = settings.ContextSetting({})
- _selected_item = settings.ContextSetting(None) # type: Optional[str]
+ _selected_item = settings.ContextSetting(None) # type: Optional[Tuple[str, int]]
want_control_area = False
@@ -1652,7 +1833,7 @@ def reset_selected(self):
with disconnected(editor.variable_changed,
self._on_variable_changed):
model.setData(midx, [], TransformRole)
- editor.set_data(var, [])
+ editor.set_data(var, transform=[])
self._invalidate()
def reset_all(self):
@@ -1711,7 +1892,7 @@ def _restore(self, ):
i = -1
if self._selected_item is not None:
for i, vec in enumerate(model):
- if vec.vtype.name == self._selected_item:
+ if vec.vtype.name_type() == self._selected_item:
break
if i == -1 and model.rowCount():
i = 0
@@ -1722,7 +1903,7 @@ def _restore(self, ):
def _on_selection_changed(self):
self.selected_index = self.selected_var_index()
if self.selected_index != -1:
- self._selected_item = self.variables_model[self.selected_index].vtype.name
+ self._selected_item = self.variables_model[self.selected_index].vtype.name_type()
else:
self._selected_item = None
self.open_editor(self.selected_index)
@@ -1739,7 +1920,7 @@ def open_editor(self, index):
if tr is None:
tr = []
editor = self._editor
- editor.set_data(vector, tr)
+ editor.set_data(vector, transform=tr)
editor.variable_changed.connect(
self._on_variable_changed, Qt.UniqueConnection
)
diff --git a/Orange/widgets/data/tests/test_oweditdomain.py b/Orange/widgets/data/tests/test_oweditdomain.py
index 51ff26c2af0..245989bf3d4 100644
--- a/Orange/widgets/data/tests/test_oweditdomain.py
+++ b/Orange/widgets/data/tests/test_oweditdomain.py
@@ -1,15 +1,17 @@
# Test methods with long descriptive names can omit docstrings
# pylint: disable=all
import pickle
+import unittest
from itertools import product
from unittest import TestCase
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
import numpy as np
from numpy.testing import assert_array_equal
from AnyQt.QtCore import QItemSelectionModel, Qt, QItemSelection
-from AnyQt.QtWidgets import QAction, QComboBox, QLineEdit, QStyleOptionViewItem
+from AnyQt.QtWidgets import QAction, QComboBox, QLineEdit, \
+ QStyleOptionViewItem, QDialog
from AnyQt.QtTest import QTest, QSignalSpy
from Orange.widgets.utils import colorpalettes
@@ -31,8 +33,8 @@
table_column_data, ReinterpretVariableEditor, CategoricalVector,
VariableEditDelegate, TransformRole,
RealVector, TimeVector, StringVector, make_dict_mapper, DictMissingConst,
- LookupMappingTransform, as_float_or_nan, column_str_repr
-)
+ LookupMappingTransform, as_float_or_nan, column_str_repr,
+ GroupItemsDialog)
from Orange.widgets.data.owcolor import OWColor, ColorRole
from Orange.widgets.tests.base import WidgetTest, GuiTest
from Orange.tests import test_filename, assert_array_nanequal
@@ -332,13 +334,14 @@ def test_discrete_editor(self):
self.assertEqual(w.get_data(), (None, []))
v = Categorical("C", ("a", "b", "c"), (("A", "1"), ("B", "b")))
- w.set_data(v)
+ values = [0, 0, 0, 1, 1, 2]
+ w.set_data_categorical(v, values)
self.assertEqual(w.name_edit.text(), v.name)
self.assertFalse(w.ordered_cb.isChecked())
self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))
self.assertEqual(w.get_data(), (v, []))
- w.set_data(None)
+ w.set_data_categorical(None, None)
self.assertEqual(w.name_edit.text(), "")
self.assertEqual(w.labels_model.get_dict(), {})
self.assertEqual(w.get_data(), (None, []))
@@ -348,17 +351,19 @@ def test_discrete_editor(self):
("b", None),
(None, "b")
]
- w.set_data(v, [CategoriesMapping(mapping)])
+ w.set_data_categorical(v, values, [CategoriesMapping(mapping)])
w.grab() # run delegate paint method
self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))
- w.set_data(v, [CategoriesMapping(mapping), ChangeOrdered(True)])
+ w.set_data_categorical(
+ v, values, [CategoriesMapping(mapping), ChangeOrdered(True)]
+ )
self.assertTrue(w.ordered_cb.isChecked())
self.assertEqual(
w.get_data()[1], [CategoriesMapping(mapping), ChangeOrdered(True)]
)
# test selection/deselection in the view
- w.set_data(v)
+ w.set_data_categorical(v, values)
view = w.values_edit
model = view.model()
assert model.rowCount()
@@ -373,7 +378,7 @@ def test_discrete_editor(self):
("b", "b"),
("c", "b")
]
- w.set_data(v, [CategoriesMapping(mapping)])
+ w.set_data_categorical(v, values, [CategoriesMapping(mapping)])
self.assertEqual(w.get_data()[1], [CategoriesMapping(mapping)])
self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 1)
self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 2)
@@ -389,7 +394,8 @@ def test_discrete_editor_add_remove_action(self):
w = DiscreteVariableEditor()
v = Categorical("C", ("a", "b", "c"),
(("A", "1"), ("B", "b")))
- w.set_data(v)
+ values = [0, 0, 0, 1, 1, 2]
+ w.set_data_categorical(v, values)
action_add = w.add_new_item
action_remove = w.remove_item
view = w.values_edit
@@ -423,13 +429,21 @@ def test_discrete_editor_add_remove_action(self):
self.assertGreaterEqual(len(changedspy), 1, "Did not change data")
w.grab()
+ # mocking exec_ make dialog never show - dialog blocks running until closed
+ @patch(
+ "Orange.widgets.data.oweditdomain.GroupLessFrequentItemsDialog.exec_",
+ Mock(side_effect=lambda: QDialog.Accepted)
+ )
def test_discrete_editor_merge_action(self):
+ """
+ This function check whether results of dialog have effect on
+ merging the attributes. The dialog itself is tested separately.
+ """
w = DiscreteVariableEditor()
v = Categorical("C", ("a", "b", "c"),
(("A", "1"), ("B", "b")))
- w.set_data(v)
- action = w.merge_items
- self.assertFalse(action.isEnabled())
+ w.set_data_categorical(v, [0, 0, 0, 1, 1, 2])
+
view = w.values_edit
model = view.model()
selmodel = view.selectionModel() # type: QItemSelectionModel
@@ -437,20 +451,12 @@ def test_discrete_editor_merge_action(self):
QItemSelection(model.index(0, 0), model.index(1, 0)),
QItemSelectionModel.ClearAndSelect
)
- self.assertTrue(action.isEnabled())
+
# trigger the action, then find the active popup, and simulate entry
- spy = QSignalSpy(w.variable_changed)
w.merge_items.trigger()
- cb = w.findChild(QComboBox)
- cb.setCurrentText("BA")
- cb.activated[str].emit("BA")
- cb.close()
- self.assertEqual(model.index(0, 0).data(Qt.EditRole), "BA")
- self.assertEqual(model.index(1, 0).data(Qt.EditRole), "BA")
-
- self.assertSequenceEqual(
- list(spy), [[]], 'variable_changed should emit exactly once'
- )
+
+ self.assertEqual(model.index(0, 0).data(Qt.EditRole), "Others")
+ self.assertEqual(model.index(1, 0).data(Qt.EditRole), "Others")
def test_time_editor(self):
w = TimeVariableEditor()
@@ -884,3 +890,81 @@ def test_pickle(self):
assert_array_equal(r, [np.nan, 0, 1, np.nan])
r_ = lookup_.transform(c)
assert_array_equal(r_, [np.nan, 0, 1, np.nan])
+
+
+class TestGroupLessFrequentItemsDialog(GuiTest):
+ def setUp(self) -> None:
+ self.v = Categorical("C", ("a", "b", "c"),
+ (("A", "1"), ("B", "b")))
+ self.data = [0, 0, 0, 1, 1, 2]
+
+ def test_dialog_open(self):
+ dialog = GroupItemsDialog(self.v, self.data, ["a", "b"])
+ self.assertTrue(dialog.selected_radio.isChecked())
+ self.assertFalse(dialog.frequent_abs_radio.isChecked())
+ self.assertFalse(dialog.frequent_rel_radio.isChecked())
+ self.assertFalse(dialog.n_values_radio.isChecked())
+
+ dialog = GroupItemsDialog(self.v, self.data, [])
+ self.assertFalse(dialog.selected_radio.isChecked())
+ self.assertTrue(dialog.frequent_abs_radio.isChecked())
+ self.assertFalse(dialog.frequent_rel_radio.isChecked())
+ self.assertFalse(dialog.n_values_radio.isChecked())
+
+ def test_group_selected(self):
+ dialog = GroupItemsDialog(self.v, self.data, ["a", "b"])
+ dialog.selected_radio.setChecked(True)
+ dialog.new_name_line_edit.setText("BA")
+
+ self.assertListEqual(dialog.get_merge_attributes(), ["a", "b"])
+ self.assertEqual(dialog.get_merged_value_name(), "BA")
+
+ def test_group_less_frequent_abs(self):
+ dialog = GroupItemsDialog(self.v, self.data, ["a", "b"])
+ dialog.frequent_abs_radio.setChecked(True)
+ dialog.frequent_abs_spin.setValue(3)
+ dialog.new_name_line_edit.setText("BA")
+
+ self.assertListEqual(dialog.get_merge_attributes(), ["b", "c"])
+ self.assertEqual(dialog.get_merged_value_name(), "BA")
+
+ dialog.frequent_abs_spin.setValue(2)
+ self.assertListEqual(dialog.get_merge_attributes(), ["c"])
+
+ dialog.frequent_abs_spin.setValue(1)
+ self.assertListEqual(dialog.get_merge_attributes(), [])
+
+ def test_group_less_frequent_rel(self):
+ dialog = GroupItemsDialog(self.v, self.data, ["a", "b"])
+ dialog.frequent_rel_radio.setChecked(True)
+ dialog.frequent_rel_spin.setValue(50)
+ dialog.new_name_line_edit.setText("BA")
+
+ self.assertListEqual(dialog.get_merge_attributes(), ["b", "c"])
+ self.assertEqual(dialog.get_merged_value_name(), "BA")
+
+ dialog.frequent_rel_spin.setValue(20)
+ self.assertListEqual(dialog.get_merge_attributes(), ["c"])
+
+ dialog.frequent_rel_spin.setValue(15)
+ self.assertListEqual(dialog.get_merge_attributes(), [])
+
+ def test_group_keep_n(self):
+ dialog = GroupItemsDialog(self.v, self.data, ["a", "b"])
+ dialog.n_values_radio.setChecked(True)
+ dialog.n_values_spin.setValue(1)
+ dialog.new_name_line_edit.setText("BA")
+
+ self.assertListEqual(dialog.get_merge_attributes(), ["b", "c"])
+ self.assertEqual(dialog.get_merged_value_name(), "BA")
+
+ dialog.n_values_spin.setValue(2)
+ self.assertListEqual(dialog.get_merge_attributes(), ["c"])
+
+ dialog.n_values_spin.setValue(3)
+ self.assertListEqual(dialog.get_merge_attributes(), [])
+
+
+if __name__ == '__main__':
+ unittest.main()
+