Skip to content

Commit

Permalink
Merge pull request #4990 from janezd/vizrank-options
Browse files Browse the repository at this point in the history
Mosaic Vizrank with fixed number of variables
  • Loading branch information
ajdapretnar committed Sep 24, 2020
2 parents aac22ce + c0dad7d commit ba08e0e
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 52 deletions.
57 changes: 39 additions & 18 deletions Orange/widgets/visualize/owmosaic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from AnyQt.QtWidgets import (
QGraphicsScene, QGraphicsLineItem, QGraphicsItemGroup)

from Orange.data import Table, filter, Variable, Domain
from Orange.data import Table, filter, Variable, Domain, DiscreteVariable
from Orange.data.sql.table import SqlTable, LARGE_TABLE, DEFAULT_SAMPLE_TIME
from Orange.preprocess import Discretize
from Orange.preprocess.discretize import EqualFreq
Expand All @@ -37,7 +37,7 @@
class MosaicVizRank(VizRankDialog, OWComponent):
"""VizRank dialog for Mosaic"""
captionTitle = "Mosaic Ranking"
max_attrs = Setting(3)
max_attrs = ContextSetting(6)

pairSelected = Signal(Variable, Variable, Variable, Variable)
_AttrRole = next(gui.OrangeUserRole)
Expand All @@ -48,10 +48,11 @@ def __init__(self, master):
OWComponent.__init__(self, master)

box = gui.hBox(self)
self.max_attr_spin = gui.spin(
box, self, "max_attrs", 2, 4,
label="Limit the number of attributes to: ",
controlWidth=50, alignment=Qt.AlignRight,
self.max_attr_combo = gui.comboBox(
box, self, "max_attrs",
label="Number of variables:", orientation=Qt.Horizontal,
items=["one", "two", "three", "four",
"at most two", "at most three", "at most four"],
callback=self.max_attr_changed)
gui.rubber(box)
self.layout().addWidget(self.button)
Expand Down Expand Up @@ -87,10 +88,10 @@ def before_running(self):
self.rank_model.clear()
self.compute_attr_order()
self.last_run_max_attr = self.max_attrs
self.max_attr_spin.setDisabled(True)
self.max_attr_combo.setDisabled(True)

def stopped(self):
self.max_attr_spin.setDisabled(False)
self.max_attr_combo.setDisabled(False)

def max_attr_changed(self):
"""
Expand All @@ -106,6 +107,15 @@ def max_attr_changed(self):
self.button.setEnabled(self.check_preconditions())

def coloring_changed(self):
item = self.max_attr_combo.model().item(0)
actflags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if self._compute_class_dists():
item.setFlags(item.flags() | actflags)
else:
item.setFlags(item.flags() & ~actflags)
if self.max_attrs == 0:
self.max_attrs = 1

self.stop_and_reset(self.initialize_keep_ordering)

def check_preconditions(self):
Expand Down Expand Up @@ -145,14 +155,22 @@ def compute_attr_order(self):
def _compute_class_dists(self):
return self.master.variable_color is not None

def attr_range(self):
n_attrs = len(self.master.discrete_data.domain.attributes)
mm = 1 if self._compute_class_dists() else 2
max_attrs = min(n_attrs, [mm, 2, 3, 4, 2, 3, 4][self.max_attrs])
min_attrs = [mm, 2, 3, 4, mm, mm, mm][self.max_attrs]
return min_attrs, max_attrs

def state_count(self):
"""
Return the number of combinations, starting with a single attribute
if Mosaic is colored by class distributions, and two if by Pearson
"""
n_attrs = len(self.master.discrete_data.domain.attributes)
min_attrs = 1 if self._compute_class_dists() else 2
max_attrs = min(n_attrs, self.max_attrs)
min_attrs, max_attrs = self.attr_range()
if min_attrs > max_attrs:
return 0
return sum(comb(n_attrs, k, exact=True)
for k in range(min_attrs, max_attrs + 1))

Expand All @@ -166,16 +184,19 @@ def iterate_states(self, state):
# `score_heuristic` would be run on every call to master's `set_data`.
master = self.master
data = master.discrete_data
min_attrs, max_attrs = self.attr_range()
if min_attrs > max_attrs:
return
if state is None: # on the first call, compute order
if self._compute_class_dists():
self.marginal = get_distribution(data, data.domain.class_var)
self.marginal.normalize()
state = [0]
state = list(range(min_attrs))
else:
self.marginal = get_distributions(data)
for dist in self.marginal:
dist.normalize()
state = [0, 1]
state = list(range(min_attrs))
n_attrs = len(data.domain.attributes)
while True:
yield state
Expand All @@ -188,7 +209,7 @@ def iterate_states(self, state):
break
state[up] = up
if state[-1] == len(self.attr_ordering):
if len(state) < min(self.max_attrs, n_attrs):
if len(state) < min(max_attrs, n_attrs):
state = list(range(len(state) + 1))
else:
break
Expand Down Expand Up @@ -286,11 +307,11 @@ class Outputs:
vizrank = SettingProvider(MosaicVizRank)
settings_version = 2
use_boxes = Setting(True)
variable1 = ContextSetting(None)
variable2 = ContextSetting(None)
variable3 = ContextSetting(None)
variable4 = ContextSetting(None)
variable_color = ContextSetting(None)
variable1: Variable = ContextSetting(None)
variable2: Variable = ContextSetting(None)
variable3: Variable = ContextSetting(None)
variable4: Variable = ContextSetting(None)
variable_color: DiscreteVariable = ContextSetting(None)
selection = Setting(set(), schema_only=True)

BAR_WIDTH = 5
Expand Down
127 changes: 93 additions & 34 deletions Orange/widgets/visualize/tests/test_owmosaic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# pylint: disable=missing-docstring,protected-access
import time
import unittest
from unittest.mock import patch

Expand Down Expand Up @@ -248,28 +247,48 @@ def test_count(self):
self.send_signal(self.widget.Inputs.data, data)

simulate.combobox_activate_index(self.widget.controls.variable_color, 0, 0)
vizrank.max_attrs = 2
vizrank.max_attrs = 1
self.assertEqual(vizrank.state_count(), 10) # 5x4 / 2
vizrank.max_attrs = 2
self.assertEqual(vizrank.state_count(), 10) # 5x4x3 / 2x3
vizrank.max_attrs = 3
self.assertEqual(vizrank.state_count(), 20) # above + 5x4x3 / 2x3
self.assertEqual(vizrank.state_count(), 5) # 5x4x3x2 / 2x3x4
vizrank.max_attrs = 4
self.assertEqual(vizrank.state_count(), 10) # 5x4 / 2
vizrank.max_attrs = 5
self.assertEqual(vizrank.state_count(), 20) # above + 5x4x3 / 2x3
vizrank.max_attrs = 6
self.assertEqual(vizrank.state_count(), 25) # above + 5x4x3x2 / 2x3x4

simulate.combobox_activate_index(self.widget.controls.variable_color, 2, 0)
vizrank.max_attrs = 0
self.assertEqual(vizrank.state_count(), 4) # 4
vizrank.max_attrs = 1
self.assertEqual(vizrank.state_count(), 6) # 4x3 / 2
vizrank.max_attrs = 2
self.assertEqual(vizrank.state_count(), 10) # 4 + 4x3 / 2
self.assertEqual(vizrank.state_count(), 4) # 4x3x2 / 3x2
vizrank.max_attrs = 3
self.assertEqual(vizrank.state_count(), 14) # above + 4x3x2 / 3x2
self.assertEqual(vizrank.state_count(), 1) # 4x3x2x1 / 2x3x4
vizrank.max_attrs = 4
self.assertEqual(vizrank.state_count(), 10) # 4 + 4x3 / 2
vizrank.max_attrs = 5
self.assertEqual(vizrank.state_count(), 14) # above + 4x3x2 / 3x2
vizrank.max_attrs = 6
self.assertEqual(vizrank.state_count(), 15) # above + 4x3x2x1 / 2x3x4

self.send_signal(self.widget.Inputs.data, self.iris_no_class)
simulate.combobox_activate_index(self.widget.controls.variable_color, 0, 0)
vizrank.max_attrs = 2
vizrank.max_attrs = 1
self.assertEqual(vizrank.state_count(), 6) # 4x3 / 2
vizrank.max_attrs = 2
self.assertEqual(vizrank.state_count(), 4) # 4x3x2 / 3x2
vizrank.max_attrs = 3
self.assertEqual(vizrank.state_count(), 10) # above + 4x3x2 / 3x2
self.assertEqual(vizrank.state_count(), 1) # 4x3x2x1 / 2x3x4
vizrank.max_attrs = 4
self.assertEqual(vizrank.state_count(), 6) # 4x3 / 2
vizrank.max_attrs = 5
self.assertEqual(vizrank.state_count(), 10) # above + 4x3x2 / 3x2
vizrank.max_attrs = 6
self.assertEqual(vizrank.state_count(), 11) # above + 4x3x2x1 / 2x3x4

def test_iteration(self):
Expand All @@ -279,7 +298,15 @@ def test_iteration(self):
self.send_signal(self.widget.Inputs.data, self.iris)
vizrank.compute_attr_order()

vizrank.max_attrs = 4
vizrank.max_attrs = 1
self.assertEqual([state.copy()
for state in vizrank.iterate_states(None)],
[[0, 1], [0, 2], [1, 2], [0, 3], [1, 3], [2, 3]])
self.assertEqual([state.copy()
for state in vizrank.iterate_states([0, 3])],
[[0, 3], [1, 3], [2, 3]])

vizrank.max_attrs = 6
self.assertEqual([state.copy()
for state in vizrank.iterate_states(None)],
[[0], [1], [2], [3],
Expand All @@ -292,7 +319,7 @@ def test_iteration(self):
[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3],
[0, 1, 2, 3]])

vizrank.max_attrs = 2
vizrank.max_attrs = 4
self.assertEqual([state.copy()
for state in vizrank.iterate_states(None)],
[[0], [1], [2], [3],
Expand All @@ -302,7 +329,7 @@ def test_iteration(self):
[[0, 3], [1, 3], [2, 3]])

widget.variable_color = None
vizrank.max_attrs = 4
vizrank.max_attrs = 6
self.assertEqual([state.copy()
for state in vizrank.iterate_states(None)],
[[0, 1], [0, 2], [1, 2], [0, 3], [1, 3], [2, 3],
Expand All @@ -314,7 +341,7 @@ def test_iteration(self):
[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3],
[0, 1, 2, 3]])

vizrank.max_attrs = 2
vizrank.max_attrs = 4
self.assertEqual([state.copy()
for state in vizrank.iterate_states(None)],
[[0, 1], [0, 2], [1, 2], [0, 3], [1, 3], [2, 3]])
Expand All @@ -334,26 +361,6 @@ def test_row_for_state(self):
item.data(self.vizrank._AttrRole),
tuple(self.vizrank.attr_ordering[i] for i in [0, 1, 3]))

@unittest.skip("Appveyor sometimes fails.")
def test_does_not_crash(self):
"""MosaicVizrank computes rankings without crashing"""
widget = self.widget
vizrank = self.vizrank
self.send_signal(self.widget.Inputs.data, self.iris)
vizrank.max_attrs = 2

widget.interior_coloring = widget.PEARSON
vizrank.toggle()
time.sleep(0.5)
self.assertEqual(vizrank.rank_model.rowCount(), 10) # 4x5 / 2
widget.interior_coloring = widget.CLASS_DISTRIBUTION
vizrank.toggle()
time.sleep(0.5)
self.assertEqual(vizrank.rank_model.rowCount(), 10) # 4 + 4x5 / 2

self.send_signal(self.widget.Inputs.data, self.iris_no_class)
vizrank.toggle()

def test_does_not_crash_cont_class(self):
"""MosaicVizrank computes rankings without crashing"""
data = Table("housing.tab")
Expand All @@ -377,6 +384,56 @@ def test_finished(self):
self.process_events(until=lambda: not self.vizrank.keep_running)
self.assertEqual(len(self.vizrank.scores), self.vizrank.state_count())

def test_max_attr_combo_1_disabling(self):
widget = self.widget
vizrank = widget.vizrank
combo = vizrank.max_attr_combo
model = combo.model()
enabled = Qt.ItemIsSelectable | Qt.ItemIsEnabled

data = Table("iris.tab")
self.send_signal(self.widget.Inputs.data, data)
self.assertEqual(model.item(0).flags() & enabled, enabled)

vizrank.max_attrs = 0
simulate.combobox_activate_index(self.widget.controls.variable_color, 0)
self.assertEqual(vizrank.max_attrs, 1)
self.assertEqual(int(model.item(0).flags() & enabled), 0)

simulate.combobox_activate_index(self.widget.controls.variable_color, 1)
self.assertEqual(vizrank.max_attrs, 1)
self.assertEqual(model.item(0).flags() & enabled, enabled)

def test_attr_range(self):
vizrank = self.widget.vizrank
data = Table("iris.tab")
domain = data.domain

self.send_signal(self.widget.Inputs.data, data)
for vizrank.max_attrs, rge in (
(0, (1, 1)), (1, (2, 2)), (2, (3, 3)), (3, (4, 4)),
(4, (1, 2)), (5, (1, 3)), (6, (1, 4))):
self.assertEqual(vizrank.attr_range(), rge,
f"failed at max_attrs={vizrank.max_attrs}")

reduced = data.transform(Domain(domain.attributes[:2], domain.class_var))
self.send_signal(self.widget.Inputs.data, reduced)
for vizrank.max_attrs, rge in (
(0, (1, 1)), (1, (2, 2)), (2, (3, 2)), (3, (4, 2)),
(4, (1, 2)), (5, (1, 2)), (6, (1, 2))):
self.assertEqual(vizrank.attr_range(), rge,
f"failed at max_attrs={vizrank.max_attrs}")
self.assertIs(vizrank.state_count() == 0, rge[0] > rge[1])

simulate.combobox_activate_index(self.widget.controls.variable_color, 0)
for vizrank.max_attrs, rge in (
(0, (2, 2)), (1, (2, 2)), (2, (3, 3)), (3, (4, 3)),
(4, (2, 2)), (5, (2, 3)), (6, (2, 3))):
self.assertEqual(vizrank.attr_range(), rge,
f"failed at max_attrs={vizrank.max_attrs}")
self.assertIs(vizrank.state_count() == 0, rge[0] > rge[1])


def test_nan_column(self):
"""
A column with only NaN-s used to throw an error
Expand All @@ -401,8 +458,10 @@ def test_color_combo(self):
GH-2133
GH-2036
"""
RESULTS = [[0, 2, 6], [0, 3, 10], [0, 4, 11],
[1, 2, 6], [1, 3, 7], [1, 4, 7]]
RESULTS = [[0, 1, 6], [0, 2, 4], [0, 3, 1],
[0, 4, 6], [0, 5, 10], [0, 6, 11],
[1, 0, 3], [1, 1, 3], [1, 2, 1], [1, 3, 0],
[1, 4, 6], [1, 5, 7], [1, 6, 7]]
table = Table("titanic")
self.send_signal(self.widget.Inputs.data, table)
color_vars = ["(Pearson residuals)"] + [str(x) for x in table.domain.variables]
Expand All @@ -421,7 +480,7 @@ def test_color_combo(self):
output = self.get_output("Data")
self.assertEqual(output.domain.class_var, table.domain.class_var)

for ma in range(2, 5):
for ma in range(i == 0, 7):
self.vizrank.max_attrs = ma
sc = self.vizrank.state_count()
self.assertTrue([i > 0, ma, sc] in RESULTS)
Expand Down

0 comments on commit ba08e0e

Please sign in to comment.