Skip to content

Commit

Permalink
Predictions: Replace list view with a combo
Browse files Browse the repository at this point in the history
  • Loading branch information
janezd committed Feb 11, 2022
1 parent 6c17080 commit 0843b72
Showing 1 changed file with 114 additions and 64 deletions.
178 changes: 114 additions & 64 deletions Orange/widgets/evaluate/owpredictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

import numpy
from AnyQt.QtWidgets import (
QTableView, QListWidget, QSplitter, QToolTip, QStyle, QApplication,
QSizePolicy
)
QTableView, QSplitter, QToolTip, QStyle, QApplication, QSizePolicy)
from AnyQt.QtGui import QPainter, QStandardItem, QPen, QColor
from AnyQt.QtCore import (
Qt, QSize, QRect, QRectF, QPoint, QLocale,
Expand Down Expand Up @@ -51,7 +49,7 @@ class OWPredictions(OWWidget):
description = "Display predictions of models for an input dataset."
keywords = []

buttons_area_orientation = None
want_control_area = False

class Inputs:
data = Input("Data", Orange.data.Table)
Expand All @@ -76,7 +74,14 @@ class Error(OWWidget.Error):
score_table = settings.SettingProvider(ScoreTable)

#: List of selected class value indices in the `class_values` list
selected_classes = settings.ContextSetting([])
PROB_OPTS = ["(None)", "(All)", "(All in data)"]
PROB_TOOLTIPS = ["Don't show probabilities",
"Show probabilities for all classes known to the model,\n"
"even if they don't appear in this data",
"Show probabilities for classes in the data\n"
"that are also known to the model."]
NO_PROBS, ALL_PROBS, DATA_PROBS = range(3)
shown_probs = settings.ContextSetting(ALL_PROBS)
selection = settings.Setting([], schema_only=True)
show_scores = settings.Setting(True)
TARGET_AVERAGE = "(Average over classes)"
Expand All @@ -94,31 +99,35 @@ def __init__(self):
self.selection_store = None
self.__pending_selection = self.selection

self.class_widgets = []

predopts = gui.hBox(None, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))
self.class_widgets = [
gui.widgetLabel(predopts, "Show probabilities for"),
gui.comboBox(
predopts, self, "shown_probs",
callback=self._update_prediction_delegate)
]
gui.rubber(predopts)
self.reset_button = gui.button(
self.controlArea, self, "Restore Original Order",
predopts, self, "Restore Original Order",
callback=self._reset_order,
tooltip="Show rows in the original order")
gui.separator(self.controlArea, 16)

gui.listBox(
self.controlArea, self, "selected_classes", "class_values",
box="Show probabilities",
callback=self._update_prediction_delegate,
selectionMode=QListWidget.ExtendedSelection,
sizePolicy=(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding),
minimumHeight=100, maximumHeight=150)

gui.rubber(self.controlArea)
tooltip="Show rows in the original order",
sizePolicy=(QSizePolicy.Fixed, QSizePolicy.Fixed))

box = gui.vBox(self.controlArea, "Model Performance")
scoreopts = gui.hBox(None, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))
gui.checkBox(
box, self, "show_scores", "Show perfomance scores",
scoreopts, self, "show_scores", "Show perfomance scores",
callback=self._update_score_table_visibility
)
self.target_selection = gui.comboBox(
box, self, "target_class", items=[], label="Target class:",
sendSelectedValue=True, callback=self._on_target_changed
)
gui.separator(scoreopts, 32)
self.class_widgets += [
gui.widgetLabel(scoreopts, "Target class:"),
gui.comboBox(
scoreopts, self, "target_class", items=[],
sendSelectedValue=True, callback=self._on_target_changed)
]
gui.rubber(scoreopts)

table_opts = dict(horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
horizontalScrollMode=QTableView.ScrollPerPixel,
Expand Down Expand Up @@ -153,9 +162,12 @@ def __init__(self):
self.splitter.addWidget(self.dataview)

self.score_table = ScoreTable(self)
self.vsplitter = gui.vBox(self.mainArea)
self.vsplitter.layout().addWidget(self.splitter)
self.vsplitter.layout().addWidget(self.score_table.view)
self.mainArea.layout().setSpacing(0)
self.mainArea.layout().setContentsMargins(4, 0, 4, 4)
self.mainArea.layout().addWidget(predopts)
self.mainArea.layout().addWidget(self.splitter)
self.mainArea.layout().addWidget(scoreopts)
self.mainArea.layout().addWidget(self.score_table.view)

def get_selection_store(self, model):
# Both models map the same, so it doesn't matter which one is used
Expand All @@ -168,6 +180,7 @@ def get_selection_store(self, model):
@check_sql_input
def set_data(self, data):
self.Warning.empty_data(shown=data is not None and not data)
self.closeContext()
self.data = data
self.selection_store = None
if not data:
Expand All @@ -194,6 +207,9 @@ def set_data(self, data):
self._update_data_sort_order, self.dataview,
self.predictionsview))

self._set_target_combos()
if self.is_discrete_class:
self.openContext(self.class_var.values)
self._invalidate_predictions()

def _store_selection(self):
Expand All @@ -203,6 +219,10 @@ def _store_selection(self):
def class_var(self):
return self.data and self.data.domain.class_var

@property
def is_discrete_class(self):
return bool(self.class_var) and self.class_var.is_discrete

@Inputs.predictors
def set_predictor(self, index, predictor: Model):
item = self.predictors[index]
Expand All @@ -219,31 +239,36 @@ def insert_predictor(self, index, predictor: Model):
def remove_predictor(self, index):
self.predictors.pop(index)

def _set_target_combos(self):
prob_combo = self.controls.shown_probs
target_combo = self.controls.target_class
prob_combo.clear()
target_combo.clear()

discrete_class = self.is_discrete_class
for widget in self.class_widgets:
widget.setVisible(discrete_class)

if discrete_class:
target_combo.addItem(self.TARGET_AVERAGE)
target_combo.addItems(self.class_var.values)

prob_combo.addItems(self.PROB_OPTS)
prob_combo.addItems(self.class_var.values)
for i, tip in enumerate(self.PROB_TOOLTIPS):
prob_combo.setItemData(i, tip, Qt.ToolTipRole)

self.shown_probs = self.DATA_PROBS
self.target_class = self.TARGET_AVERAGE

def _set_class_values(self):
class_values = []
for slot in self.predictors:
class_var = slot.predictor.domain.class_var
if class_var and class_var.is_discrete:
for value in class_var.values:
if value not in class_values:
class_values.append(value)

self.target_selection.clear()
self.target_selection.addItem(self.TARGET_AVERAGE)
if self.class_var and self.class_var.is_discrete:
values = self.class_var.values
self.target_selection.addItems(values)
self.target_selection.box.setVisible(True)
self.class_values = sorted(
class_values, key=lambda val: val not in values)
self.selected_classes = [
i for i, name in enumerate(class_values) if name in values]
self.controls.selected_classes.box.setVisible(True)
else:
self.class_values = class_values # This assignment updates listview
self.selected_classes = []
self.controls.selected_classes.box.setVisible(False)
self.target_selection.box.setVisible(False)
self.class_values = []
for slot in self.predictors:
class_var = slot.predictor.domain.class_var
if class_var.is_discrete:
for value in class_var.values:
if value not in self.class_values:
self.class_values.append(value)

def handleNewSignals(self):
# Disconnect the model: the model and the delegate will be inconsistent
Expand Down Expand Up @@ -317,8 +342,7 @@ def _call_predictors(self):

def _update_scores(self):
model = self.score_table.model
if self.class_var and self.class_var.is_discrete \
and self.target_class != self.TARGET_AVERAGE:
if self.is_discrete_class and self.target_class != self.TARGET_AVERAGE:
target = self.class_var.values.index(self.target_class)
else:
target = None
Expand Down Expand Up @@ -565,13 +589,22 @@ def _get_colors(self):
def _update_prediction_delegate(self):
self._delegates.clear()
colors = self._get_colors()
if self.shown_probs >= len(self.PROB_OPTS):
shown_probs = [self.shown_probs - len(self.PROB_OPTS)]
for col, slot in enumerate(self.predictors):
target = slot.predictor.domain.class_var
shown_probs = (
() if target.is_continuous else
[val if self.class_values[val] in target.values else None
for val in self.selected_classes]
)
if target.is_continuous or self.shown_probs == self.NO_PROBS \
or self.shown_probs == self.DATA_PROBS \
and not self.is_discrete_class:
shown_probs = ()
elif self.shown_probs == self.ALL_PROBS:
shown_probs = [
idx for idx, value in enumerate(self.class_values)
if value in target.values]
elif self.shown_probs == self.DATA_PROBS:
shown_probs = [
self.class_values.index(value) if value in target.values else None
for value in self.class_var.values]
delegate = PredictionsItemDelegate(
None if target.is_continuous else self.class_values,
colors,
Expand All @@ -587,7 +620,19 @@ def _update_prediction_delegate(self):
self.predictionsview.resizeColumnsToContents()
self._recompute_splitter_sizes()
if self.predictionsview.model() is not None:
self.predictionsview.model().setProbInd(self.selected_classes)
inds = None
if self.is_discrete_class:
if self.shown_probs == self.DATA_PROBS:
inds = [
idx for idx, value in enumerate(self.class_values)
if value in self.class_var.values]
elif self.shown_probs == self.ALL_PROBS:
inds = list(range(len(self.class_values)))
elif self.shown_probs >= len(self.PROB_OPTS):
shown_class = self.class_var.values[shown_probs[0]]
if shown_class in self.class_values:
inds = [self.class_values.index(shown_class)]
self.predictionsview.model().setProbInd(inds)

def _recompute_splitter_sizes(self):
if not self.data:
Expand Down Expand Up @@ -623,7 +668,7 @@ def _commit_evaluation_results(self):
results.actual = data.Y.ravel()
results.predicted = numpy.vstack(
tuple(p.results.predicted[0][~nanmask] for p in slots))
if self.class_var and self.class_var.is_discrete:
if self.is_discrete_class:
results.probabilities = numpy.array(
[p.results.probabilities[0][~nanmask] for p in slots])
results.learner_names = [p.name for p in slots]
Expand Down Expand Up @@ -730,10 +775,15 @@ def merge_data_with_predictions():

if self.data:
text = self._get_details().replace('\n', '<br>')
if self.selected_classes:
text += '<br>Showing probabilities for: '
text += ', '. join([self.class_values[i]
for i in self.selected_classes])
if self.is_discrete_class and self.shown_probs != self.NO_PROBS:
text += '<br>Showing probabilities for '
if self.shown_probs == self.ALL_PROBS:
text += "all classes known to models"
elif self.shown_probs == self.DATA_PROBS:
text += "all classes that appear in the data"
else:
class_idx = self.shown_probs - len(self.PROB_OPTS)
text += f"'{self.class_var[class_idx]}'"
self.report_paragraph('Info', text)
self.report_table("Data & Predictions", merge_data_with_predictions(),
header_rows=1, header_columns=1)
Expand Down

0 comments on commit 0843b72

Please sign in to comment.