def make_list_view(self, callback, append_to): data = unc.years() list = QListView() list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) model = QStandardItemModel(0, 1) for i in range(1, len(data)): item = QStandardItem(str(data[i])) model.appendRow(item) model.itemChanged.connect(callback) list.setModel(model) list.setEditTriggers(QAbstractItemView.NoEditTriggers) list.setSelectionMode(QAbstractItemView.ExtendedSelection) proxy_model = FindFilterProxyModel() proxy_model.setSourceModel(model) proxy_model.setFilterKeyColumn(-1) list.setModel(proxy_model) list.selectionModel().selectionChanged.connect(callback) append_to.layout().addWidget(list) return [list, proxy_model, model]
def move_rows(self, view: QListView, offset: int, roles=(Qt.EditRole,)): rows = [idx.row() for idx in view.selectionModel().selectedRows()] model = view.model() # type: QAbstractItemModel rowcount = model.rowCount() newrows = [min(max(0, row + offset), rowcount - 1) for row in rows] def itemData(index): return {role: model.data(index, role) for role in roles} for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): d1 = itemData(model.index(row, 0)) d2 = itemData(model.index(newrow, 0)) model.setItemData(model.index(row, 0), d2) model.setItemData(model.index(newrow, 0), d1) selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) self.commit()
class OWDiscretize(widget.OWWidget): name = "Discretize" description = "Discretize the numeric data features." icon = "icons/Discretize.svg" inputs = [InputSignal("Data", Orange.data.Table, "set_data", doc="Input data table")] outputs = [OutputSignal("Data", Orange.data.Table, doc="Table with discretized features")] settingsHandler = settings.DomainContextHandler() saved_var_states = settings.ContextSetting({}) default_method = settings.Setting(2) default_k = settings.Setting(3) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = range(7) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = 0 self.k = 5 box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self.default_bbox = rbox = gui.radioButtons( box, self, "default_method", callback=self._default_disc_changed) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) options = self.options = [ self.tr("Default"), self.tr("Leave numeric"), self.tr("Entropy-MDL discretization"), self.tr("Equal-frequency discretization"), self.tr("Equal-width discretization"), self.tr("Remove numeric variables") ] for opt in options[1:]: t = gui.appendRadioButton(rbox, opt) # This condition is ugly, but it keeps the same order of # options for backward compatibility of saved schemata [right, self.left][opt.startswith("Equal")].layout().addWidget(t) gui.separator(right, 18, 18) def _intbox(widget, attr, callback): box = gui.indentedBox(widget) s = gui.spin( box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) vlayout = QHBoxLayout() box = gui.widgetBox( self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8 ) # List view with all attributes self.varview = QListView(selectionMode=QListView.ExtendedSelection) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed ) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed ) vlayout.addWidget(controlbox) for opt in options[:5]: gui.appendRadioButton(controlbox, opt) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute") gui.rubber(controlbox) controlbox.setEnabled(False) self.controlbox = controlbox box = gui.auto_commit( self.controlArea, self, "autosend", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 20) box.layout().insertWidget(0, self.report_button) self._update_spin_positions() def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() else: self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain if var.is_continuous] self.varmodel[:] = cvars class_var = data.domain.class_var has_disc_class = data.domain.has_discrete_class self.default_bbox.buttons[self.MDL - 1].setEnabled(has_disc_class) self.bbox.buttons[self.MDL].setEnabled(has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL - 1: self.default_method = 0 if not has_disc_class and self.method == self.MDL: self.method = 0 # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_bbox.buttons[self.MDL - 1].setEnabled(True) self.bbox.buttons[self.MDL].setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None or not len(self.data): return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar else: assert False for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) def _method_index(self, method): return METHODS.index((type(method), )) def _current_default_method(self): method = self.default_method + 1 k = self.default_k if method == OWDiscretize.Leave: def_method = Leave() elif method == OWDiscretize.MDL: def_method = MDL() elif method == OWDiscretize.EqualFreq: def_method = EqualFreq(k) elif method == OWDiscretize.EqualWidth: def_method = EqualWidth(k) elif method == OWDiscretize.Remove: def_method = Remove() else: assert False return def_method def _current_method(self): if self.method == OWDiscretize.Default: method = Default(self._current_default_method()) elif self.method == OWDiscretize.Leave: method = Leave() elif self.method == OWDiscretize.MDL: method = MDL() elif self.method == OWDiscretize.EqualFreq: method = EqualFreq(self.k) elif self.method == OWDiscretize.EqualWidth: method = EqualWidth(self.k) elif self.method == OWDiscretize.Remove: method = Remove() elif self.method == OWDiscretize.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): self.k_general.setDisabled(self.default_method not in [2, 3]) if self.default_method == 2: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == 3: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in [3, 4]) if self.method == 3: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == 4: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self.commit() def _var_selection_changed(self, *args): indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] mset = set(methods) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = self._method_index(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def discretized_var(self, source): index = list(self.varmodel).index(source) state = self.var_state[index] if state.disc_var is None: return None elif state.disc_var is source: return source elif state.points == []: return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None def disc_var(source): if source and source.is_continuous: return self.discretized_var(source) else: return source attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_var = disc_var(self.data.domain.class_var) domain = Orange.data.Domain( attributes, class_var, metas=self.data.domain.metas ) return domain def commit(self): output = None if self.data is not None and len(self.data): domain = self.discretized_domain() output = self.data.from_table(domain, self.data) self.send("Data", output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items(( ("Default method", self.options[self.default_method + 1]),)) if self.varmodel: self.report_items("Thresholds", [ (var.name, DiscDelegate.cutsText(self.var_state[i]) or "leave numeric") for i, var in enumerate(self.varmodel)])
class OWDistributions(widget.OWWidget): name = "Distributions" description = "Display value distributions of a data feature in a graph." icon = "icons/Distribution.svg" priority = 120 class Inputs: data = Input("Data", Orange.data.Table, doc="Set the input data set") settingsHandler = settings.DomainContextHandler( match_values=settings.DomainContextHandler.MATCH_VALUES_ALL) #: Selected variable index variable_idx = settings.ContextSetting(-1) #: Selected group variable groupvar_idx = settings.ContextSetting(0) relative_freq = settings.Setting(False) disc_cont = settings.Setting(False) smoothing_index = settings.Setting(5) show_prob = settings.ContextSetting(0) graph_name = "plot" ASH_HIST = 50 bins = [2, 3, 4, 5, 8, 10, 12, 15, 20, 30, 50] smoothing_facs = list(reversed([0.1, 0.2, 0.4, 0.6, 0.8, 1, 1.5, 2, 4, 6, 10])) def __init__(self): super().__init__() self.data = None self.distributions = None self.contingencies = None self.var = self.cvar = None varbox = gui.vBox(self.controlArea, "Variable") self.varmodel = itemmodels.VariableListModel() self.groupvarmodel = [] self.varview = QListView( selectionMode=QListView.SingleSelection) self.varview.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Expanding) self.varview.setModel(self.varmodel) self.varview.setSelectionModel( itemmodels.ListSingleSelectionModel(self.varmodel)) self.varview.selectionModel().selectionChanged.connect( self._on_variable_idx_changed) varbox.layout().addWidget(self.varview) box = gui.vBox(self.controlArea, "Precision") gui.separator(self.controlArea, 4, 4) box2 = gui.hBox(box) self.l_smoothing_l = gui.widgetLabel(box2, "Smooth") gui.hSlider(box2, self, "smoothing_index", minValue=0, maxValue=len(self.smoothing_facs) - 1, callback=self._on_set_smoothing, createLabel=False) self.l_smoothing_r = gui.widgetLabel(box2, "Precise") self.cb_disc_cont = gui.checkBox( gui.indentedBox(box, sep=4), self, "disc_cont", "Bin numeric variables", callback=self._on_groupvar_idx_changed, tooltip="Show numeric variables as categorical.") box = gui.vBox(self.controlArea, "Group by") self.icons = gui.attributeIconDict self.groupvarview = gui.comboBox( box, self, "groupvar_idx", callback=self._on_groupvar_idx_changed, valueType=str, contentsLength=12) box2 = gui.indentedBox(box, sep=4) self.cb_rel_freq = gui.checkBox( box2, self, "relative_freq", "Show relative frequencies", callback=self._on_relative_freq_changed, tooltip="Normalize probabilities so that probabilities " "for each group-by value sum to 1.") gui.separator(box2) self.cb_prob = gui.comboBox( box2, self, "show_prob", label="Show probabilities:", orientation=Qt.Horizontal, callback=self._on_relative_freq_changed, tooltip="Show probabilities for a chosen group-by value " "(at each point probabilities for all group-by values sum to 1).") self.plotview = pg.PlotWidget(background=None) self.plotview.setRenderHint(QPainter.Antialiasing) self.mainArea.layout().addWidget(self.plotview) w = QLabel() w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.mainArea.layout().addWidget(w, Qt.AlignCenter) self.ploti = pg.PlotItem() self.plot = self.ploti.vb self.ploti.hideButtons() self.plotview.setCentralItem(self.ploti) self.plot_prob = pg.ViewBox() self.ploti.hideAxis('right') self.ploti.scene().addItem(self.plot_prob) self.ploti.getAxis("right").linkToView(self.plot_prob) self.ploti.getAxis("right").setLabel("Probability") self.plot_prob.setZValue(10) self.plot_prob.setXLink(self.ploti) self.update_views() self.ploti.vb.sigResized.connect(self.update_views) self.plot_prob.setRange(yRange=[0, 1]) def disable_mouse(plot): plot.setMouseEnabled(False, False) plot.setMenuEnabled(False) disable_mouse(self.plot) disable_mouse(self.plot_prob) self.tooltip_items = [] self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) pen = QPen(self.palette().color(QPalette.Text)) for axis in ("left", "bottom"): self.ploti.getAxis(axis).setPen(pen) self._legend = LegendItem() self._legend.setParentItem(self.plot) self._legend.hide() self._legend.anchor((1, 0), (1, 0)) def update_views(self): self.plot_prob.setGeometry(self.plot.sceneBoundingRect()) self.plot_prob.linkedViewChanged(self.plot, self.plot_prob.XAxis) @Inputs.data def set_data(self, data): self.closeContext() self.clear() self.warning() self.data = data self.distributions = None self.contingencies = None if self.data is not None: if not self.data: self.warning("Empty input data cannot be visualized") return domain = self.data.domain self.varmodel[:] = list(domain.variables) + \ [meta for meta in domain.metas if meta.is_continuous or meta.is_discrete] self.groupvarview.clear() self.groupvarmodel = \ ["(None)"] + [var for var in domain.variables if var.is_discrete] + \ [meta for meta in domain.metas if meta.is_discrete] self.groupvarview.addItem("(None)") for var in self.groupvarmodel[1:]: self.groupvarview.addItem(self.icons[var], var.name) if domain.has_discrete_class: self.groupvar_idx = \ self.groupvarmodel[1:].index(domain.class_var) + 1 self.openContext(domain) self.variable_idx = min(max(self.variable_idx, 0), len(self.varmodel) - 1) self.groupvar_idx = min(max(self.groupvar_idx, 0), len(self.groupvarmodel) - 1) itemmodels.select_row(self.varview, self.variable_idx) self._setup() def clear(self): self.plot.clear() self.plot_prob.clear() self.varmodel[:] = [] self.groupvarmodel = [] self.variable_idx = -1 self.groupvar_idx = 0 self._legend.clear() self._legend.hide() self.groupvarview.clear() self.cb_prob.clear() def _setup_smoothing(self): if not self.disc_cont and self.var and self.var.is_continuous: self.cb_disc_cont.setText("Bin numeric variables") self.l_smoothing_l.setText("Smooth") self.l_smoothing_r.setText("Precise") else: self.cb_disc_cont.setText("Bin numeric variables into {} bins". format(self.bins[self.smoothing_index])) self.l_smoothing_l.setText(" " + str(self.bins[0])) self.l_smoothing_r.setText(" " + str(self.bins[-1])) @property def smoothing_factor(self): return self.smoothing_facs[self.smoothing_index] def _setup(self): self.plot.clear() self.plot_prob.clear() self._legend.clear() self._legend.hide() varidx = self.variable_idx self.var = self.cvar = None if varidx >= 0: self.var = self.varmodel[varidx] if self.groupvar_idx > 0: self.cvar = self.groupvarmodel[self.groupvar_idx] self.cb_prob.clear() self.cb_prob.addItem("(None)") self.cb_prob.addItems(self.cvar.values) self.cb_prob.addItem("(All)") self.show_prob = min(max(self.show_prob, 0), len(self.cvar.values) + 1) data = self.data self._setup_smoothing() if self.var is None: return if self.disc_cont: domain = Orange.data.Domain( [self.var, self.cvar] if self.cvar else [self.var]) data = Orange.data.Table(domain, data) disc = EqualWidth(n=self.bins[self.smoothing_index]) data = Discretize(method=disc, remove_const=False)(data) self.var = data.domain[0] self.set_left_axis_name() self.enable_disable_rel_freq() if self.cvar: self.contingencies = \ contingency.get_contingency(data, self.var, self.cvar) self.display_contingency() else: self.distributions = \ distribution.get_distribution(data, self.var) self.display_distribution() self.plot.autoRange() def help_event(self, ev): self.plot.mapSceneToView(ev.scenePos()) ctooltip = [] for vb, item in self.tooltip_items: mouse_over_curve = isinstance(item, pg.PlotCurveItem) \ and item.mouseShape().contains(vb.mapSceneToView(ev.scenePos())) mouse_over_bar = isinstance(item, DistributionBarItem) \ and item.boundingRect().contains(vb.mapSceneToView(ev.scenePos())) if mouse_over_curve or mouse_over_bar: ctooltip.append(item.tooltip) if ctooltip: QToolTip.showText(ev.screenPos(), "\n\n".join(ctooltip), widget=self.plotview) return True return False def display_distribution(self): dist = self.distributions var = self.var if dist is None or not len(dist): return self.plot.clear() self.plot_prob.clear() self.ploti.hideAxis('right') self.tooltip_items = [] bottomaxis = self.ploti.getAxis("bottom") bottomaxis.setLabel(var.name) bottomaxis.resizeEvent() self.set_left_axis_name() if var and var.is_continuous: bottomaxis.setTicks(None) if not len(dist[0]): return edges, curve = ash_curve(dist, None, m=OWDistributions.ASH_HIST, smoothing_factor=self.smoothing_factor) edges = edges + (edges[1] - edges[0])/2 edges = edges[:-1] item = pg.PlotCurveItem() pen = QPen(QBrush(Qt.white), 3) pen.setCosmetic(True) item.setData(edges, curve, antialias=True, stepMode=False, fillLevel=0, brush=QBrush(Qt.gray), pen=pen) self.plot.addItem(item) item.tooltip = "Density" self.tooltip_items.append((self.plot, item)) else: bottomaxis.setTicks([list(enumerate(var.values))]) for i, w in enumerate(dist): geom = QRectF(i - 0.33, 0, 0.66, w) item = DistributionBarItem(geom, [1.0], [QColor(128, 128, 128)]) self.plot.addItem(item) item.tooltip = "Frequency for %s: %r" % (var.values[i], w) self.tooltip_items.append((self.plot, item)) def _on_relative_freq_changed(self): self.set_left_axis_name() if self.cvar and self.cvar.is_discrete: self.display_contingency() else: self.display_distribution() self.plot.autoRange() def display_contingency(self): """ Set the contingency to display. """ cont = self.contingencies var, cvar = self.var, self.cvar if cont is None or not len(cont): return self.plot.clear() self.plot_prob.clear() self._legend.clear() self.tooltip_items = [] if self.show_prob: self.ploti.showAxis('right') else: self.ploti.hideAxis('right') bottomaxis = self.ploti.getAxis("bottom") bottomaxis.setLabel(var.name) bottomaxis.resizeEvent() cvar_values = cvar.values colors = [QColor(*col) for col in cvar.colors] if var and var.is_continuous: bottomaxis.setTicks(None) weights, cols, cvar_values, curves = [], [], [], [] for i, dist in enumerate(cont): v, W = dist if len(v): weights.append(numpy.sum(W)) cols.append(colors[i]) cvar_values.append(cvar.values[i]) curves.append(ash_curve( dist, cont, m=OWDistributions.ASH_HIST, smoothing_factor=self.smoothing_factor)) weights = numpy.array(weights) sumw = numpy.sum(weights) weights /= sumw colors = cols curves = [(X, Y * w) for (X, Y), w in zip(curves, weights)] curvesline = [] #from histograms to lines for X, Y in curves: X = X + (X[1] - X[0])/2 X = X[:-1] X = numpy.array(X) Y = numpy.array(Y) curvesline.append((X, Y)) for t in ["fill", "line"]: curve_data = list(zip(curvesline, colors, weights, cvar_values)) for (X, Y), color, w, cval in reversed(curve_data): item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3) pen.setCosmetic(True) color = QColor(color) color.setAlphaF(0.2) item.setData(X, Y/(w if self.relative_freq else 1), antialias=True, stepMode=False, fillLevel=0 if t == "fill" else None, brush=QBrush(color), pen=pen) self.plot.addItem(item) if t == "line": item.tooltip = "{}\n{}={}".format( "Normalized density " if self.relative_freq else "Density ", cvar.name, cval) self.tooltip_items.append((self.plot, item)) if self.show_prob: all_X = numpy.array(numpy.unique(numpy.hstack([X for X, _ in curvesline]))) inter_X = numpy.array(numpy.linspace(all_X[0], all_X[-1], len(all_X)*2)) curvesinterp = [numpy.interp(inter_X, X, Y) for (X, Y) in curvesline] sumprob = numpy.sum(curvesinterp, axis=0) legal = sumprob > 0.05 * numpy.max(sumprob) i = len(curvesinterp) + 1 show_all = self.show_prob == i for Y, color, cval in reversed(list(zip(curvesinterp, colors, cvar_values))): i -= 1 if show_all or self.show_prob == i: item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3, style=Qt.DotLine) pen.setCosmetic(True) prob = Y[legal] / sumprob[legal] item.setData( inter_X[legal], prob, antialias=True, stepMode=False, fillLevel=None, brush=None, pen=pen) self.plot_prob.addItem(item) item.tooltip = "Probability that \n" + cvar.name + "=" + cval self.tooltip_items.append((self.plot_prob, item)) elif var and var.is_discrete: bottomaxis.setTicks([list(enumerate(var.values))]) cont = numpy.array(cont) maxh = 0 #maximal column height maxrh = 0 #maximal relative column height scvar = cont.sum(axis=1) #a cvar with sum=0 with allways have distribution counts 0, #therefore we can divide it by anything scvar[scvar == 0] = 1 for i, (value, dist) in enumerate(zip(var.values, cont.T)): maxh = max(maxh, max(dist)) maxrh = max(maxrh, max(dist/scvar)) for i, (value, dist) in enumerate(zip(var.values, cont.T)): dsum = sum(dist) geom = QRectF(i - 0.333, 0, 0.666, maxrh if self.relative_freq else maxh) if self.show_prob: prob = dist / dsum ci = 1.96 * numpy.sqrt(prob * (1 - prob) / dsum) else: ci = None item = DistributionBarItem(geom, dist/scvar/maxrh if self.relative_freq else dist/maxh, colors) self.plot.addItem(item) tooltip = "\n".join( "%s: %.*f" % (n, 3 if self.relative_freq else 1, v) for n, v in zip(cvar_values, dist/scvar if self.relative_freq else dist)) item.tooltip = "{} ({}={}):\n{}".format( "Normalized frequency " if self.relative_freq else "Frequency ", cvar.name, value, tooltip) self.tooltip_items.append((self.plot, item)) if self.show_prob: item.tooltip += "\n\nProbabilities:" for ic, a in enumerate(dist): if self.show_prob - 1 != ic and \ self.show_prob - 1 != len(dist): continue position = -0.333 + ((ic+0.5)*0.666/len(dist)) if dsum < 1e-6: continue prob = a / dsum if not 1e-6 < prob < 1 - 1e-6: continue ci = 1.96 * sqrt(prob * (1 - prob) / dsum) item.tooltip += "\n%s: %.3f ± %.3f" % (cvar_values[ic], prob, ci) mark = pg.ScatterPlotItem() errorbar = pg.ErrorBarItem() pen = QPen(QBrush(QColor(0)), 1) pen.setCosmetic(True) errorbar.setData(x=[i+position], y=[prob], bottom=min(numpy.array([ci]), prob), top=min(numpy.array([ci]), 1 - prob), beam=numpy.array([0.05]), brush=QColor(1), pen=pen) mark.setData([i+position], [prob], antialias=True, symbol="o", fillLevel=None, pxMode=True, size=10, brush=QColor(colors[ic]), pen=pen) self.plot_prob.addItem(errorbar) self.plot_prob.addItem(mark) for color, name in zip(colors, cvar_values): self._legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, shape="s"), escape(name) ) self._legend.show() def set_left_axis_name(self): leftaxis = self.ploti.getAxis("left") set_label = leftaxis.setLabel if self.var and self.var.is_continuous: set_label(["Density", "Relative density"] [self.cvar is not None and self.relative_freq]) else: set_label(["Frequency", "Relative frequency"] [self.cvar is not None and self.relative_freq]) leftaxis.resizeEvent() def enable_disable_rel_freq(self): self.cb_prob.setDisabled(self.var is None or self.cvar is None) self.cb_rel_freq.setDisabled( self.var is None or self.cvar is None) def _on_variable_idx_changed(self): self.variable_idx = selected_index(self.varview) self._setup() def _on_groupvar_idx_changed(self): self._setup() def _on_set_smoothing(self): self._setup() def onDeleteWidget(self): self.plot.clear() super().onDeleteWidget() def get_widget_name_extension(self): if self.variable_idx >= 0: return self.varmodel[self.variable_idx] def send_report(self): self.plotview.scene().setSceneRect(self.plotview.sceneRect()) if self.variable_idx < 0: return self.report_plot() text = "Distribution of '{}'".format( self.varmodel[self.variable_idx]) if self.groupvar_idx: group_var = self.groupvarmodel[self.groupvar_idx] prob = self.cb_prob indiv_probs = 0 < prob.currentIndex() < prob.count() - 1 if not indiv_probs or self.relative_freq: text += " grouped by '{}'".format(group_var) if self.relative_freq: text += " (relative frequencies)" if indiv_probs: text += "; probabilites for '{}={}'".format( group_var, prob.currentText()) self.report_caption(text)
class DiscreteVariableEditor(VariableEditor): """An editor widget for editing a discrete variable. Extends the :class:`VariableEditor` to enable editing of variables values. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) form = self.layout().itemAt(0) assert isinstance(form, QFormLayout) #: A list model of discrete variable's values. self.values_model = itemmodels.PyListModel( flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) vlayout = QVBoxLayout(spacing=1, margin=0) self.values_edit = QListView(editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed) self.values_edit.setItemDelegate(CategoriesEditDelegate(self)) self.values_edit.setModel(self.values_model) self.values_model.dataChanged.connect(self.on_values_changed) self.values_edit.selectionModel().selectionChanged.connect( self.on_value_selection_changed) self.values_model.layoutChanged.connect( self.on_value_selection_changed) self.values_model.rowsMoved.connect(self.on_value_selection_changed) vlayout.addWidget(self.values_edit) hlayout = QHBoxLayout(spacing=1, margin=0) self.categories_action_group = group = QActionGroup( self, objectName="action-group-categories", enabled=False) self.move_value_up = QAction( "\N{UPWARDS ARROW}", group, toolTip="Move the selected item up.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketLeft), shortcutContext=Qt.WidgetShortcut, ) self.move_value_up.triggered.connect(self.move_up) self.move_value_down = QAction( "\N{DOWNWARDS ARROW}", group, toolTip="Move the selected item down.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketRight), shortcutContext=Qt.WidgetShortcut, ) self.move_value_down.triggered.connect(self.move_down) self.add_new_item = QAction( "+", group, objectName="action-add-item", toolTip="Append a new item.", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut, ) self.remove_item = QAction( "\N{MINUS SIGN}", group, objectName="action-remove-item", toolTip="Delete the selected item.", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut, ) self.add_new_item.triggered.connect(self._add_category) self.remove_item.triggered.connect(self._remove_category) button1 = FixedSizeButton(self, defaultAction=self.move_value_up, accessibleName="Move up") button2 = FixedSizeButton(self, defaultAction=self.move_value_down, accessibleName="Move down") button3 = FixedSizeButton(self, defaultAction=self.add_new_item, accessibleName="Add") button4 = FixedSizeButton(self, defaultAction=self.remove_item, accessibleName="Remove") self.values_edit.addActions([ self.move_value_up, self.move_value_down, self.add_new_item, self.remove_item ]) hlayout.addWidget(button1) hlayout.addWidget(button2) hlayout.addSpacing(3) hlayout.addWidget(button3) hlayout.addWidget(button4) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.insertRow(1, "Values:", vlayout) QWidget.setTabOrder(self.name_edit, self.values_edit) QWidget.setTabOrder(self.values_edit, button1) QWidget.setTabOrder(button1, button2) QWidget.setTabOrder(button2, button3) QWidget.setTabOrder(button3, button4) def set_data(self, var, transform=()): # type: (Optional[Categorical], Sequence[Transform]) -> None """ Set the variable to edit. """ super().set_data(var, transform) tr = None # type: Optional[CategoriesMapping] for tr_ in transform: if isinstance(tr_, CategoriesMapping): tr = tr_ items = [] if tr is not None: ci_index = {c: i for i, c in enumerate(var.categories)} for ci, cj in tr.mapping: if ci is None and cj is not None: # level added item = { Qt.EditRole: cj, EditStateRole: ItemEditState.Added, SourcePosRole: None } elif ci is not None and cj is None: # ci level dropped item = { Qt.EditRole: ci, EditStateRole: ItemEditState.Dropped, SourcePosRole: ci_index[ci], SourceNameRole: ci } elif ci is not None and cj is not None: # rename or reorder item = { Qt.EditRole: cj, EditStateRole: ItemEditState.NoState, SourcePosRole: ci_index[ci], SourceNameRole: ci } else: assert False, "invalid mapping: {!r}".format(tr.mapping) items.append(item) elif var is not None: items = [{ Qt.EditRole: c, EditStateRole: ItemEditState.NoState, SourcePosRole: i, SourceNameRole: c } for i, c in enumerate(var.categories)] else: items = [] with disconnected(self.values_model.dataChanged, self.on_values_changed): self.values_model.clear() self.values_model.insertRows(0, len(items)) for i, item in enumerate(items): self.values_model.setItemData(self.values_model.index(i, 0), item) self.add_new_item.actionGroup().setEnabled(var is not None) def __categories_mapping(self): # type: () -> CategoriesMappingType model = self.values_model source = self.var.categories res = [] for i in range(model.rowCount()): midx = model.index(i, 0) category = midx.data(Qt.EditRole) source_pos = midx.data(SourcePosRole) # type: Optional[int] if source_pos is not None: source_name = source[source_pos] else: source_name = None state = midx.data(EditStateRole) if state == ItemEditState.Dropped: res.append((source_name, None)) elif state == ItemEditState.Added: res.append((None, category)) else: res.append((source_name, category)) return res def get_data(self): """Retrieve the modified variable """ var, tr = super().get_data() if var is None: return var, tr mapping = self.__categories_mapping() if any(_1 != _2 or _2 != _3 for (_1, _2), _3 in zip_longest(mapping, var.categories)): tr.append(CategoriesMapping(mapping)) return var, tr def clear(self): """Clear the model state. """ super().clear() self.values_model.clear() def move_rows(self, rows, offset): if not rows: return assert len(rows) == 1 i = rows[0].row() if offset > 0: offset += 1 self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset) self.variable_changed.emit() def move_up(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, -1) def move_down(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, 1) @Slot() def on_values_changed(self): self.variable_changed.emit() @Slot() def on_value_selection_changed(self): rows = self.values_edit.selectionModel().selectedRows() if rows: i = rows[0].row() self.move_value_up.setEnabled(i) self.move_value_down.setEnabled( i != self.values_model.rowCount() - 1) else: self.move_value_up.setEnabled(False) self.move_value_down.setEnabled(False) def _remove_category(self): """ Remove the current selected category. If the item is an existing category present in the source variable it is marked as removed in the view. But if it was added in the set transformation it is removed entirely from the model and view. """ view = self.values_edit rows = view.selectionModel().selectedRows(0) if not rows: return assert len(rows) == 1 index = rows[0] # type: QModelIndex model = index.model() state = index.data(EditStateRole) pos = index.data(Qt.UserRole) if pos is not None and pos >= 0: # existing level -> only mark/toggle its dropped state model.setData( index, ItemEditState.Dropped if state != ItemEditState.Dropped else ItemEditState.NoState, EditStateRole) elif state == ItemEditState.Added: # new level -> remove it model.removeRow(index.row()) else: assert False, "invalid state '{}' for {}" \ .format(state, index.row()) def _add_category(self): """ Add a new category """ view = self.values_edit model = view.model() with disconnected(model.dataChanged, self.on_values_changed, Qt.UniqueConnection): row = model.rowCount() if not model.insertRow(model.rowCount()): return index = model.index(row, 0) model.setItemData( index, { Qt.EditRole: "", SourcePosRole: None, EditStateRole: ItemEditState.Added }) view.setCurrentIndex(index) view.edit(index) self.on_values_changed()
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction( "+", self, toolTip="Add a new sort key") self._updateAction = QAction( "Update", self, toolTip="Update/save current selection") self._removeAction = QAction( "\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource(self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource(self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg("Model based imputer does not work for sparse data") DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default()] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() # copy METHODS (some are modified by the widget) self.methods = copy.deepcopy(OWImpute.METHODS) main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.methods): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.methods): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.learner = None self.modified = False self.default_method = self.methods[self.default_method_index] self.executor = qconcurrent.ThreadExecutor(self) self.__task = None @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.methods[self.default_method_index] self.methods[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): method = self.variable_methods.get( index.row(), self.methods[self.DEFAULT]) self.varmodel.setData(index, method, Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.methods[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button(self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.update_varview() self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.methods[self.DEFAULT]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [ (i, var, self.variable_methods.get(i, self.default_method)) for i, var in enumerate(self.varmodel) ] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit( impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'". format(var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items(( ("Default method", default), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = [self.get_method_for_column(i.row()) for i in indexes] def method_key(method): """ Decompose method into its type and parameters. """ # The return value should be hashable and __eq__ comparable if isinstance(method, AsDefault): return AsDefault, (method.method,) elif isinstance(method, impute.Model): return impute.Model, (method.learner,) elif isinstance(method, impute.Default): return impute.Default, (method.default,) else: return type(method), None methods = set(method_key(m) for m in methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for i, m in enumerate(self.methods): if method_type == type(m): self.variable_button_group.button(i).setChecked(True) if method_type is impute.Default: (fixed_value,) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None for method, button in zip(self.methods, self.variable_button_group.buttons()): enabled = all(method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: value_stack_enabled = False current_value_widget = None self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index.row(), None) elif method_index == OWImpute.AS_INPUT: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: method = impute.Default(default=value) self.variable_methods[index.row()] = method else: method = self.methods[method_index] for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(self.AS_INPUT).setChecked(True) self.set_method_for_current_selection(self.AS_INPUT) def reset_variable_methods(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input dataset." icon = "icons/FeatureConstructor.svg" keywords = ['function', 'lambda'] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") class Warning(OWWidget.Warning): renamed_var = Msg("Recently added variable has been renamed, " "to avoid duplicates.\n") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def generate_newname(fmt): return unique_name(fmt, self.reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("Categorical") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("Text") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("Date/Time") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, minimumHeight=50, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) gui.button(self.buttonsArea, self, "Send", callback=self.apply, default=True) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: self.Warning.clear() editor = self.editorstack.currentWidget() proposed = editor.editorData().name unique = get_unique_names(self.reserved_names(self.currentIndex), proposed) feature = editor.editorData() if editor.editorData().name != unique: self.Warning.renamed_var() feature = feature.__class__(unique, *feature[1:]) self.featuremodel[self.currentIndex] = feature self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) def reserved_names(self, idx_=None): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [ desc.name for idx, desc in enumerate(self.featuremodel) if idx != idx_ ] return set(varnames) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data self.info.set_input_summary(self.info.NoInput) if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) self.info.set_input_summary(len(data), format_summary_details(data)) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.info.set_output_summary(self.info.NoOutput) self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: for variable in new_variables: variable.compute_value.mask_exceptions = False data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return finally: for variable in new_variables: variable.compute_value.mask_exceptions = True disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.info.set_output_summary(len(data), format_summary_details(data)) self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[ feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) elif isinstance(feature, DateTimeDescriptor): items[feature.name] = "{} (date/time)".format( feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items(report.plural("Constructed feature{s}", len(items)), items)
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input data set." icon = "icons/FeatureConstructor.svg" inputs = [("Data", Orange.data.Table, "setData")] outputs = [("Data", Orange.data.Table)] want_main_area = False settingsHandler = FeatureConstructorSettingsHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [ (ContinuousDescriptor, ContinuousFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor) ] class Error(OWWidget.Error): more_values_needed = Msg("Discrete feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget( sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) ) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton( "New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New ) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [var.name for var in self.data.domain.variables + self.data.domain.metas] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Continuous") cont.triggered.connect( lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3)) ) disc = menu.addAction("Discrete") disc.triggered.connect( lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", ("A", "B"), -1, False)) ) string = menu.addAction("String") string.triggered.connect( lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), "")) ) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton( "Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete ) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView( minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) ) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged ) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) box.layout().addWidget(self.report_button) self.report_button.setMinimumWidth(180) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.send("Data", None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and len(self.featuremodel) > 0: # Deleting the last item clears selection self.setCurrentIndex(len(self.featuremodel) - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) def check_attrs_values(self, attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) source_vars = tuple(self.data.domain) + self.data.domain.metas new_variables = construct_variables(desc, source_vars) attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas) ) try: data = Orange.data.Table(new_domain, self.data) except Exception as err: log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.send("Data", data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[feature.name] = "{} (discrete with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items( report.plural("Constructed feature{s}", len(items)), items)
class StatisticalTestWidget(OWWidget): """ Perform statistical tests """ name = 'Statistical test' description = 'Do statistical tests and returns p-value.\n' \ 'Many samples should be in same table as columns.' icon = 'icons/test_widget.svg' want_main_area = False inputs = [('Data', Orange.data.Table, 'set_data')] outputs = [('p-value', float)] def __init__(self): super().__init__() self.data = None self.available_tests = [TTest(), ZTest(), FisherTest(), Anova()] self.active_tests = [] self.available_columns = itemmodels.VariableListModel(parent=self) self.available_corrections = [ None, BonferroniCorrection(), SidakCorrection()] vlayout = QHBoxLayout() # Data selection gui.widgetBox( self.controlArea, self.tr('Chose data to test'), orientation=vlayout, spacing=16 ) self.varview = QListView(selectionMode=QListView.MultiSelection) self.varview.setModel(self.available_columns) self.varview.selectionModel().selectionChanged.connect( self.update_column_selection ) vlayout.addWidget(self.varview) # Test selection self.tests = itemmodels.VariableListModel(parent=self) self.testview = QListView(selectionMode=QListView.SingleSelection) self.testview.setModel(self.tests) self.testview.selectionModel().selectionChanged.connect( self.test_selected ) vlayout.addWidget(self.testview) # Correction selection self.corrections = itemmodels.VariableListModel(parent=self) self.corrections[:] = \ [str(None), BonferroniCorrection().name, SidakCorrection().name] self.cor_varview = QListView(selectionMode=QListView.SingleSelection) self.cor_varview.setModel(self.corrections) self.cor_varview.selectionModel().selectionChanged.connect( self.set_correction ) vlayout.addWidget(self.cor_varview) self.n_of_tests = 1 self.n_of_tests_input = gui.lineEdit( self.controlArea, self, 'n_of_tests', label='<p align="right">Number of tests</p>', callbackOnType=True, controlWidth=150, orientation=Qt.Horizontal, callback=self.number_of_tests_changed, ) self.chosen_correction = None pval_box = gui.vBox(self.controlArea) self.pval_infolabel = gui.widgetLabel( pval_box, '<p align="left"><b>p-value: </b></p>', ) def show_p_value(self, p_value: float): """ Draw p-value in widget if only is numeric. """ if isinstance(p_value, int) or isinstance(p_value, float): p_val_str = str(p_value) else: p_val_str = '~ ? ~' self.pval_infolabel.setText( '<p align="left"><b>p-value: {}</b></p>'.format(p_val_str)) def update_column_selection(self, *args): """ Method called when selection of samples changed. Enables matching tests for selected samples. """ columns_index = self.selected_columns self.active_tests = [] self.tests[:] = [] if (not self.selected_data_is_continuous and not self.selected_data_is_discrete): return elif len(columns_index) == 1: self.enable_test_with_one_sample() elif len(columns_index) == 2: self.enable_tests_with_two_samples() elif len(columns_index) >= 2: self.enable_tests_with_many_samples() def enable_test_with_one_sample(self): """ Enable tests which can be apply to one sample. """ self.active_tests = [test for test in self.available_tests if test.can_be_used_with_one_sample(self)] self.tests[:] = [test.name for test in self.active_tests] def enable_tests_with_two_samples(self): """ Enable tests which can be apply to two samples. """ self.active_tests = [test for test in self.available_tests if test.can_be_used_with_two_sample(self)] self.tests[:] = [test.name for test in self.active_tests] def enable_tests_with_many_samples(self): """ Enable tests which can be apply to many samples. It should not be called for one or two data samples! """ self.active_tests = [test for test in self.available_tests if test.can_be_used_with_many_sample(self)] self.tests[:] = [test.name for test in self.active_tests] @property def selected_columns(self): """ :return: Indexes of actually selected columns. """ rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] @property def selected_data_is_continuous(self): """ :return: Tell whether all selected samples are continuous. """ return all([self.available_columns[i].is_continuous for i in self.selected_columns]) @property def selected_data_is_discrete(self): """ :return: Tell whether all selected samples are discrete. """ return all([not self.available_columns[i].is_continuous for i in self.selected_columns]) @property def selected_test(self): """ :return: index of actually selected test. """ # FIXME: I have no idea why test list selection sometimes disappearing try: _ = self.testview.selectionModel().selectedRows()[0].row() self.last_selected_test = _ except IndexError: pass return self.last_selected_test def set_data(self, data): return self.update_data(data) def update_data(self, data): """ Method called when input data updated. :param data: input data """ if data: self.data = data self.available_columns[:] = data.domain def test_selected(self): """ Method called when test selected. """ return self.do_test() def do_test(self): """ Main method of widget. Do all test logic. Call it if you want to do test or recalculate it. """ if not self.data: return try: test = self.active_tests[self.selected_test] except IndexError: return except AttributeError: return columns_indexes = self.selected_columns number_of_columns = len(columns_indexes) if number_of_columns == 0: return elif number_of_columns == 1: p_value = test.one_sample_test( self.data[:, columns_indexes].X) elif number_of_columns == 2: p_value = test.two_sample_test( *[self.data[:, column_index].X for column_index in columns_indexes]) else: p_value = test.many_sample_test( *[self.data[:, column_index].X for column_index in columns_indexes]) if self.chosen_correction: chosen_ = self.available_corrections[self.chosen_correction] p_value = chosen_.calculate_correction(p_value, self.n_of_tests) self.show_p_value(p_value) return self.send('p-value', p_value) def number_of_tests_changed(self): """ Method called when number of test in correction changed. """ try: self.n_of_tests = int(self.n_of_tests) except TypeError: self.n_of_tests = 1 self.n_of_tests_input = "1" self.test_selected() def set_correction(self): """ Method called when correction method changed. """ new_cor = self.cor_varview.selectionModel().selectedRows()[0].row() recalculate = False if self.chosen_correction != new_cor: self.chosen_correction = new_cor recalculate = True if recalculate: self.test_selected()
class OWFeatureConstructor(OWWidget): name = "特征构造器(Feature Constructor)" description = "用输入数据集中的现有特征构造新特征。" icon = "icons/FeatureConstructor.svg" keywords = ['tezheng', 'gouzao', 'tezhenggouzao'] category = "数据(Data)" icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("数据(Data)", Orange.data.Table, replaces=['Data']) class Outputs: data = Output("数据(Data)", Orange.data.Table, replaces=['Data']) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) expressions_with_values = ContextSetting(False) settings_version = 2 EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") class Warning(OWWidget.Warning): renamed_var = Msg("Recently added variable has been renamed, " "to avoid duplicates.\n") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "变量定义") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("新建", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def generate_newname(fmt): return unique_name(fmt, self.reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("数值数据") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("分类数据") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("文本") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("日期/时间") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("复制选中变量") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("删除", toolTip="删除选中变量", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, minimumHeight=50, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) self.fix_button = gui.button(self.buttonsArea, self, "Upgrade Expressions", callback=self.fix_expressions) self.fix_button.setHidden(True) gui.button(self.buttonsArea, self, "Send", callback=self.apply, default=True) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: self.Warning.clear() editor = self.editorstack.currentWidget() proposed = editor.editorData().name uniq = get_unique_names(self.reserved_names(self.currentIndex), proposed) feature = editor.editorData() if editor.editorData().name != uniq: self.Warning.renamed_var() feature = feature.__class__(uniq, *feature[1:]) self.featuremodel[self.currentIndex] = feature self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) def reserved_names(self, idx_=None): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [ desc.name for idx, desc in enumerate(self.featuremodel) if idx != idx_ ] return set(varnames) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data self.expressions_with_values = False self.descriptors = [] self.currentIndex = -1 if self.data is not None: self.openContext(data) # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect(self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect(self._on_selectedVariableChanged) self.fix_button.setHidden(not self.expressions_with_values) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) self.fix_button.setHidden(True) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data, self.expressions_with_values) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: for variable in new_variables: variable.compute_value.mask_exceptions = False data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return finally: for variable in new_variables: variable.compute_value.mask_exceptions = True disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): desc = "categorical" if feature.values: desc += " with values " \ + ", ".join(f"'{val}'" for val in feature.values) if feature.ordered: desc += "; ordered" elif isinstance(feature, ContinuousDescriptor): desc = "numeric" elif isinstance(feature, DateTimeDescriptor): desc = "date/time" else: desc = "text" items[feature.name] = f"{feature.expression} ({desc})" self.report_items(report.plural("Constructed feature{s}", len(items)), items) def fix_expressions(self): dlg = QMessageBox( QMessageBox.Question, "Fix Expressions", "This widget's behaviour has changed. Values of categorical " "variables are now inserted as their textual representations " "(strings); previously they appeared as integer numbers, with an " "attribute '.value' that contained the text.\n\n" "The widget currently runs in compatibility mode. After " "expressions are updated, manually check for their correctness.") dlg.addButton("Update", QMessageBox.ApplyRole) dlg.addButton("Cancel", QMessageBox.RejectRole) if dlg.exec() == QMessageBox.RejectRole: return def fixer(mo): var = domain[mo.group(2)] if mo.group(3) == ".value": # uses string; remove `.value` return "".join(mo.group(1, 2, 4)) # Uses ints: get them by indexing return mo.group(1) + "{" + \ ", ".join(f"'{val}': {i}" for i, val in enumerate(var.values)) + \ f"}}[{var.name}]" + mo.group(4) domain = self.data.domain disc_vars = "|".join(f"{var.name}" for var in chain(domain.variables, domain.metas) if var.is_discrete) expr = re.compile(r"(^|\W)(" + disc_vars + r")(\.value)?(\W|$)") self.descriptors[:] = [ descriptor._replace( expression=expr.sub(fixer, descriptor.expression)) for descriptor in self.descriptors ] self.expressions_with_values = False self.fix_button.hide() index = self.currentIndex self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(index) self.apply() @classmethod def migrate_context(cls, context, version): if version is None or version < 2: used_vars = set( chain(*( freevars(ast.parse(descriptor.expression, mode="eval"), []) for descriptor in context.values["descriptors"] if descriptor.expression))) disc_vars = { name for (name, vtype) in chain(context.attributes.items(), context.metas.items()) if vtype == 1 } if used_vars & disc_vars: context.values["expressions_with_values"] = True
class OWDiscretize(widget.OWWidget): # pylint: disable=too-many-instance-attributes name = "Discretize" description = "Discretize the numeric data features." icon = "icons/Discretize.svg" keywords = [] class Inputs: data = Input("Data", Orange.data.Table, doc="Input data table") class Outputs: data = Output("Data", Orange.data.Table, doc="Table with discretized features") settingsHandler = settings.DomainContextHandler() settings_version = 2 saved_var_states = settings.ContextSetting({}) #: The default method name default_method_name = settings.Setting(Methods.EqualFreq.name) #: The k for Equal{Freq,Width} default_k = settings.Setting(3) #: The default cut points for custom entry default_cutpoints: Tuple[float, ...] = settings.Setting(()) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = list(Methods) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None self.class_var = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = Methods.Default self.k = 5 self.cutpoints = () self.default_cutpoints = () box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self._default_method_ = 0 self.default_bbox = rbox = gui.radioButtons( box, self, "_default_method_", callback=self._default_disc_changed) self.default_button_group = bg = rbox.findChild(QButtonGroup) bg.buttonClicked[int].connect(self.set_default_method) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) self.options = [ (Methods.Default, self.tr("Default")), (Methods.Leave, self.tr("Leave numeric")), (Methods.MDL, self.tr("Entropy-MDL discretization")), (Methods.EqualFreq, self.tr("Equal-frequency discretization")), (Methods.EqualWidth, self.tr("Equal-width discretization")), (Methods.Remove, self.tr("Remove numeric variables")), (Methods.Custom, self.tr("Manual")), ] for id_, opt in self.options[1:]: t = gui.appendRadioButton(rbox, opt) bg.setId(t, id_) t.setChecked(id_ == self.default_method) [right, self.left][opt.startswith("Equal")].layout().addWidget(t) def _intbox(parent, attr, callback): box = gui.indentedBox(parent) s = gui.spin(box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) sp = box.sizePolicy() sp.setControlType(sp.SpinBox) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) def manual_cut_editline(text="") -> QLineEdit: edit = QLineEdit( text=text, placeholderText="e.g. 0.0, 0.5, 1.0", toolTip="Enter fixed discretization cut points (a comma " "separated list of strictly increasing numbers e.g. " "0.0, 0.5, 1.0).", ) @edit.textChanged.connect def update(): validator = edit.validator() if validator is not None: state, _, _ = validator.validate(edit.text(), 0) else: state = QValidator.Acceptable palette = edit.palette() colors = { QValidator.Intermediate: (Qt.yellow, Qt.black), QValidator.Invalid: (Qt.red, Qt.black), }.get(state, None) if colors is None: palette = QPalette() else: palette.setColor(QPalette.Base, colors[0]) palette.setColor(QPalette.Text, colors[1]) cr = edit.cursorRect() p = edit.mapToGlobal(cr.bottomRight()) edit.setPalette(palette) if state != QValidator.Acceptable and edit.isVisible(): show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText) else: show_tip(edit, p, "") return edit self.manual_cuts_edit = manual_cut_editline( text=", ".join(map(str, self.default_cutpoints))) def set_manual_default_cuts(): text = self.manual_cuts_edit.text() self.default_cutpoints = tuple( float(s.strip()) for s in text.split(",") if s.strip()) self._default_disc_changed() self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts) validator = IncreasingNumbersListValidator() self.manual_cuts_edit.setValidator(validator) ibox = gui.indentedBox(right, orientation=Qt.Horizontal) ibox.layout().addWidget(self.manual_cuts_edit) right.layout().addStretch(10) self.left.layout().addStretch(10) self.connect_control( "default_cutpoints", lambda values: self.manual_cuts_edit.setText(", ".join( map(str, values)))) vlayout = QHBoxLayout() box = gui.widgetBox(self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8) # List view with all attributes self.varview = QListView( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True, ) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed) vlayout.addWidget(controlbox) self.variable_button_group = bg = controlbox.findChild(QButtonGroup) for id_, opt in self.options[:5]: b = gui.appendRadioButton(controlbox, opt) bg.setId(b, id_) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute", id=Methods.Remove) b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom) self.manual_cuts_specific = manual_cut_editline() self.manual_cuts_specific.setValidator(validator) b.toggled[bool].connect(self.manual_cuts_specific.setEnabled) def set_manual_cuts(): text = self.manual_cuts_specific.text() points = [t for t in text.split(",") if t.split()] self.cutpoints = tuple(float(t) for t in points) self._disc_method_changed() self.manual_cuts_specific.editingFinished.connect(set_manual_cuts) self.connect_control( "cutpoints", lambda values: self.manual_cuts_specific.setText(", ".join( map(str, values)))) ibox = gui.indentedBox(controlbox, orientation=Qt.Horizontal) self.copy_current_to_manual_button = b = FixedSizeButton( text="CC", toolTip="Copy the current cut points to manual mode", enabled=False) b.clicked.connect(self._copy_to_manual) ibox.layout().addWidget(self.manual_cuts_specific) ibox.layout().addWidget(b) gui.rubber(controlbox) controlbox.setEnabled(False) bg.button(self.method) self.controlbox = controlbox box = gui.auto_apply(self.controlArea, self, "autosend") box.button.setFixedWidth(180) box.layout().insertStretch(0) self._update_spin_positions() self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) @property def default_method(self) -> Methods: return Methods[self.default_method_name] @default_method.setter def default_method(self, method): self.set_default_method(method) def set_default_method(self, method: Methods): if isinstance(method, int): method = Methods(method) else: method = Methods.from_method(method) if method != self.default_method: self.default_method_name = method.name self.default_button_group.button(method).setChecked(True) self._default_disc_changed() @Inputs.data def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() self.info.set_input_summary(len(data), format_summary_details(data)) else: self.info.set_input_summary(self.info.NoInput) self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain.variables if var.is_continuous] self.varmodel[:] = cvars has_disc_class = data.domain.has_discrete_class def set_enabled(box: QWidget, id_: Methods, state: bool): bg = box.findChild(QButtonGroup) b = bg.button(id_) b.setEnabled(state) set_enabled(self.default_bbox, self.MDL, has_disc_class) bg = self.bbox.findChild(QButtonGroup) b = bg.button(Methods.MDL) b.setEnabled(has_disc_class) set_enabled(self.bbox, self.MDL, has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL: self.default_method = Methods.Leave if not has_disc_class and self.method == self.MDL: self.method = Methods.Default # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_button_group.button(self.MDL).setEnabled(True) self.variable_button_group.button(self.MDL).setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None: return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar raise ValueError for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) def _current_default_method(self): method = self.default_method k = self.default_k if method == Methods.Leave: def_method = Leave() elif method == Methods.MDL: def_method = MDL() elif method == Methods.EqualFreq: def_method = EqualFreq(k) elif method == Methods.EqualWidth: def_method = EqualWidth(k) elif method == Methods.Remove: def_method = Remove() elif method == Methods.Custom: def_method = Custom(self.default_cutpoints) else: assert False return def_method def _current_method(self): if self.method == Methods.Default: method = Default(self._current_default_method()) elif self.method == Methods.Leave: method = Leave() elif self.method == Methods.MDL: method = MDL() elif self.method == Methods.EqualFreq: method = EqualFreq(self.k) elif self.method == Methods.EqualWidth: method = EqualWidth(self.k) elif self.method == Methods.Remove: method = Remove() elif self.method == Methods.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): kmethods = [Methods.EqualFreq, Methods.EqualWidth] self.k_general.setDisabled(self.default_method not in kmethods) if self.default_method == Methods.EqualFreq: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == Methods.EqualWidth: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in kmethods) if self.method == Methods.EqualFreq: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == Methods.EqualWidth: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self._copy_to_manual_update_enabled() self.commit() def _copy_to_manual(self): indices = self.selected_indices() # set of all methods for the current selection if len(indices) != 1: return index = indices[0] state = self.var_state[index] var = self.varmodel[index] fmt = var.repr_val points = state.points if points is None: points = () else: points = tuple(state.points) state = state._replace(method=Custom(points), points=None, disc_var=None) self._set_var_state(index, state) self.method = Methods.Custom self.cutpoints = points self.manual_cuts_specific.setText(", ".join(map(fmt, points))) self._update_points() self.commit() def _copy_to_manual_update_enabled(self): indices = self.selected_indices() methods = [self.var_state[i].method for i in indices] self.copy_current_to_manual_button.setEnabled( len(indices) == 1 and not isinstance(methods[0], Custom)) def _var_selection_changed(self, *_): self._copy_to_manual_update_enabled() indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] def key(method): if isinstance(method, Default): return Default, (None, ) return type(method), tuple(method) mset = list(unique_everseen(methods, key=key)) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = Methods.from_method(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def method_for_index(self, index): state = self.var_state[index] return state.method def discretized_var(self, index): # type: (int) -> Optional[Orange.data.DiscreteVariable] state = self.var_state[index] if state.disc_var is not None and state.points == []: # Removed by MDL Entropy return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None # a mapping of all applied changes for variables in `varmodel` mapping = { var: self.discretized_var(i) for i, var in enumerate(self.varmodel) } def disc_var(source): return mapping.get(source, source) # map the full input domain to the new variables (where applicable) attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_vars = [disc_var(v) for v in self.data.domain.class_vars] class_vars = [v for v in class_vars if v is not None] domain = Orange.data.Domain(attributes, class_vars, metas=self.data.domain.metas) return domain def commit(self): output = None if self.data is not None: domain = self.discretized_domain() output = self.data.transform(domain) summary = len(output) if output else self.info.NoOutput details = format_summary_details(output) if output else "" self.info.set_output_summary(summary, details) self.Outputs.data.send(output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items( (("Default method", self.options[self.default_method][1]), )) if self.varmodel: self.report_items( "Thresholds", [(var.name, DiscDelegate.cutsText(self.var_state[i], var.repr_val) or "leave numeric") for i, var in enumerate(self.varmodel)]) @classmethod def migrate_settings(cls, settings, version): # pylint: disable=redefined-outer-name if version is None or version < 2: # was stored as int indexing Methods (but offset by 1) default = settings.pop("default_method", 0) default = Methods(default + 1) settings["default_method_name"] = default.name
class LabelSelectionWidget(QWidget): """ A widget for selection of label values. The widget displays the contents of the model with two widgets: * The top level model items are displayed in a combo box. * The children of the current selected top level item are displayed in a subordinate list view. .. note:: This is not a QAbstractItemView subclass. """ # Current group/root index has changed. groupChanged = Signal(int) # Selection for the current group/root has changed. groupSelectionChanged = Signal(int) def __init__(self): super().__init__() self.__model = None self.__selectionMode = QListView.ExtendedSelection self.__currentIndex = -1 self.__selections = {} self.__parent = None self.targets = [] layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) def group_box(title): box = QGroupBox(title) box.setFlat(True) lay = QVBoxLayout() lay.setContentsMargins(0, 0, 0, 0) box.setLayout(lay) return box self.labels_combo = QComboBox() self.values_view = QListView(selectionMode=self.__selectionMode) self.labels_combo.currentIndexChanged.connect( self.__onCurrentIndexChanged) l_box = group_box(self.tr("Label")) v_box = group_box(self.tr("Values")) l_box.layout().addWidget(self.labels_combo) v_box.layout().addWidget(self.values_view) layout.addWidget(l_box) layout.addWidget(v_box) self.setLayout(layout) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def set_selection(self): model = self.model() # FIX: This assumes fixed target key/values order. indices = [ model.index(i, 0, model.index(keyind, 0)) for keyind, selected in enumerate(self.__parent.stored_selections) for i in selected ] selection = itemselection(indices) self.setCurrentGroupIndex(self.__parent.current_group_index) self.setSelection(selection) def selected_split(self): group_index = self.currentGroupIndex() if not (0 <= group_index < len(self.targets)): return None, [] group = self.targets[group_index] selection = [ ind.row() for ind in self.currentGroupSelection().indexes() ] return group, selection def set_data(self, parent, data): """ Initialize widget state after receiving new data. """ self.__parent = parent # self.__currentIndex = parent.current_group_index if data is not None: column_groups, row_groups = group_candidates(data) self.targets = column_groups + row_groups model = QStandardItemModel() for item in [standarditem_from(desc) for desc in self.targets]: model.appendRow(item) self.setModel(model) else: self.targets = [] self.setModel(None) def clear(self): """ Clear the widget/model (same as ``setModel(None)``). """ if self.__model is not None: self.values_view.selectionModel().clearSelection() self.values_view.selectionModel().selectionChanged.disconnect( self.__onSelectionChanged) self.values_view.setModel(None) self.labels_combo.setModel(QStandardItemModel(self.labels_combo)) self.__currentIndex = -1 self.__selections = {} self.__model = None def setModel(self, model): """ Set the source model for display. The model should be a tree model with depth 2. Parameters ---------- model : QtCore.QAbstractItemModel """ if model is self.__model: return self.clear() if model is None: return self.__model = model self.values_view.setModel(model) self.values_view.setRootIndex(model.index(0, 0)) self.values_view.selectionModel().selectionChanged.connect( self.__onSelectionChanged) # will emit the currentIndexChanged (if the model is not empty) self.labels_combo.setModel(model) def model(self): """ Return the current model. Returns ------- model : QtCore.QAbstractItemModel """ return self.__model def setCurrentGroupIndex(self, index): """ Set the current selected group/root index row. Parameters ---------- index : int Group index. """ self.labels_combo.setCurrentIndex(index) def currentGroupIndex(self): """ Return the current selected group/root index row. Returns ------- row : index Current group row index (-1 if there is no current index) """ return self.labels_combo.currentIndex() def setSelection(self, selection): """ Set the model item selection. Parameters ---------- selection : QtCore.QItemSelection Item selection. """ if self.values_view.selectionModel() is not None: indices = selection.indexes() pind = defaultdict(list) for index in indices: parent = index.parent() if parent.isValid(): if parent == self.__model.index(parent.row(), parent.column()): pind[parent.row()].append(QPersistentModelIndex(index)) else: warnings.warn("Die Die Die") else: # top level index pass self.__selections = pind self.__restoreSelection() def selection(self): """ Return the item selection. Returns ------- selection : QtCore.QItemSelection """ selection = QItemSelection() if self.__model is None: return selection for pind in chain(*self.__selections.values()): ind = self.__model.index(pind.row(), pind.column(), pind.parent()) if ind.isValid(): selection.select(ind, ind) return selection def currentGroupSelection(self): """ Return the item selection for the current group only. """ if self.values_view.selectionModel() is not None: return self.values_view.selectionModel().selection() else: return QItemSelection() def __onCurrentIndexChanged(self, index): self.__storeSelection(self.__currentIndex, self.values_view.selectedIndexes()) self.__currentIndex = index if self.__model is not None: root = self.__model.index(index, 0) self.values_view.setRootIndex(root) self.__restoreSelection() self.groupChanged.emit(index) def __onSelectionChanged(self, old, new): self.__storeSelection(self.__currentIndex, self.values_view.selectedIndexes()) self.groupSelectionChanged.emit(self.__currentIndex) def __storeSelection(self, groupind, indices): # Store current values selection for the current group groupind = self.__currentIndex indices = [ QPersistentModelIndex(ind) for ind in self.values_view.selectedIndexes() ] self.__selections[groupind] = indices def __restoreSelection(self): # Restore previous selection for root (if available) assert self.__model is not None groupind = self.__currentIndex root = self.__model.index(groupind, 0) sel = self.__selections.get(groupind, []) indices = [ self.__model.index(pind.row(), pind.column(), root) for pind in sel if pind.isValid() and pind.parent() == root ] selection = QItemSelection() for ind in indices: selection.select(ind, ind) self.values_view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) def sizeHint(self): """Reimplemented from QWidget.sizeHint""" return QSize(100, 200)
class SelectionSetsWidget(QFrame): """ Widget for managing multiple stored item selections """ selectionModified = Signal(bool) def __init__(self, parent): QFrame.__init__(self, parent) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(1) self._setNameLineEdit = QLineEdit(self) layout.addWidget(self._setNameLineEdit) self._setListView = QListView(self) self._listModel = QStandardItemModel(self) self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSourceModel(self._listModel) self._setListView.setModel(self._proxyModel) self._setListView.setItemDelegate(ListItemDelegate(self)) self._setNameLineEdit.textChanged.connect( self._proxyModel.setFilterFixedString) self._completer = QCompleter(self._listModel, self) self._setNameLineEdit.setCompleter(self._completer) self._listModel.itemChanged.connect(self._onSetNameChange) layout.addWidget(self._setListView) buttonLayout = QHBoxLayout() self._addAction = QAction("+", self, toolTip="Add a new sort key") self._updateAction = QAction("Update", self, toolTip="Update/save current selection") self._removeAction = QAction("\u2212", self, toolTip="Remove selected sort key.") self._addToolButton = QToolButton(self) self._updateToolButton = QToolButton(self) self._removeToolButton = QToolButton(self) self._updateToolButton.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) self._addToolButton.setDefaultAction(self._addAction) self._updateToolButton.setDefaultAction(self._updateAction) self._removeToolButton.setDefaultAction(self._removeAction) buttonLayout.addWidget(self._addToolButton) buttonLayout.addWidget(self._updateToolButton) buttonLayout.addWidget(self._removeToolButton) layout.addLayout(buttonLayout) self.setLayout(layout) self._addAction.triggered.connect(self.addCurrentSelection) self._updateAction.triggered.connect(self.updateSelectedSelection) self._removeAction.triggered.connect(self.removeSelectedSelection) self._setListView.selectionModel().selectionChanged.connect( self._onListViewSelectionChanged) self.selectionModel = None self._selections = [] def sizeHint(self): size = QFrame.sizeHint(self) return QSize(size.width(), 150) def _onSelectionChanged(self, selected, deselected): self.setSelectionModified(True) def _onListViewSelectionChanged(self, selected, deselected): try: index = self._setListView.selectedIndexes()[0] except IndexError: return self.commitSelection(self._proxyModel.mapToSource(index).row()) def _onSetNameChange(self, item): self.selections[item.row()].name = str(item.text()) def _setButtonStates(self, val): self._updateToolButton.setEnabled(val) def setSelectionModel(self, selectionModel): if self.selectionModel: self.selectionModel.selectionChanged.disconnect( self._onSelectionChanged) self.selectionModel = selectionModel self.selectionModel.selectionChanged.connect(self._onSelectionChanged) def addCurrentSelection(self): item = self.addSelection( SelectionByKey(self.selectionModel.selection(), name="New selection", key=(1, 2, 3, 10))) index = self._proxyModel.mapFromSource(item.index()) self._setListView.setCurrentIndex(index) self._setListView.edit(index) self.setSelectionModified(False) def removeSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self._listModel.takeRow(i) del self.selections[i] def updateCurentSelection(self): i = self._proxyModel.mapToSource( self._setListView.selectedIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def addSelection(self, selection, name=""): self._selections.append(selection) item = QStandardItem(selection.name) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listModel.appendRow(item) self.setSelectionModified(False) return item def updateSelectedSelection(self): i = self._proxyModel.mapToSource( self._setListView.currentIndex()).row() self.selections[i].setSelection(self.selectionModel.selection()) self.setSelectionModified(False) def setSelectionModified(self, val): self._selectionModified = val self._setButtonStates(val) self.selectionModified.emit(bool(val)) def commitSelection(self, index): selection = self.selections[index] selection.select(self.selectionModel) def setSelections(self, selections): self._listModel.clear() for selection in selections: self.addSelection(selection) def selections(self): return self._selections selections = property(selections, setSelections)
class OWDiscretize(widget.OWWidget): name = "Discretize" description = "Discretize the numeric data features." icon = "icons/Discretize.svg" inputs = [ InputSignal("Data", Orange.data.Table, "set_data", doc="Input data table") ] outputs = [ OutputSignal("Data", Orange.data.Table, doc="Table with discretized features") ] settingsHandler = settings.DomainContextHandler() saved_var_states = settings.ContextSetting({}) default_method = settings.Setting(2) default_k = settings.Setting(3) autosend = settings.Setting(True) #: Discretization methods Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = range(7) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() #: input data self.data = None #: Current variable discretization state self.var_state = {} #: Saved variable discretization settings (context setting) self.saved_var_states = {} self.method = 0 self.k = 5 box = gui.vBox(self.controlArea, self.tr("Default Discretization")) self.default_bbox = rbox = gui.radioButtons( box, self, "default_method", callback=self._default_disc_changed) rb = gui.hBox(rbox) self.left = gui.vBox(rb) right = gui.vBox(rb) rb.layout().setStretch(0, 1) rb.layout().setStretch(1, 1) options = self.options = [ self.tr("Default"), self.tr("Leave numeric"), self.tr("Entropy-MDL discretization"), self.tr("Equal-frequency discretization"), self.tr("Equal-width discretization"), self.tr("Remove numeric variables") ] for opt in options[1:]: t = gui.appendRadioButton(rbox, opt) # This condition is ugly, but it keeps the same order of # options for backward compatibility of saved schemata [right, self.left][opt.startswith("Equal")].layout().addWidget(t) gui.separator(right, 18, 18) def _intbox(widget, attr, callback): box = gui.indentedBox(widget) s = gui.spin(box, self, attr, minv=2, maxv=10, label="Num. of intervals:", callback=callback) s.setMaximumWidth(60) s.setAlignment(Qt.AlignRight) gui.rubber(s.box) return box.box self.k_general = _intbox(self.left, "default_k", self._default_disc_changed) self.k_general.layout().setContentsMargins(0, 0, 0, 0) vlayout = QHBoxLayout() box = gui.widgetBox(self.controlArea, "Individual Attribute Settings", orientation=vlayout, spacing=8) # List view with all attributes self.varview = QListView(selectionMode=QListView.ExtendedSelection) self.varview.setItemDelegate(DiscDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._var_selection_changed) vlayout.addWidget(self.varview) # Controls for individual attr settings self.bbox = controlbox = gui.radioButtons( box, self, "method", callback=self._disc_method_changed) vlayout.addWidget(controlbox) for opt in options[:5]: gui.appendRadioButton(controlbox, opt) self.k_specific = _intbox(controlbox, "k", self._disc_method_changed) gui.appendRadioButton(controlbox, "Remove attribute") gui.rubber(controlbox) controlbox.setEnabled(False) self.controlbox = controlbox box = gui.auto_commit(self.controlArea, self, "autosend", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 20) box.layout().insertWidget(0, self.report_button) self._update_spin_positions() def set_data(self, data): self.closeContext() self.data = data if self.data is not None: self._initialize(data) self.openContext(data) # Restore the per variable discretization settings self._restore(self.saved_var_states) # Complete the induction of cut points self._update_points() else: self._clear() self.unconditional_commit() def _initialize(self, data): # Initialize the default variable states for new data. self.class_var = data.domain.class_var cvars = [var for var in data.domain if var.is_continuous] self.varmodel[:] = cvars class_var = data.domain.class_var has_disc_class = data.domain.has_discrete_class self.default_bbox.buttons[self.MDL - 1].setEnabled(has_disc_class) self.bbox.buttons[self.MDL].setEnabled(has_disc_class) # If the newly disabled MDL button is checked then change it if not has_disc_class and self.default_method == self.MDL - 1: self.default_method = 0 if not has_disc_class and self.method == self.MDL: self.method = 0 # Reset (initialize) the variable discretization states. self._reset() def _restore(self, saved_state): # Restore variable states from a saved_state dictionary. def_method = self._current_default_method() for i, var in enumerate(self.varmodel): key = variable_key(var) if key in saved_state: state = saved_state[key] if isinstance(state.method, Default): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _reset(self): # restore the individual variable settings back to defaults. def_method = self._current_default_method() self.var_state = {} for i in range(len(self.varmodel)): state = DState(Default(def_method), None, None) self._set_var_state(i, state) def _set_var_state(self, index, state): # set the state of variable at `index` to `state`. self.var_state[index] = state self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole) def _clear(self): self.data = None self.varmodel[:] = [] self.var_state = {} self.saved_var_states = {} self.default_bbox.buttons[self.MDL - 1].setEnabled(True) self.bbox.buttons[self.MDL].setEnabled(True) def _update_points(self): """ Update the induced cut points. """ if self.data is None or not len(self.data): return def induce_cuts(method, data, var): dvar = _dispatch[type(method)](method, data, var) if dvar is None: # removed return [], None elif dvar is var: # no transformation took place return None, var elif is_discretized(dvar): return dvar.compute_value.points, dvar else: assert False for i, var in enumerate(self.varmodel): state = self.var_state[i] if state.points is None and state.disc_var is None: points, dvar = induce_cuts(state.method, self.data, var) new_state = state._replace(points=points, disc_var=dvar) self._set_var_state(i, new_state) def _method_index(self, method): return METHODS.index((type(method), )) def _current_default_method(self): method = self.default_method + 1 k = self.default_k if method == OWDiscretize.Leave: def_method = Leave() elif method == OWDiscretize.MDL: def_method = MDL() elif method == OWDiscretize.EqualFreq: def_method = EqualFreq(k) elif method == OWDiscretize.EqualWidth: def_method = EqualWidth(k) elif method == OWDiscretize.Remove: def_method = Remove() else: assert False return def_method def _current_method(self): if self.method == OWDiscretize.Default: method = Default(self._current_default_method()) elif self.method == OWDiscretize.Leave: method = Leave() elif self.method == OWDiscretize.MDL: method = MDL() elif self.method == OWDiscretize.EqualFreq: method = EqualFreq(self.k) elif self.method == OWDiscretize.EqualWidth: method = EqualWidth(self.k) elif self.method == OWDiscretize.Remove: method = Remove() elif self.method == OWDiscretize.Custom: method = Custom(self.cutpoints) else: assert False return method def _update_spin_positions(self): self.k_general.setDisabled(self.default_method not in [2, 3]) if self.default_method == 2: self.left.layout().insertWidget(1, self.k_general) elif self.default_method == 3: self.left.layout().insertWidget(2, self.k_general) self.k_specific.setDisabled(self.method not in [3, 4]) if self.method == 3: self.bbox.layout().insertWidget(4, self.k_specific) elif self.method == 4: self.bbox.layout().insertWidget(5, self.k_specific) def _default_disc_changed(self): self._update_spin_positions() method = self._current_default_method() state = DState(Default(method), None, None) for i, _ in enumerate(self.varmodel): if isinstance(self.var_state[i].method, Default): self._set_var_state(i, state) self._update_points() self.commit() def _disc_method_changed(self): self._update_spin_positions() indices = self.selected_indices() method = self._current_method() state = DState(method, None, None) for idx in indices: self._set_var_state(idx, state) self._update_points() self.commit() def _var_selection_changed(self, *args): indices = self.selected_indices() # set of all methods for the current selection methods = [self.var_state[i].method for i in indices] mset = set(methods) self.controlbox.setEnabled(len(mset) > 0) if len(mset) == 1: method = mset.pop() self.method = self._method_index(method) if isinstance(method, (EqualFreq, EqualWidth)): self.k = method.k elif isinstance(method, Custom): self.cutpoints = method.points else: # deselect the current button self.method = -1 bg = self.controlbox.group button_group_reset(bg) self._update_spin_positions() def selected_indices(self): rows = self.varview.selectionModel().selectedRows() return [index.row() for index in rows] def discretized_var(self, source): index = list(self.varmodel).index(source) state = self.var_state[index] if state.disc_var is None: return None elif state.disc_var is source: return source elif state.points == []: return None else: return state.disc_var def discretized_domain(self): """ Return the current effective discretized domain. """ if self.data is None: return None def disc_var(source): if source and source.is_continuous: return self.discretized_var(source) else: return source attributes = [disc_var(v) for v in self.data.domain.attributes] attributes = [v for v in attributes if v is not None] class_var = disc_var(self.data.domain.class_var) domain = Orange.data.Domain(attributes, class_var, metas=self.data.domain.metas) return domain def commit(self): output = None if self.data is not None and len(self.data): domain = self.discretized_domain() output = self.data.transform(domain) self.send("Data", output) def storeSpecificSettings(self): super().storeSpecificSettings() self.saved_var_states = { variable_key(var): self.var_state[i]._replace(points=None, disc_var=None) for i, var in enumerate(self.varmodel) } def send_report(self): self.report_items( (("Default method", self.options[self.default_method + 1]), )) if self.varmodel: self.report_items( "Thresholds", [(var.name, DiscDelegate.cutsText(self.var_state[i]) or "leave numeric") for i, var in enumerate(self.varmodel)])
def _set_selection(view: QListView, indices: List[int]): view.clearSelection() sm = view.selectionModel() model = view.model() for ind in indices: sm.select(model.index(ind), QItemSelectionModel.Select)
class OWOntology(OWWidget, ConcurrentWidgetMixin): name = "Ontology" description = "" icon = "icons/Ontology.svg" priority = 1110 keywords = [] CACHED, LIBRARY = range(2) # library list modification types RUN_BUTTON, INC_BUTTON = "Generate", "Include" settingsHandler = DomainContextHandler() ontology_library: List[Dict] = Setting([ {"name": Ontology.generate_name([]), "ontology": {}}, ]) ontology_index: int = Setting(0) ontology: OntoType = Setting((), schema_only=True) include_children = Setting(True) auto_commit = Setting(True) class Inputs: words = Input("Words", Table) class Outputs: words = Output("Words", Table, dynamic=False) class Warning(OWWidget.Warning): no_words_column = Msg("Input is missing 'Words' column.") class Error(OWWidget.Error): load_error = Msg("{}") def __init__(self): OWWidget.__init__(self) ConcurrentWidgetMixin.__init__(self) self.__onto_handler = OntologyHandler() flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable self.__model = PyListModel([], self, flags=flags) self.__input_model = QStandardItemModel() self.__library_view: QListView = None self.__input_view: ListViewSearch = None self.__ontology_view: EditableTreeView = None self.ontology_info = "" self._setup_gui() self._restore_state() self.settingsAboutToBePacked.connect(self._save_state) def _setup_gui(self): # control area library_box: QGroupBox = gui.vBox(self.controlArea, "Library") library_box.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) edit_triggers = QListView.DoubleClicked | QListView.EditKeyPressed self.__library_view = QListView( editTriggers=int(edit_triggers), minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), ) self.__library_view.setFixedHeight(100) self.__library_view.setItemDelegate(LibraryItemDelegate(self)) self.__library_view.setModel(self.__model) self.__library_view.selectionModel().selectionChanged.connect( self.__on_selection_changed ) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) tool_tip = "Add a new ontology to the library" action = QAction("+", self, toolTip=tool_tip) action.triggered.connect(self.__on_add) actions_widget.addAction(action) tool_tip = "Remove the ontology from the library" action = QAction("\N{MINUS SIGN}", self, toolTip=tool_tip) action.triggered.connect(self.__on_remove) actions_widget.addAction(action) tool_tip = "Save changes in the editor to the library" action = QAction("Update", self, toolTip=tool_tip) action.triggered.connect(self.__on_update) actions_widget.addAction(action) gui.rubber(actions_widget) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Ontology from File", self) new_from_file.triggered.connect(self.__on_import_file) new_from_url = QAction("Import Ontology from URL", self) new_from_url.triggered.connect(self.__on_import_url) save_to_file = QAction("Save Ontology to File", self) save_to_file.triggered.connect(self.__on_save) menu = QMenu(actions_widget) menu.addAction(new_from_file) menu.addAction(new_from_url) menu.addAction(save_to_file) action.setMenu(menu) button = actions_widget.addAction(action) button.setPopupMode(QToolButton.InstantPopup) vlayout = QVBoxLayout() vlayout.setSpacing(1) vlayout.setContentsMargins(0, 0, 0, 0) vlayout.addWidget(self.__library_view) vlayout.addWidget(actions_widget) library_box.layout().setSpacing(1) library_box.layout().addLayout(vlayout) input_box: QGroupBox = gui.vBox(self.controlArea, "Input") self.__input_view = ListViewSearch( sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), selectionMode=QListView.ExtendedSelection, dragEnabled=True, ) self.__input_view.setModel(self.__input_model) self.__input_view.selectionModel().selectionChanged.connect( self._enable_include_button ) self.__inc_button = gui.button( None, self, self.INC_BUTTON, enabled=False, toolTip="Include selected words into the ontology", autoDefault=False, callback=self.__on_toggle_include ) input_box.layout().setSpacing(1) input_box.layout().addWidget(self.__input_view) input_box.layout().addWidget(self.__inc_button) self.__run_button = gui.button( self.controlArea, self, self.RUN_BUTTON, callback=self.__on_toggle_run ) gui.checkBox( self.controlArea, self, "include_children", "Include subtree", box="Output", callback=self.commit.deferred ) box = gui.vBox(self.controlArea, "Ontology info") gui.label(box, self, "%(ontology_info)s") gui.auto_send(self.buttonsArea, self, "auto_commit") # main area ontology_box: QGroupBox = gui.vBox(self.mainArea, box=True) self.__ontology_view = EditableTreeView(self) self.__ontology_view.dataChanged.connect( self.__on_ontology_data_changed ) self.__ontology_view.selectionChanged.connect(self.commit.deferred) ontology_box.layout().setSpacing(1) ontology_box.layout().addWidget(self.__ontology_view) self._enable_include_button() def __on_selection_changed(self, selection: QItemSelection, *_): self.Error.load_error.clear() if selection.indexes(): self.ontology_index = row = selection.indexes()[0].row() data = self.__model[row].cached_word_tree self.__ontology_view.set_data(data) self.__update_score() error_msg = self.__model[row].error_msg if error_msg: self.Error.load_error(error_msg) def __on_add(self): name = Ontology.generate_name([l.name for l in self.__model]) data = self.__ontology_view.get_data() self.__model.append(Ontology(name, data)) self.__set_selected_row(len(self.__model) - 1) def __on_remove(self): index = self.__get_selected_row() if index is not None: del self.__model[index] self.__set_selected_row(max(index - 1, 0)) def __on_update(self): self.__set_current_modified(self.LIBRARY) def __on_import_file(self): ontology = read_from_file(self) self._import_ontology(ontology) def __on_import_url(self): ontology = read_from_url(self) self._import_ontology(ontology) def __on_save(self): index = self.__get_selected_row() if index is not None: filename = self.__model[index].filename if filename: filename, _ = os.path.splitext(filename) else: filename = os.path.expanduser("~/") save_ontology(self, filename, self.__ontology_view.get_data()) QApplication.setActiveWindow(self) def __on_toggle_include(self): if self.task is not None: self._cancel_tasks() else: self._run_insert() def __on_toggle_run(self): if self.task is not None: self._cancel_tasks() else: self._run() def __on_ontology_data_changed(self): self.__set_current_modified(self.CACHED) self.__update_score() self._enable_include_button() self.commit.deferred() @Inputs.words def set_words(self, words: Optional[Table]): self.Warning.no_words_column.clear() self.__input_model.clear() if words: if WORDS_COLUMN_NAME in words.domain and words.domain[ WORDS_COLUMN_NAME].attributes.get("type") == "words": for word in words.get_column_view(WORDS_COLUMN_NAME)[0]: self.__input_model.appendRow(QStandardItem(word)) else: self.Warning.no_words_column() @gui.deferred def commit(self): if self.include_children: words = self.__ontology_view.get_selected_words_with_children() else: words = self.__ontology_view.get_selected_words() words_table = self._create_output_table(sorted(words)) self.Outputs.words.send(words_table) @staticmethod def _create_output_table(words: List[str]) -> Optional[Table]: if not words: return None return create_words_table(words) def _cancel_tasks(self): self.cancel() self.__inc_button.setText(self.INC_BUTTON) self.__run_button.setText(self.RUN_BUTTON) def _run(self): self.__run_button.setText("Stop") words = self.__ontology_view.get_words() handler = self.__onto_handler.generate self.start(_run, handler, (words,)) def _run_insert(self): self.__inc_button.setText("Stop") tree = self.__ontology_view.get_data() words = self.__get_selected_input_words() handler = self.__onto_handler.insert self.start(_run, handler, (tree, words)) def on_done(self, data: Dict): self.__inc_button.setText(self.INC_BUTTON) self.__run_button.setText(self.RUN_BUTTON) self.__ontology_view.set_data(data, keep_history=True) self.__set_current_modified(self.CACHED) self.__update_score() def __update_score(self): tree = self.__ontology_view.get_data() score = round(self.__onto_handler.score(tree), 2) \ if len(tree) == 1 and list(tree.values())[0] else "/" self.ontology_info = f"Score: {score}" def on_exception(self, ex: Exception): raise ex def on_partial_result(self, _: Any): pass def onDeleteWidget(self): self.shutdown() super().onDeleteWidget() def __set_selected_row(self, row: int): self.__library_view.selectionModel().select( self.__model.index(row, 0), QItemSelectionModel.ClearAndSelect ) def __get_selected_row(self) -> Optional[int]: rows = self.__library_view.selectionModel().selectedRows() return rows[0].row() if rows else None def __set_current_modified(self, mod_type: int): index = self.__get_selected_row() if index is not None: if mod_type == self.LIBRARY: ontology = self.__ontology_view.get_data() self.__model[index].word_tree = ontology self.__model[index].cached_word_tree = ontology self.__model[index].update_rule_flag = Ontology.NotModified elif mod_type == self.CACHED: ontology = self.__ontology_view.get_data() self.__model[index].cached_word_tree = ontology else: raise NotImplementedError self.__model.emitDataChanged(index) self.__library_view.repaint() def __get_selected_input_words(self) -> List[str]: return [self.__input_view.model().data(index) for index in self.__input_view.selectedIndexes()] def _import_ontology(self, ontology: Ontology): if ontology is not None: self.__model.append(ontology) self.__set_selected_row(len(self.__model) - 1) QApplication.setActiveWindow(self) def _restore_state(self): source = [Ontology.from_dict(s) for s in self.ontology_library] self.__model.wrap(source) self.__set_selected_row(self.ontology_index) if self.ontology: self.__ontology_view.set_data(self.ontology) self.__set_current_modified(self.CACHED) self.__update_score() self.commit.now() def _save_state(self): self.ontology_library = [s.as_dict() for s in self.__model] self.ontology = self.__ontology_view.get_data(with_selection=True) def _enable_include_button(self): tree = self.__ontology_view.get_data() words = self.__get_selected_input_words() enabled = len(tree) == 1 and len(words) > 0 self.__inc_button.setEnabled(enabled) def send_report(self): model = self.__model library = model[self.ontology_index].name if model else "/" self.report_items("Settings", [("Library", library)]) ontology = self.__ontology_view.get_data() style = """ <style> ul { padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 20px; } </style> """ self.report_raw("Ontology", style + _tree_to_html(ontology))
class OWEditDomain(widget.OWWidget): name = "Edit Domain" description = "Rename features and their values." icon = "icons/EditDomain.svg" priority = 3125 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Data", Orange.data.Table)] settingsHandler = settings.DomainContextHandler() domain_change_hints = settings.ContextSetting({}) selected_index = settings.ContextSetting({}) autocommit = settings.Setting(False) def __init__(self): super().__init__() self.data = None self.input_vars = () self._invalidated = False box = gui.vBox(self.controlArea, "Domain Features") self.domain_model = itemmodels.VariableListModel() self.domain_view = QListView( selectionMode=QListView.SingleSelection ) self.domain_view.setModel(self.domain_model) self.domain_view.selectionModel().selectionChanged.connect( self._on_selection_changed) box.layout().addWidget(self.domain_view) box = gui.hBox(self.controlArea) gui.button(box, self, "Reset Selected", callback=self.reset_selected) gui.button(box, self, "Reset All", callback=self.reset_all) gui.auto_commit(self.controlArea, self, "autocommit", "Apply") box = gui.vBox(self.mainArea, "Edit") self.editor_stack = QStackedWidget() self.editor_stack.addWidget(DiscreteVariableEditor()) self.editor_stack.addWidget(ContinuousVariableEditor()) self.editor_stack.addWidget(VariableEditor()) box.layout().addWidget(self.editor_stack) @check_sql_input def set_data(self, data): """Set input data set.""" self.closeContext() self.clear() self.data = data if self.data is not None: self._initialize() self.openContext(self.data) self._restore() self.unconditional_commit() def clear(self): """Clear the widget state.""" self.data = None self.domain_model[:] = [] self.input_vars = [] self.domain_change_hints = {} self.selected_index = -1 def reset_selected(self): """Reset the currently selected variable to its original state.""" ind = self.selected_var_index() if ind >= 0: var = self.input_vars[ind] desc = variable_description(var, skip_attributes=True) if desc in self.domain_change_hints: del self.domain_change_hints[desc] self.domain_model[ind] = var self.editor_stack.currentWidget().set_data(var) self._invalidate() def reset_all(self): """Reset all variables to their original state.""" self.domain_change_hints = {} if self.data is not None: # To invalidate stored hints self.domain_model[:] = self.input_vars itemmodels.select_row(self.domain_view, self.selected_index) self._invalidate() def selected_var_index(self): """Return the selected row in 'Domain Features' view.""" rows = self.domain_view.selectedIndexes() assert len(rows) <= 1 return rows[0].row() if rows else -1 def _initialize(self): domain = self.data.domain self.input_vars = tuple(domain) + domain.metas self.domain_model[:] = list(self.input_vars) def _restore(self): # Restore the variable states from saved settings. def transform(var): vdesc = variable_description(var, skip_attributes=True) if vdesc in self.domain_change_hints: return variable_from_description( self.domain_change_hints[vdesc], compute_value=Orange.preprocess.transformation.Identity(var)) else: return var self.domain_model[:] = map(transform, self.input_vars) # Restore the variable selection if possible index = self.selected_index if index >= len(self.input_vars): index = 0 if len(self.input_vars) else -1 if index >= 0: itemmodels.select_row(self.domain_view, index) def _on_selection_changed(self): self.selected_index = self.selected_var_index() self.open_editor(self.selected_index) def open_editor(self, index): self.clear_editor() if index < 0: return var = self.domain_model[index] editor_index = 2 if var.is_discrete: editor_index = 0 elif var.is_continuous: editor_index = 1 editor = self.editor_stack.widget(editor_index) self.editor_stack.setCurrentWidget(editor) editor.set_data(var) editor.variable_changed.connect(self._on_variable_changed) def clear_editor(self): current = self.editor_stack.currentWidget() try: current.variable_changed.disconnect(self._on_variable_changed) except Exception: pass current.set_data(None) def _on_variable_changed(self): """User edited the current variable in editor.""" assert 0 <= self.selected_index <= len(self.domain_model) editor = self.editor_stack.currentWidget() # Replace the variable in the 'Domain Features' view/model old_var = self.input_vars[self.selected_index] new_var = editor.get_data().copy(compute_value=Orange.preprocess.transformation.Identity(old_var)) self.domain_model[self.selected_index] = new_var # Store the transformation hint. old_var_desc = variable_description(old_var, skip_attributes=True) self.domain_change_hints[old_var_desc] = variable_description(new_var) self._invalidate() def _invalidate(self): self.commit() def commit(self): """Send the changed data to output.""" new_data = None if self.data is not None: input_domain = self.data.domain n_attrs = len(input_domain.attributes) n_vars = len(input_domain.variables) n_class_vars = len(input_domain.class_vars) all_new_vars = list(self.domain_model) attrs = all_new_vars[: n_attrs] class_vars = all_new_vars[n_attrs: n_attrs + n_class_vars] new_metas = all_new_vars[n_attrs + n_class_vars:] new_domain = Orange.data.Domain(attrs, class_vars, new_metas) new_data = self.data.from_table(new_domain, self.data) self.send("Data", new_data) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(660, 550)) def send_report(self): if self.data is not None: self.report_raw("", EditDomainReport( old_domain=chain(self.data.domain.variables, self.data.domain.metas), new_domain=self.domain_model).to_html()) else: self.report_data(None)
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 inputs = [ ("in_data", Orange.data.Table, "setExampleTable", widget.Default), # ("in_distance", Orange.misc.SymMatrix, "setDistanceMatrix", # widget.Default), ("in_learner", Learner, "setLearner", widget.Default), ("in_classifier", Model, "setClassifier", widget.Default), ("in_object", object, "setObject") ] outputs = [ ( "out_data", Orange.data.Table, ), # ("out_distance", Orange.misc.SymMatrix, ), ( "out_learner", Learner, ), ("out_classifier", Model, widget.Dynamic), ("out_object", object, widget.Dynamic) ] libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(True) class Error(OWWidget.Error): pass def __init__(self): super().__init__() self.in_data = None self.in_distance = None self.in_learner = None self.in_classifier = None self.in_object = None for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + \ "<li>".join(t.name for t in self.inputs) + \ "</ul></p><p>Output variables:<ul><li>" + \ "<li>".join(t.name for t in self.outputs) + \ "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.auto_commit(self.controlArea, self, "auto_execute", "Execute", auto_label="Auto Execute") self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def setExampleTable(self, et): self.in_data = et def setDistanceMatrix(self, dm): self.in_distance = dm def setLearner(self, learner): self.in_learner = learner def setClassifier(self, classifier): self.in_classifier = classifier def setObject(self, obj): self.in_object = obj def handleNewSignals(self): self.unconditional_commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = dict([(i.name, getattr(self, i.name, None)) for i in self.inputs]) d.update(dict([(o.name, None) for o in self.outputs])) return d def commit(self): self.Error.clear() self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for out in self.outputs: signal = out.name out_var = self.console.locals.get(signal, None) if not isinstance(out_var, out.type) and out_var is not None: self.Error.add_message( signal, "Variable '{}' has to be an instance of '{}'.".format( signal, out.type.__name__)) getattr(self.Error, signal)() out_var = None self.send(signal, out_var)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg("Model based imputer does not work for sparse data") settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(int(Method.Leave)) # type: int # Per-variable imputation state (synced in storeSpecificSettings) _variable_imputation_state = settings.ContextSetting({}) # type: VariableState autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self.learner = None # type: Optional[Learner] self.default_learner = SimpleTreeLearner() self.modified = False self.executor = qconcurrent.ThreadExecutor(self) self.__task = None main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for method, _ in list(METHODS.items())[1:-1]: imputer = self.create_imputer(method) button = QRadioButton(imputer.name) button.setChecked(method == self.default_method_index) button_group.addButton(button, method) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection, uniformItemSizes=True ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for method in Method: imputer = self.create_imputer(method) button = QRadioButton(text=imputer.name) button_group.addButton(button, method) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_state, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.button.setFixedWidth(180) box.layout().insertStretch(0) def create_imputer(self, method, *args): # type: (Method, ...) -> impute.BaseImputeMethod if method == Method.Model: if self.learner is not None: return impute.Model(self.learner) else: return impute.Model(self.default_learner) elif method == Method.AsAboveSoBelow: assert self.default_method_index != Method.AsAboveSoBelow default = self.create_imputer(Method(self.default_method_index)) m = AsDefault() m.method = default return m else: return METHODS[method](*args) @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: assert index != Method.AsAboveSoBelow self._default_method_index = index self.default_button_group.button(index).setChecked(True) # update variable view self.update_varview() self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self._variable_imputation_state = {} # type: VariableState self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) # restore per variable imputation state self._restore_state(self._variable_imputation_state) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.default_learner imputer = self.create_imputer(Method.Model) button = self.default_button_group.button(Method.Model) button.setText(imputer.name) variable_button = self.variable_button_group.button(Method.Model) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = Method.Model self.update_varview() self.commit() def get_method_for_column(self, column_index): # type: (int) -> impute.BaseImputeMethod """ Return the imputation method for column by its index. """ assert 0 <= column_index < len(self.varmodel) idx = self.varmodel.index(column_index, 0) state = idx.data(StateRole) if state is None: state = (Method.AsAboveSoBelow, ()) return self.create_imputer(state[0], *state[1]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [ (i, var, self.get_method_for_column(i)) for i, var in enumerate(self.varmodel) ] # normalize to the effective method bypasing AsDefault impute_state = [ (i, var, m.method if isinstance(m, AsDefault) else m) for i, var, m in impute_state ] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit( impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'". format(var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.get_method_for_column(i) if not isinstance(method, AsDefault): specific.append("{} ({})".format(var.name, str(method))) default = self.create_imputer(Method.AsAboveSoBelow) if specific: self.report_items(( ("Default method", default.name), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default.name),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() defmethod = (Method.AsAboveSoBelow, ()) methods = [index.data(StateRole) for index in indexes] methods = [m if m is not None else defmethod for m in methods] methods = set(methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for m in Method: if method_type == m: self.variable_button_group.button(m).setChecked(True) if method_type == Method.Default: (fixed_value,) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None # Update variable methods GUI enabled state based on selection. for method in Method: # use a default constructed imputer to query support imputer = self.create_imputer(method) enabled = all(imputer.supports_variable(var) for var in selected_vars) button = self.variable_button_group.button(method) button.setEnabled(enabled) # Update the "Value" edit GUI. if not has_discrete: # no discrete variables -> allow mass edit for all (continuous vars) value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: # single discrete var -> enable and fill the values combo value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: # mixed type selection -> disable value_stack_enabled = False current_value_widget = None self.variable_button_group.button(Method.Default).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: # set current value if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): # type: (Method) -> None indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): # type: (List[QModelIndex], Method) -> None if method_index == Method.AsAboveSoBelow: for index in indexes: self.varmodel.setData(index, None, StateRole) elif method_index == Method.Default: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: state = (int(Method.Default), (value,)) self.varmodel.setData(index, state, StateRole) else: state = (int(method_index), ()) for index in indexes: self.varmodel.setData(index, state, StateRole) self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData( index, self.get_method_for_column(index.row()), DisplayMethodRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(Method.Default).setChecked(True) self.set_method_for_current_selection(Method.Default) def reset_variable_state(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, Method.AsAboveSoBelow) self.variable_button_group.button(Method.AsAboveSoBelow).setChecked(True) def _store_state(self): # type: () -> VariableState """ Save the current variable imputation state """ state = {} # type: VariableState for i, var in enumerate(self.varmodel): index = self.varmodel.index(i) m = index.data(StateRole) if m is not None: state[var_key(var)] = m return state def _restore_state(self, state): # type: (VariableState) -> None """ Restore the variable imputation state from the saved state """ def check(state): # check if state is a proper State if isinstance(state, tuple) and len(state) == 2: m, p = state if isinstance(m, int) and isinstance(p, tuple) and \ 0 <= m < len(Method): return True return False for i, var in enumerate(self.varmodel): m = state.get(var_key(var), None) if check(m): self.varmodel.setData(self.varmodel.index(i), m, StateRole) def storeSpecificSettings(self): self._variable_imputation_state = self._store_state() super().storeSpecificSettings()
class OWPythonScript(OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." category = "Transform" icon = "icons/PythonScript.svg" priority = 3150 keywords = ["program", "function"] class Inputs: data = MultiInput("Data", Table, replaces=["in_data"], default=True) learner = MultiInput("Learner", Learner, replaces=["in_learner"], default=True) classifier = MultiInput("Classifier", Model, replaces=["in_classifier"], default=True) object = MultiInput("Object", object, replaces=["in_object"], default=False) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") settings_version = 2 scriptLibrary: 'List[_ScriptData]' = Setting([{ "name": "Table from numpy", "script": DEFAULT_SCRIPT, "filename": None }]) currentScriptIndex = Setting(0) scriptText: Optional[str] = Setting(None, schema_only=True) splitterState: Optional[bytes] = Setting(None) vimModeEnabled = Setting(False) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, []) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) # Styling self.defaultFont = defaultFont = ( 'Menlo' if sys.platform == 'darwin' else 'Courier' if sys.platform in ['win32', 'cygwin'] else 'DejaVu Sans Mono') self.defaultFontSize = defaultFontSize = 13 self.editorBox = gui.vBox(self, box="Editor", spacing=4) self.splitCanvas.addWidget(self.editorBox) darkMode = QApplication.instance().property('darkMode') scheme_name = 'Dark' if darkMode else 'Light' syntax_highlighting_scheme = SYNTAX_HIGHLIGHTING_STYLES[scheme_name] self.pygments_style_class = make_pygments_style(scheme_name) eFont = QFont(defaultFont) eFont.setPointSize(defaultFontSize) # Fake Signature self.func_sig = func_sig = FunctionSignature( self.editorBox, syntax_highlighting_scheme, eFont) # Editor editor = PythonEditor(self) editor.setFont(eFont) editor.setup_completer_appearance((300, 180), eFont) # Fake return return_stmt = ReturnStatement(self.editorBox, syntax_highlighting_scheme, eFont) self.return_stmt = return_stmt # Match indentation textEditBox = QWidget(self.editorBox) textEditBox.setLayout(QHBoxLayout()) char_4_width = QFontMetrics(eFont).horizontalAdvance('0000') @editor.viewport_margins_updated.connect def _(width): func_sig.setIndent(width) textEditMargin = max(0, round(char_4_width - width)) return_stmt.setIndent(textEditMargin + width) textEditBox.layout().setContentsMargins(textEditMargin, 0, 0, 0) self.text = editor textEditBox.layout().addWidget(editor) self.editorBox.layout().addWidget(func_sig) self.editorBox.layout().addWidget(textEditBox) self.editorBox.layout().addWidget(return_stmt) self.editorBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) # Controls self.editor_controls = gui.vBox(self.controlArea, box='Preferences') self.vim_box = gui.hBox(self.editor_controls, spacing=20) self.vim_indicator = VimIndicator(self.vim_box) vim_sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) vim_sp.setRetainSizeWhenHidden(True) self.vim_indicator.setSizePolicy(vim_sp) def enable_vim_mode(): editor.vimModeEnabled = self.vimModeEnabled self.vim_indicator.setVisible(self.vimModeEnabled) enable_vim_mode() gui.checkBox(self.vim_box, self, 'vimModeEnabled', 'Vim mode', tooltip="Only for the coolest.", callback=enable_vim_mode) self.vim_box.layout().addWidget(self.vim_indicator) @editor.vimModeIndicationChanged.connect def _(color, text): self.vim_indicator.indicator_color = color self.vim_indicator.indicator_text = text self.vim_indicator.update() # Library self.libraryListSource = [] self._cachedDocuments = {} self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.buttonsArea, self, 'Run', callback=self.commit) self.run_action = QAction("Run script", self, triggered=self.commit, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)) self.addAction(self.run_action) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self.splitCanvas, 'Console') self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.splitCanvas.setSizes([2, 1]) self.controlArea.layout().addStretch(10) self._restoreState() self.settingsAboutToBePacked.connect(self._saveState) def sizeHint(self) -> QSize: return super().sizeHint().expandedTo(QSize(800, 600)) def _restoreState(self): self.libraryListSource = [ Script.fromdict(s) for s in self.scriptLibrary ] self.libraryList.wrap(self.libraryListSource) select_row(self.libraryView, self.currentScriptIndex) if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) def _saveState(self): self.scriptLibrary = [s.asdict() for s in self.libraryListSource] self.scriptText = self.text.toPlainText() self.splitterState = bytes(self.splitCanvas.saveState()) def set_input(self, index, obj, signal): dic = getattr(self, signal) dic[index] = obj def insert_input(self, index, obj, signal): dic = getattr(self, signal) dic.insert(index, obj) def remove_input(self, index, signal): dic = getattr(self, signal) dic.pop(index) @Inputs.data def set_data(self, index, data): self.set_input(index, data, "data") @Inputs.data.insert def insert_data(self, index, data): self.insert_input(index, data, "data") @Inputs.data.remove def remove_data(self, index): self.remove_input(index, "data") @Inputs.learner def set_learner(self, index, learner): self.set_input(index, learner, "learner") @Inputs.learner.insert def insert_learner(self, index, learner): self.insert_input(index, learner, "learner") @Inputs.learner.remove def remove_learner(self, index): self.remove_input(index, "learner") @Inputs.classifier def set_classifier(self, index, classifier): self.set_input(index, classifier, "classifier") @Inputs.classifier.insert def insert_classifier(self, index, classifier): self.insert_input(index, classifier, "classifier") @Inputs.classifier.remove def remove_classifier(self, index): self.remove_input(index, "classifier") @Inputs.object def set_object(self, index, object): self.set_input(index, object, "object") @Inputs.object.insert def insert_object(self, index, object): self.insert_input(index, object, "object") @Inputs.object.remove def remove_object(self, index): self.remove_input(index, "object") def handleNewSignals(self): # update fake signature labels self.func_sig.update_signal_text( {n: len(getattr(self, n)) for n in self.signal_names}) self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *_): self.libraryList.append( Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *_): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) with tokenize.open(filename) as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *_): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *_): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, _deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if not isinstance(script, Script): script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PygmentsHighlighter(doc) doc.highlighter.set_style(self.pygments_style_class) doc.setDefaultFont( QFont(self.defaultFont, pointSize=self.defaultFontSize)) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *_): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): self.Error.clear() lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var) def keyPressEvent(self, e): if e.matches(QKeySequence.InsertLineSeparator): # run on Shift+Enter, Ctrl+Enter self.run_action.trigger() e.accept() else: super().keyPressEvent(e) def dragEnterEvent(self, event): # pylint: disable=no-self-use urls = event.mimeData().urls() if urls: # try reading the file as text c = read_file_content(urls[0].toLocalFile(), limit=1000) if c is not None: event.acceptProposedAction() @classmethod def migrate_settings(cls, settings, version): if version is not None and version < 2: scripts = settings.pop("libraryListSource") # type: List[Script] library = [ dict(name=s.name, script=s.script, filename=s.filename) for s in scripts ] # type: List[_ScriptData] settings["scriptLibrary"] = library def onDeleteWidget(self): self.text.terminate() super().onDeleteWidget()
class OWWordList(OWWidget): name = "Word List" description = "Create a list of words." icon = "icons/WordList.svg" priority = 1000 class Inputs: words = Input("Words", Table) class Outputs: selected_words = Output("Selected Words", Table) words = Output("Words", Table) class Warning(OWWidget.Warning): no_string_vars = Msg("Input needs at least one Text variable.") NONE, CACHED, LIBRARY = range(3) # library list modification types want_main_area = False resizing_enabled = True settingsHandler = DomainContextHandler() word_list_library: List[Dict] = Setting([ { "name": WordList.generate_word_list_name([]), "words": [] }, ]) word_list_index: int = Setting(0) words_var: Optional[StringVariable] = ContextSetting(None) update_rule_index: int = Setting(UpdateRules.INTERSECT) words: List[str] = Setting(None, schema_only=True) selected_words: Set[str] = Setting(set(), schema_only=True) def __init__(self): super().__init__(self) flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable self.library_model = PyListModel([], self, flags=flags) self.words_model = PyListModel([], self, flags=flags, enable_dnd=True) self.library_view: QListView = None self.words_view: ListView = None self.__input_words_model = DomainModel(valid_types=(StringVariable, )) self.__input_words: Optional[Table] = None self.__library_box: QGroupBox = gui.vBox(None, "Library") self.__input_box: QGroupBox = gui.vBox(None, "Input") self.__words_box: QGroupBox = gui.vBox(None, box=True) self.__update_rule_rb: QRadioButton = None self.__add_word_action: QAction = None self.__remove_word_action: QAction = None self._setup_gui() self._restore_state() self.settingsAboutToBePacked.connect(self._save_state) def _setup_gui(self): layout = QGridLayout() gui.widgetBox(self.controlArea, orientation=layout) self._setup_library_box() self._setup_input_box() self._setup_words_box() layout.addWidget(self.__library_box, 0, 0) layout.addWidget(self.__input_box, 1, 0) layout.addWidget(self.__words_box, 0, 1, 0, 1) def _setup_library_box(self): self.library_view = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Expanding), ) self.library_view.setItemDelegate(WordListItemDelegate(self)) self.library_view.setModel(self.library_model) self.library_view.selectionModel().selectionChanged.connect( self.__on_library_selection_changed) self.__library_box.layout().setSpacing(1) self.__library_box.layout().addWidget(self.library_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self) action.setToolTip("Add a new word list to the library") action.triggered.connect(self.__on_add_word_list) actions_widget.addAction(action) action = QAction("\N{MINUS SIGN}", self) action.setToolTip("Remove word list from library") action.triggered.connect(self.__on_remove_word_list) actions_widget.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.__on_update_word_list) actions_widget.addAction(action) gui.rubber(actions_widget.layout()) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Words from File", self) new_from_file.triggered.connect(self.__on_import_word_list) save_to_file = QAction("Save Words to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) save_to_file.triggered.connect(self.__on_save_word_list) menu = QMenu(actions_widget) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = actions_widget.addAction(action) button.setPopupMode(QToolButton.InstantPopup) self.__library_box.layout().addWidget(actions_widget) def __on_library_selection_changed(self, selected: QItemSelection, *_): index = [i.row() for i in selected.indexes()] if index: current = index[0] word_list: WordList = self.library_model[current] self.word_list_index = current self.selected_words = set() self.words_model.wrap(list(word_list.cached_words)) self._apply_update_rule() def __on_add_word_list(self): taken = [l.name for l in self.library_model] name = WordList.generate_word_list_name(taken) word_list = WordList(name, self.words_model[:]) self.library_model.append(word_list) self._set_selected_word_list(len(self.library_model) - 1) def __on_remove_word_list(self): index = self._get_selected_word_list_index() if index is not None: del self.library_model[index] self._set_selected_word_list(max(index - 1, 0)) self._apply_update_rule() def __on_update_word_list(self): self._set_word_list_modified(mod_type=self.LIBRARY) def __on_import_word_list(self): filename, _ = QFileDialog.getOpenFileName( self, "Open Word List", os.path.expanduser("~/"), "Text files (*.txt)\nAll files(*.*)") if filename: name = os.path.basename(filename) with open(filename, encoding="utf-8") as f: words = [line.strip() for line in f.readlines()] self.library_model.append(WordList(name, words, filename=filename)) self._set_selected_word_list(len(self.library_model) - 1) self._apply_update_rule() def __on_save_word_list(self): index = self._get_selected_word_list_index() if index is not None: word_list = self.library_model[index] filename = word_list.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, "Save Word List", filename, "Text files (*.txt)\nAll files(*.*)") if filename: head, tail = os.path.splitext(filename) if not tail: filename = head + ".txt" with open(filename, "w", encoding="utf-8") as f: for word in self.words_model: f.write(f"{word}\n") def _setup_input_box(self): gui.comboBox(self.__input_box, self, "words_var", label="Word variable:", orientation=Qt.Vertical, model=self.__input_words_model, callback=self._apply_update_rule) gui.radioButtons(self.__input_box, self, "update_rule_index", UpdateRules.ITEMS, label="Update: ", orientation=Qt.Vertical, callback=self.__on_update_rule_changed) self.__input_box.setEnabled(False) def __on_update_rule_changed(self): self._enable_words_actions() self._apply_update_rule() def _setup_words_box(self): self.words_view = ListView() self.words_view.drop_finished.connect(self.__on_words_data_changed) self.words_view.setModel(self.words_model) self.words_view.selectionModel().selectionChanged.connect( self.__on_words_selection_changed) self.words_model.dataChanged.connect(self.__on_words_data_changed) self.__words_box.layout().setSpacing(1) self.__words_box.layout().addWidget(self.words_view) actions_widget = ModelActionsWidget() actions_widget.layout().setSpacing(1) action = QAction("+", self.words_view, toolTip="Add a new word") action.triggered.connect(self.__on_add_word) actions_widget.addAction(action) self.__add_word_action = action action = QAction("\N{MINUS SIGN}", self, toolTip="Remove word") action.triggered.connect(self.__on_remove_word) actions_widget.addAction(action) self.__remove_word_action = action gui.rubber(actions_widget) action = QAction("Sort", self) action.setToolTip("Sort words alphabetically") action.triggered.connect(self.__on_apply_sorting) actions_widget.addAction(action) self.__words_box.layout().addWidget(actions_widget) def __on_words_data_changed(self): self._set_word_list_modified(mod_type=self.CACHED) self.commit() def __on_words_selection_changed(self): self.commit() def __on_add_word(self): row = self.words_model.rowCount() if not self.words_model.insertRow(self.words_model.rowCount()): return with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): self._set_selected_words([0]) index = self.words_model.index(row, 0) self.words_view.setCurrentIndex(index) self.words_model.setItemData(index, {Qt.EditRole: ""}) self.words_view.edit(index) def __on_remove_word(self): rows = self.words_view.selectionModel().selectedRows(0) if not rows: return indices = sorted([row.row() for row in rows], reverse=True) with disconnected(self.words_view.selectionModel().selectionChanged, self.__on_words_selection_changed): for index in indices: self.words_model.removeRow(index) if self.words_model: self._set_selected_words([max(0, indices[-1] - 1)]) self.__on_words_data_changed() def __on_apply_sorting(self): if not self.words_model: return words = self.words_model[:] mask = np.zeros(len(words), dtype=bool) selection = self._get_selected_words_indices() if selection: mask[selection] = True indices = np.argsort(words) self.words_model.wrap([words[i] for i in indices]) self._set_word_list_modified(mod_type=self.CACHED) if selection: self._set_selected_words(list(np.flatnonzero(mask[indices]))) else: self.commit() @Inputs.words def set_words(self, words: Optional[Table]): self.closeContext() self.__input_words = words self._check_input_words() self._init_controls() self.openContext(self.__input_words) self._apply_update_rule() def _check_input_words(self): self.Warning.no_string_vars.clear() if self.__input_words: metas = self.__input_words.domain.metas if not any(isinstance(m, StringVariable) for m in metas): self.Warning.no_string_vars() self.__input_words = None def _init_controls(self): words = self.__input_words domain = words.domain if words is not None else None self.__input_words_model.set_domain(domain) if len(self.__input_words_model) > 0: self.words_var = self.__input_words_model[0] self.__input_box.setEnabled(bool(self.__input_words_model)) self._enable_words_actions() def _enable_words_actions(self): if bool(self.__input_words_model) \ and self.update_rule_index != UpdateRules.LIBRARY: self.words_view.setEditTriggers(QListView.NoEditTriggers) self.__add_word_action.setEnabled(False) self.__remove_word_action.setEnabled(False) else: self.words_view.setEditTriggers(QListView.DoubleClicked | QListView.EditKeyPressed) self.__add_word_action.setEnabled(True) self.__remove_word_action.setEnabled(True) def _apply_update_rule(self): lib_index = self._get_selected_word_list_index() lib_words, in_words, update_rule = [], [], UpdateRules.LIBRARY if lib_index is not None: lib_words = self.library_model[lib_index].cached_words else: lib_words = self.words_model[:] if self.__input_words is not None: in_words = self.__input_words.get_column_view(self.words_var)[0] in_words = list(in_words) update_rule = self.update_rule_index UpdateRules.update(self.words_model, lib_words, in_words, update_rule) if lib_index is not None: cached = self.library_model[lib_index].cached_words modified = WordList.NotModified if cached == self.words_model[:] \ else WordList.Modified self.library_model[lib_index].update_rule_flag = modified self._set_word_list_modified(mod_type=self.NONE) self.library_view.repaint() # Apply selection. selection_changed invokes commit(). # If there is no selection, call commit explicitly. if any(w in self.selected_words for w in self.words_model): self.set_selected_words() self.words_view.repaint() else: self.commit() def commit(self): selection = self._get_selected_words_indices() self.selected_words = set(np.array(self.words_model)[selection]) words, selected_words = None, None if self.words_model: words_var = StringVariable("Words") words_var.attributes = {"type": "words"} domain = Domain([], metas=[words_var]) _words = Table.from_list(domain, [[w] for w in self.words_model]) _words.name = "Words" if selection: selected_words = _words[selection] words = create_annotated_table(_words, selection) self.Outputs.words.send(words) self.Outputs.selected_words.send(selected_words) def _set_word_list_modified(self, mod_type): index = self._get_selected_word_list_index() if index is not None: if mod_type == self.LIBRARY: self.library_model[index].words = self.words_model[:] self.library_model[index].cached_words = self.words_model[:] self.library_model[index].update_rule_flag \ = WordList.NotModified elif mod_type == self.CACHED: self.library_model[index].cached_words = self.words_model[:] elif mod_type == self.NONE: pass else: raise NotImplementedError self.library_model.emitDataChanged(index) self.library_view.repaint() def _set_selected_word_list(self, index: int): sel_model: QItemSelectionModel = self.library_view.selectionModel() sel_model.select(self.library_model.index(index, 0), QItemSelectionModel.ClearAndSelect) def _get_selected_word_list_index(self) -> Optional[int]: rows = self.library_view.selectionModel().selectedRows() return rows[0].row() if rows else None def _set_selected_words(self, indices: List[int]): selection = QItemSelection() sel_model: QItemSelectionModel = self.words_view.selectionModel() for i in indices: selection.append(QItemSelectionRange(self.words_model.index(i, 0))) sel_model.select(selection, QItemSelectionModel.ClearAndSelect) def _get_selected_words_indices(self) -> List[int]: rows = self.words_view.selectionModel().selectedRows() return [row.row() for row in rows] def set_selected_words(self): if self.selected_words: indices = [ i for i, w in enumerate(self.words_model) if w in self.selected_words ] self._set_selected_words(indices) def _restore_state(self): source = [WordList.from_dict(s) for s in self.word_list_library] self.library_model.wrap(source) # __on_library_selection_changed() (invoked by _set_selected_word_list) # clears self.selected_words selected_words = self.selected_words self._set_selected_word_list(self.word_list_index) if self.words is not None: self.words_model.wrap(list(self.words)) self._set_word_list_modified(mod_type=self.CACHED) if selected_words: self.selected_words = selected_words self.set_selected_words() elif len(self.word_list_library) > self.word_list_index and \ self.word_list_library[self.word_list_index] != self.words: self.commit() def _save_state(self): self.word_list_library = [s.as_dict() for s in self.library_model] self.words = self.words_model[:] def send_report(self): library = self.library_model[self.word_list_index].name \ if self.library_model else "/" settings = [("Library", library)] if self.__input_words: self.report_data("Input Words", self.__input_words) settings.append(("Word variable", self.words_var)) rule = UpdateRules.ITEMS[self.update_rule_index] settings.append(("Update", rule)) self.report_items("Settings", settings) self.report_paragraph("Words", ", ".join(self.words_model[:]))
class DiscreteVariableEditor(VariableEditor): """An editor widget for editing a discrete variable. Extends the :class:`VariableEditor` to enable editing of variables values. """ def setup_gui(self): layout = QVBoxLayout() self.setLayout(layout) self.main_form = QFormLayout() self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) layout.addLayout(self.main_form) self._setup_gui_name() self._setup_gui_values() self._setup_gui_labels() def _setup_gui_values(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.setSpacing(1) self.values_edit = QListView() self.values_edit.setEditTriggers(QTreeView.CurrentChanged) self.values_model = itemmodels.PyListModel(flags=Qt.ItemIsSelectable | \ Qt.ItemIsEnabled | Qt.ItemIsEditable) self.values_edit.setModel(self.values_model) self.values_edit.selectionModel().selectionChanged.connect( self.on_value_selection_changed) self.values_model.dataChanged.connect(self.on_values_changed) vlayout.addWidget(self.values_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.setSpacing(1) self.move_value_up = QAction(unicodedata.lookup("UPWARDS ARROW"), self, toolTip="Move up.", triggered=self.move_up, enabled=False, shortcut=QKeySequence(QKeySequence.New)) self.move_value_down = QAction(unicodedata.lookup("DOWNWARDS ARROW"), self, toolTip="Move down.", triggered=self.move_down, enabled=False, shortcut=QKeySequence( QKeySequence.Delete)) button_size = gui.toolButtonSizeHint() button_size = QSize(button_size, button_size) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.move_value_up) hlayout.addWidget(button) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.move_value_down) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) self.main_form.addRow("Values:", vlayout) def set_data(self, var): """Set the variable to edit """ VariableEditor.set_data(self, var) self.values_model[:] = list(var.values) if var is not None else [] def get_data(self): """Retrieve the modified variable """ name = str(self.name_edit.text()).strip() labels = self.labels_model.get_dict() values = map(str, self.values_model) if self.var is not None and not self.is_same(): var = type(self.var)(name, values=values) var.attributes.update(labels) self.var = var else: var = self.var return var def is_same(self): """Is the current model state the same as the input. """ values = list(map(str, self.values_model)) return (VariableEditor.is_same(self) and self.var is not None and self.var.values == values) def clear(self): """Clear the model state. """ VariableEditor.clear(self) self.values_model.clear() def move_rows(self, rows, offset): i = rows[0].row() self.values_model[i], self.values_model[i+offset] = \ self.values_model[i+offset], self.values_model[i] self.maybe_commit() def move_up(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, -1) def move_down(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, 1) @Slot() def on_values_changed(self): self.maybe_commit() @Slot() def on_value_selection_changed(self): rows = self.values_edit.selectionModel().selectedRows() if rows: i = rows[0].row() self.move_value_up.setEnabled(i) self.move_value_down.setEnabled(i != len(self.var.values) - 1) else: self.move_value_up.setEnabled(False) self.move_value_down.setEnabled(False)
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input data set." icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [ (ContinuousDescriptor, ContinuousFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor) ] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget( sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) ) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton( "New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New ) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [var.name for var in self.data.domain.variables + self.data.domain.metas] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect( lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3)) ) disc = menu.addAction("Categorical") disc.triggered.connect( lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", ("A", "B"), -1, False)) ) string = menu.addAction("Text") string.triggered.connect( lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), "")) ) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton( "Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete ) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView( minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) ) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged ) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and len(self.featuremodel) > 0: # Deleting the last item clears selection self.setCurrentIndex(len(self.featuremodel) - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) def check_attrs_values(self, attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) source_vars = self.data.domain.variables + self.data.domain.metas new_variables = construct_variables(desc, source_vars) attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas) ) try: data = self.data.transform(new_domain) except Exception as err: log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items( report.plural("Constructed feature{s}", len(items)), items)
class OWSpiralogram(widget.OWWidget): name = 'Spiralogram' description = "Visualize time series' periodicity in a spiral heatmap." icon = 'icons/Spiralogram.svg' priority = 120 class Inputs: time_series = Input("Time series", Table) class Outputs: time_series = Output("Time series", Timeseries) settingsHandler = settings.DomainContextHandler() ax1 = settings.ContextSetting('months of year') ax2 = settings.ContextSetting('years') agg_attr = settings.ContextSetting([]) agg_func = settings.ContextSetting(0) invert_date_order = settings.Setting(False) graph_name = 'chart' def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2_model = VariableListModel(parent=self) self.combo_ax1_model = VariableListModel(parent=self) for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] self.combo_ax2 = gui.comboBox( box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax2_model) self.combo_ax1 = gui.comboBox( box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax1_model) gui.checkBox(box, self, 'invert_date_order', 'Invert Y axis order', callback=self.replot) box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox( box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.SingleSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart) def attrlist_selectionChanged(self): self.agg_attr = [self.attrlist_model[i.row()] for i in self.attrlist.selectionModel().selectedIndexes()] self.replot() @Inputs.time_series def set_data(self, data): self.data = data = None if data is None else Timeseries.from_data_table(data) def init_combos(): for model in (self.combo_ax1_model, self.combo_ax2_model): model.clear() newmodel = [] if data is not None and data.time_variable is not None: for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] for var in data.domain.variables if data is not None else []: if (var.is_primitive() and (var is not data.time_variable or isinstance(var, TimeVariable) and data.time_delta is None)): newmodel.append(var) if var.is_discrete: for model in (self.combo_ax1_model, self.combo_ax2_model): model.append(var) self.attrlist_model.wrap(newmodel) init_combos() self.chart.clear() if data is None: self.commit() return self.closeContext() self.ax2 = next((self.combo_ax2.itemText(i) for i in range(self.combo_ax2.count())), '') self.ax1 = next((self.combo_ax1.itemText(i) for i in range(1, self.combo_ax1.count())), self.ax2) self.agg_attr = [data.domain.variables[0]] if len(data.domain.variables) else [] self.agg_func = 0 if getattr(data, 'time_variable', None) is not None: self.openContext(data.domain) if self.agg_attr: self.attrlist.blockSignals(True) self.attrlist.selectionModel().clear() for attr in self.agg_attr: try: row = self.attrlist_model.indexOf(attr) except ValueError: continue self.attrlist.selectionModel().select( self.attrlist_model.index(row), QItemSelectionModel.SelectCurrent) self.attrlist.blockSignals(False) self.replot() def replot(self): if not self.combo_ax1.count() or not self.agg_attr: return self.chart.clear() vars = self.agg_attr func = AGG_FUNCTIONS[self.agg_func] if any(var.is_discrete for var in vars) and func != Mode: self.combo_func.setCurrentIndex(AGG_FUNCTIONS.index(Mode)) func = Mode try: ax1 = Spiralogram.AxesCategories[_enum_str(self.ax1, True)] except KeyError: ax1 = self.data.domain[self.ax1] # TODO: Allow having only a sinle (i.e. radial) axis try: ax2 = Spiralogram.AxesCategories[_enum_str(self.ax2, True)] except KeyError: ax2 = self.data.domain[self.ax2] self.chart.setSeries(self.data, vars, ax1, ax2, func) def on_selection(self, indices): self.indices = self.chart.selection_indices(indices) self.commit() def commit(self): self.Outputs.time_series.send(self.data[self.indices] if self.data else None)
class OWEditDomain(widget.OWWidget): name = "Edit Domain" description = "Rename features and their values." icon = "icons/EditDomain.svg" priority = 3125 class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) settingsHandler = settings.DomainContextHandler() domain_change_hints = settings.ContextSetting({}) selected_index = settings.ContextSetting({}) autocommit = settings.Setting(True) def __init__(self): super().__init__() self.data = None self.input_vars = () self._invalidated = False box = gui.vBox(self.controlArea, "Domain Features") self.domain_model = itemmodels.VariableListModel() self.domain_view = QListView(selectionMode=QListView.SingleSelection, uniformItemSizes=True) self.domain_view.setModel(self.domain_model) self.domain_view.selectionModel().selectionChanged.connect( self._on_selection_changed) box.layout().addWidget(self.domain_view) box = gui.hBox(self.controlArea) gui.button(box, self, "Reset Selected", callback=self.reset_selected) gui.button(box, self, "Reset All", callback=self.reset_all) gui.auto_commit(self.controlArea, self, "autocommit", "Apply") box = gui.vBox(self.mainArea, "Edit") self.editor_stack = QStackedWidget() self.editor_stack.addWidget(DiscreteVariableEditor()) self.editor_stack.addWidget(ContinuousVariableEditor()) self.editor_stack.addWidget(VariableEditor()) box.layout().addWidget(self.editor_stack) self.Error.add_message("duplicate_var_name", "A variable name is duplicated.") @Inputs.data @check_sql_input def set_data(self, data): """Set input dataset.""" self.closeContext() self.clear() self.data = data if self.data is not None: self._initialize() self.openContext(self.data) self._restore() self.unconditional_commit() def clear(self): """Clear the widget state.""" self.data = None self.domain_model[:] = [] self.input_vars = [] self.domain_change_hints = {} self.selected_index = -1 def reset_selected(self): """Reset the currently selected variable to its original state.""" ind = self.selected_var_index() if ind >= 0: var = self.input_vars[ind] desc = variable_description(var, skip_attributes=True) if desc in self.domain_change_hints: del self.domain_change_hints[desc] self.domain_model[ind] = var self.editor_stack.currentWidget().set_data(var) self._invalidate() def reset_all(self): """Reset all variables to their original state.""" self.domain_change_hints = {} if self.data is not None: # To invalidate stored hints self.domain_model[:] = self.input_vars itemmodels.select_row(self.domain_view, self.selected_index) self._invalidate() def selected_var_index(self): """Return the selected row in 'Domain Features' view.""" rows = self.domain_view.selectedIndexes() assert len(rows) <= 1 return rows[0].row() if rows else -1 def _initialize(self): domain = self.data.domain self.input_vars = domain.variables + domain.metas self.domain_model[:] = list(self.input_vars) def _restore(self): # Restore the variable states from saved settings. def transform(var): vdesc = variable_description(var, skip_attributes=True) if vdesc in self.domain_change_hints: return variable_from_description( self.domain_change_hints[vdesc], compute_value=Orange.preprocess.transformation.Identity( var), ) else: return var self.domain_model[:] = map(transform, self.input_vars) # Restore the variable selection if possible index = self.selected_index if index >= len(self.input_vars): index = 0 if len(self.input_vars) else -1 if index >= 0: itemmodels.select_row(self.domain_view, index) def _on_selection_changed(self): self.selected_index = self.selected_var_index() self.open_editor(self.selected_index) def open_editor(self, index): self.clear_editor() if index < 0: return var = self.domain_model[index] editor_index = 2 if var.is_discrete: editor_index = 0 elif var.is_continuous: editor_index = 1 editor = self.editor_stack.widget(editor_index) self.editor_stack.setCurrentWidget(editor) editor.set_data(var) editor.variable_changed.connect(self._on_variable_changed) def clear_editor(self): current = self.editor_stack.currentWidget() try: current.variable_changed.disconnect(self._on_variable_changed) except Exception: pass current.set_data(None) def _on_variable_changed(self): """User edited the current variable in editor.""" assert 0 <= self.selected_index <= len(self.domain_model) editor = self.editor_stack.currentWidget() # Replace the variable in the 'Domain Features' view/model old_var = self.input_vars[self.selected_index] new_var = editor.get_data().copy( compute_value=Orange.preprocess.transformation.Identity(old_var)) self.domain_model[self.selected_index] = new_var # Store the transformation hint. old_var_desc = variable_description(old_var, skip_attributes=True) self.domain_change_hints[old_var_desc] = variable_description(new_var) self._invalidate() def _invalidate(self): self.commit() def commit(self): """Send the changed data to output.""" new_data = None var_names = [vn.name for vn in self.domain_model] self.Error.duplicate_var_name.clear() if self.data is not None: if len(var_names) == len(set(var_names)): input_domain = self.data.domain n_attrs = len(input_domain.attributes) n_class_vars = len(input_domain.class_vars) all_new_vars = list(self.domain_model) attrs = all_new_vars[:n_attrs] class_vars = all_new_vars[n_attrs:n_attrs + n_class_vars] new_metas = all_new_vars[n_attrs + n_class_vars:] new_domain = Orange.data.Domain(attrs, class_vars, new_metas) new_data = self.data.transform(new_domain) else: self.Error.duplicate_var_name() self.Outputs.data.send(new_data) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(660, 550)) def send_report(self): if self.data is not None: self.report_raw( "", EditDomainReport( old_domain=chain(self.data.domain.variables, self.data.domain.metas), new_domain=self.domain_model, ).to_html(), ) else: self.report_data(None)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg("Model based imputer does not work for sparse data") DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default()] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() # copy METHODS (some are modified by the widget) self.methods = copy.deepcopy(OWImpute.METHODS) main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.methods): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.methods): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.button.setFixedWidth(180) box.layout().insertStretch(0) self.data = None self.learner = None self.modified = False self.default_method = self.methods[self.default_method_index] self.executor = qconcurrent.ThreadExecutor(self) self.__task = None @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.methods[self.default_method_index] self.methods[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): method = self.variable_methods.get( index.row(), self.methods[self.DEFAULT]) self.varmodel.setData(index, method, Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.methods[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button(self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.update_varview() self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.methods[self.DEFAULT]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [ (i, var, self.variable_methods.get(i, self.default_method)) for i, var in enumerate(self.varmodel) ] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit( impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'". format(var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items(( ("Default method", default), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = [self.get_method_for_column(i.row()) for i in indexes] def method_key(method): """ Decompose method into its type and parameters. """ # The return value should be hashable and __eq__ comparable if isinstance(method, AsDefault): return AsDefault, (method.method,) elif isinstance(method, impute.Model): return impute.Model, (method.learner,) elif isinstance(method, impute.Default): return impute.Default, (method.default,) else: return type(method), None methods = set(method_key(m) for m in methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for i, m in enumerate(self.methods): if method_type == type(m): self.variable_button_group.button(i).setChecked(True) if method_type is impute.Default: (fixed_value,) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None for method, button in zip(self.methods, self.variable_button_group.buttons()): enabled = all(method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: value_stack_enabled = False current_value_widget = None self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index.row(), None) elif method_index == OWImpute.AS_INPUT: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: method = impute.Default(default=value) self.variable_methods[index.row()] = method else: method = self.methods[method_index] for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(self.AS_INPUT).setChecked(True) self.set_method_for_current_selection(self.AS_INPUT) def reset_variable_methods(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 inputs = [("Data", Orange.data.Table, "set_data"), ("Learner", Learner, "set_learner")] outputs = [("Data", Orange.data.Table)] DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default()] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(False) default_value = settings.Setting(0.) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.METHODS): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView( selectionMode=QListView.ExtendedSelection ) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed ) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.METHODS): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected ) self.value_combo.currentIndexChanged.connect(self._on_value_changed) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, value=self.default_value ) self.value_stack = value_stack = QStackedLayout() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addLayout(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection ) method_layout.addStretch(2) reset_button = QPushButton( "Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit( self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.modified = False self.default_method = self.METHODS[self.default_method_index] self.update_varview() @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.METHODS[self.default_method_index] self.METHODS[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): self.varmodel.setData(index, self.variable_methods.get(index.row(), self.METHODS[self.DEFAULT]), Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.METHODS[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button(self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.METHODS[self.DEFAULT]) def _invalidate(self): self.modified = True self.commit() def commit(self): data = self.data if self.data is not None: if not len(self.data): self.send("Data", self.data) self.modified = False return drop_mask = np.zeros(len(self.data), bool) attributes = [] class_vars = [] self.warning() with self.progressBar(len(self.varmodel)) as progress: for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, self.default_method) if not method.supports_variable(var): self.warning("Default method can not handle '{}'". format(var.name)) elif isinstance(method, impute.DropInstances): drop_mask |= method(self.data, var) else: var = method(self.data, var) if isinstance(var, Orange.data.Variable): var = [var] if i < len(self.data.domain.attributes): attributes.extend(var) else: class_vars.extend(var) progress.advance() domain = Orange.data.Domain(attributes, class_vars, self.data.domain.metas) data = self.data.from_table(domain, self.data[~drop_mask]) self.send("Data", data) self.modified = False def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items(( ("Default method", default), ("Specific imputers", ", ".join(specific)) )) else: self.report_items((("Method", default),)) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = set(self.get_method_for_column(i.row()).name for i in indexes) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) if len(methods) == 1: method = methods.pop() for i, m in enumerate(self.METHODS): if method == m.name: self.variable_button_group.button(i).setChecked(True) elif self.variable_button_group.checkedButton() is not None: self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) for method, button in zip(self.METHODS, self.variable_button_group.buttons()): enabled = all(method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_double) self._on_value_changed() elif len(selected_vars) == 1: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_combo) self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) self._on_value_changed() else: self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(False) def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index, None) else: method = self.METHODS[method_index].copy() for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): self.variable_button_group.button(self.AS_INPUT).setChecked(True) self._on_value_changed() def _on_value_changed(self): widget = self.value_stack.currentWidget() if widget is self.value_combo: value = self.value_combo.currentText() else: value = self.value_double.value() self.default_value = value self.METHODS[self.AS_INPUT].default = value index = self.variable_button_group.checkedId() if index == self.AS_INPUT: self.set_method_for_current_selection(index) def reset_variable_methods(self): indexes = map(self.varmodel.index, range(len(self.varmodel))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)
class OWGenotypeDistances(widget.OWWidget): name = "Expression Profile Distances" description = ("Compute distances between expression profiles of " "different experimental factors.") icon = "../widgets/icons/GenotypeDistances.svg" priority = 1050 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Distances", Orange.misc.DistMatrix), ("Sorted Data", Orange.data.Table)] settingsHandler = SetContextHandler() separate_keys = settings.ContextSetting({}) relevant_keys = settings.ContextSetting({}) distance_measure = settings.Setting(0) auto_commit = settings.Setting(False) DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman) ] def __init__(self, parent=None): super().__init__(self, parent) self.data = None self.partitions = [] self.matrix = None self.split_groups = [] self._disable_updates = False ######## # GUI ######## box = gui.widgetBox(self.controlArea, "Input") self.info_box = gui.widgetLabel(box, "No data on input\n") box = gui.widgetBox(self.controlArea, "Separate By", addSpace=True) self.separate_view = QListView(selectionMode=QListView.MultiSelection) box.layout().addWidget(self.separate_view) box = gui.widgetBox(self.controlArea, "Sort By", addSpace=True) self.relevant_view = QListView(selectionMode=QListView.MultiSelection) box.layout().addWidget(self.relevant_view) self.distance_view = gui.comboBox( self.controlArea, self, "distance_measure", box="Distance Measure", items=[name for name, _ in self.DISTANCE_FUNCTIONS]) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self.groups_box = gui.widgetBox(self.mainArea, "Groups") self.groups_scroll_area = QScrollArea() self.groups_box.layout().addWidget(self.groups_scroll_area) def sizeHint(self): return QSize(800, 600) def clear(self): self.data = None self.partitions = [] self.split_groups = [] self.matrix = None def get_suitable_keys(self, data): """Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def set_data(self, data=None): """Set the input data table. """ self.closeContext() self.clear() self.error(0) self.warning(0) if data and not self.get_suitable_keys(data): self.error(0, "Data has no suitable column labels.") data = None self.data = data if data: self.info_box.setText("{0} genes\n{1} experiments".format( len(data), len(data.domain))) self.update_control() self.split_data() else: self.separate_view.setModel(itemmodels.PyListModel([])) self.relevant_view.setModel(itemmodels.PyListModel([])) self.groups_scroll_area.setWidget(QWidget()) self.info_box.setText("No data on input.\n") self.commit() def update_control(self): """Update the control area of the widget. Populate the list views with keys from attribute labels. """ keys = self.get_suitable_keys(self.data) model = itemmodels.PyListModel(keys) self.separate_view.setModel(model) self.separate_view.selectionModel().selectionChanged.connect( self.on_separate_key_changed) model = itemmodels.PyListModel(keys) self.relevant_view.setModel(model) self.relevant_view.selectionModel().selectionChanged.connect( self.on_relevant_key_changed) self.openContext(keys) # Get the selected keys from the open context separate_keys = self.separate_keys relevant_keys = self.relevant_keys def select(model, selection_model, selected_items): all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] selection = QItemSelection() for ind in indices: index = model.index(ind) selection.select(index, index) selection_model.select(selection, QItemSelectionModel.Select) self._disable_updates = True try: select(self.relevant_view.model(), self.relevant_view.selectionModel(), relevant_keys) select(self.separate_view.model(), self.separate_view.selectionModel(), separate_keys) finally: self._disable_updates = False def on_separate_key_changed(self, *args): if not self._disable_updates: self.separate_keys = self.selected_separeate_by_keys() self.split_data() def on_relevant_key_changed(self, *args): if not self._disable_updates: self.relevant_keys = self.selected_relevant_keys() self.split_data() def selected_separeate_by_keys(self): """Return the currently selected separate by keys """ rows = self.separate_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.separate_view.model()[row] for row in rows] return keys def selected_relevant_keys(self): """Return the currently selected relevant keys """ rows = self.relevant_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.relevant_view.model()[row] for row in rows] return keys def split_data(self): """Split the data and update the Groups widget """ separate_keys = self.selected_separeate_by_keys() relevant_keys = self.selected_relevant_keys() self.warning(0) if not separate_keys: self.warning(0, "No separate by column selected.") partitions, uniquepos = separate_by(self.data, separate_keys, consider=relevant_keys) partitions = partitions.items() all_values = defaultdict(set) for a in [at.attributes for at in self.data.domain.attributes]: for k, v in a.items(): all_values[k].add(v) # sort groups pkeys = [key for key, _ in partitions] types = [ data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0])) ] partitions = sorted(partitions, key=lambda x: tuple(types[i](v) for i, v in enumerate(x[0]))) split_groups = [] # Collect relevant key value pairs for all columns relevant_items = None for keys, indices in partitions: if relevant_items == None: relevant_items = [defaultdict(set) for _ in indices] for i, ind in enumerate(indices): if ind is not None: attr = self.data.domain[ind] for key in relevant_keys: relevant_items[i][key].add(attr.attributes[key]) #those with different values between rows are not relevant for d in relevant_items: for k, s in list(d.items()): if len(s) > 1: del d[k] else: d[k] = s.pop() def get_attr(attr_index, i): if attr_index is None: attr = Orange.data.ContinuousVariable( next(missing_name_gen), compute_value=lambda x: None) attr.attributes.update(relevant_items[i]) return attr else: return self.data.domain[attr_index] for keys, indices in partitions: attrs = [ get_attr(attr_index, i) for i, attr_index in enumerate(indices) ] for attr in attrs: attr.attributes.update(zip(separate_keys, keys)) domain = Orange.data.Domain(attrs, [], self.data.domain.metas) split_groups.append((keys, domain)) self.set_groups(separate_keys, split_groups, relevant_keys, relevant_items, all_values, uniquepos) self.partitions = partitions self.split_groups = split_groups self.commit() def set_groups(self, keys, groups, relevant_keys, relevant_items, all_values, uniquepos): """Set the current data groups and update the Group widget """ layout = QVBoxLayout() header_widths = [] header_views = [] palette = self.palette() all_values = all_values.keys() def for_print(rd): attrs = [] for d in rd: attr = Orange.data.ContinuousVariable(next(inactive_name_gen)) attr.attributes.update(d) attrs.append(attr) return Orange.data.Domain(attrs, None) for separatev, domain in [(None, for_print(relevant_items))] + groups: label = None if separatev is not None: ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \ for key, val in zip(keys, separatev)]) label = QLabel(ann_vals) model = QStandardItemModel() for i, attr in enumerate(domain.attributes): item = QStandardItem() if separatev is not None: isunique = uniquepos[separatev][i] else: isunique = all(a[i] for a in uniquepos.values()) if str(attr.name).startswith( "!!missing " ): # TODO: Change this to not depend on name header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setFlags(Qt.NoItemFlags) item.setData(QColor(Qt.red), Qt.ForegroundRole) item.setData( palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) item.setData("Missing feature.", Qt.ToolTipRole) elif str(attr.name).startswith("!!inactive "): header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "No descriptor" item.setData(header_text, Qt.DisplayRole) item.setData( palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) else: header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setData(attr.name, Qt.ToolTipRole) if not isunique: item.setData(QColor(Qt.red), Qt.ForegroundRole) model.setHorizontalHeaderItem(i, item) attr_count = len(domain.attributes) view = MyHeaderView(Qt.Horizontal) view.setResizeMode(QHeaderView.Fixed) view.setModel(model) hint = view.sizeHint() view.setMaximumHeight(hint.height()) widths = [view.sectionSizeHint(i) for i in range(attr_count)] header_widths.append(widths) header_views.append(view) if label: layout.addWidget(label) layout.addWidget(view) layout.addSpacing(8) # Make all header sections the same width width_sum = 0 max_header_count = max([h.count() for h in header_views]) for i in range(max_header_count): max_width = max([w[i] for w in header_widths if i < len(w)] or [0]) for view in header_views: if i < view.count(): view.resizeSection(i, max_width) width_sum += max_width + 2 for h in header_views: h.setMinimumWidth(h.length() + 4) widget = QWidget() widget.setLayout(layout) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) layout.activate() max_width = max(h.length() for h in header_views) + 20 left, _, right, _ = self.getContentsMargins() widget.setMinimumWidth(width_sum) widget.setMinimumWidth(max_width + left + right) self.groups_scroll_area.setWidget(widget) def compute_distances(self, separate_keys, partitions, data): """Compute the distances between genotypes. """ if separate_keys and partitions: self.progressBarInit() # matrix = Orange.misc.DistMatrix(len(partitions)) matrix = numpy.zeros((len(partitions), len(partitions))) profiles = [linearize(data, indices) for _, indices in partitions] dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1] # from Orange.utils import progress_bar_milestones count = (len(profiles) * len(profiles) - 1) / 2 # milestones = progress_bar_milestones(count) iter_count = 0 for i in range(len(profiles)): for j in range(i + 1, len(profiles)): matrix[i, j] = dist_func(profiles[i], profiles[j]) matrix[j, i] = matrix[i, j] iter_count += 1 # if iter_count in milestones: self.progressBarSet(100.0 * iter_count / count) self.progressBarFinished() items = [[ "{0}={1}".format(key, value) for key, value in zip(separate_keys, values) ] for values, _ in partitions] items = [" | ".join(item) for item in items] # matrix.setattr("items", items) matrix = Orange.misc.DistMatrix(matrix) else: matrix = None self.matrix = matrix def commit(self): separate_keys = self.selected_separeate_by_keys() self.compute_distances(separate_keys, self.partitions, self.data) if self.split_groups: all_attrs = [] for group, domain in self.split_groups: attrs = [] group_name = " | ".join("{0}={1}".format(*item) for item in zip(separate_keys, group)) for attr in domain.attributes: newattr = clone_attr(attr) newattr.attributes[ "<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets. attrs.append(newattr) all_attrs.extend(attrs) domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars, self.data.domain.metas) data = Orange.data.Table(domain, self.data) else: data = None self.send("Sorted Data", data) self.send("Distances", self.matrix) def send_report(self): self.report_items( (("Separate By", ", ".join(self.selected_separeate_by_keys())), ("Sort By", ", ".join(self.selected_relevant_keys())), ("Distance Measure", self.DISTANCE_FUNCTIONS[self.distance_measure][0]))) layout = self.groups_scroll_area.widget().layout() html = "<table>" for i in range(layout.count()): item = layout.itemAt(i) if isinstance(item, QSpacerItem): html += "<tr><td></td></tr>" elif isinstance(item, QWidgetItem): hor = item.widget() if isinstance(hor, QLabel): label = hor.text() html += "<tr><td><b>%s</b></td></tr>" % label elif isinstance(hor, QHeaderView): model = hor.model() content = (model.horizontalHeaderItem(col) for col in range(model.columnCount())) content = (item.text().replace('\n', "<br/>") for item in content) html += "<tr>" + ''.join("<td>{}</td>".format(item) for item in content) + "</tr>" html += "</table>" self.report_raw("Groups", html)
class OWDistributions(widget.OWWidget): name = "Distributions" description = "Display value distributions of a data feature in a graph." icon = "icons/Distribution.svg" priority = 120 class Inputs: data = Input("Data", Orange.data.Table, doc="Set the input dataset") settingsHandler = settings.DomainContextHandler( match_values=settings.DomainContextHandler.MATCH_VALUES_ALL ) #: Selected variable index variable_idx = settings.ContextSetting(-1) #: Selected group variable groupvar_idx = settings.ContextSetting(0) relative_freq = settings.Setting(False) disc_cont = settings.Setting(False) smoothing_index = settings.Setting(5) show_prob = settings.ContextSetting(0) graph_name = "plot" ASH_HIST = 50 bins = [2, 3, 4, 5, 8, 10, 12, 15, 20, 30, 50] smoothing_facs = list(reversed([0.1, 0.2, 0.4, 0.6, 0.8, 1, 1.5, 2, 4, 6, 10])) def __init__(self): super().__init__() self.data = None self.distributions = None self.contingencies = None self.var = self.cvar = None varbox = gui.vBox(self.controlArea, "Variable") self.varmodel = itemmodels.VariableListModel() self.groupvarmodel = [] self.varview = QListView( selectionMode=QListView.SingleSelection, uniformItemSizes=True ) self.varview.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.varview.setModel(self.varmodel) self.varview.setSelectionModel( itemmodels.ListSingleSelectionModel(self.varmodel) ) self.varview.selectionModel().selectionChanged.connect( self._on_variable_idx_changed ) varbox.layout().addWidget(self.varview) box = gui.vBox(self.controlArea, "Precision") gui.separator(self.controlArea, 4, 4) box2 = gui.hBox(box) self.l_smoothing_l = gui.widgetLabel(box2, "Smooth") gui.hSlider( box2, self, "smoothing_index", minValue=0, maxValue=len(self.smoothing_facs) - 1, callback=self._on_set_smoothing, createLabel=False, ) self.l_smoothing_r = gui.widgetLabel(box2, "Precise") self.cb_disc_cont = gui.checkBox( gui.indentedBox(box, sep=4), self, "disc_cont", "Bin numeric variables", callback=self._on_groupvar_idx_changed, tooltip="Show numeric variables as categorical.", ) box = gui.vBox(self.controlArea, "Group by") self.icons = gui.attributeIconDict self.groupvarview = gui.comboBox( box, self, "groupvar_idx", callback=self._on_groupvar_idx_changed, valueType=str, contentsLength=12, ) box2 = gui.indentedBox(box, sep=4) self.cb_rel_freq = gui.checkBox( box2, self, "relative_freq", "Show relative frequencies", callback=self._on_relative_freq_changed, tooltip="Normalize probabilities so that probabilities " "for each group-by value sum to 1.", ) gui.separator(box2) self.cb_prob = gui.comboBox( box2, self, "show_prob", label="Show probabilities:", orientation=Qt.Horizontal, callback=self._on_relative_freq_changed, tooltip="Show probabilities for a chosen group-by value " "(at each point probabilities for all group-by values sum to 1).", ) self.plotview = pg.PlotWidget(background=None) self.plotview.setRenderHint(QPainter.Antialiasing) self.mainArea.layout().addWidget(self.plotview) w = QLabel() w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.mainArea.layout().addWidget(w, Qt.AlignCenter) self.ploti = pg.PlotItem() self.plot = self.ploti.vb self.ploti.hideButtons() self.plotview.setCentralItem(self.ploti) self.plot_prob = pg.ViewBox() self.ploti.hideAxis("right") self.ploti.scene().addItem(self.plot_prob) self.ploti.getAxis("right").linkToView(self.plot_prob) self.ploti.getAxis("right").setLabel("Probability") self.plot_prob.setZValue(10) self.plot_prob.setXLink(self.ploti) self.update_views() self.ploti.vb.sigResized.connect(self.update_views) self.plot_prob.setRange(yRange=[0, 1]) def disable_mouse(plot): plot.setMouseEnabled(False, False) plot.setMenuEnabled(False) disable_mouse(self.plot) disable_mouse(self.plot_prob) self.tooltip_items = [] self.plot.scene().installEventFilter(HelpEventDelegate(self.help_event, self)) pen = QPen(self.palette().color(QPalette.Text)) for axis in ("left", "bottom"): self.ploti.getAxis(axis).setPen(pen) self._legend = LegendItem() self._legend.setParentItem(self.plot) self._legend.hide() self._legend.anchor((1, 0), (1, 0)) def update_views(self): self.plot_prob.setGeometry(self.plot.sceneBoundingRect()) self.plot_prob.linkedViewChanged(self.plot, self.plot_prob.XAxis) @Inputs.data def set_data(self, data): self.closeContext() self.clear() self.warning() self.data = data self.distributions = None self.contingencies = None if self.data is not None: if not self.data: self.warning("Empty input data cannot be visualized") return domain = self.data.domain self.varmodel[:] = list(domain.variables) + [ meta for meta in domain.metas if meta.is_continuous or meta.is_discrete ] self.groupvarview.clear() self.groupvarmodel = ( ["(None)"] + [var for var in domain.variables if var.is_discrete] + [meta for meta in domain.metas if meta.is_discrete] ) self.groupvarview.addItem("(None)") for var in self.groupvarmodel[1:]: self.groupvarview.addItem(self.icons[var], var.name) if domain.has_discrete_class: self.groupvar_idx = self.groupvarmodel[1:].index(domain.class_var) + 1 self.openContext(domain) self.variable_idx = min(max(self.variable_idx, 0), len(self.varmodel) - 1) self.groupvar_idx = min( max(self.groupvar_idx, 0), len(self.groupvarmodel) - 1 ) itemmodels.select_row(self.varview, self.variable_idx) self._setup() def clear(self): self.plot.clear() self.plot_prob.clear() self.varmodel[:] = [] self.groupvarmodel = [] self.variable_idx = -1 self.groupvar_idx = 0 self._legend.clear() self._legend.hide() self.groupvarview.clear() self.cb_prob.clear() def _setup_smoothing(self): if not self.disc_cont and self.var and self.var.is_continuous: self.cb_disc_cont.setText("Bin numeric variables") self.l_smoothing_l.setText("Smooth") self.l_smoothing_r.setText("Precise") else: self.cb_disc_cont.setText( "Bin numeric variables into {} bins".format( self.bins[self.smoothing_index] ) ) self.l_smoothing_l.setText(" " + str(self.bins[0])) self.l_smoothing_r.setText(" " + str(self.bins[-1])) @property def smoothing_factor(self): return self.smoothing_facs[self.smoothing_index] def _setup(self): self.plot.clear() self.plot_prob.clear() self._legend.clear() self._legend.hide() varidx = self.variable_idx self.var = self.cvar = None if varidx >= 0: self.var = self.varmodel[varidx] if self.groupvar_idx > 0: self.cvar = self.groupvarmodel[self.groupvar_idx] self.cb_prob.clear() self.cb_prob.addItem("(None)") self.cb_prob.addItems(self.cvar.values) self.cb_prob.addItem("(All)") self.show_prob = min(max(self.show_prob, 0), len(self.cvar.values) + 1) data = self.data self._setup_smoothing() if self.var is None: return if self.disc_cont: domain = Orange.data.Domain( [self.var, self.cvar] if self.cvar else [self.var] ) data = Orange.data.Table(domain, data) disc = EqualWidth(n=self.bins[self.smoothing_index]) data = Discretize(method=disc, remove_const=False)(data) self.var = data.domain[0] self.set_left_axis_name() self.enable_disable_rel_freq() if self.cvar: self.contingencies = contingency.get_contingency(data, self.var, self.cvar) self.display_contingency() else: self.distributions = distribution.get_distribution(data, self.var) self.display_distribution() self.plot.autoRange() def help_event(self, ev): self.plot.mapSceneToView(ev.scenePos()) ctooltip = [] for vb, item in self.tooltip_items: mouse_over_curve = isinstance( item, pg.PlotCurveItem ) and item.mouseShape().contains(vb.mapSceneToView(ev.scenePos())) mouse_over_bar = isinstance( item, DistributionBarItem ) and item.boundingRect().contains(vb.mapSceneToView(ev.scenePos())) if mouse_over_curve or mouse_over_bar: ctooltip.append(item.tooltip) if ctooltip: QToolTip.showText( ev.screenPos(), "\n\n".join(ctooltip), widget=self.plotview ) return True return False def display_distribution(self): dist = self.distributions var = self.var if dist is None or not len(dist): return self.plot.clear() self.plot_prob.clear() self.ploti.hideAxis("right") self.tooltip_items = [] bottomaxis = self.ploti.getAxis("bottom") bottomaxis.setLabel(var.name) bottomaxis.resizeEvent() self.set_left_axis_name() if var and var.is_continuous: bottomaxis.setTicks(None) if not len(dist[0]): return edges, curve = ash_curve( dist, None, m=OWDistributions.ASH_HIST, smoothing_factor=self.smoothing_factor, ) edges = edges + (edges[1] - edges[0]) / 2 edges = edges[:-1] item = pg.PlotCurveItem() pen = QPen(QBrush(Qt.white), 3) pen.setCosmetic(True) item.setData( edges, curve, antialias=True, stepMode=False, fillLevel=0, brush=QBrush(Qt.gray), pen=pen, ) self.plot.addItem(item) item.tooltip = "Density" self.tooltip_items.append((self.plot, item)) else: bottomaxis.setTicks([list(enumerate(var.values))]) for i, w in enumerate(dist): geom = QRectF(i - 0.33, 0, 0.66, w) item = DistributionBarItem(geom, [1.0], [QColor(128, 128, 128)]) self.plot.addItem(item) item.tooltip = "Frequency for %s: %r" % (var.values[i], w) self.tooltip_items.append((self.plot, item)) def _on_relative_freq_changed(self): self.set_left_axis_name() if self.cvar and self.cvar.is_discrete: self.display_contingency() else: self.display_distribution() self.plot.autoRange() def display_contingency(self): """ Set the contingency to display. """ cont = self.contingencies var, cvar = self.var, self.cvar if cont is None or not len(cont): return self.plot.clear() self.plot_prob.clear() self._legend.clear() self.tooltip_items = [] if self.show_prob: self.ploti.showAxis("right") else: self.ploti.hideAxis("right") bottomaxis = self.ploti.getAxis("bottom") bottomaxis.setLabel(var.name) bottomaxis.resizeEvent() cvar_values = cvar.values colors = [QColor(*col) for col in cvar.colors] if var and var.is_continuous: bottomaxis.setTicks(None) weights, cols, cvar_values, curves = [], [], [], [] for i, dist in enumerate(cont): v, W = dist if len(v): weights.append(numpy.sum(W)) cols.append(colors[i]) cvar_values.append(cvar.values[i]) curves.append( ash_curve( dist, cont, m=OWDistributions.ASH_HIST, smoothing_factor=self.smoothing_factor, ) ) weights = numpy.array(weights) sumw = numpy.sum(weights) weights /= sumw colors = cols curves = [(X, Y * w) for (X, Y), w in zip(curves, weights)] curvesline = [] # from histograms to lines for X, Y in curves: X = X + (X[1] - X[0]) / 2 X = X[:-1] X = numpy.array(X) Y = numpy.array(Y) curvesline.append((X, Y)) for t in ["fill", "line"]: curve_data = list(zip(curvesline, colors, weights, cvar_values)) for (X, Y), color, w, cval in reversed(curve_data): item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3) pen.setCosmetic(True) color = QColor(color) color.setAlphaF(0.2) item.setData( X, Y / (w if self.relative_freq else 1), antialias=True, stepMode=False, fillLevel=0 if t == "fill" else None, brush=QBrush(color), pen=pen, ) self.plot.addItem(item) if t == "line": item.tooltip = "{}\n{}={}".format( "Normalized density " if self.relative_freq else "Density ", cvar.name, cval, ) self.tooltip_items.append((self.plot, item)) if self.show_prob: all_X = numpy.array( numpy.unique(numpy.hstack([X for X, _ in curvesline])) ) inter_X = numpy.array( numpy.linspace(all_X[0], all_X[-1], len(all_X) * 2) ) curvesinterp = [numpy.interp(inter_X, X, Y) for (X, Y) in curvesline] sumprob = numpy.sum(curvesinterp, axis=0) legal = sumprob > 0.05 * numpy.max(sumprob) i = len(curvesinterp) + 1 show_all = self.show_prob == i for Y, color, cval in reversed( list(zip(curvesinterp, colors, cvar_values)) ): i -= 1 if show_all or self.show_prob == i: item = pg.PlotCurveItem() pen = QPen(QBrush(color), 3, style=Qt.DotLine) pen.setCosmetic(True) prob = Y[legal] / sumprob[legal] item.setData( inter_X[legal], prob, antialias=True, stepMode=False, fillLevel=None, brush=None, pen=pen, ) self.plot_prob.addItem(item) item.tooltip = "Probability that \n" + cvar.name + "=" + cval self.tooltip_items.append((self.plot_prob, item)) elif var and var.is_discrete: bottomaxis.setTicks([list(enumerate(var.values))]) cont = numpy.array(cont) maxh = 0 # maximal column height maxrh = 0 # maximal relative column height scvar = cont.sum(axis=1) # a cvar with sum=0 with allways have distribution counts 0, # therefore we can divide it by anything scvar[scvar == 0] = 1 for i, (value, dist) in enumerate(zip(var.values, cont.T)): maxh = max(maxh, max(dist)) maxrh = max(maxrh, max(dist / scvar)) for i, (value, dist) in enumerate(zip(var.values, cont.T)): dsum = sum(dist) geom = QRectF( i - 0.333, 0, 0.666, maxrh if self.relative_freq else maxh ) if self.show_prob: prob = dist / dsum ci = 1.96 * numpy.sqrt(prob * (1 - prob) / dsum) else: ci = None item = DistributionBarItem( geom, dist / scvar / maxrh if self.relative_freq else dist / maxh, colors, ) self.plot.addItem(item) tooltip = "\n".join( "%s: %.*f" % (n, 3 if self.relative_freq else 1, v) for n, v in zip( cvar_values, dist / scvar if self.relative_freq else dist ) ) item.tooltip = "{} ({}={}):\n{}".format( "Normalized frequency " if self.relative_freq else "Frequency ", cvar.name, value, tooltip, ) self.tooltip_items.append((self.plot, item)) if self.show_prob: item.tooltip += "\n\nProbabilities:" for ic, a in enumerate(dist): if self.show_prob - 1 != ic and self.show_prob - 1 != len(dist): continue position = -0.333 + ((ic + 0.5) * 0.666 / len(dist)) if dsum < 1e-6: continue prob = a / dsum if not 1e-6 < prob < 1 - 1e-6: continue ci = 1.96 * sqrt(prob * (1 - prob) / dsum) item.tooltip += "\n%s: %.3f ± %.3f" % ( cvar_values[ic], prob, ci, ) mark = pg.ScatterPlotItem() errorbar = pg.ErrorBarItem() pen = QPen(QBrush(QColor(0)), 1) pen.setCosmetic(True) errorbar.setData( x=[i + position], y=[prob], bottom=min(numpy.array([ci]), prob), top=min(numpy.array([ci]), 1 - prob), beam=numpy.array([0.05]), brush=QColor(1), pen=pen, ) mark.setData( [i + position], [prob], antialias=True, symbol="o", fillLevel=None, pxMode=True, size=10, brush=QColor(colors[ic]), pen=pen, ) self.plot_prob.addItem(errorbar) self.plot_prob.addItem(mark) for color, name in zip(colors, cvar_values): self._legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, shape="s"), escape(name), ) self._legend.show() def set_left_axis_name(self): leftaxis = self.ploti.getAxis("left") set_label = leftaxis.setLabel if self.var and self.var.is_continuous: set_label( ["Density", "Relative density"][ self.cvar is not None and self.relative_freq ] ) else: set_label( ["Frequency", "Relative frequency"][ self.cvar is not None and self.relative_freq ] ) leftaxis.resizeEvent() def enable_disable_rel_freq(self): self.cb_prob.setDisabled(self.var is None or self.cvar is None) self.cb_rel_freq.setDisabled(self.var is None or self.cvar is None) def _on_variable_idx_changed(self): self.variable_idx = selected_index(self.varview) self._setup() def _on_groupvar_idx_changed(self): self._setup() def _on_set_smoothing(self): self._setup() def onDeleteWidget(self): self.plot.clear() super().onDeleteWidget() def get_widget_name_extension(self): if self.variable_idx >= 0: return self.varmodel[self.variable_idx] def send_report(self): self.plotview.scene().setSceneRect(self.plotview.sceneRect()) if self.variable_idx < 0: return self.report_plot() text = "Distribution of '{}'".format(self.varmodel[self.variable_idx]) if self.groupvar_idx: group_var = self.groupvarmodel[self.groupvar_idx] prob = self.cb_prob indiv_probs = 0 < prob.currentIndex() < prob.count() - 1 if not indiv_probs or self.relative_freq: text += " grouped by '{}'".format(group_var) if self.relative_freq: text += " (relative frequencies)" if indiv_probs: text += "; probabilites for '{}={}'".format( group_var, prob.currentText() ) self.report_caption(text)
class DiscreteVariableEditor(VariableEditor): """An editor widget for editing a discrete variable. Extends the :class:`VariableEditor` to enable editing of variables values. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) form = self.layout().itemAt(0) assert isinstance(form, QFormLayout) #: A list model of discrete variable's values. self.values_model = itemmodels.PyListModel( flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable ) vlayout = QVBoxLayout(spacing=1, margin=0) self.values_edit = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed ) self.values_edit.setItemDelegate(CategoriesEditDelegate(self)) self.values_edit.setModel(self.values_model) self.values_model.dataChanged.connect(self.on_values_changed) self.values_edit.selectionModel().selectionChanged.connect( self.on_value_selection_changed) self.values_model.layoutChanged.connect(self.on_value_selection_changed) self.values_model.rowsMoved.connect(self.on_value_selection_changed) vlayout.addWidget(self.values_edit) hlayout = QHBoxLayout(spacing=1, margin=0) self.categories_action_group = group = QActionGroup( self, objectName="action-group-categories", enabled=False ) self.move_value_up = QAction( "\N{UPWARDS ARROW}", group, toolTip="Move the selected item up.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketLeft), shortcutContext=Qt.WidgetShortcut, ) self.move_value_up.triggered.connect(self.move_up) self.move_value_down = QAction( "\N{DOWNWARDS ARROW}", group, toolTip="Move the selected item down.", shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_BracketRight), shortcutContext=Qt.WidgetShortcut, ) self.move_value_down.triggered.connect(self.move_down) self.add_new_item = QAction( "+", group, objectName="action-add-item", toolTip="Append a new item.", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut, ) self.remove_item = QAction( "\N{MINUS SIGN}", group, objectName="action-remove-item", toolTip="Delete the selected item.", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut, ) self.add_new_item.triggered.connect(self._add_category) self.remove_item.triggered.connect(self._remove_category) button1 = FixedSizeButton( self, defaultAction=self.move_value_up, accessibleName="Move up" ) button2 = FixedSizeButton( self, defaultAction=self.move_value_down, accessibleName="Move down" ) button3 = FixedSizeButton( self, defaultAction=self.add_new_item, accessibleName="Add" ) button4 = FixedSizeButton( self, defaultAction=self.remove_item, accessibleName="Remove" ) self.values_edit.addActions([self.move_value_up, self.move_value_down, self.add_new_item, self.remove_item]) hlayout.addWidget(button1) hlayout.addWidget(button2) hlayout.addSpacing(3) hlayout.addWidget(button3) hlayout.addWidget(button4) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.insertRow(1, "Values:", vlayout) QWidget.setTabOrder(self.name_edit, self.values_edit) QWidget.setTabOrder(self.values_edit, button1) QWidget.setTabOrder(button1, button2) QWidget.setTabOrder(button2, button3) QWidget.setTabOrder(button3, button4) def set_data(self, var, transform=()): # type: (Optional[Categorical], Sequence[Transform]) -> None """ Set the variable to edit. """ super().set_data(var, transform) tr = None # type: Optional[CategoriesMapping] for tr_ in transform: if isinstance(tr_, CategoriesMapping): tr = tr_ items = [] if tr is not None: ci_index = {c: i for i, c in enumerate(var.categories)} for ci, cj in tr.mapping: if ci is None and cj is not None: # level added item = { Qt.EditRole: cj, EditStateRole: ItemEditState.Added, SourcePosRole: None } elif ci is not None and cj is None: # ci level dropped item = { Qt.EditRole: ci, EditStateRole: ItemEditState.Dropped, SourcePosRole: ci_index[ci], SourceNameRole: ci } elif ci is not None and cj is not None: # rename or reorder item = { Qt.EditRole: cj, EditStateRole: ItemEditState.NoState, SourcePosRole: ci_index[ci], SourceNameRole: ci } else: assert False, "invalid mapping: {!r}".format(tr.mapping) items.append(item) elif var is not None: items = [ {Qt.EditRole: c, EditStateRole: ItemEditState.NoState, SourcePosRole: i, SourceNameRole: c} for i, c in enumerate(var.categories) ] else: items = [] with disconnected(self.values_model.dataChanged, self.on_values_changed): self.values_model.clear() self.values_model.insertRows(0, len(items)) for i, item in enumerate(items): self.values_model.setItemData( self.values_model.index(i, 0), item ) self.add_new_item.actionGroup().setEnabled(var is not None) def __categories_mapping(self): # type: () -> CategoriesMappingType model = self.values_model source = self.var.categories res = [] for i in range(model.rowCount()): midx = model.index(i, 0) category = midx.data(Qt.EditRole) source_pos = midx.data(SourcePosRole) # type: Optional[int] if source_pos is not None: source_name = source[source_pos] else: source_name = None state = midx.data(EditStateRole) if state == ItemEditState.Dropped: res.append((source_name, None)) elif state == ItemEditState.Added: res.append((None, category)) else: res.append((source_name, category)) return res def get_data(self): """Retrieve the modified variable """ var, tr = super().get_data() if var is None: return var, tr mapping = self.__categories_mapping() if any(_1 != _2 or _2 != _3 for (_1, _2), _3 in zip_longest(mapping, var.categories)): tr.append(CategoriesMapping(mapping)) return var, tr def clear(self): """Clear the model state. """ super().clear() self.values_model.clear() def move_rows(self, rows, offset): if not rows: return assert len(rows) == 1 i = rows[0].row() if offset > 0: offset += 1 self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset) self.variable_changed.emit() def move_up(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, -1) def move_down(self): rows = self.values_edit.selectionModel().selectedRows() self.move_rows(rows, 1) @Slot() def on_values_changed(self): self.variable_changed.emit() @Slot() def on_value_selection_changed(self): rows = self.values_edit.selectionModel().selectedRows() if rows: i = rows[0].row() self.move_value_up.setEnabled(i) self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1) else: self.move_value_up.setEnabled(False) self.move_value_down.setEnabled(False) def _remove_category(self): """ Remove the current selected category. If the item is an existing category present in the source variable it is marked as removed in the view. But if it was added in the set transformation it is removed entirely from the model and view. """ view = self.values_edit rows = view.selectionModel().selectedRows(0) if not rows: return assert len(rows) == 1 index = rows[0] # type: QModelIndex model = index.model() state = index.data(EditStateRole) pos = index.data(Qt.UserRole) if pos is not None and pos >= 0: # existing level -> only mark/toggle its dropped state model.setData( index, ItemEditState.Dropped if state != ItemEditState.Dropped else ItemEditState.NoState, EditStateRole) elif state == ItemEditState.Added: # new level -> remove it model.removeRow(index.row()) else: assert False, "invalid state '{}' for {}" \ .format(state, index.row()) def _add_category(self): """ Add a new category """ view = self.values_edit model = view.model() with disconnected(model.dataChanged, self.on_values_changed, Qt.UniqueConnection): row = model.rowCount() if not model.insertRow(model.rowCount()): return index = model.index(row, 0) model.setItemData( index, { Qt.EditRole: "", SourcePosRole: None, EditStateRole: ItemEditState.Added } ) view.setCurrentIndex(index) view.edit(index) self.on_values_changed()
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 keywords = ["file", "program"] class Inputs: data = Input("Data", Table, replaces=["in_data"], default=True, multiple=True) learner = Input("Learner", Learner, replaces=["in_learner"], default=True, multiple=True) classifier = Input("Classifier", Model, replaces=["in_classifier"], default=True, multiple=True) object = Input("Object", object, replaces=["in_object"], default=False, multiple=True) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") settingsHandler = PrepareSavingSettingsHandler() libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) scriptText = Setting(None, schema_only=True) splitterState = Setting(None) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, {}) for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + "<li>".join(map("in_{0}, in_{0}s".format, self.signal_names)) + "</ul></p><p>Output variables:<ul><li>" + "<li>".join(map("out_{0}".format, self.signal_names)) + "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) ) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged ) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.controlArea, self, 'Run', callback=self.commit) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.restoreScriptText() self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def storeSpecificSettings(self): self.saveScriptText() def restoreScriptText(self): if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) def saveScriptText(self): self.scriptText = self.text.toPlainText() def handle_input(self, obj, id, signal): id = id[0] dic = getattr(self, signal) if obj is None: if id in dic.keys(): del dic[id] else: dic[id] = obj @Inputs.data def set_data(self, data, id): self.handle_input(data, id, "data") @Inputs.learner def set_learner(self, data, id): self.handle_input(data, id, "learner") @Inputs.classifier def set_classifier(self, data, id): self.handle_input(data, id, "classifier") @Inputs.object def set_object(self, data, id): self.handle_input(data, id, "object") def handleNewSignals(self): self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)' ) if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)' ) if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value.values()) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): self.Error.clear() self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message(signal, "'{}' has to be an instance of '{}'.". format(signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var)
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 keywords = ["substitute", "missing"] class Inputs: data = Input("Data", Orange.data.Table) learner = Input("Learner", Learner) class Outputs: data = Output("Data", Orange.data.Table) class Error(OWWidget.Error): imputation_failed = Msg("Imputation failed for '{}'") model_based_imputer_sparse = Msg( "Model based imputer does not work for sparse data") settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(int(Method.Leave)) # type: int # Per-variable imputation state (synced in storeSpecificSettings) _variable_imputation_state = settings.ContextSetting( {}) # type: VariableState autocommit = settings.Setting(True) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() self.data = None # type: Optional[Orange.data.Table] self.learner = None # type: Optional[Learner] self.default_learner = SimpleTreeLearner() self.modified = False self.executor = qconcurrent.ThreadExecutor(self) self.__task = None main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for method, _ in list(METHODS.items())[1:-1]: imputer = self.create_imputer(method) button = QRadioButton(imputer.name) button.setChecked(method == self.default_method_index) button_group.addButton(button, method) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView(selectionMode=QListView.ExtendedSelection, uniformItemSizes=True) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for method in Method: imputer = self.create_imputer(method) button = QRadioButton(text=imputer.name) button_group.addButton(button, method) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, ) self.value_stack = value_stack = QStackedWidget() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addWidget(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection) method_layout.addStretch(2) reset_button = QPushButton("Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_state, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit(self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.button.setFixedWidth(180) box.layout().insertStretch(0) def create_imputer(self, method, *args): # type: (Method, ...) -> impute.BaseImputeMethod if method == Method.Model: if self.learner is not None: return impute.Model(self.learner) else: return impute.Model(self.default_learner) elif method == Method.AsAboveSoBelow: assert self.default_method_index != Method.AsAboveSoBelow default = self.create_imputer(Method(self.default_method_index)) m = AsDefault() m.method = default return m else: return METHODS[method](*args) @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: assert index != Method.AsAboveSoBelow self._default_method_index = index self.default_button_group.button(index).setChecked(True) # update variable view self.update_varview() self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @Inputs.data @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self._variable_imputation_state = {} # type: VariableState self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) # restore per variable imputation state self._restore_state(self._variable_imputation_state) self.update_varview() self.unconditional_commit() @Inputs.learner def set_learner(self, learner): self.learner = learner or self.default_learner imputer = self.create_imputer(Method.Model) button = self.default_button_group.button(Method.Model) button.setText(imputer.name) variable_button = self.variable_button_group.button(Method.Model) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = Method.Model self.update_varview() self.commit() def get_method_for_column(self, column_index): # type: (int) -> impute.BaseImputeMethod """ Return the imputation method for column by its index. """ assert 0 <= column_index < len(self.varmodel) idx = self.varmodel.index(column_index, 0) state = idx.data(StateRole) if state is None: state = (Method.AsAboveSoBelow, ()) return self.create_imputer(state[0], *state[1]) def _invalidate(self): self.modified = True if self.__task is not None: self.cancel() self.commit() def commit(self): self.cancel() self.warning() self.Error.imputation_failed.clear() self.Error.model_based_imputer_sparse.clear() if self.data is None or len(self.data) == 0 or len(self.varmodel) == 0: self.Outputs.data.send(self.data) self.modified = False return data = self.data impute_state = [(i, var, self.get_method_for_column(i)) for i, var in enumerate(self.varmodel)] # normalize to the effective method bypasing AsDefault impute_state = [(i, var, m.method if isinstance(m, AsDefault) else m) for i, var, m in impute_state] def impute_one(method, var, data): # type: (impute.BaseImputeMethod, Variable, Table) -> Any if isinstance(method, impute.Model) and data.is_sparse(): raise SparseNotSupported() elif isinstance(method, impute.DropInstances): return RowMask(method(data, var)) elif not method.supports_variable(var): raise VariableNotSupported(var) else: return method(data, var) futures = [] for _, var, method in impute_state: f = self.executor.submit(impute_one, copy.deepcopy(method), var, data) futures.append(f) w = qconcurrent.FutureSetWatcher(futures) w.doneAll.connect(self.__commit_finish) w.progressChanged.connect(self.__progress_changed) self.__task = Task(futures, w) self.progressBarInit(processEvents=False) self.setBlocking(True) @Slot() def __commit_finish(self): assert QThread.currentThread() is self.thread() assert self.__task is not None futures = self.__task.futures assert len(futures) == len(self.varmodel) assert self.data is not None self.__task = None self.setBlocking(False) self.progressBarFinished() data = self.data attributes = [] class_vars = [] drop_mask = np.zeros(len(self.data), bool) for i, (var, fut) in enumerate(zip(self.varmodel, futures)): assert fut.done() newvar = [] try: res = fut.result() except SparseNotSupported: self.Error.model_based_imputer_sparse() # ?? break except VariableNotSupported: self.warning("Default method can not handle '{}'".format( var.name)) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error for %s", var, exc_info=True) self.Error.imputation_failed(var.name) attributes = class_vars = None break else: if isinstance(res, RowMask): drop_mask |= res.mask newvar = var else: newvar = res if isinstance(newvar, Orange.data.Variable): newvar = [newvar] if i < len(data.domain.attributes): attributes.extend(newvar) else: class_vars.extend(newvar) if attributes is None: data = None else: domain = Orange.data.Domain(attributes, class_vars, data.domain.metas) try: data = self.data.from_table(domain, data[~drop_mask]) except Exception: # pylint: disable=broad-except log = logging.getLogger(__name__) log.info("Error", exc_info=True) self.Error.imputation_failed("Unknown") data = None self.Outputs.data.send(data) self.modified = False @Slot(int, int) def __progress_changed(self, n, d): assert QThread.currentThread() is self.thread() assert self.__task is not None self.progressBarSet(100. * n / d) def cancel(self): if self.__task is not None: task, self.__task = self.__task, None task.cancel() task.watcher.doneAll.disconnect(self.__commit_finish) task.watcher.progressChanged.disconnect(self.__progress_changed) concurrent.futures.wait(task.futures) task.watcher.flush() self.progressBarFinished() self.setBlocking(False) def onDeleteWidget(self): self.cancel() super().onDeleteWidget() def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.get_method_for_column(i) if not isinstance(method, AsDefault): specific.append("{} ({})".format(var.name, str(method))) default = self.create_imputer(Method.AsAboveSoBelow) if specific: self.report_items((("Default method", default.name), ("Specific imputers", ", ".join(specific)))) else: self.report_items((("Method", default.name), )) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() defmethod = (Method.AsAboveSoBelow, ()) methods = [index.data(StateRole) for index in indexes] methods = [m if m is not None else defmethod for m in methods] methods = set(methods) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) fixed_value = None value_stack_enabled = False current_value_widget = None if len(methods) == 1: method_type, parameters = methods.pop() for m in Method: if method_type == m: self.variable_button_group.button(m).setChecked(True) if method_type == Method.Default: (fixed_value, ) = parameters elif self.variable_button_group.checkedButton() is not None: # Uncheck the current button self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) assert self.variable_button_group.checkedButton() is None # Update variable methods GUI enabled state based on selection. for method in Method: # use a default constructed imputer to query support imputer = self.create_imputer(method) enabled = all( imputer.supports_variable(var) for var in selected_vars) button = self.variable_button_group.button(method) button.setEnabled(enabled) # Update the "Value" edit GUI. if not has_discrete: # no discrete variables -> allow mass edit for all (continuous vars) value_stack_enabled = True current_value_widget = self.value_double elif len(selected_vars) == 1: # single discrete var -> enable and fill the values combo value_stack_enabled = True current_value_widget = self.value_combo self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) else: # mixed type selection -> disable value_stack_enabled = False current_value_widget = None self.variable_button_group.button(Method.Default).setEnabled(False) self.value_stack.setEnabled(value_stack_enabled) if current_value_widget is not None: self.value_stack.setCurrentWidget(current_value_widget) if fixed_value is not None: # set current value if current_value_widget is self.value_combo: self.value_combo.setCurrentIndex(fixed_value) elif current_value_widget is self.value_double: self.value_double.setValue(fixed_value) else: assert False def set_method_for_current_selection(self, method_index): # type: (Method) -> None indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): # type: (List[QModelIndex], Method) -> None if method_index == Method.AsAboveSoBelow: for index in indexes: self.varmodel.setData(index, None, StateRole) elif method_index == Method.Default: current = self.value_stack.currentWidget() if current is self.value_combo: value = self.value_combo.currentIndex() else: value = self.value_double.value() for index in indexes: state = (int(Method.Default), (value, )) self.varmodel.setData(index, state, StateRole) else: state = (int(method_index), ()) for index in indexes: self.varmodel.setData(index, state, StateRole) self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), DisplayMethodRole) def _on_value_selected(self): # The fixed 'Value' in the widget has been changed by the user. self.variable_button_group.button(Method.Default).setChecked(True) self.set_method_for_current_selection(Method.Default) def reset_variable_state(self): indexes = list(map(self.varmodel.index, range(len(self.varmodel)))) self.set_method_for_indexes(indexes, Method.AsAboveSoBelow) self.variable_button_group.button( Method.AsAboveSoBelow).setChecked(True) def _store_state(self): # type: () -> VariableState """ Save the current variable imputation state """ state = {} # type: VariableState for i, var in enumerate(self.varmodel): index = self.varmodel.index(i) m = index.data(StateRole) if m is not None: state[var_key(var)] = m return state def _restore_state(self, state): # type: (VariableState) -> None """ Restore the variable imputation state from the saved state """ def check(state): # check if state is a proper State if isinstance(state, tuple) and len(state) == 2: m, p = state if isinstance(m, int) and isinstance(p, tuple) and \ 0 <= m < len(Method): return True return False for i, var in enumerate(self.varmodel): m = state.get(var_key(var), None) if check(m): self.varmodel.setData(self.varmodel.index(i), m, StateRole) def storeSpecificSettings(self): self._variable_imputation_state = self._store_state() super().storeSpecificSettings()
class OWSpiralogram(widget.OWWidget): name = 'Spiralogram' description = "Visualize time series' periodicity in a spiral heatmap." icon = 'icons/Spiralogram.svg' priority = 120 class Inputs: time_series = Input("Time series", Table) class Outputs: time_series = Output("Time series", Timeseries) settingsHandler = settings.DomainContextHandler() ax1 = settings.ContextSetting('months of year') ax2 = settings.ContextSetting('years') agg_attr = settings.ContextSetting([]) agg_func = settings.ContextSetting(0) invert_date_order = settings.Setting(False) graph_name = 'chart' class Error(widget.OWWidget.Error): no_time_variable = widget.Msg( 'Spiralogram requires time series with a time variable.') def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2_model = VariableListModel(parent=self) self.combo_ax1_model = VariableListModel(parent=self) for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] self.combo_ax2 = gui.comboBox(box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax2_model) self.combo_ax1 = gui.comboBox(box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax1_model) gui.checkBox(box, self, 'invert_date_order', 'Invert Y axis order', callback=self.replot) box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox(box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.SingleSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart) def attrlist_selectionChanged(self): self.agg_attr = [ self.attrlist_model[i.row()] for i in self.attrlist.selectionModel().selectedIndexes() ] self.replot() @Inputs.time_series def set_data(self, data): self.Error.clear() self.data = data = None if data is None else Timeseries.from_data_table( data) if data is None: self.commit() return if self.data.time_variable is None or not isinstance( self.data.time_variable, TimeVariable): self.Error.no_time_variable() self.commit() return def init_combos(): for model in (self.combo_ax1_model, self.combo_ax2_model): model.clear() newmodel = [] if data is not None and data.time_variable is not None: for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [ _enum_str(i) for i in Spiralogram.AxesCategories ] for var in data.domain.variables if data is not None else []: if (var.is_primitive() and (var is not data.time_variable or isinstance( var, TimeVariable) and data.time_delta is None)): newmodel.append(var) if var.is_discrete: for model in (self.combo_ax1_model, self.combo_ax2_model): model.append(var) self.attrlist_model.wrap(newmodel) init_combos() self.chart.clear() self.closeContext() self.ax2 = next((self.combo_ax2.itemText(i) for i in range(self.combo_ax2.count())), '') self.ax1 = next((self.combo_ax1.itemText(i) for i in range(1, self.combo_ax1.count())), self.ax2) self.agg_attr = [data.domain.variables[0]] if len( data.domain.variables) else [] self.agg_func = 0 if getattr(data, 'time_variable', None) is not None: self.openContext(data.domain) if self.agg_attr: self.attrlist.blockSignals(True) self.attrlist.selectionModel().clear() for attr in self.agg_attr: try: row = self.attrlist_model.indexOf(attr) except ValueError: continue self.attrlist.selectionModel().select( self.attrlist_model.index(row), QItemSelectionModel.SelectCurrent) self.attrlist.blockSignals(False) self.replot() def replot(self): if not self.combo_ax1.count() or not self.agg_attr: return self.chart.clear() vars = self.agg_attr func = AGG_FUNCTIONS[self.agg_func] if any(var.is_discrete for var in vars) and func != Mode: self.combo_func.setCurrentIndex(AGG_FUNCTIONS.index(Mode)) func = Mode try: ax1 = Spiralogram.AxesCategories[_enum_str(self.ax1, True)] except KeyError: ax1 = self.data.domain[self.ax1] # TODO: Allow having only a single (i.e. radial) axis try: ax2 = Spiralogram.AxesCategories[_enum_str(self.ax2, True)] except KeyError: ax2 = self.data.domain[self.ax2] self.chart.setSeries(self.data, vars, ax1, ax2, func) def on_selection(self, indices): self.indices = self.chart.selection_indices(indices) self.commit() def commit(self): self.Outputs.time_series.send( self.data[self.indices] if self.data else None)
class OWGenotypeDistances(widget.OWWidget): name = "Expression Profile Distances" description = ("Compute distances between expression profiles of " "different experimental factors.") icon = "../widgets/icons/GenotypeDistances.svg" priority = 1050 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Distances", Orange.misc.DistMatrix), ("Sorted Data", Orange.data.Table)] settingsHandler = SetContextHandler() separate_keys = settings.ContextSetting({}) relevant_keys = settings.ContextSetting({}) distance_measure = settings.Setting(0) auto_commit = settings.Setting(False) DISTANCE_FUNCTIONS = [ ("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman) ] def __init__(self, parent=None): super().__init__(self, parent) self.data = None self.partitions = [] self.matrix = None self.split_groups = [] self._disable_updates = False ######## # GUI ######## box = gui.widgetBox(self.controlArea, "Input") self.info_box = gui.widgetLabel(box, "No data on input\n") box = gui.widgetBox(self.controlArea, "Separate By", addSpace=True) self.separate_view = QListView( selectionMode=QListView.MultiSelection ) box.layout().addWidget(self.separate_view) box = gui.widgetBox(self.controlArea, "Sort By", addSpace=True) self.relevant_view = QListView( selectionMode=QListView.MultiSelection) box.layout().addWidget(self.relevant_view) self.distance_view = gui.comboBox( self.controlArea, self, "distance_measure", box="Distance Measure", items=[name for name, _ in self.DISTANCE_FUNCTIONS]) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self.groups_box = gui.widgetBox(self.mainArea, "Groups") self.groups_scroll_area = QScrollArea() self.groups_box.layout().addWidget(self.groups_scroll_area) def sizeHint(self): return QSize(800, 600) def clear(self): self.data = None self.partitions = [] self.split_groups = [] self.matrix = None def get_suitable_keys(self, data): """Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def set_data(self, data=None): """Set the input data table. """ self.closeContext() self.clear() self.error(0) self.warning(0) if data and not self.get_suitable_keys(data): self.error(0, "Data has no suitable column labels.") data = None self.data = data if data: self.info_box.setText("{0} genes\n{1} experiments" .format(len(data), len(data.domain))) self.update_control() self.split_data() else: self.separate_view.setModel(itemmodels.PyListModel([])) self.relevant_view.setModel(itemmodels.PyListModel([])) self.groups_scroll_area.setWidget(QWidget()) self.info_box.setText("No data on input.\n") self.commit() def update_control(self): """Update the control area of the widget. Populate the list views with keys from attribute labels. """ keys = self.get_suitable_keys(self.data) model = itemmodels.PyListModel(keys) self.separate_view.setModel(model) self.separate_view.selectionModel().selectionChanged.connect( self.on_separate_key_changed) model = itemmodels.PyListModel(keys) self.relevant_view.setModel(model) self.relevant_view.selectionModel().selectionChanged.connect( self.on_relevant_key_changed) self.openContext(keys) # Get the selected keys from the open context separate_keys = self.separate_keys relevant_keys = self.relevant_keys def select(model, selection_model, selected_items): all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] selection = QItemSelection() for ind in indices: index = model.index(ind) selection.select(index, index) selection_model.select(selection, QItemSelectionModel.Select) self._disable_updates = True try: select(self.relevant_view.model(), self.relevant_view.selectionModel(), relevant_keys) select(self.separate_view.model(), self.separate_view.selectionModel(), separate_keys) finally: self._disable_updates = False def on_separate_key_changed(self, *args): if not self._disable_updates: self.separate_keys = self.selected_separeate_by_keys() self.split_data() def on_relevant_key_changed(self, *args): if not self._disable_updates: self.relevant_keys = self.selected_relevant_keys() self.split_data() def selected_separeate_by_keys(self): """Return the currently selected separate by keys """ rows = self.separate_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.separate_view.model()[row] for row in rows] return keys def selected_relevant_keys(self): """Return the currently selected relevant keys """ rows = self.relevant_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.relevant_view.model()[row] for row in rows] return keys def split_data(self): """Split the data and update the Groups widget """ separate_keys = self.selected_separeate_by_keys() relevant_keys = self.selected_relevant_keys() self.warning(0) if not separate_keys: self.warning(0, "No separate by column selected.") partitions, uniquepos = separate_by( self.data, separate_keys, consider=relevant_keys) partitions = partitions.items() all_values = defaultdict(set) for a in [at.attributes for at in self.data.domain.attributes]: for k, v in a.items(): all_values[k].add(v) # sort groups pkeys = [key for key, _ in partitions] types = [data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0]))] partitions = sorted(partitions, key=lambda x: tuple(types[i](v) for i,v in enumerate(x[0]))) split_groups = [] # Collect relevant key value pairs for all columns relevant_items = None for keys, indices in partitions: if relevant_items == None: relevant_items = [defaultdict(set) for _ in indices] for i, ind in enumerate(indices): if ind is not None: attr = self.data.domain[ind] for key in relevant_keys: relevant_items[i][key].add(attr.attributes[key]) #those with different values between rows are not relevant for d in relevant_items: for k, s in list(d.items()): if len(s) > 1: del d[k] else: d[k] = s.pop() def get_attr(attr_index, i): if attr_index is None: attr = Orange.data.ContinuousVariable(next(missing_name_gen), compute_value=lambda x: None) attr.attributes.update(relevant_items[i]) return attr else: return self.data.domain[attr_index] for keys, indices in partitions: attrs = [get_attr(attr_index, i) for i, attr_index in enumerate(indices)] for attr in attrs: attr.attributes.update(zip(separate_keys, keys)) domain = Orange.data.Domain(attrs, [], self.data.domain.metas) split_groups.append((keys, domain)) self.set_groups(separate_keys, split_groups, relevant_keys, relevant_items, all_values, uniquepos) self.partitions = partitions self.split_groups = split_groups self.commit() def set_groups(self, keys, groups, relevant_keys, relevant_items, all_values, uniquepos): """Set the current data groups and update the Group widget """ layout = QVBoxLayout() header_widths = [] header_views = [] palette = self.palette() all_values = all_values.keys() def for_print(rd): attrs = [] for d in rd: attr = Orange.data.ContinuousVariable(next(inactive_name_gen)) attr.attributes.update(d) attrs.append(attr) return Orange.data.Domain(attrs, None) for separatev, domain in [(None, for_print(relevant_items))] + groups: label = None if separatev is not None: ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \ for key, val in zip(keys, separatev)]) label = QLabel(ann_vals) model = QStandardItemModel() for i, attr in enumerate(domain.attributes): item = QStandardItem() if separatev is not None: isunique = uniquepos[separatev][i] else: isunique = all(a[i] for a in uniquepos.values()) if str(attr.name).startswith("!!missing "): # TODO: Change this to not depend on name header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setFlags(Qt.NoItemFlags) item.setData(QColor(Qt.red), Qt.ForegroundRole) item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) item.setData("Missing feature.", Qt.ToolTipRole) elif str(attr.name).startswith("!!inactive "): header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "No descriptor" item.setData(header_text, Qt.DisplayRole) item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) else: header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setData(attr.name, Qt.ToolTipRole) if not isunique: item.setData(QColor(Qt.red), Qt.ForegroundRole) model.setHorizontalHeaderItem(i, item) attr_count = len(domain.attributes) view = MyHeaderView(Qt.Horizontal) view.setResizeMode(QHeaderView.Fixed) view.setModel(model) hint = view.sizeHint() view.setMaximumHeight(hint.height()) widths = [view.sectionSizeHint(i) for i in range(attr_count)] header_widths.append(widths) header_views.append(view) if label: layout.addWidget(label) layout.addWidget(view) layout.addSpacing(8) # Make all header sections the same width width_sum = 0 max_header_count = max([h.count() for h in header_views]) for i in range(max_header_count): max_width = max([w[i] for w in header_widths if i < len(w)] or [0]) for view in header_views: if i < view.count(): view.resizeSection(i, max_width) width_sum += max_width + 2 for h in header_views: h.setMinimumWidth(h.length() + 4) widget = QWidget() widget.setLayout(layout) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) layout.activate() max_width = max(h.length() for h in header_views) + 20 left, _, right, _ = self.getContentsMargins() widget.setMinimumWidth(width_sum) widget.setMinimumWidth(max_width + left + right) self.groups_scroll_area.setWidget(widget) def compute_distances(self, separate_keys, partitions, data): """Compute the distances between genotypes. """ if separate_keys and partitions: self.progressBarInit() # matrix = Orange.misc.DistMatrix(len(partitions)) matrix = numpy.zeros((len(partitions), len(partitions))) profiles = [linearize(data, indices) for _, indices in partitions] dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1] # from Orange.utils import progress_bar_milestones count = (len(profiles) * len(profiles) - 1) / 2 # milestones = progress_bar_milestones(count) iter_count = 0 for i in range(len(profiles)): for j in range(i + 1, len(profiles)): matrix[i, j] = dist_func(profiles[i], profiles[j]) matrix[j, i] = matrix[i, j] iter_count += 1 # if iter_count in milestones: self.progressBarSet(100.0 * iter_count / count) self.progressBarFinished() items = [["{0}={1}".format(key, value) for key, value in zip(separate_keys, values)] for values, _ in partitions] items = [" | ".join(item) for item in items] # matrix.setattr("items", items) matrix = Orange.misc.DistMatrix(matrix) else: matrix = None self.matrix = matrix def commit(self): separate_keys = self.selected_separeate_by_keys() self.compute_distances(separate_keys, self.partitions, self.data) if self.split_groups: all_attrs = [] for group, domain in self.split_groups: attrs = [] group_name = " | ".join("{0}={1}".format(*item) for item in zip(separate_keys, group)) for attr in domain.attributes: newattr = clone_attr(attr) newattr.attributes["<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets. attrs.append(newattr) all_attrs.extend(attrs) domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars, self.data.domain.metas) data = Orange.data.Table(domain, self.data) else: data = None self.send("Sorted Data", data) self.send("Distances", self.matrix) def send_report(self): self.report_items(( ("Separate By", ", ".join(self.selected_separeate_by_keys())), ("Sort By", ", ".join(self.selected_relevant_keys())), ("Distance Measure", self.DISTANCE_FUNCTIONS[self.distance_measure][0]) )) layout = self.groups_scroll_area.widget().layout() html = "<table>" for i in range(layout.count()): item = layout.itemAt(i) if isinstance(item, QSpacerItem): html += "<tr><td></td></tr>" elif isinstance(item, QWidgetItem): hor = item.widget() if isinstance(hor, QLabel): label = hor.text() html += "<tr><td><b>%s</b></td></tr>" % label elif isinstance(hor, QHeaderView): model = hor.model() content = (model.horizontalHeaderItem(col) for col in range(model.columnCount())) content = (item.text().replace('\n', "<br/>") for item in content) html += "<tr>" + ''.join("<td>{}</td>".format(item) for item in content) + "</tr>" html += "</table>" self.report_raw("Groups", html)
class OWQualityControl(widget.OWWidget): name = "Quality Control" description = "Experiment quality control" icon = "../widgets/icons/QualityControl.svg" priority = 5000 inputs = [("Experiment Data", Orange.data.Table, "set_data")] outputs = [] DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman)] settingsHandler = SetContextHandler() split_by_labels = settings.ContextSetting({}) sort_by_labels = settings.ContextSetting({}) selected_distance_index = settings.Setting(0) def __init__(self, parent=None): super().__init__(parent) ## Attributes self.data = None self.distances = None self.groups = None self.unique_pos = None self.base_group_index = 0 ## GUI box = gui.widgetBox(self.controlArea, "Info") self.info_box = gui.widgetLabel(box, "\n") ## Separate By box box = gui.widgetBox(self.controlArea, "Separate By") self.split_by_model = itemmodels.PyListModel(parent=self) self.split_by_view = QListView() self.split_by_view.setSelectionMode(QListView.ExtendedSelection) self.split_by_view.setModel(self.split_by_model) box.layout().addWidget(self.split_by_view) self.split_by_view.selectionModel().selectionChanged.connect( self.on_split_key_changed) ## Sort By box box = gui.widgetBox(self.controlArea, "Sort By") self.sort_by_model = itemmodels.PyListModel(parent=self) self.sort_by_view = QListView() self.sort_by_view.setSelectionMode(QListView.ExtendedSelection) self.sort_by_view.setModel(self.sort_by_model) box.layout().addWidget(self.sort_by_view) self.sort_by_view.selectionModel().selectionChanged.connect( self.on_sort_key_changed) ## Distance box box = gui.widgetBox(self.controlArea, "Distance Measure") gui.comboBox(box, self, "selected_distance_index", items=[name for name, _ in self.DISTANCE_FUNCTIONS], callback=self.on_distance_measure_changed) self.scene = QGraphicsScene() self.scene_view = QGraphicsView(self.scene) self.scene_view.setRenderHints(QPainter.Antialiasing) self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().addWidget(self.scene_view) self.scene_view.installEventFilter(self) self._disable_updates = False self._cached_distances = {} self._base_index_hints = {} self.main_widget = None self.resize(800, 600) def clear(self): """Clear the widget state.""" self.data = None self.distances = None self.groups = None self.unique_pos = None with disable_updates(self): self.split_by_model[:] = [] self.sort_by_model[:] = [] self.main_widget = None self.scene.clear() self.info_box.setText("\n") self._cached_distances = {} def set_data(self, data=None): """Set input experiment data.""" self.closeContext() self.clear() self.error(0) self.warning(0) if data is not None: keys = self.get_suitable_keys(data) if not keys: self.error(0, "Data has no suitable feature labels.") data = None self.data = data if data is not None: self.on_new_data() def update_label_candidates(self): """Update the label candidates selection GUI (Group/Sort By views). """ keys = self.get_suitable_keys(self.data) with disable_updates(self): self.split_by_model[:] = keys self.sort_by_model[:] = keys def get_suitable_keys(self, data): """ Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def selected_split_by_labels(self): """Return the current selected split labels. """ sel_m = self.split_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_sort_by_labels(self): """Return the current selected sort labels """ sel_m = self.sort_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_distance(self): """Return the selected distance function. """ return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1] def selected_base_group_index(self): """Return the selected base group index """ return self.base_group_index def selected_base_indices(self, base_group_index=None): indices = [] for g, ind in self.groups: if base_group_index is None: label = group_label(self.selected_split_by_labels(), g) ind = [i for i in ind if i is not None] i = self._base_index_hints.get(label, ind[0] if ind else None) else: i = ind[base_group_index] indices.append(i) return indices def on_new_data(self): """We have new data and need to recompute all. """ self.closeContext() self.update_label_candidates() self.info_box.setText( "%s genes \n%s experiments" % (len(self.data), len(self.data.domain.attributes)) ) self.base_group_index = 0 keys = self.get_suitable_keys(self.data) self.openContext(keys) ## Restore saved context settings (split/sort selection) split_by_labels = self.split_by_labels sort_by_labels = self.sort_by_labels def select(model, selection_model, selected_items): """Select items in a Qt item model view """ all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] for ind in indices: selection_model.select(model.index(ind), QItemSelectionModel.Select) with disable_updates(self): select(self.split_by_view.model(), self.split_by_view.selectionModel(), split_by_labels) select(self.sort_by_view.model(), self.sort_by_view.selectionModel(), sort_by_labels) with widget_disable(self): self.split_and_update() def on_split_key_changed(self, *args): """Split key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.split_by_labels = self.selected_split_by_labels() self.split_and_update() def on_sort_key_changed(self, *args): """Sort key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.sort_by_labels = self.selected_sort_by_labels() self.split_and_update() def on_distance_measure_changed(self): """Distance measure has changed """ if self.data is not None: with widget_disable(self): self.update_distances() self.replot_experiments() def on_view_resize(self, size): """The view with the quality plot has changed """ if self.main_widget: current = self.main_widget.size() self.main_widget.resize(size.width() - 6, current.height()) self.scene.setSceneRect(self.scene.itemsBoundingRect()) def on_rug_item_clicked(self, item): """An ``item`` in the quality plot has been clicked. """ update = False sort_by_labels = self.selected_sort_by_labels() if sort_by_labels and item.in_group: ## The item is part of the group if item.group_index != self.base_group_index: self.base_group_index = item.group_index update = True else: if sort_by_labels: # If the user clicked on an background item it # invalidates the sorted labels selection with disable_updates(self): self.sort_by_view.selectionModel().clear() update = True index = item.index group = item.group label = group_label(self.selected_split_by_labels(), group) if self._base_index_hints.get(label, 0) != index: self._base_index_hints[label] = index update = True if update: with widget_disable(self): self.split_and_update() def eventFilter(self, obj, event): if obj is self.scene_view and event.type() == QEvent.Resize: self.on_view_resize(event.size()) return super().eventFilter(obj, event) def split_and_update(self): """ Split the data based on the selected sort/split labels and update the quality plot. """ split_labels = self.selected_split_by_labels() sort_labels = self.selected_sort_by_labels() self.warning(0) if not split_labels: self.warning(0, "No separate by label selected.") self.groups, self.unique_pos = \ exp.separate_by(self.data, split_labels, consider=sort_labels, add_empty=True) self.groups = sorted(self.groups.items(), key=lambda t: list(map(float_if_posible, t[0]))) self.unique_pos = sorted(self.unique_pos.items(), key=lambda t: list(map(float_if_posible, t[0]))) if self.groups: if sort_labels: group_base = self.selected_base_group_index() base_indices = self.selected_base_indices(group_base) else: base_indices = self.selected_base_indices() self.update_distances(base_indices) self.replot_experiments() def get_cached_distances(self, measure): if measure not in self._cached_distances: attrs = self.data.domain.attributes mat = numpy.zeros((len(attrs), len(attrs))) self._cached_distances[measure] = \ (mat, set(zip(range(len(attrs)), range(len(attrs))))) return self._cached_distances[measure] def get_cached_distance(self, measure, i, j): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) if key in computed: return matrix[i, j] else: return None def get_distance(self, measure, i, j): d = self.get_cached_distance(measure, i, j) if d is None: vec_i = take_columns(self.data, [i]) vec_j = take_columns(self.data, [j]) d = measure(vec_i, vec_j) mat, computed = self.get_cached_distances(measure) mat[i, j] = d key = key = (i, j) if i < j else (j, i) computed.add(key) return d def store_distance(self, measure, i, j, dist): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) matrix[j, i] = matrix[i, j] = dist computed.add(key) def update_distances(self, base_indices=()): """Recompute the experiment distances. """ distance = self.selected_distance() if base_indices == (): base_group_index = self.selected_base_group_index() base_indices = [ind[base_group_index] \ for _, ind in self.groups] assert(len(base_indices) == len(self.groups)) base_distances = [] attributes = self.data.domain.attributes pb = gui.ProgressBar(self, len(self.groups) * len(attributes)) for (group, indices), base_index in zip(self.groups, base_indices): # Base column of the group if base_index is not None: base_vec = take_columns(self.data, [base_index]) distances = [] # Compute the distances between base column # and all the rest data columns. for i in range(len(attributes)): if i == base_index: distances.append(0.0) elif self.get_cached_distance(distance, i, base_index) is not None: distances.append(self.get_cached_distance(distance, i, base_index)) else: vec_i = take_columns(self.data, [i]) dist = distance(base_vec, vec_i) self.store_distance(distance, i, base_index, dist) distances.append(dist) pb.advance() base_distances.append(distances) else: base_distances.append(None) pb.finish() self.distances = base_distances def replot_experiments(self): """Replot the whole quality plot. """ self.scene.clear() labels = [] max_dist = numpy.nanmax(list(filter(None, self.distances))) rug_widgets = [] group_pen = QPen(Qt.black) group_pen.setWidth(2) group_pen.setCapStyle(Qt.RoundCap) background_pen = QPen(QColor(0, 0, 250, 150)) background_pen.setWidth(1) background_pen.setCapStyle(Qt.RoundCap) main_widget = QGraphicsWidget() layout = QGraphicsGridLayout() attributes = self.data.domain.attributes if self.data is not None: for (group, indices), dist_vec in zip(self.groups, self.distances): indices_set = set(indices) rug_items = [] if dist_vec is not None: for i, attr in enumerate(attributes): # Is this a within group distance or background in_group = i in indices_set if in_group: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 1.0, self.on_rug_item_clicked) rug_item.setPen(group_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group_index = indices.index(i) rug_item.setZValue(rug_item.zValue() + 1) else: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 0.85, self.on_rug_item_clicked) rug_item.setPen(background_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group = group rug_item.index = i rug_item.in_group = in_group rug_items.append(rug_item) rug_widget = RugGraphicsWidget(parent=main_widget) rug_widget.set_rug(rug_items) rug_widgets.append(rug_widget) label = group_label(self.selected_split_by_labels(), group) label_item = QGraphicsSimpleTextItem(label, main_widget) label_item = GraphicsSimpleTextLayoutItem(label_item, parent=layout) label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) labels.append(label_item) for i, (label, rug_w) in enumerate(zip(labels, rug_widgets)): layout.addItem(label, i, 0, Qt.AlignVCenter) layout.addItem(rug_w, i, 1) layout.setRowMaximumHeight(i, 30) main_widget.setLayout(layout) self.scene.addItem(main_widget) self.main_widget = main_widget self.rug_widgets = rug_widgets self.labels = labels self.on_view_resize(self.scene_view.size())
class OWPythonScript(widget.OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 inputs = [("in_data", Orange.data.Table, "setExampleTable", widget.Default), # ("in_distance", Orange.misc.SymMatrix, "setDistanceMatrix", # widget.Default), ("in_learner", Learner, "setLearner", widget.Default), ("in_classifier", Model, "setClassifier", widget.Default), ("in_object", object, "setObject")] outputs = [("out_data", Orange.data.Table, ), # ("out_distance", Orange.misc.SymMatrix, ), ("out_learner", Learner, ), ("out_classifier", Model, widget.Dynamic), ("out_object", object, widget.Dynamic)] libraryListSource = \ Setting([Script("Hello world", "print('Hello world')\n")]) currentScriptIndex = Setting(0) splitterState = Setting(None) auto_execute = Setting(False) def __init__(self): super().__init__() self.in_data = None self.in_distance = None self.in_learner = None self.in_classifier = None self.in_object = None self.auto_execute = False for s in self.libraryListSource: s.flags = 0 self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + \ "<li>".join(t.name for t in self.inputs) + \ "</ul></p><p>Output variables:<ul><li>" + \ "<li>".join(t.name for t in self.outputs) + \ "</ul></p>" ) self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) ) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged ) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.auto_commit( self.controlArea, self, "auto_execute", "Execute", auto_label="Auto Execute") self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) select_row(self.libraryView, self.currentScriptIndex) self.splitCanvas.setSizes([2, 1]) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) self.splitCanvas.splitterMoved[int, int].connect(self.onSpliterMoved) self.controlArea.layout().addStretch(1) self.resize(800, 600) def setExampleTable(self, et): self.in_data = et def setDistanceMatrix(self, dm): self.in_distance = dm def setLearner(self, learner): self.in_learner = learner def setClassifier(self, classifier): self.in_classifier = classifier def setObject(self, obj): self.in_object = obj def handleNewSignals(self): self.unconditional_commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *args): self.libraryList.append(Script("New script", "", 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *args): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)' ) if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *args): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *args): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if type(script) != Script: script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *args): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def onSpliterMoved(self, pos, ind): self.splitterState = bytes(self.splitCanvas.saveState()) def updateSelecetdScriptState(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] self.libraryList[index] = Script(script.name, self.text.toPlainText(), 0) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)' ) if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = dict([(i.name, getattr(self, i.name, None)) for i in self.inputs]) d.update(dict([(o.name, None) for o in self.outputs])) return d def commit(self): self._script = str(self.text.toPlainText()) lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for out in self.outputs: signal = out.name self.send(signal, self.console.locals.get(signal, None))
class OWPythonScript(OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." icon = "icons/PythonScript.svg" priority = 3150 keywords = ["file", "program"] class Inputs: data = Input("Data", Table, replaces=["in_data"], default=True, multiple=True) learner = Input("Learner", Learner, replaces=["in_learner"], default=True, multiple=True) classifier = Input("Classifier", Model, replaces=["in_classifier"], default=True, multiple=True) object = Input("Object", object, replaces=["in_object"], default=False, multiple=True) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") settings_version = 2 scriptLibrary: 'List[_ScriptData]' = Setting([{ "name": "Hello world", "script": "print('Hello world')\n", "filename": None }]) currentScriptIndex = Setting(0) scriptText: Optional[str] = Setting(None, schema_only=True) splitterState: Optional[bytes] = Setting(None) # Widgets in the same schema share namespace through a dictionary whose # key is self.signalManager. ales-erjavec expressed concern (and I fully # agree!) about widget being aware of the outside world. I am leaving this # anyway. If this causes any problems in the future, replace this with # shared_namespaces = {} and thus use a common namespace for all instances # of # PythonScript even if they are in different schemata. shared_namespaces = weakref.WeakKeyDictionary() class Error(OWWidget.Error): pass def __init__(self): super().__init__() self.libraryListSource = [] for name in self.signal_names: setattr(self, name, {}) self._cachedDocuments = {} self.infoBox = gui.vBox(self.controlArea, 'Info') gui.label( self.infoBox, self, "<p>Execute python script.</p><p>Input variables:<ul><li> " + "<li>".join(map("in_{0}, in_{0}s".format, self.signal_names)) + "</ul></p><p>Output variables:<ul><li>" + "<li>".join(map("out_{0}".format, self.signal_names)) + "</ul></p>") self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.controlArea, self, 'Run', callback=self.commit) run = QAction("Run script", self, triggered=self.commit, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)) self.addAction(run) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) self.defaultFont = defaultFont = \ "Monaco" if sys.platform == "darwin" else "Courier" self.textBox = gui.vBox(self, 'Python Script') self.splitCanvas.addWidget(self.textBox) self.text = PythonScriptEditor(self) self.textBox.layout().addWidget(self.text) self.textBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self, 'Console') self.splitCanvas.addWidget(self.consoleBox) self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.console.setTabStopWidth(4) self.splitCanvas.setSizes([2, 1]) self.setAcceptDrops(True) self.controlArea.layout().addStretch(10) self._restoreState() self.settingsAboutToBePacked.connect(self._saveState) def sizeHint(self) -> QSize: return super().sizeHint().expandedTo(QSize(800, 600)) def _restoreState(self): self.libraryListSource = [ Script.fromdict(s) for s in self.scriptLibrary ] self.libraryList.wrap(self.libraryListSource) select_row(self.libraryView, self.currentScriptIndex) if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) def _saveState(self): self.scriptLibrary = [s.asdict() for s in self.libraryListSource] self.scriptText = self.text.toPlainText() self.splitterState = bytes(self.splitCanvas.saveState()) def handle_input(self, obj, sig_id, signal): dic = getattr(self, signal) if obj is None: if sig_id in dic.keys(): del dic[sig_id] else: dic[sig_id] = obj @Inputs.data def set_data(self, data, sig_id): self.handle_input(data, sig_id, "data") @Inputs.learner def set_learner(self, data, sig_id): self.handle_input(data, sig_id, "learner") @Inputs.classifier def set_classifier(self, data, sig_id): self.handle_input(data, sig_id, "classifier") @Inputs.object def set_object(self, data, sig_id): self.handle_input(data, sig_id, "object") def handleNewSignals(self): self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *_): self.libraryList.append( Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *_): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) # TODO: use `tokenize.detect_encoding` with open(filename, encoding="utf-8") as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *_): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *_): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, _deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if not isinstance(script, Script): script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PythonSyntaxHighlighter(doc) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *_): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = self.shared_namespaces.setdefault(self.signalManager, {}).copy() for name in self.signal_names: value = getattr(self, name) all_values = list(value.values()) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def update_namespace(self, namespace): not_saved = reduce(set.union, ({f"in_{name}s", f"in_{name}", f"out_{name}"} for name in self.signal_names)) self.shared_namespaces.setdefault(self.signalManager, {}).update({ name: value for name, value in namespace.items() if name not in not_saved }) def commit(self): self.Error.clear() lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) self.update_namespace(self.console.locals) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var) def dragEnterEvent(self, event): # pylint: disable=no-self-use urls = event.mimeData().urls() if urls: # try reading the file as text c = read_file_content(urls[0].toLocalFile(), limit=1000) if c is not None: event.acceptProposedAction() def dropEvent(self, event): """Handle file drops""" urls = event.mimeData().urls() if urls: self.text.pasteFile(urls[0]) @classmethod def migrate_settings(cls, settings, version): if version is not None and version < 2: scripts = settings.pop("libraryListSource") # type: List[Script] library = [ dict(name=s.name, script=s.script, filename=s.filename) for s in scripts ] # type: List[_ScriptData] settings["scriptLibrary"] = library
class OWImpute(OWWidget): name = "Impute" description = "Impute missing values in the data table." icon = "icons/Impute.svg" priority = 2130 inputs = [("Data", Orange.data.Table, "set_data"), ("Learner", Learner, "set_learner")] outputs = [("Data", Orange.data.Table)] DEFAULT_LEARNER = SimpleTreeLearner() METHODS = [ AsDefault(), impute.DoNotImpute(), impute.Average(), impute.AsValue(), impute.Model(DEFAULT_LEARNER), impute.Random(), impute.DropInstances(), impute.Default() ] DEFAULT, DO_NOT_IMPUTE, MODEL_BASED_IMPUTER, AS_INPUT = 0, 1, 4, 7 settingsHandler = settings.DomainContextHandler() _default_method_index = settings.Setting(DO_NOT_IMPUTE) variable_methods = settings.ContextSetting({}) autocommit = settings.Setting(False) default_value = settings.Setting(0.) want_main_area = False resizing_enabled = False def __init__(self): super().__init__() main_layout = QVBoxLayout() main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) box = QGroupBox(title=self.tr("Default Method"), flat=False) box_layout = QVBoxLayout(box) main_layout.addWidget(box) button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) for i, method in enumerate(self.METHODS): if not method.columns_only: button = QRadioButton(method.name) button.setChecked(i == self.default_method_index) button_group.addButton(button, i) box_layout.addWidget(button) self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), flat=False) main_layout.addWidget(box) horizontal_layout = QHBoxLayout(box) main_layout.addWidget(box) self.varview = QListView(selectionMode=QListView.ExtendedSelection) self.varview.setItemDelegate(DisplayFormatDelegate()) self.varmodel = itemmodels.VariableListModel() self.varview.setModel(self.varmodel) self.varview.selectionModel().selectionChanged.connect( self._on_var_selection_changed) self.selection = self.varview.selectionModel() horizontal_layout.addWidget(self.varview) method_layout = QVBoxLayout() horizontal_layout.addLayout(method_layout) button_group = QButtonGroup() for i, method in enumerate(self.METHODS): button = QRadioButton(text=method.name) button_group.addButton(button, i) method_layout.addWidget(button) self.value_combo = QComboBox( minimumContentsLength=8, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength, activated=self._on_value_selected) self.value_combo.currentIndexChanged.connect(self._on_value_changed) self.value_double = QDoubleSpinBox( editingFinished=self._on_value_selected, minimum=-1000., maximum=1000., singleStep=.1, decimals=3, value=self.default_value) self.value_stack = value_stack = QStackedLayout() value_stack.addWidget(self.value_combo) value_stack.addWidget(self.value_double) method_layout.addLayout(value_stack) button_group.buttonClicked[int].connect( self.set_method_for_current_selection) method_layout.addStretch(2) reset_button = QPushButton("Restore All to Default", checked=False, checkable=False, clicked=self.reset_variable_methods, default=False, autoDefault=False) method_layout.addWidget(reset_button) self.variable_button_group = button_group box = gui.auto_commit(self.controlArea, self, "autocommit", "Apply", orientation=Qt.Horizontal, checkbox_label="Apply automatically") box.layout().insertSpacing(0, 80) box.layout().insertWidget(0, self.report_button) self.data = None self.modified = False self.default_method = self.METHODS[self.default_method_index] self.update_varview() @property def default_method_index(self): return self._default_method_index @default_method_index.setter def default_method_index(self, index): if self._default_method_index != index: self._default_method_index = index self.default_button_group.button(index).setChecked(True) self.default_method = self.METHODS[self.default_method_index] self.METHODS[self.DEFAULT].method = self.default_method # update variable view for index in map(self.varmodel.index, range(len(self.varmodel))): self.varmodel.setData( index, self.variable_methods.get(index.row(), self.METHODS[self.DEFAULT]), Qt.UserRole) self._invalidate() def set_default_method(self, index): """Set the current selected default imputation method. """ self.default_method_index = index @check_sql_input def set_data(self, data): self.closeContext() self.varmodel[:] = [] self.variable_methods = {} self.modified = False self.data = data if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) self.update_varview() self.unconditional_commit() def set_learner(self, learner): self.learner = learner or self.DEFAULT_LEARNER imputer = self.METHODS[self.MODEL_BASED_IMPUTER] imputer.learner = self.learner button = self.default_button_group.button(self.MODEL_BASED_IMPUTER) button.setText(imputer.name) variable_button = self.variable_button_group.button( self.MODEL_BASED_IMPUTER) variable_button.setText(imputer.name) if learner is not None: self.default_method_index = self.MODEL_BASED_IMPUTER self.commit() def get_method_for_column(self, column_index): """Returns the imputation method for column by its index. """ if not isinstance(column_index, int): column_index = column_index.row() return self.variable_methods.get(column_index, self.METHODS[self.DEFAULT]) def _invalidate(self): self.modified = True self.commit() def commit(self): data = self.data if self.data is not None: drop_mask = np.zeros(len(self.data), bool) attributes = [] class_vars = [] self.warning() with self.progressBar(len(self.varmodel)) as progress: for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, self.default_method) if not method.supports_variable(var): self.warning( "Default method can not handle '{}'".format( var.name)) elif isinstance(method, impute.DropInstances): drop_mask |= method(self.data, var) else: var = method(self.data, var) if isinstance(var, Orange.data.Variable): var = [var] if i < len(self.data.domain.attributes): attributes.extend(var) else: class_vars.extend(var) progress.advance() domain = Orange.data.Domain(attributes, class_vars, self.data.domain.metas) data = self.data.from_table(domain, self.data[~drop_mask]) self.send("Data", data) self.modified = False def send_report(self): specific = [] for i, var in enumerate(self.varmodel): method = self.variable_methods.get(i, None) if method is not None: specific.append("{} ({})".format(var.name, str(method))) default = self.default_method.name if specific: self.report_items((("Default method", default), ("Specific imputers", ", ".join(specific)))) else: self.report_items((("Method", default), )) def _on_var_selection_changed(self): indexes = self.selection.selectedIndexes() methods = set( self.get_method_for_column(i.row()).name for i in indexes) selected_vars = [self.varmodel[index.row()] for index in indexes] has_discrete = any(var.is_discrete for var in selected_vars) if len(methods) == 1: method = methods.pop() for i, m in enumerate(self.METHODS): if method == m.name: self.variable_button_group.button(i).setChecked(True) elif self.variable_button_group.checkedButton() is not None: self.variable_button_group.setExclusive(False) self.variable_button_group.checkedButton().setChecked(False) self.variable_button_group.setExclusive(True) for method, button in zip(self.METHODS, self.variable_button_group.buttons()): enabled = all( method.supports_variable(var) for var in selected_vars) button.setEnabled(enabled) if not has_discrete: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_double) self._on_value_changed() elif len(selected_vars) == 1: self.value_stack.setEnabled(True) self.value_stack.setCurrentWidget(self.value_combo) self.value_combo.clear() self.value_combo.addItems(selected_vars[0].values) self._on_value_changed() else: self.variable_button_group.button(self.AS_INPUT).setEnabled(False) self.value_stack.setEnabled(False) def set_method_for_current_selection(self, method_index): indexes = self.selection.selectedIndexes() self.set_method_for_indexes(indexes, method_index) def set_method_for_indexes(self, indexes, method_index): if method_index == self.DEFAULT: for index in indexes: self.variable_methods.pop(index, None) else: method = self.METHODS[method_index].copy() for index in indexes: self.variable_methods[index.row()] = method self.update_varview(indexes) self._invalidate() def update_varview(self, indexes=None): if indexes is None: indexes = map(self.varmodel.index, range(len(self.varmodel))) for index in indexes: self.varmodel.setData(index, self.get_method_for_column(index.row()), Qt.UserRole) def _on_value_selected(self): self.variable_button_group.button(self.AS_INPUT).setChecked(True) self._on_value_changed() def _on_value_changed(self): widget = self.value_stack.currentWidget() if widget is self.value_combo: value = self.value_combo.currentText() else: value = self.value_double.value() self.default_value = value self.METHODS[self.AS_INPUT].default = value index = self.variable_button_group.checkedId() if index == self.AS_INPUT: self.set_method_for_current_selection(index) def reset_variable_methods(self): indexes = map(self.varmodel.index, range(len(self.varmodel))) self.set_method_for_indexes(indexes, self.DEFAULT) self.variable_button_group.button(self.DEFAULT).setChecked(True)