diff --git a/orangecontrib/educational/widgets/owpolynomialclassification.py b/orangecontrib/educational/widgets/owpolynomialclassification.py
index 16ca808..717499c 100644
--- a/orangecontrib/educational/widgets/owpolynomialclassification.py
+++ b/orangecontrib/educational/widgets/owpolynomialclassification.py
@@ -1,79 +1,79 @@
-from Orange.widgets.utils import itemmodels
-from math import isnan
-from os import path
import copy
+from colorsys import rgb_to_hsv, hsv_to_rgb
+from xml.sax.saxutils import escape
+from itertools import chain
import numpy as np
-import scipy.sparse as sp
from scipy.interpolate import splprep, splev
from scipy.ndimage.filters import gaussian_filter
-from AnyQt.QtCore import Qt
-from AnyQt.QtGui import QPixmap, QColor, QIcon
+from AnyQt.QtCore import Qt, QRectF, QObject, QPointF, QEvent
+from AnyQt.QtGui import QPalette, QPen, QFont, QCursor, QColor, QBrush
+from AnyQt.QtWidgets import QGraphicsSceneMouseEvent, QGraphicsTextItem
-from Orange.base import Learner as InputLearner
-from Orange.data import (
- ContinuousVariable, Table, Domain, StringVariable, DiscreteVariable)
-from Orange.widgets import settings, gui
+import pyqtgraph as pg
+
+from Orange.data import \
+ Table, Domain, ContinuousVariable, StringVariable, DiscreteVariable
+from Orange.base import Learner
+from Orange.classification import \
+ LogisticRegressionLearner, RandomForestLearner, TreeLearner
+from Orange.preprocess.transformation import Indicator
+
+from Orange.widgets import gui
+from Orange.widgets.settings import \
+ DomainContextHandler, Setting, SettingProvider, ContextSetting
+from Orange.widgets.utils.colorpalettes import DiscretePalette
+from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.owlearnerwidget import OWBaseLearner
-from Orange.classification import (
- LogisticRegressionLearner,
- RandomForestLearner,
- TreeLearner
-)
+from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase
+from Orange.widgets.visualize.utils.plotutils import SymbolItemSample
from Orange.widgets.widget import Msg, Input, Output
-from orangewidget.report import report
-
from orangecontrib.educational.widgets.utils.polynomialtransform \
import PolynomialTransform
-from orangecontrib.educational.widgets.utils.color_transform \
- import rgb_hash_brighter, rgb_to_hex
from orangecontrib.educational.widgets.utils.contour import Contour
-from orangecontrib.educational.widgets.highcharts import Highchart
+GRID_SIZE = 60
-class Scatterplot(Highchart):
- """
- Scatterplot extends Highchart and just defines some defaults:
- * disable scroll-wheel zooming,
- * disable all points selection
- * set cursor for series to move
- * adds javascript for contour
- """
- def __init__(self, **kwargs):
- with open(path.join(path.dirname(__file__), 'resources', 'highcharts-contour.js'),
- encoding='utf-8') as f:
- contour_js = f.read()
+class HoverEventDelegate(QObject):
+ def __init__(self, delegate, parent=None):
+ super().__init__(parent)
+ self.delegate = delegate
- super().__init__(enable_zoom=False,
- enable_select='',
- javascript=contour_js,
- **kwargs)
+ def eventFilter(self, obj, event):
+ return isinstance(event, QGraphicsSceneMouseEvent) \
+ and self.delegate(event)
- def remove_contours(self):
- self.evalJS("""
- for(i=chart.series.length - 1; i >= 0; i--){
- if(chart.series[i].type == "spline")
- {
- chart.series[i].remove(false);
- }
- }""")
- def add_series(self, series):
- for i, s in enumerate(series):
- self.exposeObject('series%d' % i, series[i])
- self.evalJS("chart.addSeries(series%d, false);" % i)
+class PolynomialPlot(OWScatterPlotBase):
+ # Like OWScatterPlotBase, but with reversed legend, so the target class
+ # is first and "others" is seecond.
+ @staticmethod
+ def _make_pen(color, width):
+ p = QPen(color, width)
+ p.setCosmetic(True)
+ return p
- def redraw_series(self):
- self.evalJS("chart.redraw();")
+ def _update_colored_legend(self, legend, labels, _):
+ if self.scatterplot_item is None or not self.palette:
+ return
+ colors = self.palette.values_to_colors(np.arange(len(labels)))
+ for color, label in reversed(list(zip(colors, labels))):
+ color = QColor(*color)
+ pen = self._make_pen(color.darker(self.DarkerValue), 1.5)
+ color.setAlpha(self.alpha_value)
+ brush = QBrush(color)
+ legend.addItem(
+ SymbolItemSample(pen=pen, brush=brush, size=10, symbol="o"),
+ escape(label))
class OWPolynomialClassification(OWBaseLearner):
name = "Polynomial Classification"
- description = "Widget that demonstrates classification in two classes " \
- "with polynomial expansion of attributes."
+ description = "Widget that demonstrates classification " \
+ "with polynomial expansion of variables."
keywords = ["polynomial classification", "classification", "class",
"classification visualization", "polynomial features"]
icon = "icons/polynomialclassification.svg"
@@ -81,428 +81,389 @@ class OWPolynomialClassification(OWBaseLearner):
resizing_enabled = True
priority = 600
- # inputs and outputs
class Inputs(OWBaseLearner.Inputs):
- learner = Input("Learner", InputLearner)
+ learner = Input("Learner", Learner)
class Outputs(OWBaseLearner.Outputs):
coefficients = Output("Coefficients", Table, default=True)
data = Output("Data", Table)
- # data attributes
- data = None
- selected_data = None
- probabilities_grid = None
- xv = None
- yv = None
-
- # learners
LEARNER = LogisticRegressionLearner
- learner_other = None
- default_preprocessor = PolynomialTransform
-
- learner_name = settings.Setting("Polynomial Classification")
-
- # widget properties
- attr_x = settings.Setting('')
- attr_y = settings.Setting('')
- target_class = settings.Setting('')
- degree = settings.Setting(1)
- legend_enabled = settings.Setting(True)
- contours_enabled = settings.Setting(False)
- contour_step = settings.Setting(0.1)
-
- graph_name = 'scatter'
-
- # settings
- grid_size = 25
- contour_color = "#1f1f1f"
-
- # layout elements
- options_box = None
- cbx = None
- cby = None
- degree_spin = None
- plot_properties_box = None
- contours_enabled_checkbox = None
- legend_enabled_checkbox = None
- contour_step_slider = None
- scatter = None
- target_class_combobox = None
- x_var_model = None
- y_var_model = None
+
+ settingsHandler = DomainContextHandler(
+ match_values=DomainContextHandler.MATCH_VALUES_CLASS)
+ graph = SettingProvider(PolynomialPlot)
+
+ learner_name = Setting("Polynomial Classification")
+ attr_x = ContextSetting(None)
+ attr_y = ContextSetting(None)
+ target_class = Setting("")
+ degree = Setting(1)
+ contours_enabled = Setting(True)
+
+ graph_name = 'graph'
class Error(OWBaseLearner.Error):
- to_few_features = Msg(
- "Polynomial classification requires at least two numeric features")
- no_class = Msg("Data must have a single discrete class attribute")
- all_none_data = Msg("One of the features has no defined values")
- no_classifier = Msg("Learner must be a classifier")
+ num_features = Msg("Data must contain at least two numeric variables.")
+ no_class = Msg("Data must have a single target attribute.")
+ no_nonnan_data = Msg("No points with defined values.")
+ same_variable = Msg("Select two different variables.")
+
+ def __init__(self, *args, **kwargs):
+ # Some attributes must be created before super().__init__
+ self._add_graph()
+ self.var_model = DomainModel(valid_types=(ContinuousVariable, ))
+
+ super().__init__(*args, **kwargs)
+ self.input_learner = None
+ self.data = None
+
+ self.learner = None
+ self.selected_data = None
+ self.probabilities_grid = None
+ self.xv = None
+ self.yv = None
+ self.contours = []
+ self.init_learner()
def add_main_layout(self):
- # var models
- self.x_var_model = itemmodels.VariableListModel()
- self.y_var_model = itemmodels.VariableListModel()
-
- # options box
- self.options_box = gui.widgetBox(self.controlArea, "Options")
- opts = dict(
- widget=self.options_box, master=self, orientation=Qt.Horizontal)
- opts_combo = dict(opts, **dict(sendSelectedValue=True))
- self.cbx = gui.comboBox(
- value='attr_x', label='X: ', callback=self.apply, **opts_combo)
- self.cby = gui.comboBox(
- value='attr_y', label='Y: ', callback=self.apply, **opts_combo)
- self.target_class_combobox = gui.comboBox(
- value='target_class', label='Target: ',
- callback=self.apply, **opts_combo)
- self.degree_spin = gui.spin(
- value='degree', label='Polynomial expansion:',
- minv=1, maxv=5, step=1, callback=self.init_learner,
- alignment=Qt.AlignRight, controlWidth=70, **opts)
-
- self.cbx.setModel(self.x_var_model)
- self.cby.setModel(self.y_var_model)
-
- # plot properties box
- self.plot_properties_box = gui.widgetBox(
- self.controlArea, "Plot Properties")
- self.legend_enabled_checkbox = gui.checkBox(
- self.plot_properties_box, self, 'legend_enabled',
- label="Show legend", callback=self.replot)
- self.contours_enabled_checkbox = gui.checkBox(
- self.plot_properties_box, self, 'contours_enabled',
- label="Show contours", callback=self.plot_contour)
- self.contour_step_slider = gui.spin(
- self.plot_properties_box, self, 'contour_step',
- minv=0.10, maxv=0.50, step=0.05, callback=self.plot_contour,
- label='Contour step:', decimals=2, spinType=float,
- alignment=Qt.AlignRight, controlWidth=70)
+ box = gui.widgetBox(self.controlArea, "Variables")
+ gui.comboBox(
+ box, self, "attr_x", model=self.var_model,
+ callback=self._on_attr_changed)
+ gui.comboBox(
+ box, self, "attr_y", model=self.var_model,
+ callback=self._on_attr_changed)
+ gui.spin(
+ box, self, value='degree', label='Polynomial expansion:',
+ minv=1, maxv=5, step=1, alignment=Qt.AlignRight, controlWidth=70,
+ callback=self._on_degree_changed)
+ gui.comboBox(
+ box, self, 'target_class', label='Target:',
+ orientation=Qt.Horizontal, sendSelectedValue=True,
+ callback=self._on_target_changed)
+
+ box = gui.widgetBox(self.controlArea, box=True)
+ gui.checkBox(
+ box, self.graph, 'show_legend',"Show legend",
+ callback=self.graph.update_legend_visibility)
+ gui.checkBox(
+ box, self, 'contours_enabled', label="Show contours",
+ callback=self.plot_contour)
gui.rubber(self.controlArea)
- # chart
- self.scatter = Scatterplot(
- xAxis_gridLineWidth=0, yAxis_gridLineWidth=0,
- xAxis_startOnTick=False, xAxis_endOnTick=False,
- yAxis_startOnTick=False, yAxis_endOnTick=False,
- xAxis_lineWidth=0, yAxis_lineWidth=0,
- yAxis_tickWidth=1, title_text='', tooltip_shared=False)
-
- # Just render an empty chart so it shows a nice 'No data to display'
- self.scatter.chart()
- self.mainArea.layout().addWidget(self.scatter)
+ def add_bottom_buttons(self):
+ pass
+ def _on_degree_changed(self):
self.init_learner()
+ self.apply()
+
+ def _on_attr_changed(self):
+ self.select_data()
+ self.apply()
+
+ def _on_target_changed(self):
+ self.select_data()
+ self.apply()
+ ##############################
+ # Input signal-related stuff
@Inputs.learner
def set_learner(self, learner):
- """
- Function is sets learner when learner is changed on input
- """
- self.learner_other = learner
+ self.input_learner = learner
self.init_learner()
def set_preprocessor(self, preprocessor):
- """
- Function adds preprocessor when it changed on input
- """
self.preprocessors = [preprocessor] if preprocessor else []
self.init_learner()
+ @Inputs.data
def set_data(self, data):
- """
- Function receives data from input and init part of widget if data
- satisfy. Otherwise set empty plot and notice
- user about that
-
- Parameters
- ----------
- data : Table
- Input data
- """
-
- def reset_combos():
- self.x_var_model[:] = []
- self.y_var_model[:] = []
- self.target_class_combobox.clear()
-
- def init_combos():
- """
- function initialize the combos with attributes
- """
- reset_combos()
-
- c_vars = [var for var in data.domain.variables if var.is_continuous]
-
- self.x_var_model[:] = c_vars
- self.y_var_model[:] = c_vars
-
- for i, var in enumerate(data.domain.class_var.values):
- pix_map = QPixmap(60, 60)
- color = tuple(data.domain.class_var.colors[i].tolist())
- pix_map.fill(QColor(*color))
- self.target_class_combobox.addItem(QIcon(pix_map), var)
+ combo = self.controls.target_class
+ self.closeContext()
self.Error.clear()
-
- # clear variables
+ combo.clear()
+ self.var_model.set_domain(None)
+ self.data = self.selected_data = None
self.xv = None
self.yv = None
self.probabilities_grid = None
- if data is None or len(data) == 0:
- self.data = None
- reset_combos()
- self.set_empty_plot()
- elif sum(True for var in data.domain.attributes
- if isinstance(var, ContinuousVariable)) < 2:
- self.data = None
- reset_combos()
- self.Error.to_few_features()
- self.set_empty_plot()
- elif (data.domain.class_var is None or
- data.domain.class_var.is_continuous or
- sum(line.get_class() == None for line in data) == len(data) or
- len(data.domain.class_var.values) < 2):
- self.data = None
- reset_combos()
+ if not data:
+ return
+ domain = data.domain
+ if domain.class_var is None or domain.class_var.is_continuous:
self.Error.no_class()
- self.set_empty_plot()
- else:
- self.data = data
- init_combos()
- self.attr_x = self.cbx.itemText(0)
- self.attr_y = self.cbx.itemText(1)
- self.target_class = self.target_class_combobox.itemText(0)
+ return
+ if sum(var.is_continuous
+ for var in chain(domain.variables, domain.metas)) < 2:
+ self.Error.num_features()
+ return
- self.apply()
+ self.data = data
+ non_empty = np.bincount(data.Y[np.isfinite(data.Y)].astype(int)) > 0
+ values = np.array(domain.class_var.values)[non_empty]
+ combo.addItems(values.tolist())
+ self.var_model.set_domain(self.data.domain)
+ self.attr_x, self.attr_y = self.var_model[:2]
+ self.target_class = combo.itemText(0)
+
+ hide_attrs = len(self.var_model) == 2
+ self.controls.attr_x.setHidden(hide_attrs)
+ self.controls.attr_y.setHidden(hide_attrs)
+
+ self.openContext(self.data)
+ self.select_data()
def init_learner(self):
- """
- Function init learner and add preprocessors to learner
- """
- if self.learner_other is not None and \
- self.learner_other.__class__.__name__ == "LinearRegressionLearner":
- # in case that learner is a Linear Regression
- self.learner = None
- self.Error.no_classifier()
- else:
- self.learner = (copy.deepcopy(self.learner_other) or
- self.LEARNER(penalty='l2', C=1e10))
- self.learner.preprocessors = (
- [self.default_preprocessor(self.degree)] +
- list(self.preprocessors or []) +
- list(self.learner.preprocessors or []))
- self.Error.no_classifier.clear()
+ self.learner = copy.copy(self.input_learner
+ or self.LEARNER(penalty='l2', C=1e10))
+ self.learner.preprocessors = (
+ [PolynomialTransform(self.degree)] +
+ list(self.preprocessors or []) +
+ list(self.learner.preprocessors or []))
+ self.send_learner()
+
+ def handleNewSignals(self):
self.apply()
- def set_empty_plot(self):
- """
- Function inits empty plot
- """
- self.scatter.clear()
-
- def replot(self):
- """
- This function performs complete replot of the graph
- """
- if self.data is None or self.selected_data is None:
- self.set_empty_plot()
+ def select_data(self):
+ """Put the two selected columns in a new Orange.data.Table"""
+ self.Error.no_nonnan_data.clear()
+ self.Error.same_variable.clear()
+
+ attr_x, attr_y = self.attr_x, self.attr_y
+ if self.attr_x is self.attr_y:
+ self.selected_data = None
+ self.Error.same_variable()
return
- attr_x = self.data.domain[self.attr_x]
- attr_y = self.data.domain[self.attr_y]
- data_x = [v[0] for v in self.data[:, attr_x] if not isnan(v[0])]
- data_y = [v[0] for v in self.data[:, attr_y] if not isnan(v[0])]
- min_x = min(data_x)
- max_x = max(data_x)
- min_y = min(data_y)
- max_y = max(data_y)
- # just in cas that diff is 0
- diff_x = (max_x - min_x) if abs(max_x - min_x) > 0.001 else 0.1
- diff_y = (max_y - min_y) if abs(max_y - min_y) > 0.001 else 0.1
- min_x, max_x = min_x - 0.03 * diff_x, max_x + 0.03 * diff_x
- min_y, max_y = min_y - 0.03 * diff_y, max_y + 0.03 * diff_y
-
- options = dict(series=[])
-
- # gradient and contour
- options['series'] += self.plot_gradient_and_contour(
- min_x, max_x, min_y, max_y)
-
- sd = self.selected_data
- # data points
- options['series'] += [
- dict(
- data=[list(p.attributes())
- for p in sd
- if (p.metas[0] == _class and
- all(v is not None for v in p.attributes()))],
- type="scatter",
- zIndex=10,
- color=rgb_to_hex(tuple(
- sd.domain.metas[0].colors[_class].tolist())),
- showInLegend=True,
- name=sd.domain.metas[0].values[_class])
- for _class in range(len(sd.domain.metas[0].values))]
-
- # add nan values as a gray dots
- options['series'] += [
- dict(
- data=[list(p.attributes())
- for p in sd
- if np.isnan(p.metas[0])],
- type="scatter",
- zIndex=10,
- color=rgb_to_hex((160, 160, 160)),
- showInLegend=False)]
-
- cls_domain = sd.domain.metas[0]
-
- target_idx = cls_domain.values.index(self.target_class)
- target_color = tuple(cls_domain.colors[target_idx].tolist())
- other_color = (tuple(cls_domain.colors[(target_idx + 1) % 2].tolist())
- if len(cls_domain.values) == 2 else (170, 170, 170))
-
- # highcharts parameters
- kwargs = dict(
- xAxis_title_text=attr_x.name,
- yAxis_title_text=attr_y.name,
- xAxis_min=min_x,
- xAxis_max=max_x,
- yAxis_min=min_y,
- yAxis_max=max_y,
- colorAxis=dict(
- labels=dict(enabled=False),
- stops=[
- [0, rgb_hash_brighter(rgb_to_hex(other_color), 0.5)],
- [0.5, '#ffffff'],
- [1, rgb_hash_brighter(rgb_to_hex(target_color), 0.5)]],
- tickInterval=0.2, min=0, max=1),
- plotOptions_contour_colsize=(max_y - min_y) / 1000,
- plotOptions_contour_rowsize=(max_x - min_x) / 1000,
- legend=dict(
- enabled=self.legend_enabled,
- layout='vertical',
- align='right',
- verticalAlign='top',
- floating=True,
- backgroundColor='rgba(255, 255, 255, 0.3)',
- symbolWidth=0,
- symbolHeight=0),
- tooltip_headerFormat="",
- tooltip_pointFormat="%s: {point.x:.2f}
"
- "%s: {point.y:.2f}" %
- (self.attr_x, self.attr_y))
-
- self.scatter.chart(options, **kwargs)
+ names = [var.name for var in (attr_x, attr_y)]
+ if names == ["x", "y"]:
+ names = [""] * 2
+ for place, name in zip(("bottom", "left"), names):
+ self.graph.plot_widget.getAxis(place).setLabel(name)
+ old_class = self.data.domain.class_var
+ values = old_class.values
+ target_idx = values.index(self.target_class)
+
+ new_class = DiscreteVariable(
+ old_class.name + "'",
+ values=(values[1 - target_idx] if len(values) == 2 else 'Others',
+ self.target_class),
+ compute_value=Indicator(old_class, target_idx))
+ new_class.palette = DiscretePalette(
+ "indicator", "indicator",
+ [[64, 64, 64], list(old_class.palette.palette[target_idx])])
+
+ domain = Domain([attr_x, attr_y], new_class, [old_class])
+
+ self.selected_data = self.data.transform(domain)
+ valid_data = \
+ np.flatnonzero(
+ np.all(
+ np.isfinite(self.selected_data.X),
+ axis=1)
+ )
+ if not valid_data.size:
+ self.Error.no_nonnan_data()
+ self.selected_data = None
+ else:
+ self.selected_data = self.selected_data[valid_data]
+
+
+ def apply(self):
+ self.update_model()
+ self.send_model()
+ self.send_coefficients()
+ self.send_data()
+
+ self.graph.reset_graph()
+ self.graph.plot_widget.addItem(self.contour_label) # Re-add the label
+ self.plot_gradient()
self.plot_contour()
- def plot_gradient_and_contour(self, x_from, x_to, y_from, y_to):
- """
- Function constructs series for gradient and contour
-
- Parameters
- ----------
- x_from : float
- Min grid x value
- x_to : float
- Max grid x value
- y_from : float
- Min grid y value
- y_to : float
- Max grid y value
-
- Returns
- -------
- list
- List containing series with background gradient and contour
- """
-
- # grid for gradient
- x = np.linspace(x_from, x_to, self.grid_size)
- y = np.linspace(y_from, y_to, self.grid_size)
+ def update_model(self):
+ self.Error.fitting_failed.clear()
+ self.model = None
+ self.probabilities_grid = None
+ if self.selected_data is not None and self.learner is not None:
+ try:
+ self.model = self.learner(self.selected_data)
+ self.model.name = self.learner_name
+ except Exception as e:
+ self.Error.fitting_failed(str(e))
+
+ ##############################
+ # Graph and its contents
+ def _add_graph(self):
+ self.graph = PolynomialPlot(self)
+ self.graph.plot_widget.setCursor(QCursor(Qt.CrossCursor))
+ self.mainArea.layout().addWidget(self.graph.plot_widget)
+ self.graph.point_width = 1
+
+ axis_color = self.palette().color(QPalette.Text)
+ axis_pen = QPen(axis_color)
+
+ tickfont = QFont(self.font())
+ tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))
+
+ for pos in ("bottom", "left"):
+ axis = self.graph.plot_widget.getAxis(pos)
+ axis.setPen(axis_pen)
+ axis.setTickFont(tickfont)
+ axis.show()
+
+ self._hover_delegate = HoverEventDelegate(self.help_event)
+ self.graph.plot_widget.scene().installEventFilter(self._hover_delegate)
+
+ self.contour_label = label = QGraphicsTextItem()
+ label.setFlag(QGraphicsTextItem.ItemIgnoresTransformations)
+ self.graph.plot_widget.addItem(label)
+ label.hide()
+ # I'm not proud of this and will brew a coffee to the person who
+ # improves it (and comes to claim it)
+ self.graph.plot_widget.scene().leaveEvent = lambda *_: label.hide()
+
+ @staticmethod
+ def _contour_pen(value, hovered):
+ return pg.mkPen(0.2,
+ width=1 + 2 * hovered + (value == 0.5),
+ style=Qt.SolidLine)
+ # Alternative:
+ # Qt.SolidLine if hovered or value == 0.5 else Qt.DashDotLine
+
+ def help_event(self, event):
+ if self.probabilities_grid is None:
+ return
+
+ pos = event.scenePos()
+ pos = self.graph.plot_widget.mapToView(pos)
+
+ # The mouse hover width is a bit larger for easier hovering, but
+ # consequently more than one line can be hovered. Pick the middle one.
+ hovereds = [item for item in self.contours
+ if item.mouseShape().contains(pos)]
+ hovered = hovereds[len(hovereds) // 2] if hovereds else None
+ # Set the pen for all lines, hovered and not hovered (any longer)
+ for item in self.contours:
+ item.setPen(self._contour_pen(item.value, item is hovered))
+
+ # Set the probability for the textitem at mouse position.
+ # If there is a hovered line - this acts as a line label.
+ # Otherwise, take the probability from the grid
+ label = self.contour_label
+ if hovered:
+ prob = hovered.value
+ else:
+ min_x, step_x, min_y, step_y = self.probabilities_grid_dimensions
+ prob = self.probabilities_grid.T[
+ int(np.clip(round((pos.x() - min_x) * step_x), 0, GRID_SIZE - 1)),
+ int(np.clip(round((pos.y() - min_y) * step_y), 0, GRID_SIZE - 1))]
+ prob_lab = f"{round(prob, 3):.3g}"
+ if "." not in prob_lab:
+ prob_lab += ".0" # Showing just 0 or 1 looks ugly
+ label.setHtml(prob_lab)
+ font = label.font()
+ font.setBold(hovered is not None)
+ label.setFont(font)
+
+ # Position the label above and left from mouse; looks nices
+ rect = label.boundingRect()
+ spos = event.scenePos()
+ x, y = spos.x() - rect.width(), spos.y() - rect.height()
+ label.setPos(self.graph.plot_widget.mapToView(QPointF(x, y)))
+
+ label.show()
+ return True
+
+ def changeEvent(self, ev):
+ # This hides the label if the user alt-tabs out of the window
+ if ev.type() == QEvent.ActivationChange and not self.isActiveWindow():
+ self.contour_label.hide()
+ super().changeEvent(ev)
+
+ def plot_gradient(self):
+ if not self.model:
+ self.probabilities_grid = None
+ return
+
+ (min_x, max_x), (min_y, max_y) = self.graph.view_box.viewRange()
+ x = np.linspace(min_x, max_x, GRID_SIZE)
+ y = np.linspace(min_y, max_y, GRID_SIZE)
self.xv, self.yv = np.meshgrid(x, y)
- # parameters to predict from grid
attr = np.hstack((self.xv.reshape((-1, 1)), self.yv.reshape((-1, 1))))
- attr_data = Table.from_numpy(
- self.selected_data.domain, attr,
- np.array([[None]] * len(attr)),
- np.array([[None]] * len(attr))
- )
-
- # results
- self.probabilities_grid = self.model(attr_data, 1)[:, 0]\
+ nil = np.full((len(attr), 1), np.nan)
+ attr_data = Table.from_numpy(self.selected_data.domain, attr, nil, nil)
+
+ self.probabilities_grid = self.model(attr_data, 1)[:, 1] \
.reshape(self.xv.shape)
+ self.probabilities_grid_dimensions = (
+ min_x, GRID_SIZE / (max_x - min_x),
+ min_y, GRID_SIZE / (max_y - min_y))
+
+ if not self._treelike:
+ self.probabilities_grid = self.blur_grid(self.probabilities_grid)
+
+ bitmap = self.probabilities_grid.copy()
+ bitmap *= 255
+ bitmap = bitmap.astype(np.uint8)
+
+ class_var = self.selected_data.domain.class_var
+ h1, s1, v1 = rgb_to_hsv(*class_var.colors[1] / 255)
+ palette = np.vstack((
+ np.linspace([h1, 0, 0.8], [h1, 0, 1], 128),
+ np.linspace([h1, 0, 1], [h1, s1 * 0.5, 0.7 + 0.3 * v1], 128)
+ ))
+ palette = 255 * np.array([hsv_to_rgb(*col) for col in palette])
+ palette = palette.astype(int)
+
+ density_img = pg.ImageItem(bitmap.T, lut=palette)
- blurred = self.blur_grid(self.probabilities_grid)
+ density_img.setRect(QRectF(min_x, min_y,
+ max_x - min_x, max_y - min_y))
+ density_img.setZValue(-1)
+ self.graph.plot_widget.addItem(density_img, ignoreBounds=True)
- is_tree = type(self.learner) in [RandomForestLearner, TreeLearner]
- return self.plot_gradient(self.xv, self.yv,
- self.probabilities_grid
- if is_tree else blurred)
+ def remove_contours(self):
+ while self.contours:
+ self.graph.plot_widget.removeItem(self.contours.pop())
- def plot_gradient(self, x, y, grid):
- """
- Function constructs background gradient
- """
- return [dict(data=[[x[j, k], y[j, k], grid[j, k]] for j in range(len(x))
- for k in range(y.shape[1])],
- grid_width=self.grid_size,
- type="contour")]
+ @property
+ def _treelike(self):
+ return isinstance(self.learner, (RandomForestLearner, TreeLearner))
def plot_contour(self):
- """
- Function constructs contour lines
- """
- self.scatter.remove_contours()
- if not self.data:
+ self.remove_contours()
+ if self.probabilities_grid is None or not self.contours_enabled:
return
- if self.contours_enabled:
- is_tree = type(self.learner) in [RandomForestLearner, TreeLearner]
- # tree does not need smoothing
- contour = Contour(
- self.xv, self.yv, self.probabilities_grid
- if is_tree else self.blur_grid(self.probabilities_grid))
- contour_lines = contour.contours(
- np.hstack(
- (np.arange(0.5, 0, - self.contour_step)[::-1],
- np.arange(0.5 + self.contour_step, 1, self.contour_step))))
- # we want to have contour for 0.5
-
- series = []
- count = 0
- for key, value in contour_lines.items():
- for line in value:
- if (len(line) > self.degree and
- type(self.learner) not in
- [RandomForestLearner, TreeLearner]):
- # if less than degree interpolation fails
- tck, u = splprep(
- [list(x) for x in zip(*reversed(line))],
- s=0.001, k=self.degree,
- per=(len(line)
- if np.allclose(line[0], line[-1])
- else 0))
- new_int = np.arange(0, 1.01, 0.01)
- interpol_line = np.array(splev(new_int, tck)).T.tolist()
- else:
- interpol_line = line
-
- series.append(dict(data=self.labeled(interpol_line, count),
- color=self.contour_color,
- type="spline",
- lineWidth=0.5,
- showInLegend=False,
- marker=dict(enabled=False),
- name="%g" % round(key, 2),
- enableMouseTracking=False
- ))
- count += 1
- self.scatter.add_series(series)
- self.scatter.redraw_series()
+
+ contour = Contour(self.xv, self.yv, self.probabilities_grid)
+ contour_lines = contour.contours(np.arange(0.1, 1, 0.1))
+ for key, value in contour_lines.items():
+ for line in value:
+ if len(line) > self.degree and not self._treelike:
+ tck, u = splprep(
+ [list(x) for x in zip(*reversed(line))],
+ s=0.001, k=self.degree,
+ per=(len(line) if np.allclose(line[0], line[-1])
+ else 0))
+ new_int = np.arange(0, 1.01, 0.01)
+ interpol_line = np.array(splev(new_int, tck)).T.tolist()
+ else:
+ interpol_line = line
+ contour = pg.PlotCurveItem(
+ *np.array(list(interpol_line)).T,
+ pen=self._contour_pen(key, False))
+ # The hover region can be narrowed by calling setClickable
+ # (with False, to keep it unclickable)
+ contour.value = key
+ self.graph.plot_widget.addItem(contour)
+ self.contours.append(contour)
@staticmethod
def blur_grid(grid):
@@ -511,158 +472,67 @@ def blur_grid(grid):
(grid < 0.55)]
return filtered
- @staticmethod
- def labeled(data, no):
- """
- Function labels data with contour levels
- """
- point = (no * 5) # to avoid points on same positions
- point += (1 if point == 0 else 0)
- point %= len(data)
-
- data[point] = dict(
- x=data[point][0],
- y=data[point][1],
- dataLabels=dict(
- enabled=True,
- format="{series.name}",
- verticalAlign='middle',
- style=dict(
- fontWeight="normal",
- color=OWPolynomialClassification.contour_color,
- textShadow=False
- )))
- return data
-
- def select_data(self):
- """
- Function takes two selected columns from data table and merge them
- in new Orange.data.Table
-
- Returns
- -------
- Table
- Table with selected columns
- """
- self.Error.clear()
-
- attr_x = self.data.domain[self.attr_x]
- attr_y = self.data.domain[self.attr_y]
- cols = []
- for attr in (attr_x, attr_y):
- subset = self.data[:, attr]
- cols.append(subset.X if not sp.issparse(subset.X) else subset.X.toarray())
- x = np.column_stack(cols)
- y_c = self.data.Y[:, None] if not sp.issparse(self.data.Y) else self.data.Y.toarray()
-
- if np.isnan(x).all(axis=0).any():
- self.Error.all_none_data()
- return None
+ # The following methods are required by OWScatterPlotBase
+ def get_coordinates_data(self):
+ if not self.selected_data:
+ return None, None
+ return self.selected_data.X.T
- cls_domain = self.data.domain.class_var
- target_idx = cls_domain.values.index(self.target_class)
- other_value = cls_domain.values[(target_idx + 1) % 2]
+ def get_color_data(self):
+ return self.selected_data and self.selected_data.Y
- class_domain = [DiscreteVariable(
- name="Transformed " + self.data.domain.class_var.name,
- values=(self.target_class, 'Others'
- if len(cls_domain.values) > 2 else other_value))]
+ def get_palette(self):
+ return self.selected_data and self.selected_data.domain.class_var.palette
- domain = Domain(
- [attr_x, attr_y],
- class_domain,
- [self.data.domain.class_var])
- y = [(0 if d.get_class().value == self.target_class else 1)
- for d in self.data]
+ def get_color_labels(self):
+ return self.selected_data and self.selected_data.domain.class_var.values
- return Table.from_numpy(domain, x, y, y_c)
+ def is_continuous_color(self):
+ return False
- def apply(self):
- """
- Applies leaner and sends new model and coefficients
- """
- self.send_learner()
- self.update_model()
- self.send_coefficients()
- if any(a is None for a in (self.data, self.model)):
- self.set_empty_plot()
- else:
- self.replot()
- self.send_data()
+ get_size_data = get_shape_data = get_shape_labels = \
+ get_subset_mask = get_label_data = get_tooltip = selection_changed = \
+ lambda *_: None
+ ##############################
+ # Output signal-related stuff
def send_learner(self):
- """
- Function sends learner on widget's output
- """
if self.learner is not None:
self.learner.name = self.learner_name
self.Outputs.learner.send(self.learner)
- def update_model(self):
- """
- Function sends model on widget's output
- """
- self.Error.fitting_failed.clear()
- self.model = None
- if self.data is not None and self.learner is not None:
- self.selected_data = self.select_data()
- if self.selected_data is not None:
- try:
- self.model = self.learner(self.selected_data)
- self.model.name = self.learner_name
- self.model.instances = self.selected_data
- except Exception as e:
- self.Error.fitting_failed(str(e))
-
+ def send_model(self):
self.Outputs.model.send(self.model)
def send_coefficients(self):
- """
- Function sends coefficients on widget's output if model has them
- """
-
- if (self.model is not None and
- isinstance(self.learner, LogisticRegressionLearner) and
- hasattr(self.model, 'skl_model')):
- model = self.model.skl_model
- domain = Domain(
- [ContinuousVariable("coef")], metas=[StringVariable("name")])
- coefficients = (model.intercept_.tolist() +
- model.coef_[0].tolist())
-
- data = self.model.instances
- for preprocessor in self.learner.preprocessors:
- data = preprocessor(data)
- names = ["Intercept"] + [x.name for x in data.domain.attributes]
-
- coefficients_table = Table.from_list(
- domain, list(zip(coefficients, names)))
- self.Outputs.coefficients.send(coefficients_table)
- else:
+ if (self.model is None
+ or not isinstance(self.learner, LogisticRegressionLearner)
+ or not hasattr(self.model, 'skl_model')):
self.Outputs.coefficients.send(None)
-
- def send_data(self):
- """
- Function sends data on widget's output
- """
- if self.data is not None:
- data = self.selected_data
- self.Outputs.data.send(data)
return
- self.Outputs.data.send(None)
- def add_bottom_buttons(self):
- pass
+ model = self.model.skl_model
+ domain = Domain(
+ [ContinuousVariable("coef")], metas=[StringVariable("name")])
+ coefficients = model.intercept_.tolist() + model.coef_[0].tolist()
+ names = ["Intercept"] + [x.name for x in self.model.domain.attributes]
+ coefficients_table = Table.from_list(
+ domain, list(zip(coefficients, names)))
+ self.Outputs.coefficients.send(coefficients_table)
+
+ def send_data(self):
+ if self.selected_data is None:
+ self.Outputs.data.send(None)
+ else:
+ expanded = PolynomialTransform(self.degree)(self.selected_data)
+ self.Outputs.data.send(expanded)
def send_report(self):
- if self.data is None:
+ if self.selected_data is None:
return
- caption = report.render_items_vert((
- ("Polynomial Expansion", self.degree),
- ))
- self.report_plot(self.scatter)
- if caption:
- self.report_caption(caption)
+ name = "" if self.degree == 1 \
+ else f"Model with polynomial expansion {self.degree}"
+ self.report_plot(name=name, plot=self.graph.plot_widget)
if __name__ == "__main__":
diff --git a/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py b/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
index cc2928d..ef69357 100644
--- a/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
+++ b/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
@@ -1,544 +1,255 @@
-from functools import reduce
import unittest
+from unittest.mock import Mock
import numpy as np
import scipy.sparse as sp
from numpy.testing import assert_array_equal
-from Orange.regression import LinearRegressionLearner
-from Orange.widgets.tests.base import WidgetTest
from Orange.data import Table, ContinuousVariable, Domain, DiscreteVariable
-from Orange.classification import (
- LogisticRegressionLearner,
- TreeLearner,
- RandomForestLearner, SVMLearner)
+from Orange.classification import \
+ LogisticRegressionLearner, TreeLearner, SVMLearner
from Orange.preprocess.preprocess import Continuize, Discretize
+from Orange.widgets.tests.base import WidgetTest
+
from orangecontrib.educational.widgets.owpolynomialclassification import \
- OWPolynomialClassification
+ OWPolynomialClassification, GRID_SIZE
from orangecontrib.educational.widgets.utils.polynomialtransform import \
PolynomialTransform
-class TestOWPolynomialClassification(WidgetTest):
-
+class TestOWPolynomialClassificationNoGrid(WidgetTest):
+ # Tests with mocked computation of probability grid and contours
+ # These are never tested and just slow everything down.
def setUp(self):
- self.widget = self.create_widget(OWPolynomialClassification) # type: OWPolynomialClassification
- self.iris = Table.from_file("iris")
-
- def test_add_main_layout(self):
- """
- With this test we check if everything is ok on widget init
- """
- w = self.widget
-
- # add main layout is called when function is initialized
-
- # check just if it is initialize on widget start
- self.assertIsNotNone(w.options_box)
- self.assertIsNotNone(w.cbx)
- self.assertIsNotNone(w.cby)
- self.assertIsNotNone(w.target_class_combobox)
- self.assertIsNotNone(w.degree_spin)
- self.assertIsNotNone(w.plot_properties_box)
- self.assertIsNotNone(w.contours_enabled_checkbox)
- self.assertIsNotNone(w.contour_step_slider)
- self.assertIsNotNone(w.scatter)
-
- # default learner must be logistic regression
- self.assertEqual(w.LEARNER.name, LogisticRegressionLearner.name)
+ # type: OWPolynomialClassification
+ self.widget = self.create_widget(OWPolynomialClassification)
- # widget have to be resizable
- self.assertTrue(w.resizing_enabled)
+ def plot_gradient():
+ self.widget.probabilities_grid = self.widget.model and np.zeros((GRID_SIZE, GRID_SIZE))
- # learner should be Logistic regression
- self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
+ self.widget.plot_gradient = plot_gradient
+ self.widget.plot_contour = Mock()
- # preprocessor should be PolynomialTransform
- self.assertEqual(
- type(w.default_preprocessor), type(PolynomialTransform))
-
- # check if there is learner on output
- self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
+ self.iris = Table.from_file("iris")
- # model and coefficients should be none because of no data
+ def test_init(self):
+ w = self.widget
+ self.assertIsInstance(w.learner, LogisticRegressionLearner)
+ self.assertIsInstance(self.get_output(w.Outputs.learner),
+ LogisticRegressionLearner)
self.assertIsNone(self.get_output(w.Outputs.model))
self.assertIsNone(self.get_output(w.Outputs.coefficients))
- # this parameters are none because no plot should be called
- self.assertIsNone(w.xv)
- self.assertIsNone(w.yv)
- self.assertIsNone(w.probabilities_grid)
-
- def test_set_learner_empty(self):
- """
- Test if learner is set correctly when no learner provided
- """
- w = self.widget
-
- # check if empty
- self.assertEqual(w.learner_other, None)
- self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
- self.assertTrue(isinstance(w.learner, w.LEARNER))
- self.assertEqual(
- type(self.get_output(w.Outputs.learner)), type(LogisticRegressionLearner()))
-
def test_set_learner(self):
- """
- Test if learner is set correctly
- """
w = self.widget
+ self.assertIsInstance(w.learner, LogisticRegressionLearner)
+ self.assertIsInstance(self.get_output(w.Outputs.learner),
+ LogisticRegressionLearner)
- learner = TreeLearner()
+ self.send_signal(w.Inputs.learner, SVMLearner())
+ self.assertIsInstance(self.get_output(w.Outputs.learner), SVMLearner)
- self.send_signal(w.Inputs.learner, learner)
- # check if learners set correctly
- self.assertEqual(w.learner_other, learner)
- self.assertEqual(type(w.learner), type(learner))
- self.assertEqual(type(self.get_output(w.Outputs.learner)), type(learner))
-
- # after learner is removed there should be LEARNER used
self.send_signal(w.Inputs.learner, None)
- self.assertEqual(w.learner_other, None)
- self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
- self.assertTrue(isinstance(w.learner, w.LEARNER))
- self.assertEqual(
- type(self.get_output(w.Outputs.learner)), type(LogisticRegressionLearner()))
+ self.assertIsInstance(w.learner, LogisticRegressionLearner)
+ self.assertIsInstance(self.get_output(w.Outputs.learner),
+ LogisticRegressionLearner)
def test_set_preprocessor(self):
- """
- Test preprocessor set
- """
w = self.widget
preprocessor = Continuize()
-
- # check if empty
- self.assertIn(w.preprocessors, [[], None])
-
self.send_signal(w.Inputs.preprocessor, preprocessor)
- # check preprocessor is set
- self.assertEqual(w.preprocessors, [preprocessor])
- self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
-
- # remove preprocessor
- self.send_signal(w.Inputs.preprocessor, None)
+ for learner in (None, SVMLearner(), None):
+ self.send_signal(w.Inputs.learner, learner)
- self.assertIn(w.preprocessors, [[], None])
- self.assertNotIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+ self.assertEqual(w.preprocessors, [preprocessor])
+ preprocessors = self.get_output(w.Outputs.learner).preprocessors
+ self.assertIn(preprocessor, preprocessors)
+ self.assertTrue(isinstance(pp, PolynomialTransform)
+ for pp in preprocessors)
- # change preprocessor
- preprocessor = Continuize()
- self.send_signal(w.Inputs.preprocessor, preprocessor)
+ self.send_signal(w.Inputs.preprocessor, None)
+ self.assertIn(w.preprocessors, [[], None])
+ self.assertNotIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+ self.assertTrue(isinstance(pp, PolynomialTransform)
+ for pp in preprocessors)
- # check preprocessor is set
- self.assertEqual(w.preprocessors, [preprocessor])
- self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+ self.send_signal(w.Inputs.preprocessor, preprocessor)
+ self.assertEqual(w.preprocessors, [preprocessor])
+ self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+ self.assertTrue(isinstance(pp, PolynomialTransform)
+ for pp in preprocessors)
def test_set_data(self):
- """
- Test widget behavior when data set
- """
w = self.widget
-
- num_continuous_attributes = sum(
- True for var in self.iris.domain.attributes
- if isinstance(var, ContinuousVariable))
+ attr0, attr1 = self.iris.domain.attributes[:2]
+ class_vals = self.iris.domain.class_var.values
self.send_signal(w.Inputs.data, self.iris[::15])
- # widget does not have any problems with that data set so
- # everything should be fine
- self.assertEqual(w.cbx.count(), num_continuous_attributes)
- self.assertEqual(w.cby.count(), num_continuous_attributes)
- self.assertEqual(
- w.target_class_combobox.count(),
- len(self.iris.domain.class_var.values))
- self.assertEqual(w.cbx.currentText(), self.iris.domain[0].name)
- self.assertEqual(w.cby.currentText(), self.iris.domain[1].name)
- self.assertEqual(
- w.target_class_combobox.currentText(),
- self.iris.domain.class_var.values[0])
-
- self.assertEqual(w.attr_x, self.iris.domain[0].name)
- self.assertEqual(w.attr_y, self.iris.domain[1].name)
- self.assertEqual(w.target_class, self.iris.domain.class_var.values[0])
-
- # change showed attributes
- w.attr_x = self.iris.domain[1].name
- w.attr_y = self.iris.domain[2].name
- w.target_class = self.iris.domain.class_var.values[1]
-
- self.assertEqual(w.cbx.currentText(), self.iris.domain[1].name)
- self.assertEqual(w.cby.currentText(), self.iris.domain[2].name)
- self.assertEqual(
- w.target_class_combobox.currentText(),
- self.iris.domain.class_var.values[1])
-
- self.assertEqual(w.attr_x, self.iris.domain[1].name)
- self.assertEqual(w.attr_y, self.iris.domain[2].name)
- self.assertEqual(w.target_class, self.iris.domain.class_var.values[1])
+ self.assertEqual(w.var_model.rowCount(), 4)
+ self.assertEqual(w.controls.target_class.count(), len(class_vals))
+ self.assertIs(w.attr_x, attr0)
+ self.assertIs(w.attr_y, attr1)
+ self.assertEqual(w.target_class, class_vals[0])
# remove data set
self.send_signal(w.Inputs.data, None)
- self.assertEqual(w.cbx.count(), 0)
- self.assertEqual(w.cby.count(), 0)
- self.assertEqual(w.target_class_combobox.count(), 0)
+ self.assertEqual(w.var_model.rowCount(), 0)
+ self.assertEqual(w.controls.target_class.count(), 0)
+
+ def _set_iris(self):
+ # Set some data so the widget goes up and test can check that it is
+ # torn down when erroneous data is received
+ w = self.widget
+ self.send_signal(w.Inputs.data, self.iris)
+ self.assert_all_up()
+
+ def assert_all_up(self):
+ w = self.widget
+ self.assertNotEqual(len(w.var_model), 0)
+ self.assertNotEqual(w.controls.target_class.count(), 0)
+ self.assertIsNotNone(w.data)
+ self.assertIsNotNone(w.selected_data)
+ self.assertIsNotNone(w.model)
+ self.assertIsNotNone(w.probabilities_grid)
+
+ def assert_all_down(self):
+ w = self.widget
+ self.assertEqual(len(w.var_model), 0)
+ self.assertEqual(w.controls.target_class.count(), 0)
+ self.assertIsNone(w.data)
+ self.assertIsNone(w.selected_data)
+ self.assertIsNone(w.model)
+ self.assertIsNone(w.probabilities_grid)
def test_set_data_no_class(self):
- """
- Test widget on data with no class
- """
w = self.widget
- table_no_class = Table.from_list(
- Domain([ContinuousVariable("x"), ContinuousVariable("y")]),
- [[1, 2], [2, 3]])
+ self._set_iris()
+
+ table_no_class = self.iris.transform(Domain(self.iris.domain.attributes, []))
self.send_signal(w.Inputs.data, table_no_class)
+ self.assert_all_down()
- self.assertEqual(w.cbx.count(), 0)
- self.assertEqual(w.cby.count(), 0)
- self.assertEqual(w.target_class_combobox.count(), 0)
- self.assertTrue(w.Error.no_class.is_shown())
+ def test_set_data_regression(self):
+ w = self.widget
+ self._set_iris()
+
+ table_regr = Table.from_numpy(
+ Domain(self.iris.domain.attributes[:3],
+ self.iris.domain.attributes[3]),
+ self.iris.X[:, :3], self.iris.X[:, 3])
+ self.send_signal(w.Inputs.data, table_regr)
+ self.assert_all_down()
def test_set_data_one_class(self):
- """
- Test widget on data with one class variable
- """
w = self.widget
+ self._set_iris()
- table_one_class = Table.from_list(
- Domain([ContinuousVariable("x"), ContinuousVariable("y")],
+ table_one_class = Table.from_numpy(
+ Domain(self.iris.domain.attributes,
DiscreteVariable("a", values=("k", ))),
- [[1, 2], [2, 3]], [0, 0])
+ self.iris.X, np.zeros(150))
self.send_signal(w.Inputs.data, table_one_class)
- self.assertEqual(w.cbx.count(), 0)
- self.assertEqual(w.cby.count(), 0)
- self.assertEqual(w.target_class_combobox.count(), 0)
- self.assertTrue(w.Error.no_class.is_shown())
+ self.assertEqual(len(w.var_model), 4)
+ self.assertEqual(w.controls.target_class.count(), 1)
+ self.assertIsNotNone(w.data)
+ self.assertIsNotNone(w.selected_data)
+ self.assertIsNone(w.model)
+ self.assertIsNone(w.probabilities_grid)
+ self.assertTrue(w.Error.fitting_failed.is_shown())
def test_set_data_wrong_var_number(self):
- """
- Test widget on data with not enough continuous variables
- """
w = self.widget
- #
+ self._set_iris()
+
table_no_enough_cont = Table.from_numpy(
Domain(
[ContinuousVariable("x"),
DiscreteVariable("y", values=("a", "b"))],
- ContinuousVariable("a")),
+ DiscreteVariable("a", values=("a", "b"))),
[[1, 0], [2, 1]], [0, 0])
self.send_signal(w.Inputs.data, table_no_enough_cont)
- self.assertEqual(w.cbx.count(), 0)
- self.assertEqual(w.cby.count(), 0)
- self.assertEqual(w.target_class_combobox.count(), 0)
- self.assertTrue(w.Error.to_few_features.is_shown())
-
- def test_init_learner(self):
- """
- Test init
- """
- w = self.widget
-
- learner = TreeLearner()
-
- # check if empty
- self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
- self.assertTrue(isinstance(w.learner, w.LEARNER))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
-
- self.send_signal(w.Inputs.learner, learner)
-
- # check if learners set correctly
- self.assertEqual(type(w.learner), type(learner))
-
- # after learner is removed there should be LEARNER used
- self.send_signal(w.Inputs.learner, None)
- self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
- self.assertTrue(isinstance(w.learner, w.LEARNER))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
-
- # set it again just in case something goes wrong
- learner = RandomForestLearner()
- self.send_signal(w.Inputs.learner, learner)
-
- self.assertEqual(type(w.learner), type(learner))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
-
- # change learner this time not from None
- learner = TreeLearner()
- self.send_signal(w.Inputs.learner, learner)
-
- self.assertEqual(type(w.learner), type(learner))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
-
- # set other preprocessor
- preprocessor = Discretize
- # selected this preprocessor because know that not exist in LogReg
- self.send_signal(w.Inputs.preprocessor, preprocessor())
-
- self.assertEqual(type(w.learner), type(learner))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, preprocessor),
- w.learner.preprocessors, False))
-
- # remove preprocessor
- self.send_signal(w.Inputs.preprocessor, None)
- self.assertEqual(type(w.learner), type(learner))
- self.assertTrue(
- reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
- w.learner.preprocessors, False))
-
- self.assertFalse(reduce(lambda x, y: x or isinstance(y, preprocessor),
- w.learner.preprocessors, False))
-
- def test_replot(self):
- """
- Test everything that is possible to test in replot
- This function tests all replot functions
- """
- w = self.widget
-
- w.replot()
-
- # test nothing happens when no data
- self.assertIsNone(w.xv)
- self.assertIsNone(w.yv)
- self.assertIsNone(w.probabilities_grid)
-
- # when data available plot happens
- self.send_signal(w.Inputs.data, self.iris)
- self.assertIsNotNone(w.xv)
- self.assertIsNotNone(w.yv)
- self.assertIsNotNone(w.probabilities_grid)
- self.assertTupleEqual(
- (w.grid_size, w.grid_size), w.probabilities_grid.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
- # check that everything works fine when contours enabled/disabled
- w.contours_enabled_checkbox.click()
-
- self.assertIsNotNone(w.xv)
- self.assertIsNotNone(w.yv)
- self.assertIsNotNone(w.probabilities_grid)
- self.assertTupleEqual(
- (w.grid_size, w.grid_size), w.probabilities_grid.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
- w.contours_enabled_checkbox.click()
-
- self.assertIsNotNone(w.xv)
- self.assertIsNotNone(w.yv)
- self.assertIsNotNone(w.probabilities_grid)
- self.assertTupleEqual(
- (w.grid_size, w.grid_size), w.probabilities_grid.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
- self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
- # when remove data
- self.send_signal(w.Inputs.data, None)
-
- self.assertIsNone(w.xv)
- self.assertIsNone(w.yv)
- self.assertIsNone(w.probabilities_grid)
-
- def test_blur_grid(self):
- w = self.widget
-
- self.send_signal(w.Inputs.data, self.iris)
- # here we can check that 0.5 remains same
- assert_array_equal(w.probabilities_grid == 0.5,
- w.blur_grid(w.probabilities_grid) == 0.5)
+ self.assert_all_down()
+ self.assertTrue(w.Error.num_features.is_shown())
def test_select_data(self):
- """
- Check if select data works properly
- """
w = self.widget
-
self.send_signal(w.Inputs.data, self.iris)
- selected_data = w.select_data()
- self.assertEqual(len(selected_data.domain.attributes), 2)
- self.assertIsNotNone(selected_data.domain.class_var)
- self.assertEqual(len(selected_data.domain.metas), 1)
- # meta with information about real cluster
- self.assertEqual(len(selected_data), len(self.iris))
+ w.select_data()
+ np.testing.assert_equal(w.selected_data.X, self.iris.X[:, :2])
+ np.testing.assert_equal(w.selected_data.Y, [1] * 50 + [0] * 100)
- # selected data none when one column only Nones
- data = Table.from_numpy(
- Domain([ContinuousVariable('a'), ContinuousVariable('b')],
- DiscreteVariable('c', values=('a', 'b'))),
- [[1, None], [1, None]], [0, 1]
- )
- self.send_signal(w.Inputs.data, data)
- selected_data = w.select_data()
- self.assertIsNone(selected_data)
+ w.attr_x, w.attr_y = self.iris.domain[1:3]
+ w.select_data()
+ np.testing.assert_equal(w.selected_data.X, self.iris.X[:, 1:3])
+ np.testing.assert_equal(w.selected_data.Y, [1] * 50 + [0] * 100)
+ self.assertIsNotNone(w.model)
+ self.assertIsNotNone(w.probabilities_grid)
+ # data without valid rows
data = Table.from_numpy(
Domain([ContinuousVariable('a'), ContinuousVariable('b')],
DiscreteVariable('c', values=('a', 'b'))),
- [[None, None], [None, None]], [0, 1]
+ [[1, None], [None, 1]], [0, 1]
)
self.send_signal(w.Inputs.data, data)
- selected_data = w.select_data()
- self.assertIsNone(selected_data)
-
- def test_send_learner(self):
- """
- Test if correct learner on output
- """
- w = self.widget
-
- self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
- self.assertTrue(isinstance(self.get_output(w.Outputs.learner), w.LEARNER))
-
- # set new learner
- learner = TreeLearner
- self.send_signal(w.Inputs.learner, learner())
- self.process_events()
- self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
- self.assertTrue(isinstance(self.get_output(w.Outputs.learner), learner))
-
- # back to default learner
- self.send_signal(w.Inputs.learner, None)
- self.process_events()
- self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
- self.assertTrue(isinstance(self.get_output(w.Outputs.learner), w.LEARNER))
+ w.select_data()
+ self.assertIsNone(w.selected_data)
+ self.assertIsNone(w.model)
+ self.assertIsNone(w.probabilities_grid)
def test_update_model(self):
- """
- Function check if correct model is on output
- """
w = self.widget
-
- # when no data
self.assertIsNone(w.model)
self.assertIsNone(self.get_output(w.Outputs.model))
- # set data
self.send_signal(w.Inputs.data, self.iris)
self.assertIsNotNone(w.model)
self.assertEqual(w.model, self.get_output(w.Outputs.model))
- # remove data
self.send_signal(w.Inputs.data, None)
self.assertIsNone(w.model)
self.assertIsNone(self.get_output(w.Outputs.model))
- @unittest.skip("Travis fails: TimeoutError")
def test_send_coefficients(self):
- """
- Coefficients are only available if Logistic regression is used
- """
w = self.widget
- # none when no data (model not build)
self.assertIsNone(self.get_output(w.Outputs.coefficients))
- # by default LogisticRegression so coefficients exists
self.send_signal(w.Inputs.data, self.iris)
- # to check correctness before degree is changed
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
- self.assertEqual(len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
- # change degree
- for j in range(1, 6):
- w.degree_spin.setValue(j)
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
+ for w.degree in range(1, 6):
+ w._on_degree_changed()
+ self.assertEqual(
+ len(self.get_output(w.Outputs.coefficients)),
+ (w.degree + 1) * (w.degree + 2) // 2,
+ f"at degree={w.degree}")
self.assertEqual(
- len(self.get_output(w.Outputs.coefficients)), num_coefficients)
+ len(self.get_output(w.Outputs.data).domain.attributes),
+ (w.degree + 1) * (w.degree + 2) // 2 - 1,
+ f"at degree={w.degree}")
# change learner which does not have coefficients
learner = TreeLearner
self.send_signal(w.Inputs.learner, learner())
self.assertIsNone(self.get_output(w.Outputs.coefficients))
+ self.assertIsNotNone(self.get_output(w.Outputs.data))
- # remove learner
self.send_signal(w.Inputs.learner, None)
-
- # to check correctness before degree is changed
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
- self.assertEqual(
- len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
- # change degree
- for j in range(1, 6):
- w.degree_spin.setValue(j)
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
- self.assertEqual(
- len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
- # manulay set LogisticRegression
- self.send_signal(w.Inputs.learner, LogisticRegressionLearner())
-
- # to check correctness before degree is changed
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
- self.assertEqual(len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
- # change degree
- for j in range(1, 6):
- w.degree_spin.setValue(j)
- num_coefficients = sum(i + 1 for i in range(w.degree + 1))
- self.assertEqual(
- len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
- def test_send_data(self):
- """
- Check data output signal
- """
- w = self.widget
-
- self.assertIsNone(self.get_output(w.Outputs.data))
-
- self.send_signal(w.Inputs.data, self.iris)
-
- # check correct number of attributes
- for j in range(1, 6):
- w.degree_spin.setValue(j)
- self.assertEqual(
- len(self.get_output(w.Outputs.data).domain.attributes), 2)
-
- self.assertEqual(len(self.get_output(w.Outputs.data).domain.metas), 1)
- self.assertIsNotNone(self.get_output(w.Outputs.data).domain.class_var)
-
- # check again none
- self.send_signal(w.Inputs.data, None)
- self.assertIsNone(self.get_output(w.Outputs.data))
-
- def test_send_report(self):
- """
- Just test everything not crashes
- """
- w = self.widget
-
- self.send_signal(w.Inputs.data, self.iris)
- self.process_events(lambda: getattr(w, w.graph_name).svg())
- w.send_report()
+ for w.degree in range(1, 6):
+ w._on_degree_changed()
+ self.assertEqual(len(self.get_output(w.Outputs.coefficients)),
+ (w.degree + 1) * (w.degree + 2) // 2,
+ f"at degree={w.degree}")
def test_bad_learner(self):
- """
- Some learners on input might raise error.
- GH-38
- """
w = self.widget
self.assertFalse(w.Error.fitting_failed.is_shown())
@@ -551,51 +262,7 @@ def test_bad_learner(self):
self.send_signal(w.Inputs.learner, learner)
self.assertFalse(w.Error.fitting_failed.is_shown())
- def test_raise_no_classifier_error(self):
- """
- Regression learner must raise error
- """
- w = self.widget
-
- # linear regression learner is regression - should raise
- learner = LinearRegressionLearner()
- self.send_signal(w.Inputs.learner, learner)
- self.assertTrue(w.Error.no_classifier.is_shown())
-
- # make it empty to test if error disappear
- self.send_signal(w.Inputs.learner, None)
- self.assertFalse(w.Error.no_classifier.is_shown())
-
- # test with some other learners
- learner = LogisticRegressionLearner()
- self.send_signal(w.Inputs.learner, learner)
- self.assertFalse(w.Error.no_classifier.is_shown())
-
- learner = TreeLearner()
- self.send_signal(w.Inputs.learner, learner)
- self.assertFalse(w.Error.no_classifier.is_shown())
-
- learner = RandomForestLearner()
- self.send_signal(w.Inputs.learner, learner)
- self.assertFalse(w.Error.no_classifier.is_shown())
-
- learner = SVMLearner()
- self.send_signal(w.Inputs.learner, learner)
- self.assertFalse(w.Error.no_classifier.is_shown())
-
- def test_no_data_contour(self):
- """
- Do not crash when there is no data and one want to draw contours.
- GH-50
- """
- self.widget.contours_enabled_checkbox.click()
-
def test_sparse(self):
- """
- Do not crash on sparse data. Convert used
- sparse columns to numpy array.
- GH-52
- """
w = self.widget
def send_sparse_data(data):
@@ -616,28 +283,61 @@ def send_sparse_data(data):
def test_non_in_data(self):
w = self.widget
- data = Table.from_file("iris")[::15]
- data.Y[:3] = np.nan
+ self.iris.Y[:10] = np.nan
+ self.iris.X[-4:, 0] = np.nan
- self.send_signal(w.Inputs.data, data)
+ self.send_signal(w.Inputs.data, self.iris)
+ np.testing.assert_equal(w.selected_data.X, self.iris.X[:-4, :2])
- num_continuous_attributes = sum(
- True for var in self.iris.domain.attributes
- if isinstance(var, ContinuousVariable))
- self.assertEqual(w.cbx.count(), num_continuous_attributes)
- self.assertEqual(w.cby.count(), num_continuous_attributes)
- self.assertEqual(
- w.target_class_combobox.count(),
- len(self.iris.domain.class_var.values))
- self.assertEqual(w.cbx.currentText(), self.iris.domain[0].name)
- self.assertEqual(w.cby.currentText(), self.iris.domain[1].name)
- self.assertEqual(
- w.target_class_combobox.currentText(),
- self.iris.domain.class_var.values[0])
-
- self.assertEqual(w.attr_x, self.iris.domain[0].name)
- self.assertEqual(w.attr_y, self.iris.domain[1].name)
- self.assertEqual(w.target_class, self.iris.domain.class_var.values[0])
+ def test_same_variable(self):
+ w = self.widget
+ self.send_signal(w.Inputs.data, self.iris)
+
+ assert w.selected_data is not None
+ assert w.attr_x is w.data.domain[0]
+
+ w.attr_y = w.data.domain[0]
+ w._on_attr_changed()
+ self.assertIsNone(w.selected_data)
+ self.assertTrue(w.Error.same_variable.is_shown())
+
+ w.attr_y = w.data.domain[1]
+ w._on_attr_changed()
+ self.assertIsNotNone(w.selected_data)
+ self.assertFalse(w.Error.same_variable.is_shown())
+
+ self.send_signal(w.Inputs.data, None)
+ self.assertFalse(w.Error.same_variable.is_shown())
+
+ self.send_signal(w.Inputs.data, self.iris)
+ w.attr_y = w.data.domain[0]
+ w._on_attr_changed()
+ self.assertTrue(w.Error.same_variable.is_shown())
+
+ self.send_signal(w.Inputs.data, Table("housing"))
+ self.assertFalse(w.Error.same_variable.is_shown())
+
+
+class TestOWPolynomialClassification(WidgetTest):
+ # Tests that compute the probability grid and contours, so the code is
+ # run at least a few times
+ def setUp(self):
+ # type: OWPolynomialClassification
+ self.widget = self.create_widget(OWPolynomialClassification)
+ self.iris = Table.from_file("iris")
+
+ def test_blur_grid(self):
+ w = self.widget
+
+ self.send_signal(w.Inputs.data, self.iris)
+ # here we can check that 0.5 remains same
+ assert_array_equal(w.probabilities_grid == 0.5,
+ w.blur_grid(w.probabilities_grid) == 0.5)
+
+ def test_send_report(self):
+ w = self.widget
+ self.send_signal(w.Inputs.data, self.iris)
+ w.send_report()
if __name__ == "__main__":
diff --git a/setup.py b/setup.py
index 5903577..a633303 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@
]
INSTALL_REQUIRES = [
- 'Orange3 >=3.27.0',
+ 'Orange3 >=3.27.1',
'BeautifulSoup4',
'numpy'
]
diff --git a/tox.ini b/tox.ini
index 6017af0..2484edc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -27,7 +27,7 @@ deps =
pyqt5==5.12.*
pyqtwebengine==5.12.*
oldest: scikit-learn~=0.22.0
- oldest: orange3==3.27.0
+ oldest: orange3==3.27.1
latest: git+git://github.com/biolab/orange3.git#egg=orange3
latest: git+git://github.com/biolab/orange-canvas-core.git#egg=orange-canvas-core
latest: git+git://github.com/biolab/orange-widget-base.git#egg=orange-widget-base