Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8db1fae
Add Cochran rule check with visual indicator to Sieve Diagram
gmolledaj Aug 19, 2025
6b91d6e
2nd change: add Cochran rule check with visual indicator to Sieve Dia…
gmolledaj Aug 19, 2025
168ac10
Merge pull request #1 from gmolledaj/2nd-sieve-cochran-check-for-test
gmolledaj Aug 19, 2025
26ac794
Add test for Cochran indicator when condition is satisfied
gmolledaj Aug 19, 2025
55015c6
Merge pull request #2 from gmolledaj/add-cochran-tests
gmolledaj Aug 19, 2025
9170d33
Refine Cochran indicator added in previous patch
gmolledaj Aug 21, 2025
0f3ca30
Merge pull request #3 from gmolledaj/gmolledaj-patch-1
gmolledaj Aug 21, 2025
3b47ef5
Fix Cochran indicator positioning in Sieve widget
gmolledaj Aug 21, 2025
6442e96
Merge pull request #4 from gmolledaj/Fix-Cochran-indicator-rendering-…
gmolledaj Aug 21, 2025
1287570
Remove trailing whitespace in test_owsieve.py
gmolledaj Aug 21, 2025
6a1514a
Merge pull request #5 from gmolledaj/Remove-trailing-whitespace-in-test
gmolledaj Aug 21, 2025
df59659
Refactor comprehensions, dict creation, and rename 'filter' import
gmolledaj Aug 21, 2025
3246db2
Merge pull request #6 from gmolledaj/refactor/lint-fixes-sieve
gmolledaj Aug 21, 2025
16cae01
Update and extend Sieve widget tests
gmolledaj Aug 22, 2025
8824a09
Merge pull request #7 from gmolledaj/sieve/tests-update
gmolledaj Aug 22, 2025
53a6a98
Cochran with message Warning and Information
gmolledaj Aug 29, 2025
99fade3
Merge pull request #8 from gmolledaj/feature/cochran-rule-widget
gmolledaj Aug 29, 2025
266675d
Add tests for Cochran’s rule messages in Sieve Diagram
gmolledaj Aug 29, 2025
7748fc4
Merge pull request #9 from gmolledaj/test/cochran-rule-widget
gmolledaj Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions Orange/widgets/visualize/owsieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from AnyQt.QtGui import QColor, QPen, QBrush
from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem, QSizePolicy

from Orange.data import Table, filter, Variable
from Orange.data import Table, filter as data_filter, Variable
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 @@ -24,7 +24,8 @@
VizRankMixin
from Orange.widgets.visualize.utils import (
CanvasText, CanvasRectangle, ViewWithPress)
from Orange.widgets.widget import OWWidget, AttributeList, Input, Output
from Orange.widgets.widget import OWWidget, AttributeList, Input, Output, \
Msg


class ChiSqStats:
Expand Down Expand Up @@ -54,7 +55,6 @@ def __init__(self, data, attr1, attr2):
self.p = chi2.sf(
self.chisq, (len(self.probs_x) - 1) * (len(self.probs_y) - 1))


class SieveRank(VizRankDialogAttrPair):
sort_names_in_row = True

Expand Down Expand Up @@ -84,6 +84,12 @@ class Outputs:

graph_name = "canvas" # QGraphicsScene

class Information(OWWidget.Information):
cochran = Msg("Meets Cochran's rule")

class Warning(OWWidget.Warning):
cochran = Msg("Does not meet Cochran's rule")

want_control_area = False

settings_version = 1
Expand All @@ -105,10 +111,10 @@ def __init__(self):
self.mainArea.layout().setSpacing(0)
self.attr_box = gui.hBox(self.mainArea, margin=0)
self.domain_model = DomainModel(valid_types=DomainModel.PRIMITIVE)
combo_args = dict(
widget=self.attr_box, master=self, contentsLength=12,
searchable=True, sendSelectedValue=True,
callback=self.attr_changed, model=self.domain_model)
combo_args = {
"widget":self.attr_box, "master":self, "contentsLength":12,
"searchable":True, "sendSelectedValue":True,
"callback":self.attr_changed, "model":self.domain_model}
fixed_size = (QSizePolicy.Fixed, QSizePolicy.Fixed)
gui.comboBox(value="attr_x", **combo_args)
gui.widgetLabel(self.attr_box, "\u2717", sizePolicy=fixed_size)
Expand Down Expand Up @@ -270,7 +276,7 @@ def resolve_shown_attributes(self):
"Features from the input signal are not present in the data")
return
old_attrs = self.attr_x, self.attr_y
self.attr_x, self.attr_y = [f for f in (features * 2)[:2]]
self.attr_x, self.attr_y = (features * 2)[:2]
self.attr_box.setEnabled(False)
if (self.attr_x, self.attr_y) != old_attrs:
self.selection = set()
Expand Down Expand Up @@ -313,9 +319,9 @@ def update_selection(self):
width = 4
val_x, val_y = area.value_pair
filts.append(
filter.Values([
filter.FilterDiscrete(self.attr_x.name, [val_x]),
filter.FilterDiscrete(self.attr_y.name, [val_y])
data_filter.Values([
data_filter.FilterDiscrete(self.attr_x.name, [val_x]),
data_filter.FilterDiscrete(self.attr_y.name, [val_y])
]))
else:
width = 1
Expand All @@ -325,7 +331,7 @@ def update_selection(self):
if len(filts) == 1:
filts = filts[0]
else:
filts = filter.Values(filts, conjunction=False)
filts = data_filter.Values(filts, conjunction=False)
selection = filts(self.discrete_data)
idset = set(selection.ids)
sel_idx = [i for i, id in enumerate(self.data.ids) if id in idset]
Expand Down Expand Up @@ -516,6 +522,23 @@ def _oper(attr, txt):
0, bottom)
# Assume similar height for both lines
text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height())
cochran_ok = self._cochran_ok(chi)
self.Information.cochran(shown=cochran_ok)
self.Warning.cochran(shown= not cochran_ok)

def _cochran_ok(self, chi):
"""
Return True if Cochran's rule is met; otherwise return False.
"""
expected = np.asarray(chi.expected, dtype=float)
cells = int(expected.size)
if cells == 0:
return False
eps = 1e-12
num_lt1 = int((expected < 1.0 - eps).sum())
num_lt5 = int((expected < 5.0 - eps).sum())
ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * cells)
return ok

def get_widget_name_extension(self):
if self.data is not None:
Expand Down
41 changes: 39 additions & 2 deletions Orange/widgets/visualize/tests/test_owsieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,30 @@ def test_chisquare(self):
chi = ChiSqStats(table, 0, 1)
self.assertFalse(isnan(chi.chisq))

def test_cochran_messages(self):
a = DiscreteVariable("A", values=("a1","a2","a3"))
b = DiscreteVariable("B", values=("b1","b2","b3"))

# PASS: 20/20/20 × 20/20/20
rows_ok = ["a1"]*20 + ["a2"]*20 + ["a3"]*20
cols_ok = ["b1"]*20 + ["b2"]*20 + ["b3"]*20
table_ok = Table.from_list(Domain([a,b]), list(zip(rows_ok, cols_ok)))
self.send_signal(self.widget.Inputs.data, table_ok)
self.widget.attr_x, self.widget.attr_y = a, b
self.widget.update_graph()
assert self.widget.Information.cochran.is_shown()
assert not self.widget.Warning.cochran.is_shown()

# FAIL: 10/20/30 × 20/20/20
rows_bad = ["a1"]*10 + ["a2"]*20 + ["a3"]*30
cols_bad = ["b1"]*20 + ["b2"]*20 + ["b3"]*20
table_bad = Table.from_list(Domain([a,b]), list(zip(rows_bad, cols_bad)))
self.send_signal(self.widget.Inputs.data, table_bad)
self.widget.attr_x, self.widget.attr_y = a, b
self.widget.update_graph()
assert not self.widget.Information.cochran.is_shown()
assert self.widget.Warning.cochran.is_shown()

def test_metadata(self):
"""
Widget should interpret meta data which are continuous or discrete in
Expand Down Expand Up @@ -168,10 +192,23 @@ def test_vizrank_receives_manual_change(self, auto_select):
def test_input_features(self):
self.assertTrue(self.widget.attr_box.isEnabled())
self.send_signal(self.widget.Inputs.data, self.iris)
self.send_signal(self.widget.Inputs.features,
AttributeList(self.iris.domain.attributes))

# Force a known initial state different from the incoming features
a0, a1, a2, a3 = self.iris.domain.attributes
self.widget.attr_x, self.widget.attr_y = a2, a3

# Send features -> triggers set_input_features -> resolve_shown_attributes
feats = AttributeList([a0, a1])
self.send_signal(self.widget.Inputs.features, feats)

# Attributes should now follow the provided features
self.assertEqual((self.widget.attr_x, self.widget.attr_y), (a0, a1))

# Existing checks
self.assertFalse(self.widget.attr_box.isEnabled())
self.assertFalse(self.widget.vizrank_button().isEnabled())

# Remove features -> widget returns to interactive mode
self.send_signal(self.widget.Inputs.features, None)
self.assertTrue(self.widget.attr_box.isEnabled())
self.assertTrue(self.widget.vizrank_button().isEnabled())
Expand Down
Loading