From 8db1faea7a19a1d452619880c551c679a875a964 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Tue, 19 Aug 2025 11:58:30 +0200 Subject: [PATCH 01/13] Add Cochran rule check with visual indicator to Sieve Diagram Implemented a Cochran condition check based on expected frequencies in the Sieve Diagram widget. A small green or red square is displayed next to the "Cochran:" label to indicate whether the rule is satisfied. This provides a quick, language-independent visual cue without altering the existing color scheme of the diagram. --- Orange/widgets/visualize/owsieve.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 8691828f8b8..19e00566c55 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -516,7 +516,28 @@ def _oper(attr, txt): 0, bottom) # Assume similar height for both lines text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height()) - + + # Cochran condition check + expected = chi.expected + total_cells = expected.size + num_lt1 = int((expected < 1).sum()) + num_lt5 = int((expected < 5).sum()) + cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * total_cells) + + bottom += 35 + label_item = text("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) + + # Green/red square indicator + rect_size = 10 + # Since the text starts at x=0, its width is enough to position the square on the right + x_ind = label_item.boundingRect().width() + 6 + y_ind = bottom - rect_size / 2 + + color = QColor("#2ecc71") if cochran_ok else QColor("#e74c3c") + indic = CanvasRectangle(self.canvas, x_ind, y_ind, rect_size, rect_size, z=0) + indic.setBrush(QBrush(color)) + indic.setPen(QPen(color)) + def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) From 6b91d6ef0616a5b15acc326ed55de7a6e72b903f Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Tue, 19 Aug 2025 21:15:07 +0200 Subject: [PATCH 02/13] 2nd change: add Cochran rule check with visual indicator to Sieve Diagram 2nd change for add tests Implemented a Cochran condition check based on expected frequencies in the Sieve Diagram widget. A small green or red square is displayed next to the "Cochran:" label to indicate whether the rule is satisfied. This provides a quick, language-independent visual cue without altering the existing color scheme of the diagram. --- Orange/widgets/visualize/owsieve.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 19e00566c55..d83c92ee4fe 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -54,7 +54,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 @@ -124,6 +123,7 @@ def __init__(self): self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self._cochran_ok = None def sizeHint(self): return QSize(450, 550) @@ -444,6 +444,7 @@ def _oper(attr, txt): self.canvas.removeItem(item) if self.data is None or len(self.data) == 0 or \ self.attr_x is None or self.attr_y is None: + self._cochran_ok = None return ddomain = self.discrete_data.domain @@ -469,6 +470,7 @@ def _oper(attr, txt): disc_x if not disc_x.values else disc_y) text(text_, view.width() / 2 + 70, view.height() / 2, Qt.AlignRight | Qt.AlignVCenter) + self._cochran_ok = None return n = chi.n curr_x = x_off @@ -518,11 +520,11 @@ def _oper(attr, txt): text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height()) # Cochran condition check - expected = chi.expected + expected = np.array(chi.expected, dtype=float) total_cells = expected.size num_lt1 = int((expected < 1).sum()) num_lt5 = int((expected < 5).sum()) - cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * total_cells) + self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * total_cells) bottom += 35 label_item = text("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) @@ -533,7 +535,7 @@ def _oper(attr, txt): x_ind = label_item.boundingRect().width() + 6 y_ind = bottom - rect_size / 2 - color = QColor("#2ecc71") if cochran_ok else QColor("#e74c3c") + color = QColor("#2ecc71") if self._cochran_ok else QColor("#e74c3c") indic = CanvasRectangle(self.canvas, x_ind, y_ind, rect_size, rect_size, z=0) indic.setBrush(QBrush(color)) indic.setPen(QPen(color)) From 26ac79496d853951c93d72297192f6088b1e2b07 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Tue, 19 Aug 2025 21:24:38 +0200 Subject: [PATCH 03/13] Add test for Cochran indicator when condition is satisfied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new unit test that checks the Cochran’s rule indicator in the Sieve widget. The test uses a balanced 3x3 contingency table, where all expected frequencies are greater than 5, ensuring that the Cochran condition is satisfied. This verifies that the widget correctly sets the internal flag and displays the green indicator. --- .../widgets/visualize/tests/test_owsieve.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index dc944dca3fd..2d78d83b9ad 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -109,6 +109,44 @@ def test_chisquare(self): table = Table.from_list(Domain([a, b]), list(zip("yynny", "ynyyn"))) chi = ChiSqStats(table, 0, 1) self.assertFalse(isnan(chi.chisq)) + + def test_cochran_indicator_passes(self): + # Truly balanced 3x3: all expected frequencies >= 5 + a = DiscreteVariable("A", values=("a1", "a2", "a3")) + b = DiscreteVariable("B", values=("b1", "b2", "b3")) + + # 60 cases total, balanced by rows and columns + # Row totals: 20, 20, 20 + # Col totals: 20, 20, 20 + # Expected per cell: (20*20)/60 = 6.666... >= 5 + rows = ["a1"] * 20 + ["a2"] * 20 + ["a3"] * 20 + cols = ["b1"] * 20 + ["b2"] * 20 + ["b3"] * 20 + + table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) + + self.send_signal(self.widget.Inputs.data, table) + # Force attributes and trigger computation + self.widget.attr_x, self.widget.attr_y = a, b + self.widget.update_graph() + + # Cochran’s rule should be satisfied + self.assertTrue(getattr(self.widget, "_cochran_ok", None)) + + def test_cochran_indicator_fails(self): + # Highly unbalanced 3x3 contingency table -> many expected < 5, some near 0 + a = DiscreteVariable("A", values=("a1", "a2", "a3")) + b = DiscreteVariable("B", values=("b1", "b2", "b3")) + # 12 cases in total, 10 concentrated in a single cell + rows = ["a1"]*10 + ["a2"]*1 + ["a3"]*1 + cols = ["b1"]*10 + ["b2"]*1 + ["b3"]*1 + table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) + + self.send_signal(self.widget.Inputs.data, table) + self.widget.attr_x, self.widget.attr_y = a, b + self.widget.update_graph() + + # Cochran’s rule should NOT be satisfied + self.assertFalse(getattr(self.widget, "_cochran_ok", True)) def test_metadata(self): """ From 9170d3376ddbe162750355a70440d50acf5f3f32 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Thu, 21 Aug 2025 09:10:07 +0200 Subject: [PATCH 04/13] Refine Cochran indicator added in previous patch This commit updates and refines the Cochran indicator logic added in the previous patch: Groups the label and square into a single QGraphicsItemGroup for consistent positioning. Ensures old indicators are properly removed before drawing new ones. Uses cosmetic pens to avoid scaling artifacts on HiDPI displays. Adds defensive checks to keep the indicator inside the scene bounds. Improves robustness so tests and different rendering backends behave consistently. This is a follow-up to the earlier patch that introduced the Cochran rule indicator, addressing review feedback and making the feature more reliable. --- Orange/widgets/visualize/owsieve.py | 76 ++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index d83c92ee4fe..489849f194a 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -521,24 +521,66 @@ def _oper(attr, txt): # Cochran condition check expected = np.array(chi.expected, dtype=float) - total_cells = expected.size - num_lt1 = int((expected < 1).sum()) - num_lt5 = int((expected < 5).sum()) - self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * total_cells) - + cells = expected.size + if cells == 0: + self._cochran_ok = None + else: + num_lt1 = int((expected < 1.0).sum()) + num_lt5 = int((expected < 5.0).sum()) + self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * cells) + # Rendering of the indicator bottom += 35 - label_item = text("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) - - # Green/red square indicator - rect_size = 10 - # Since the text starts at x=0, its width is enough to position the square on the right - x_ind = label_item.boundingRect().width() + 6 - y_ind = bottom - rect_size / 2 - - color = QColor("#2ecc71") if self._cochran_ok else QColor("#e74c3c") - indic = CanvasRectangle(self.canvas, x_ind, y_ind, rect_size, rect_size, z=0) - indic.setBrush(QBrush(color)) - indic.setPen(QPen(color)) + # Remove a previous indicator if it exists + if hasattr(self, "_cochran_group") and self._cochran_group is not None: + try: + self.canvas.removeItem(self._cochran_group) + except Exception: + pass + self._cochran_group = None + + scene = self.canvas + view = self.canvasView + if not scene or not view or view.width() <= 0 or view.height() <= 0: + return + + try: + # 1) Label "Cochran:" + label_item = text("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) + label_w = label_item.boundingRect().width() + + # 2) Green/red square indicator to the right of the label + rect_size = 10 + x_ind = int(round(label_w + 6)) + y_ind = int(round(bottom - rect_size / 2)) + + # Bounds check: make sure it stays inside the scene + if x_ind + rect_size > view.width(): + x_ind = max(0, int(view.width() - rect_size - 1)) + if y_ind + rect_size > view.height(): + y_ind = max(0, int(view.height() - rect_size - 1)) + + color = QColor("#2ecc71") if self._cochran_ok else QColor("#e74c3c") + + # Cosmetic pen/brush to avoid scaling issues on HiDPI or offscreen rendering + pen = QPen(color, 1) + pen.setCosmetic(True) + brush = QBrush(color) + + square = CanvasRectangle(scene, x_ind, y_ind, rect_size, rect_size, z=0) + square.setPen(pen) + square.setBrush(brush) + + # 3) Group label and square together so they can be easily removed later + from AnyQt.QtWidgets import QGraphicsItemGroup + group = QGraphicsItemGroup() + scene.addItem(group) + group.addToGroup(label_item) + group.addToGroup(square) + self._cochran_group = group + + except Exception: + # Fail-safe: never let a rendering error break the widget + self._cochran_group = None def get_widget_name_extension(self): if self.data is not None: From 3b47ef573d82d10d87377bed476a089d24f452a7 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Thu, 21 Aug 2025 12:54:56 +0200 Subject: [PATCH 05/13] Fix Cochran indicator positioning in Sieve widget Adjusted bottom margin so the Cochran indicator is rendered correctly and does not get clipped when resizing the window. --- Orange/widgets/visualize/owsieve.py | 118 ++++++++++++++-------------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 489849f194a..0d73cbeca90 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -6,7 +6,7 @@ from AnyQt.QtCore import Qt, QSize from AnyQt.QtGui import QColor, QPen, QBrush -from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem, QSizePolicy +from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem, QSizePolicy, QGraphicsItemGroup from Orange.data import Table, filter, Variable from Orange.data.sql.table import SqlTable, LARGE_TABLE, DEFAULT_SAMPLE_TIME @@ -124,6 +124,7 @@ def __init__(self): self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._cochran_ok = None + self._cochran_group = None def sizeHint(self): return QSize(450, 550) @@ -457,7 +458,7 @@ def _oper(attr, txt): max_ylabel_w = min(max_ylabel_w, 200) x_off = height(attr_y.name) + max_ylabel_w y_off = 15 - square_size = min(view.width() - x_off - 35, view.height() - y_off - 80) + square_size = min(view.width() - x_off - 35, view.height() - y_off - 105) square_size = max(square_size, 10) self.canvasView.setSceneRect(0, 0, view.width(), view.height()) if not disc_x.values or not disc_y.values: @@ -518,70 +519,65 @@ def _oper(attr, txt): 0, bottom) # Assume similar height for both lines text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height()) - - # Cochran condition check + + # Draw Cochran indicator (returns updated bottom with spacing applied) + bottom = self._draw_cochran_indicator(chi, bottom, text) + + def _draw_cochran_indicator(self, chi, bottom: int, text_fn) -> int: + """ + Draw Cochran indicator (label + colored square) and set self._cochran_ok. + Returns updated bottom (with spacing applied). + """ + # --- Evaluate Cochran’s rule --- expected = np.array(chi.expected, dtype=float) - cells = expected.size + cells = int(expected.size) if cells == 0: self._cochran_ok = None - else: - num_lt1 = int((expected < 1.0).sum()) - num_lt5 = int((expected < 5.0).sum()) - self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * cells) - # Rendering of the indicator + return bottom + + num_lt1 = int((expected < 1.0).sum()) + num_lt5 = int((expected < 5.0).sum()) + self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * cells) + + # --- Spacing before the indicator line --- bottom += 35 - # Remove a previous indicator if it exists - if hasattr(self, "_cochran_group") and self._cochran_group is not None: - try: - self.canvas.removeItem(self._cochran_group) - except Exception: - pass - self._cochran_group = None - + + if getattr(self, "_cochran_label", None) is not None: + sc = self._cochran_label.scene() + if sc is not None: + sc.removeItem(self._cochran_label) + self._cochran_label = None + scene = self.canvas - view = self.canvasView - if not scene or not view or view.width() <= 0 or view.height() <= 0: - return - - try: - # 1) Label "Cochran:" - label_item = text("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) - label_w = label_item.boundingRect().width() - - # 2) Green/red square indicator to the right of the label - rect_size = 10 - x_ind = int(round(label_w + 6)) - y_ind = int(round(bottom - rect_size / 2)) - - # Bounds check: make sure it stays inside the scene - if x_ind + rect_size > view.width(): - x_ind = max(0, int(view.width() - rect_size - 1)) - if y_ind + rect_size > view.height(): - y_ind = max(0, int(view.height() - rect_size - 1)) - - color = QColor("#2ecc71") if self._cochran_ok else QColor("#e74c3c") - - # Cosmetic pen/brush to avoid scaling issues on HiDPI or offscreen rendering - pen = QPen(color, 1) - pen.setCosmetic(True) - brush = QBrush(color) - - square = CanvasRectangle(scene, x_ind, y_ind, rect_size, rect_size, z=0) - square.setPen(pen) - square.setBrush(brush) - - # 3) Group label and square together so they can be easily removed later - from AnyQt.QtWidgets import QGraphicsItemGroup - group = QGraphicsItemGroup() - scene.addItem(group) - group.addToGroup(label_item) - group.addToGroup(square) - self._cochran_group = group - - except Exception: - # Fail-safe: never let a rendering error break the widget - self._cochran_group = None - + srect = scene.sceneRect() + scene_w, scene_h = int(srect.width()), int(srect.height()) + if scene_w <= 0 or scene_h <= 0: + return bottom + + # 1) Draw the label + label_item = text_fn("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) + self._cochran_label = label_item # keep a handle for next redraw + + # 2) Create the colored square + rect_size = 10 + color = QColor(Qt.green) if self._cochran_ok else QColor(Qt.red) + pen = QPen(color, 1) + pen.setCosmetic(True) + brush = QBrush(color) + + square = CanvasRectangle(scene, 0, 0, rect_size, rect_size, z=0) + square.setPen(pen) + square.setBrush(brush) + square.setParentItem(label_item) + + # Position it to the right of the label, vertically centered + lrect = label_item.boundingRect() + x_local = lrect.right() + 6 + y_local = lrect.center().y() - rect_size / 2 + square.setPos(int(x_local), int(y_local)) + + return bottom + def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) From 128757079cd8e0bff41bcb469f9b218c697cf6a1 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Thu, 21 Aug 2025 13:10:28 +0200 Subject: [PATCH 06/13] Remove trailing whitespace in test_owsieve.py Cleaned up trailing spaces to improve code style consistency and pass linting checks. --- Orange/widgets/visualize/tests/test_owsieve.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index 2d78d83b9ad..588bcf154c5 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -109,29 +109,29 @@ def test_chisquare(self): table = Table.from_list(Domain([a, b]), list(zip("yynny", "ynyyn"))) chi = ChiSqStats(table, 0, 1) self.assertFalse(isnan(chi.chisq)) - + def test_cochran_indicator_passes(self): # Truly balanced 3x3: all expected frequencies >= 5 a = DiscreteVariable("A", values=("a1", "a2", "a3")) b = DiscreteVariable("B", values=("b1", "b2", "b3")) - + # 60 cases total, balanced by rows and columns # Row totals: 20, 20, 20 # Col totals: 20, 20, 20 # Expected per cell: (20*20)/60 = 6.666... >= 5 rows = ["a1"] * 20 + ["a2"] * 20 + ["a3"] * 20 cols = ["b1"] * 20 + ["b2"] * 20 + ["b3"] * 20 - + table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) - + self.send_signal(self.widget.Inputs.data, table) # Force attributes and trigger computation self.widget.attr_x, self.widget.attr_y = a, b self.widget.update_graph() - + # Cochran’s rule should be satisfied self.assertTrue(getattr(self.widget, "_cochran_ok", None)) - + def test_cochran_indicator_fails(self): # Highly unbalanced 3x3 contingency table -> many expected < 5, some near 0 a = DiscreteVariable("A", values=("a1", "a2", "a3")) @@ -140,11 +140,11 @@ def test_cochran_indicator_fails(self): rows = ["a1"]*10 + ["a2"]*1 + ["a3"]*1 cols = ["b1"]*10 + ["b2"]*1 + ["b3"]*1 table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) - + self.send_signal(self.widget.Inputs.data, table) self.widget.attr_x, self.widget.attr_y = a, b self.widget.update_graph() - + # Cochran’s rule should NOT be satisfied self.assertFalse(getattr(self.widget, "_cochran_ok", True)) From df59659b8462038b27ba29c1bc9020902614fd08 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Thu, 21 Aug 2025 15:36:56 +0200 Subject: [PATCH 07/13] Refactor comprehensions, dict creation, and rename 'filter' import - Replaced redundant list comprehensions with simpler list() calls or slicing. - Switched from dict() constructor to dictionary literal for readability. - Renamed imported 'filter' to avoid redefining the built-in name. These are code quality improvements only, with no functional changes. --- Orange/widgets/visualize/owsieve.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 0d73cbeca90..3fdd4bdfaf3 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -6,9 +6,9 @@ from AnyQt.QtCore import Qt, QSize from AnyQt.QtGui import QColor, QPen, QBrush -from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem, QSizePolicy, QGraphicsItemGroup +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 @@ -104,10 +104,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) @@ -124,7 +124,7 @@ def __init__(self): self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self._cochran_ok = None - self._cochran_group = None + self._cochran_label = None def sizeHint(self): return QSize(450, 550) @@ -271,7 +271,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() @@ -314,9 +314,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 @@ -326,7 +326,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] From 16cae01a02b32799b4281563dad8700747cadc2c Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Fri, 22 Aug 2025 10:03:23 +0200 Subject: [PATCH 08/13] Update and extend Sieve widget tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added new tests to cover Cochran’s rule indicator (pass and fail cases). - Extended test_input_features to verify attribute resolution. - Added test for update_selection to ensure selected and annotated outputs are produced. --- .../widgets/visualize/tests/test_owsieve.py | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index 588bcf154c5..75ced233a14 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -110,43 +110,34 @@ def test_chisquare(self): chi = ChiSqStats(table, 0, 1) self.assertFalse(isnan(chi.chisq)) - def test_cochran_indicator_passes(self): - # Truly balanced 3x3: all expected frequencies >= 5 + def test_cochran_indicator(self): + # 1) Data that PASS Cochran: balanced 3x3 (expected ~6.67) a = DiscreteVariable("A", values=("a1", "a2", "a3")) b = DiscreteVariable("B", values=("b1", "b2", "b3")) + 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))) - # 60 cases total, balanced by rows and columns - # Row totals: 20, 20, 20 - # Col totals: 20, 20, 20 - # Expected per cell: (20*20)/60 = 6.666... >= 5 - rows = ["a1"] * 20 + ["a2"] * 20 + ["a3"] * 20 - cols = ["b1"] * 20 + ["b2"] * 20 + ["b3"] * 20 - - table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) - - self.send_signal(self.widget.Inputs.data, table) - # Force attributes and trigger computation + self.send_signal(self.widget.Inputs.data, table_ok) self.widget.attr_x, self.widget.attr_y = a, b self.widget.update_graph() - - # Cochran’s rule should be satisfied - self.assertTrue(getattr(self.widget, "_cochran_ok", None)) - - def test_cochran_indicator_fails(self): - # Highly unbalanced 3x3 contingency table -> many expected < 5, some near 0 - a = DiscreteVariable("A", values=("a1", "a2", "a3")) - b = DiscreteVariable("B", values=("b1", "b2", "b3")) - # 12 cases in total, 10 concentrated in a single cell - rows = ["a1"]*10 + ["a2"]*1 + ["a3"]*1 - cols = ["b1"]*10 + ["b2"]*1 + ["b3"]*1 - table = Table.from_list(Domain([a, b]), list(zip(rows, cols))) - - self.send_signal(self.widget.Inputs.data, table) + # Ensure Cochran was actually evaluated + self.assertIsNotNone(getattr(self.widget, "_cochran_ok", None)) + self.assertTrue(self.widget._cochran_ok) + + # 2) Data that FAIL Cochran: 3 expected cells < 5 (e.g. 10/20/30 vs 20/20/20) + rows_bad = ["a1"]*10 + ["a2"]*20 + ["a3"]*30 + cols_bad = ["b1"]*20 + ["b2"]*20 + ["b3"]*20 + table_bad = Table.from_list(Table.from_list(Domain([a, b]), list(zip(rows_bad, cols_bad))).domain, + list(zip(rows_bad, cols_bad))) + + self.send_signal(self.widget.Inputs.data, table_bad) + # Re-assign attrs in case the widget resets them in handle signals self.widget.attr_x, self.widget.attr_y = a, b self.widget.update_graph() - # Cochran’s rule should NOT be satisfied - self.assertFalse(getattr(self.widget, "_cochran_ok", True)) + self.assertIsNotNone(getattr(self.widget, "_cochran_ok", None)) + self.assertFalse(self.widget._cochran_ok) def test_metadata(self): """ @@ -206,10 +197,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()) From 53a6a98c327f30bc5a6e54a4bc96d964eb6b0889 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Fri, 29 Aug 2025 19:55:37 +0200 Subject: [PATCH 09/13] Cochran with message Warning and Information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added _cochran_ok() method to evaluate Cochran’s rule - Show Information message when the rule is met - Show Warning message when the rule is not met --- Orange/widgets/visualize/owsieve.py | 80 ++++++++--------------------- 1 file changed, 21 insertions(+), 59 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 3fdd4bdfaf3..56dcfd92bcb 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -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: @@ -83,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 @@ -123,8 +130,6 @@ def __init__(self): self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self._cochran_ok = None - self._cochran_label = None def sizeHint(self): return QSize(450, 550) @@ -445,7 +450,6 @@ def _oper(attr, txt): self.canvas.removeItem(item) if self.data is None or len(self.data) == 0 or \ self.attr_x is None or self.attr_y is None: - self._cochran_ok = None return ddomain = self.discrete_data.domain @@ -458,7 +462,7 @@ def _oper(attr, txt): max_ylabel_w = min(max_ylabel_w, 200) x_off = height(attr_y.name) + max_ylabel_w y_off = 15 - square_size = min(view.width() - x_off - 35, view.height() - y_off - 105) + square_size = min(view.width() - x_off - 35, view.height() - y_off - 80) square_size = max(square_size, 10) self.canvasView.setSceneRect(0, 0, view.width(), view.height()) if not disc_x.values or not disc_y.values: @@ -471,7 +475,6 @@ def _oper(attr, txt): disc_x if not disc_x.values else disc_y) text(text_, view.width() / 2 + 70, view.height() / 2, Qt.AlignRight | Qt.AlignVCenter) - self._cochran_ok = None return n = chi.n curr_x = x_off @@ -519,64 +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) - # Draw Cochran indicator (returns updated bottom with spacing applied) - bottom = self._draw_cochran_indicator(chi, bottom, text) - - def _draw_cochran_indicator(self, chi, bottom: int, text_fn) -> int: + def _cochran_ok(self, chi): """ - Draw Cochran indicator (label + colored square) and set self._cochran_ok. - Returns updated bottom (with spacing applied). + Return True if Cochran's rule is met; otherwise return False. """ - # --- Evaluate Cochran’s rule --- - expected = np.array(chi.expected, dtype=float) + expected = np.asarray(chi.expected, dtype=float) cells = int(expected.size) if cells == 0: - self._cochran_ok = None - return bottom - - num_lt1 = int((expected < 1.0).sum()) - num_lt5 = int((expected < 5.0).sum()) - self._cochran_ok = (num_lt1 == 0) and (num_lt5 <= 0.2 * cells) - - # --- Spacing before the indicator line --- - bottom += 35 - - if getattr(self, "_cochran_label", None) is not None: - sc = self._cochran_label.scene() - if sc is not None: - sc.removeItem(self._cochran_label) - self._cochran_label = None - - scene = self.canvas - srect = scene.sceneRect() - scene_w, scene_h = int(srect.width()), int(srect.height()) - if scene_w <= 0 or scene_h <= 0: - return bottom - - # 1) Draw the label - label_item = text_fn("Cochran:", 0, bottom, Qt.AlignLeft | Qt.AlignVCenter) - self._cochran_label = label_item # keep a handle for next redraw - - # 2) Create the colored square - rect_size = 10 - color = QColor(Qt.green) if self._cochran_ok else QColor(Qt.red) - pen = QPen(color, 1) - pen.setCosmetic(True) - brush = QBrush(color) - - square = CanvasRectangle(scene, 0, 0, rect_size, rect_size, z=0) - square.setPen(pen) - square.setBrush(brush) - square.setParentItem(label_item) - - # Position it to the right of the label, vertically centered - lrect = label_item.boundingRect() - x_local = lrect.right() + 6 - y_local = lrect.center().y() - rect_size / 2 - square.setPos(int(x_local), int(y_local)) - - return bottom + 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: From 266675d5ccac5e33a97aa1c2100aa2a0cb6dbe62 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Fri, 29 Aug 2025 20:09:44 +0200 Subject: [PATCH 10/13] =?UTF-8?q?Add=20tests=20for=20Cochran=E2=80=99s=20r?= =?UTF-8?q?ule=20messages=20in=20Sieve=20Diagram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added tests with balanced and unbalanced contingency tables - Verify that Information is shown when the rule is met - Verify that Warning is shown when the rule is not met --- .../widgets/visualize/tests/test_owsieve.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index 75ced233a14..7290b83e5e4 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -110,34 +110,29 @@ def test_chisquare(self): chi = ChiSqStats(table, 0, 1) self.assertFalse(isnan(chi.chisq)) - def test_cochran_indicator(self): - # 1) Data that PASS Cochran: balanced 3x3 (expected ~6.67) - a = DiscreteVariable("A", values=("a1", "a2", "a3")) - b = DiscreteVariable("B", values=("b1", "b2", "b3")) + 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))) - + 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() - # Ensure Cochran was actually evaluated - self.assertIsNotNone(getattr(self.widget, "_cochran_ok", None)) - self.assertTrue(self.widget._cochran_ok) + assert self.widget.Information.cochran.is_shown() + assert not self.widget.Warning.cochran.is_shown() - # 2) Data that FAIL Cochran: 3 expected cells < 5 (e.g. 10/20/30 vs 20/20/20) + # 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(Table.from_list(Domain([a, b]), list(zip(rows_bad, cols_bad))).domain, - list(zip(rows_bad, cols_bad))) - + table_bad = Table.from_list(Domain([a,b]), list(zip(rows_bad, cols_bad))) self.send_signal(self.widget.Inputs.data, table_bad) - # Re-assign attrs in case the widget resets them in handle signals self.widget.attr_x, self.widget.attr_y = a, b self.widget.update_graph() - - self.assertIsNotNone(getattr(self.widget, "_cochran_ok", None)) - self.assertFalse(self.widget._cochran_ok) + assert not self.widget.Information.cochran.is_shown() + assert self.widget.Warning.cochran.is_shown() def test_metadata(self): """ From b16232ad0c1bbadae659574a27d8505f4cf4b008 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Wed, 1 Oct 2025 09:18:36 +0200 Subject: [PATCH 11/13] =?UTF-8?q?Refactor=20Cochran=E2=80=99s=20rule=20han?= =?UTF-8?q?dling=20in=20Sieve=20widget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Information.cochran with Warning.cochran only when Cochran’s rule is violated. - Implement _check_cochran returning None (OK) or an explanatory error string. - Clear warning when the rule is satisfied, instead of showing “Meets Cochran’s rule”. - Provide more informative warning messages, e.g. “some expected frequencies < 1” or “more than 20% of expected frequencies < 5”. - This prevents showing info messages when everything is fine, as suggested in review. --- Orange/widgets/visualize/owsieve.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 56dcfd92bcb..04f1a5e934f 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -84,11 +84,8 @@ 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") + cochran = Msg("Does not meet Cochran's rule\n{}") want_control_area = False @@ -522,23 +519,29 @@ 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) + msg = self._check_cochran(chi) + if msg is None: + self.Warning.cochran.clear() + else: + self.Warning.cochran(msg) - def _cochran_ok(self, chi): + def _check_cochran(self, chi): """ - Return True if Cochran's rule is met; otherwise return False. + Check Cochran's rule. + Return None if it is met, a string describing the problem. """ expected = np.asarray(chi.expected, dtype=float) cells = int(expected.size) if cells == 0: - return False + return "no cells in contingency table" 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 + if num_lt1 > 0: + return "some expected frequencies < 1" + if num_lt5 > 0.2 * cells: + return "more than 20% of expected frequencies < 5" + return None def get_widget_name_extension(self): if self.data is not None: From f5bc3eb47b115e7c6cd8bc4d47e724983bf03827 Mon Sep 17 00:00:00 2001 From: Guillermo Molleda Jimena Date: Wed, 1 Oct 2025 09:51:00 +0200 Subject: [PATCH 12/13] =?UTF-8?q?Update=20tests=20for=20Cochran=E2=80=99s?= =?UTF-8?q?=20rule=20warning=20in=20Sieve=20widget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adapt test_cochran_messages to new behavior: * Remove references to Information.cochran (no longer exists). * Assert no warning is shown when Cochran’s rule is satisfied. * Assert a warning is shown when the rule is violated, and that the warning message contains informative text. - Use str(self.widget.Warning.cochran) instead of .text to check message contents. - Keeps test coverage consistent after widget refactor. --- Orange/widgets/visualize/tests/test_owsieve.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index 7290b83e5e4..7405492edb8 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -114,25 +114,25 @@ 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 + # PASS: all expected frequencies >= 5 (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 + # FAIL: some expected frequencies < 5 (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() + msg_text = str(self.widget.Warning.cochran) + assert "expected" in msg_text.lower() def test_metadata(self): """ From ad9a64fb6f8189d81a23ccdfc81cdd2f1ad80d21 Mon Sep 17 00:00:00 2001 From: janezd Date: Thu, 23 Oct 2025 12:42:31 +0200 Subject: [PATCH 13/13] OWSieve: cosmetic changes in Cochran rule testing; translations to Slovenian --- Orange/widgets/visualize/owsieve.py | 31 +++++++++---------- .../widgets/visualize/tests/test_owsieve.py | 22 +++++++------ i18n/si/msgs.jaml | 12 +++++-- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/Orange/widgets/visualize/owsieve.py b/Orange/widgets/visualize/owsieve.py index 04f1a5e934f..d89774a2c4f 100644 --- a/Orange/widgets/visualize/owsieve.py +++ b/Orange/widgets/visualize/owsieve.py @@ -85,7 +85,7 @@ class Outputs: graph_name = "canvas" # QGraphicsScene class Warning(OWWidget.Warning): - cochran = Msg("Does not meet Cochran's rule\n{}") + cochran = Msg("Data does not meet the Cochran's rule\n{}") want_control_area = False @@ -108,10 +108,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 = { - "widget":self.attr_box, "master":self, "contentsLength":12, - "searchable":True, "sendSelectedValue":True, - "callback":self.attr_changed, "model":self.domain_model} + combo_args = dict( + 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) @@ -442,7 +442,7 @@ def _oper(attr, txt): return f"{xt}
{yt}
{ct}" - + self.Warning.cochran.clear() for item in self.canvas.items(): self.canvas.removeItem(item) if self.data is None or len(self.data) == 0 or \ @@ -520,27 +520,26 @@ def _oper(attr, txt): # Assume similar height for both lines text("N = " + fmt(chi.n), 0, bottom - xl.boundingRect().height()) msg = self._check_cochran(chi) - if msg is None: - self.Warning.cochran.clear() - else: + if msg is not None: self.Warning.cochran(msg) - def _check_cochran(self, chi): + @staticmethod + def _check_cochran(chi): """ Check Cochran's rule. - Return None if it is met, a string describing the problem. + Return None if it is met, otherwise a string describing the problem. """ expected = np.asarray(chi.expected, dtype=float) - cells = int(expected.size) + cells = expected.size if cells == 0: return "no cells in contingency table" eps = 1e-12 - num_lt1 = int((expected < 1.0 - eps).sum()) - num_lt5 = int((expected < 5.0 - eps).sum()) + num_lt1 = (expected < 1.0 - eps).sum() + num_lt5 = (expected < 5.0 - eps).sum() if num_lt1 > 0: - return "some expected frequencies < 1" + return "some expected frequencies are below 1" if num_lt5 > 0.2 * cells: - return "more than 20% of expected frequencies < 5" + return "more than 20% of expected frequencies are below 5" return None def get_widget_name_extension(self): diff --git a/Orange/widgets/visualize/tests/test_owsieve.py b/Orange/widgets/visualize/tests/test_owsieve.py index 7405492edb8..4b5a96e0868 100644 --- a/Orange/widgets/visualize/tests/test_owsieve.py +++ b/Orange/widgets/visualize/tests/test_owsieve.py @@ -111,28 +111,31 @@ def test_chisquare(self): self.assertFalse(isnan(chi.chisq)) def test_cochran_messages(self): - a = DiscreteVariable("A", values=("a1","a2","a3")) - b = DiscreteVariable("B", values=("b1","b2","b3")) + a = DiscreteVariable("A", values=("a1", "a2", "a3")) + b = DiscreteVariable("B", values=("b1", "b2", "b3")) # PASS: all expected frequencies >= 5 (20/20/20 × 20/20/20) - rows_ok = ["a1"]*20 + ["a2"]*20 + ["a3"]*20 - cols_ok = ["b1"]*20 + ["b2"]*20 + ["b3"]*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 not self.widget.Warning.cochran.is_shown() + self.assertFalse(self.widget.Warning.cochran.is_shown()) # FAIL: some expected frequencies < 5 (10/20/30 × 20/20/20) - rows_bad = ["a1"]*10 + ["a2"]*20 + ["a3"]*30 - cols_bad = ["b1"]*20 + ["b2"]*20 + ["b3"]*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 self.widget.Warning.cochran.is_shown() + self.assertTrue(self.widget.Warning.cochran.is_shown()) msg_text = str(self.widget.Warning.cochran) - assert "expected" in msg_text.lower() + self.assertIn("expected", msg_text.lower()) + + self.send_signal(self.widget.Inputs.data, None) + self.assertFalse(self.widget.Warning.cochran.is_shown()) def test_metadata(self): """ @@ -204,7 +207,6 @@ def test_input_features(self): # 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()) diff --git a/i18n/si/msgs.jaml b/i18n/si/msgs.jaml index 4453c3afbaf..434f76f9384 100644 --- a/i18n/si/msgs.jaml +++ b/i18n/si/msgs.jaml @@ -196,9 +196,9 @@ util.py: def `funcv`: unsafe: false version.py: - 3.39.0: false - 3.39.0.dev0+0a43fd4: false - 0a43fd438e4e20613c4d9899c1781f874d33d921: false + 3.40.0: false + 3.40.0.dev0+e599b5b: false + e599b5bcc1999b32954f7a5693c9d7011fcabab7: false .dev: false canvas/__main__.py: ORANGE_STATISTICS_API_URL: false @@ -14942,6 +14942,8 @@ widgets/visualize/owsieve.py: class `Outputs`: Selected Data: Izbrani podatki canvas: false + class `Warning`: + Data does not meet the Cochran's rule\n{}: Podatki ne ustrezajo Cochranovemu pravilu\n{} def `__init__`: attr_x: false \u2717: true @@ -14980,6 +14982,10 @@ widgets/visualize/owsieve.py: Feature {} has no values: Spremenljivka {} nima znanih vrednosti. χ²={:.2f}, p={:.3f}: true 'N = ': true + def `_check_cochran`: + no cells in contingency table: kontingenčna tabela nima celic + some expected frequencies are below 1: nekatere pričakovane pogostosti so manjše od 1 + more than 20% of expected frequencies are below 5: več kot 20 % pričakovanih pogostosti je manjših od 5 def `get_widget_name_extension`: {} vs {}: {} in {} __main__: false