Total Complexity | 172 |
Total Lines | 897 |
Duplicated Lines | 0 % |
Complex classes like Orange.widgets.unsupervised.OWMDS often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | import sys |
||
94 | class OWMDS(widget.OWWidget): |
||
95 | name = "MDS" |
||
96 | description = "Two-dimensional data projection by multidimensional " \ |
||
97 | "scaling constructed from a distance matrix." |
||
98 | icon = "icons/MDS.svg" |
||
99 | inputs = [("Data", Orange.data.Table, "set_data"), |
||
100 | ("Distances", Orange.misc.DistMatrix, "set_disimilarity")] |
||
101 | outputs = [("Data", Orange.data.Table, widget.Default), |
||
102 | ("Selected Data", Orange.data.Table)] |
||
103 | |||
104 | #: Initialization type |
||
105 | PCA, Random = 0, 1 |
||
106 | |||
107 | #: Refresh rate |
||
108 | RefreshRate = [ |
||
109 | ("Every iteration", 1), |
||
110 | ("Every 5 steps", 5), |
||
111 | ("Every 10 steps", 10), |
||
112 | ("Every 25 steps", 25), |
||
113 | ("Every 50 steps", 50), |
||
114 | ("None", -1) |
||
115 | ] |
||
116 | |||
117 | #: Runtime state |
||
118 | Running, Finished, Waiting = 1, 2, 3 |
||
119 | |||
120 | settingsHandler = settings.DomainContextHandler() |
||
121 | |||
122 | max_iter = settings.Setting(300) |
||
123 | initialization = settings.Setting(PCA) |
||
124 | refresh_rate = settings.Setting(3) |
||
125 | |||
126 | # output embedding role. |
||
127 | NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3 |
||
128 | |||
129 | output_embedding_role = settings.Setting(2) |
||
130 | autocommit = settings.Setting(True) |
||
131 | |||
132 | color_value = settings.ContextSetting("") |
||
133 | shape_value = settings.ContextSetting("") |
||
134 | size_value = settings.ContextSetting("") |
||
135 | label_value = settings.ContextSetting("") |
||
136 | |||
137 | symbol_size = settings.Setting(8) |
||
138 | symbol_opacity = settings.Setting(230) |
||
139 | connected_pairs = settings.Setting(5) |
||
140 | spread_equal_points = settings.Setting(False) |
||
141 | |||
142 | legend_anchor = settings.Setting(((1, 0), (1, 0))) |
||
143 | |||
144 | want_graph = True |
||
145 | |||
146 | def __init__(self, parent=None): |
||
147 | super().__init__(parent) |
||
148 | self.matrix = None |
||
149 | self.data = None |
||
150 | self.matrix_data = None |
||
151 | self.signal_data = None |
||
152 | |||
153 | self._pen_data = None |
||
154 | self._shape_data = None |
||
155 | self._size_data = None |
||
156 | self._label_data = None |
||
157 | self._similar_pairs = None |
||
158 | self._scatter_item = None |
||
159 | self._legend_item = None |
||
160 | self._selection_mask = None |
||
161 | self._invalidated = False |
||
162 | self._effective_matrix = None |
||
163 | |||
164 | self.__update_loop = None |
||
165 | self.__state = OWMDS.Waiting |
||
166 | self.__in_next_step = False |
||
167 | self.__draw_similar_pairs = False |
||
168 | |||
169 | box = gui.widgetBox(self.controlArea, "MDS Optimization") |
||
170 | form = QtGui.QFormLayout( |
||
171 | labelAlignment=Qt.AlignLeft, |
||
172 | formAlignment=Qt.AlignLeft, |
||
173 | fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, |
||
174 | verticalSpacing=10 |
||
175 | ) |
||
176 | |||
177 | form.addRow("Max iterations:", |
||
178 | gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) |
||
179 | |||
180 | form.addRow("Initialization", |
||
181 | gui.comboBox(box, self, "initialization", |
||
182 | items=["PCA (Torgerson)", "Random"], |
||
183 | callback=self.__invalidate_embedding)) |
||
184 | |||
185 | box.layout().addLayout(form) |
||
186 | form.addRow("Refresh", |
||
187 | gui.comboBox( |
||
188 | box, self, "refresh_rate", |
||
189 | items=[t for t, _ in OWMDS.RefreshRate], |
||
190 | callback=self.__invalidate_refresh)) |
||
191 | gui.separator(box, 10) |
||
192 | gui.checkBox(box, self, "spread_equal_points", |
||
193 | "Spread points at zero-distances", |
||
194 | callback=self.__invalidate_embedding) |
||
195 | gui.separator(box, 10) |
||
196 | self.runbutton = gui.button( |
||
197 | box, self, "Run", callback=self._toggle_run) |
||
198 | |||
199 | box = gui.widgetBox(self.controlArea, "Graph") |
||
200 | self.colorvar_model = itemmodels.VariableListModel() |
||
201 | |||
202 | common_options = {"sendSelectedValue": True, "valueType": str, |
||
203 | "orientation": "horizontal", "labelWidth": 50, |
||
204 | "contentsLength": 12} |
||
205 | |||
206 | self.cb_color_value = gui.comboBox( |
||
207 | box, self, "color_value", label="Color", |
||
208 | callback=self._on_color_index_changed, **common_options) |
||
209 | self.cb_color_value.setModel(self.colorvar_model) |
||
210 | |||
211 | self.shapevar_model = itemmodels.VariableListModel() |
||
212 | self.cb_shape_value = gui.comboBox( |
||
213 | box, self, "shape_value", label="Shape", |
||
214 | callback=self._on_shape_index_changed, **common_options) |
||
215 | self.cb_shape_value.setModel(self.shapevar_model) |
||
216 | |||
217 | self.sizevar_model = itemmodels.VariableListModel() |
||
218 | self.cb_size_value = gui.comboBox( |
||
219 | box, self, "size_value", label="Size", |
||
220 | callback=self._on_size_index_changed, **common_options) |
||
221 | self.cb_size_value.setModel(self.sizevar_model) |
||
222 | |||
223 | self.labelvar_model = itemmodels.VariableListModel() |
||
224 | self.cb_label_value = gui.comboBox( |
||
225 | box, self, "label_value", label="Label", |
||
226 | callback=self._on_label_index_changed, **common_options) |
||
227 | self.cb_label_value.setModel(self.labelvar_model) |
||
228 | |||
229 | form = QtGui.QFormLayout( |
||
230 | labelAlignment=Qt.AlignLeft, |
||
231 | formAlignment=Qt.AlignLeft, |
||
232 | fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, |
||
233 | verticalSpacing=10 |
||
234 | ) |
||
235 | form.addRow("Symbol size", |
||
236 | gui.hSlider(box, self, "symbol_size", |
||
237 | minValue=1, maxValue=20, |
||
238 | callback=self._on_size_index_changed, |
||
239 | createLabel=False)) |
||
240 | form.addRow("Symbol opacity", |
||
241 | gui.hSlider(box, self, "symbol_opacity", |
||
242 | minValue=100, maxValue=255, step=100, |
||
243 | callback=self._on_color_index_changed, |
||
244 | createLabel=False)) |
||
245 | form.addRow("Show similar pairs", |
||
246 | gui.hSlider( |
||
247 | gui.widgetBox(self.controlArea, |
||
248 | orientation="horizontal"), |
||
249 | self, "connected_pairs", minValue=0, maxValue=20, |
||
250 | createLabel=False, |
||
251 | callback=self._on_connected_changed)) |
||
252 | box.layout().addLayout(form) |
||
253 | |||
254 | gui.rubber(self.controlArea) |
||
255 | |||
256 | box = QtGui.QGroupBox("Zoom/Select", ) |
||
257 | box.setLayout(QtGui.QHBoxLayout()) |
||
258 | box.layout().setMargin(2) |
||
259 | |||
260 | group = QtGui.QActionGroup(self, exclusive=True) |
||
261 | |||
262 | def icon(name): |
||
263 | path = "icons/Dlg_{}.png".format(name) |
||
264 | path = pkg_resources.resource_filename(widget.__name__, path) |
||
265 | return QtGui.QIcon(path) |
||
266 | |||
267 | action_select = QtGui.QAction( |
||
268 | "Select", self, checkable=True, checked=True, icon=icon("arrow"), |
||
269 | shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) |
||
270 | action_zoom = QtGui.QAction( |
||
271 | "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), |
||
272 | shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) |
||
273 | action_pan = QtGui.QAction( |
||
274 | "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), |
||
275 | shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) |
||
276 | |||
277 | action_reset_zoom = QtGui.QAction( |
||
278 | "Zoom to fit", self, icon=icon("zoom_reset"), |
||
279 | shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) |
||
280 | action_reset_zoom.triggered.connect( |
||
281 | lambda: self.plot.autoRange(padding=0.1, |
||
282 | items=[self._scatter_item])) |
||
283 | group.addAction(action_select) |
||
284 | group.addAction(action_zoom) |
||
285 | group.addAction(action_pan) |
||
286 | self.addActions(group.actions() + [action_reset_zoom]) |
||
287 | action_select.setChecked(True) |
||
288 | |||
289 | def button(action): |
||
290 | b = QtGui.QToolButton() |
||
291 | b.setToolButtonStyle(Qt.ToolButtonIconOnly) |
||
292 | b.setDefaultAction(action) |
||
293 | return b |
||
294 | |||
295 | box.layout().addWidget(button(action_select)) |
||
296 | box.layout().addWidget(button(action_zoom)) |
||
297 | box.layout().addWidget(button(action_pan)) |
||
298 | box.layout().addSpacing(4) |
||
299 | box.layout().addWidget(button(action_reset_zoom)) |
||
300 | box.layout().addStretch() |
||
301 | |||
302 | self.controlArea.layout().addWidget(box) |
||
303 | |||
304 | box = gui.widgetBox(self.controlArea, "Output") |
||
305 | gui.comboBox(box, self, "output_embedding_role", |
||
306 | items=["Original features only", |
||
307 | "Coordinates only", |
||
308 | "Coordinates as features", |
||
309 | "Coordinates as meta attributes"], |
||
310 | callback=self._invalidate_output, addSpace=4) |
||
311 | gui.auto_commit(box, self, "autocommit", "Send data", |
||
312 | checkbox_label="Send after any change", |
||
313 | box=None) |
||
314 | |||
315 | self.plot = pg.PlotWidget(background="w", enableMenu=False) |
||
316 | self.plot.getPlotItem().hideAxis("bottom") |
||
317 | self.plot.getPlotItem().hideAxis("left") |
||
318 | self.plot.getPlotItem().hideButtons() |
||
319 | self.plot.setRenderHint(QtGui.QPainter.Antialiasing) |
||
320 | self.mainArea.layout().addWidget(self.plot) |
||
321 | |||
322 | self.selection_tool = PlotSelectionTool(parent=self) |
||
323 | self.zoom_tool = PlotZoomTool(parent=self) |
||
324 | self.pan_tool = PlotPanTool(parent=self) |
||
325 | self.pinch_tool = PlotPinchZoomTool(parent=self) |
||
326 | self.pinch_tool.setViewBox(self.plot.getViewBox()) |
||
327 | self.selection_tool.setViewBox(self.plot.getViewBox()) |
||
328 | self.selection_tool.selectionFinished.connect(self.__selection_end) |
||
329 | self.current_tool = self.selection_tool |
||
330 | |||
331 | def activate_tool(action): |
||
332 | self.current_tool.setViewBox(None) |
||
333 | |||
334 | if action is action_select: |
||
335 | active, cur = self.selection_tool, Qt.ArrowCursor |
||
336 | elif action is action_zoom: |
||
337 | active, cur = self.zoom_tool, Qt.ArrowCursor |
||
338 | elif action is action_pan: |
||
339 | active, cur = self.pan_tool, Qt.OpenHandCursor |
||
340 | self.current_tool = active |
||
341 | self.current_tool.setViewBox(self.plot.getViewBox()) |
||
342 | self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) |
||
343 | |||
344 | group.triggered[QtGui.QAction].connect(activate_tool) |
||
345 | self.graphButton.clicked.connect(self.save_graph) |
||
346 | |||
347 | self._initialize() |
||
348 | |||
349 | def set_data(self, data): |
||
350 | self.signal_data = data |
||
351 | |||
352 | if self.matrix is not None and data is not None and len(self.matrix) == len(data): |
||
353 | self.closeContext() |
||
354 | self.data = data |
||
355 | self.update_controls() |
||
356 | self.openContext(data) |
||
357 | else: |
||
358 | self._invalidated = True |
||
359 | self._selection_mask = None |
||
360 | |||
361 | def set_disimilarity(self, matrix): |
||
362 | self.matrix = matrix |
||
363 | if matrix is not None and matrix.row_items: |
||
364 | self.matrix_data = matrix.row_items |
||
365 | if matrix is None: |
||
366 | self.matrix_data = None |
||
367 | self._invalidated = True |
||
368 | self._selection_mask = None |
||
369 | |||
370 | def _clear(self): |
||
371 | self._pen_data = None |
||
372 | self._shape_data = None |
||
373 | self._size_data = None |
||
374 | self._label_data = None |
||
375 | self._similar_pairs = None |
||
376 | |||
377 | self.colorvar_model[:] = ["Same color"] |
||
378 | self.shapevar_model[:] = ["Same shape"] |
||
379 | self.sizevar_model[:] = ["Same size"] |
||
380 | self.labelvar_model[:] = ["No labels"] |
||
381 | |||
382 | self.color_value = self.colorvar_model[0] |
||
383 | self.shape_value = self.shapevar_model[0] |
||
384 | self.size_value = self.sizevar_model[0] |
||
385 | self.label_value = self.labelvar_model[0] |
||
386 | |||
387 | self.__set_update_loop(None) |
||
388 | self.__state = OWMDS.Waiting |
||
389 | |||
390 | def _clear_plot(self): |
||
391 | self.plot.clear() |
||
392 | self._scatter_item = None |
||
393 | if self._legend_item is not None: |
||
394 | anchor = legend_anchor_pos(self._legend_item) |
||
395 | if anchor is not None: |
||
396 | self.legend_anchor = anchor |
||
397 | if self._legend_item.scene() is not None: |
||
398 | self._legend_item.scene().removeItem(self._legend_item) |
||
399 | self._legend_item = None |
||
400 | |||
401 | def update_controls(self): |
||
402 | if getattr(self.matrix, 'axis', 1) == 0: |
||
403 | # Column-wise distances |
||
404 | attr = "Attribute names" |
||
405 | self.labelvar_model[:] = ["No labels", attr] |
||
406 | self.shapevar_model[:] = ["Same shape", attr] |
||
407 | self.colorvar_model[:] = ["Same color", attr] |
||
408 | |||
409 | self.color_value = attr |
||
410 | self.shape_value = attr |
||
411 | else: |
||
412 | # initialize the graph state from data |
||
413 | domain = self.data.domain |
||
414 | all_vars = list(domain.variables + domain.metas) |
||
415 | cd_vars = [var for var in all_vars if var.is_primitive()] |
||
416 | disc_vars = [var for var in all_vars if var.is_discrete] |
||
417 | cont_vars = [var for var in all_vars if var.is_continuous] |
||
418 | shape_vars = [var for var in disc_vars |
||
419 | if len(var.values) <= len(ScatterPlotItem.Symbols) - 1] |
||
420 | self.colorvar_model[:] = chain(["Same color"], |
||
421 | [self.colorvar_model.Separator], |
||
422 | cd_vars) |
||
423 | self.shapevar_model[:] = chain(["Same shape"], |
||
424 | [self.shapevar_model.Separator], |
||
425 | shape_vars) |
||
426 | self.sizevar_model[:] = chain(["Same size", "Stress"], |
||
427 | [self.sizevar_model.Separator], |
||
428 | cont_vars) |
||
429 | self.labelvar_model[:] = chain(["No labels"], |
||
430 | [self.labelvar_model.Separator], |
||
431 | all_vars) |
||
432 | |||
433 | if domain.class_var is not None: |
||
434 | self.color_value = domain.class_var.name |
||
435 | |||
436 | def _initialize(self): |
||
437 | # clear everything |
||
438 | self.closeContext() |
||
439 | self._clear() |
||
440 | self.data = None |
||
441 | self._effective_matrix = None |
||
442 | self.embedding = None |
||
443 | |||
444 | # if no data nor matrix is present reset plot |
||
445 | if self.signal_data is None and self.matrix is None: |
||
446 | return |
||
447 | |||
448 | if self.signal_data and self.matrix_data and len(self.signal_data) != len(self.matrix_data): |
||
449 | self.error(1, "Data and distances dimensions do not match.") |
||
450 | self._update_plot() |
||
451 | return |
||
452 | |||
453 | self.error(1) |
||
454 | |||
455 | if self.signal_data: |
||
456 | self.data = self.signal_data |
||
457 | elif self.matrix_data: |
||
458 | self.data = self.matrix_data |
||
459 | |||
460 | if self.matrix is not None: |
||
461 | self._effective_matrix = self.matrix |
||
462 | if self.matrix.axis == 0: |
||
463 | self.data = None |
||
464 | else: |
||
465 | preprocessed_data = Orange.projection.MDS().preprocess(self.data) |
||
466 | self._effective_matrix = Orange.distance.Euclidean(preprocessed_data) |
||
467 | |||
468 | self.update_controls() |
||
469 | self.openContext(self.data) |
||
470 | |||
471 | def _toggle_run(self): |
||
472 | if self.__state == OWMDS.Running: |
||
473 | self.stop() |
||
474 | self._invalidate_output() |
||
475 | else: |
||
476 | self.start() |
||
477 | |||
478 | def start(self): |
||
479 | if self.__state == OWMDS.Running: |
||
480 | return |
||
481 | elif self.__state == OWMDS.Finished: |
||
482 | # Resume/continue from a previous run |
||
483 | self.__start() |
||
484 | elif self.__state == OWMDS.Waiting and \ |
||
485 | self._effective_matrix is not None: |
||
486 | self.__start() |
||
487 | |||
488 | def stop(self): |
||
489 | if self.__state == OWMDS.Running: |
||
490 | self.__set_update_loop(None) |
||
491 | |||
492 | def __start(self): |
||
493 | self.__draw_similar_pairs = False |
||
494 | X = self._effective_matrix |
||
495 | if self.spread_equal_points: |
||
496 | maxval = numpy.max(X) |
||
497 | X = numpy.clip(X, maxval / 10, maxval) |
||
498 | |||
499 | if self.embedding is not None: |
||
500 | init = self.embedding |
||
501 | elif self.initialization == OWMDS.PCA: |
||
502 | init = torgerson(X, n_components=2) |
||
503 | else: |
||
504 | init = None |
||
505 | |||
506 | # number of iterations per single GUI update step |
||
507 | _, step_size = OWMDS.RefreshRate[self.refresh_rate] |
||
508 | if step_size == -1: |
||
509 | step_size = self.max_iter |
||
510 | |||
511 | def update_loop(X, max_iter, step, init): |
||
512 | """ |
||
513 | return an iterator over successive improved MDS point embeddings. |
||
514 | """ |
||
515 | # NOTE: this code MUST NOT call into QApplication.processEvents |
||
516 | done = False |
||
517 | iterations_done = 0 |
||
518 | oldstress = numpy.finfo(numpy.float).max |
||
519 | |||
520 | while not done: |
||
521 | step_iter = min(max_iter - iterations_done, step) |
||
522 | mds = Orange.projection.MDS( |
||
523 | dissimilarity="precomputed", n_components=2, |
||
524 | n_init=1, max_iter=step_iter) |
||
525 | |||
526 | mdsfit = mds.fit(X, init=init) |
||
527 | iterations_done += step_iter |
||
528 | |||
529 | embedding, stress = mdsfit.embedding_, mdsfit.stress_ |
||
530 | stress /= numpy.sqrt(numpy.sum(embedding ** 2, axis=1)).sum() |
||
531 | |||
532 | if iterations_done >= max_iter: |
||
533 | done = True |
||
534 | elif (oldstress - stress) < mds.params["eps"]: |
||
535 | done = True |
||
536 | init = embedding |
||
537 | oldstress = stress |
||
538 | |||
539 | yield embedding, mdsfit.stress_, iterations_done / max_iter |
||
540 | |||
541 | self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) |
||
542 | self.progressBarInit(processEvents=None) |
||
543 | |||
544 | def __set_update_loop(self, loop): |
||
545 | """ |
||
546 | Set the update `loop` coroutine. |
||
547 | |||
548 | The `loop` is a generator yielding `(embedding, stress, progress)` |
||
549 | tuples where `embedding` is a `(N, 2) ndarray` of current updated |
||
550 | MDS points, `stress` is the current stress and `progress` a float |
||
551 | ratio (0 <= progress <= 1) |
||
552 | |||
553 | If an existing update loop is already in palace it is interrupted |
||
554 | (closed). |
||
555 | |||
556 | .. note:: |
||
557 | The `loop` must not explicitly yield control flow to the event |
||
558 | loop (i.e. call `QApplication.processEvents`) |
||
559 | |||
560 | """ |
||
561 | if self.__update_loop is not None: |
||
562 | self.__update_loop.close() |
||
563 | self.__update_loop = None |
||
564 | self.progressBarFinished(processEvents=None) |
||
565 | |||
566 | self.__update_loop = loop |
||
567 | |||
568 | if loop is not None: |
||
569 | self.progressBarInit(processEvents=None) |
||
570 | self.setStatusMessage("Running") |
||
571 | self.runbutton.setText("Stop") |
||
572 | self.__state = OWMDS.Running |
||
573 | QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) |
||
574 | else: |
||
575 | self.setStatusMessage("") |
||
576 | self.runbutton.setText("Start") |
||
577 | self.__state = OWMDS.Finished |
||
578 | |||
579 | def __next_step(self): |
||
580 | if self.__update_loop is None: |
||
581 | return |
||
582 | |||
583 | loop = self.__update_loop |
||
584 | try: |
||
585 | embedding, stress, progress = next(self.__update_loop) |
||
586 | assert self.__update_loop is loop |
||
587 | except StopIteration: |
||
588 | self.__set_update_loop(None) |
||
589 | self.unconditional_commit() |
||
590 | self.__draw_similar_pairs = True |
||
591 | self._update_plot() |
||
592 | self.plot.autoRange(padding=0.1, items=[self._scatter_item]) |
||
593 | else: |
||
594 | self.progressBarSet(100.0 * progress, processEvents=None) |
||
595 | self.embedding = embedding |
||
596 | self._update_plot() |
||
597 | self.plot.autoRange(padding=0.1, items=[self._scatter_item]) |
||
598 | # schedule next update |
||
599 | QtGui.QApplication.postEvent( |
||
600 | self, QEvent(QEvent.User), Qt.LowEventPriority) |
||
601 | |||
602 | def customEvent(self, event): |
||
603 | if event.type() == QEvent.User and self.__update_loop is not None: |
||
604 | if not self.__in_next_step: |
||
605 | self.__in_next_step = True |
||
606 | try: |
||
607 | self.__next_step() |
||
608 | finally: |
||
609 | self.__in_next_step = False |
||
610 | else: |
||
611 | warnings.warn( |
||
612 | "Re-entry in update loop detected. " |
||
613 | "A rogue `proccessEvents` is on the loose.", |
||
614 | RuntimeWarning) |
||
615 | # re-schedule the update iteration. |
||
616 | QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) |
||
617 | return super().customEvent(event) |
||
618 | |||
619 | def __invalidate_embedding(self): |
||
620 | # reset/invalidate the MDS embedding, to the default initialization |
||
621 | # (Random or PCA), restarting the optimization if necessary. |
||
622 | if self.embedding is None: |
||
623 | return |
||
624 | state = self.__state |
||
625 | if self.__update_loop is not None: |
||
626 | self.__set_update_loop(None) |
||
627 | |||
628 | X = self._effective_matrix |
||
629 | |||
630 | if self.initialization == OWMDS.PCA: |
||
631 | self.embedding = torgerson(X) |
||
632 | else: |
||
633 | self.embedding = numpy.random.rand(len(X), 2) |
||
634 | |||
635 | self._update_plot() |
||
636 | self.plot.autoRange(padding=0.1, items=[self._scatter_item]) |
||
637 | |||
638 | # restart the optimization if it was interrupted. |
||
639 | if state == OWMDS.Running: |
||
640 | self.__start() |
||
641 | |||
642 | def __invalidate_refresh(self): |
||
643 | state = self.__state |
||
644 | |||
645 | if self.__update_loop is not None: |
||
646 | self.__set_update_loop(None) |
||
647 | |||
648 | # restart the optimization if it was interrupted. |
||
649 | # TODO: decrease the max iteration count by the already |
||
650 | # completed iterations count. |
||
651 | if state == OWMDS.Running: |
||
652 | self.__start() |
||
653 | |||
654 | def handleNewSignals(self): |
||
655 | if self._invalidated: |
||
656 | self._invalidated = False |
||
657 | self._initialize() |
||
658 | self.start() |
||
659 | self.__draw_similar_pairs = False |
||
660 | self._update_plot() |
||
661 | self.plot.autoRange(padding=0.1) |
||
662 | self.unconditional_commit() |
||
663 | |||
664 | def _invalidate_output(self): |
||
665 | self.commit() |
||
666 | |||
667 | def _on_color_index_changed(self): |
||
668 | self._pen_data = None |
||
669 | self._update_plot() |
||
670 | |||
671 | def _on_shape_index_changed(self): |
||
672 | self._shape_data = None |
||
673 | self._update_plot() |
||
674 | |||
675 | def _on_size_index_changed(self): |
||
676 | self._size_data = None |
||
677 | self._update_plot() |
||
678 | |||
679 | def _on_label_index_changed(self): |
||
680 | self._label_data = None |
||
681 | self._update_plot() |
||
682 | |||
683 | def _on_connected_changed(self): |
||
684 | self._similar_pairs = None |
||
685 | self._update_plot() |
||
686 | |||
687 | def _update_plot(self): |
||
688 | self._clear_plot() |
||
689 | |||
690 | if self.embedding is not None: |
||
691 | self._setup_plot() |
||
692 | |||
693 | def _setup_plot(self): |
||
694 | have_data = self.data is not None |
||
695 | have_matrix_transposed = self.matrix is not None and not self.matrix.axis |
||
696 | plotstyle = mdsplotutils.plotstyle |
||
697 | |||
698 | def column(data, variable): |
||
699 | a, _ = data.get_column_view(variable) |
||
700 | return a.ravel() |
||
701 | |||
702 | def attributes(matrix): |
||
703 | return matrix.row_items.domain.attributes |
||
704 | |||
705 | def scale(a): |
||
706 | dmin, dmax = numpy.nanmin(a), numpy.nanmax(a) |
||
707 | if dmax - dmin > 0: |
||
708 | return (a - dmin) / (dmax - dmin) |
||
709 | else: |
||
710 | return numpy.zeros_like(a) |
||
711 | |||
712 | if self._pen_data is None: |
||
713 | if self._selection_mask is not None: |
||
714 | pointflags = numpy.where( |
||
715 | self._selection_mask, |
||
716 | mdsplotutils.Selected, mdsplotutils.NoFlags) |
||
717 | else: |
||
718 | pointflags = None |
||
719 | |||
720 | color_index = self.cb_color_value.currentIndex() |
||
721 | if have_data and color_index > 0: |
||
722 | color_var = self.colorvar_model[color_index] |
||
723 | if color_var.is_discrete: |
||
724 | palette = colorpalette.ColorPaletteGenerator( |
||
725 | len(color_var.values) |
||
726 | ) |
||
727 | plotstyle = plotstyle.updated(discrete_palette=palette) |
||
728 | else: |
||
729 | palette = None |
||
730 | |||
731 | color_data = mdsplotutils.color_data( |
||
732 | self.data, color_var, plotstyle=plotstyle) |
||
733 | color_data = numpy.hstack( |
||
734 | (color_data, |
||
735 | numpy.full((len(color_data), 1), self.symbol_opacity)) |
||
736 | ) |
||
737 | pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags) |
||
738 | brush_data = mdsplotutils.brush_data(color_data) |
||
739 | elif have_matrix_transposed and \ |
||
740 | self.colorvar_model[color_index] == 'Attribute names': |
||
741 | attr = attributes(self.matrix) |
||
742 | palette = colorpalette.ColorPaletteGenerator(len(attr)) |
||
743 | color_data = [palette.getRGB(i) for i in range(len(attr))] |
||
744 | color_data = numpy.hstack(( |
||
745 | color_data, |
||
746 | numpy.full((len(color_data), 1), self.symbol_opacity)) |
||
747 | ) |
||
748 | pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags) |
||
749 | brush_data = mdsplotutils.brush_data(color_data) |
||
750 | else: |
||
751 | pen_data = make_pen(QtGui.QColor(Qt.darkGray), cosmetic=True) |
||
752 | if self._selection_mask is not None: |
||
753 | pen_data = numpy.array( |
||
754 | [pen_data, plotstyle.selected_pen]) |
||
755 | pen_data = pen_data[self._selection_mask.astype(int)] |
||
756 | else: |
||
757 | pen_data = numpy.full(len(self.data), pen_data, |
||
758 | dtype=object) |
||
759 | brush_data = numpy.full( |
||
760 | len(self.data), |
||
761 | pg.mkColor((192, 192, 192, self.symbol_opacity)), |
||
762 | dtype=object) |
||
763 | |||
764 | self._pen_data = pen_data |
||
765 | self._brush_data = brush_data |
||
766 | |||
767 | if self._shape_data is None: |
||
768 | shape_index = self.cb_shape_value.currentIndex() |
||
769 | if have_data and shape_index > 0: |
||
770 | Symbols = ScatterPlotItem.Symbols |
||
771 | symbols = numpy.array(list(Symbols.keys())) |
||
772 | |||
773 | shape_var = self.shapevar_model[shape_index] |
||
774 | data = column(self.data, shape_var) |
||
775 | data = data % (len(Symbols) - 1) |
||
776 | data[numpy.isnan(data)] = len(Symbols) - 1 |
||
777 | shape_data = symbols[data.astype(int)] |
||
778 | elif have_matrix_transposed and \ |
||
779 | self.shapevar_model[shape_index] == 'Attribute names': |
||
780 | Symbols = ScatterPlotItem.Symbols |
||
781 | symbols = numpy.array(list(Symbols.keys())) |
||
782 | attr = [i % (len(Symbols) - 1) |
||
783 | for i, _ in enumerate(attributes(self.matrix))] |
||
784 | shape_data = symbols[attr] |
||
785 | else: |
||
786 | shape_data = "o" |
||
787 | self._shape_data = shape_data |
||
788 | |||
789 | if self._size_data is None: |
||
790 | MinPointSize = 3 |
||
791 | point_size = self.symbol_size + MinPointSize |
||
792 | size_index = self.cb_size_value.currentIndex() |
||
793 | if have_data and size_index == 1: |
||
794 | # size by stress |
||
795 | size_data = stress(self.embedding, self._effective_matrix) |
||
796 | size_data = scale(size_data) |
||
797 | size_data = MinPointSize + size_data * point_size |
||
798 | elif have_data and size_index > 0: |
||
799 | size_var = self.sizevar_model[size_index] |
||
800 | size_data = column(self.data, size_var) |
||
801 | size_data = scale(size_data) |
||
802 | size_data = MinPointSize + size_data * point_size |
||
803 | else: |
||
804 | size_data = point_size |
||
805 | self._size_data = size_data |
||
806 | |||
807 | if self._label_data is None: |
||
808 | label_index = self.cb_label_value.currentIndex() |
||
809 | if have_data and label_index > 0: |
||
810 | label_var = self.labelvar_model[label_index] |
||
811 | label_data = column(self.data, label_var) |
||
812 | label_data = [label_var.str_val(val) for val in label_data] |
||
813 | label_items = [pg.TextItem(text, anchor=(0.5, 0), color=0.0) |
||
814 | for text in label_data] |
||
815 | elif have_matrix_transposed and \ |
||
816 | self.labelvar_model[label_index] == 'Attribute names': |
||
817 | attr = attributes(self.matrix) |
||
818 | label_items = [pg.TextItem(str(text), anchor=(0.5, 0)) |
||
819 | for text in attr] |
||
820 | else: |
||
821 | label_items = None |
||
822 | self._label_data = label_items |
||
823 | |||
824 | emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1] |
||
825 | |||
826 | if self.connected_pairs and self.__draw_similar_pairs: |
||
827 | if self._similar_pairs is None: |
||
828 | # This code requires storing lower triangle of X (n x n / 2 |
||
829 | # doubles), n x n / 2 * 2 indices to X, n x n / 2 indices for |
||
830 | # argsort result. If this becomes an issue, it can be reduced to |
||
831 | # n x n argsort indices by argsorting the entire X. Then we |
||
832 | # take the first n + 2 * p indices. We compute their coordinates |
||
833 | # i, j in the original matrix. We keep those for which i < j. |
||
834 | # n + 2 * p will suffice to exclude the diagonal (i = j). If the |
||
835 | # number of those for which i < j is smaller than p, we instead |
||
836 | # take i > j. Among those that remain, we take the first p. |
||
837 | # Assuming that MDS can't show so many points that memory could |
||
838 | # become an issue, I preferred using simpler code. |
||
839 | m = self._effective_matrix |
||
840 | n = len(m) |
||
841 | p = (n * (n - 1) // 2 * self.connected_pairs) // 100 |
||
842 | indcs = numpy.triu_indices(n, 1) |
||
843 | sorted = numpy.argsort(m[indcs])[:p] |
||
844 | self._similar_pairs = fpairs = numpy.empty(2 * p, dtype=int) |
||
845 | fpairs[::2] = indcs[0][sorted] |
||
846 | fpairs[1::2] = indcs[1][sorted] |
||
847 | for i in range(int(len(emb_x[self._similar_pairs]) / 2)): |
||
848 | item = QtGui.QGraphicsLineItem( |
||
849 | emb_x[self._similar_pairs][i * 2], |
||
850 | emb_y[self._similar_pairs][i * 2], |
||
851 | emb_x[self._similar_pairs][i * 2 + 1], |
||
852 | emb_y[self._similar_pairs][i * 2 + 1] |
||
853 | ) |
||
854 | pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(204, 204, 204)), 2) |
||
855 | pen.setCosmetic(True) |
||
856 | item.setPen(pen) |
||
857 | self.plot.addItem(item) |
||
858 | |||
859 | data = numpy.arange(len(self.data if have_data else self.matrix)) |
||
860 | self._scatter_item = item = ScatterPlotItem( |
||
861 | x=emb_x, y=emb_y, |
||
862 | pen=self._pen_data, brush=self._brush_data, symbol=self._shape_data, |
||
863 | size=self._size_data, data=data, |
||
864 | antialias=True |
||
865 | ) |
||
866 | self.plot.addItem(item) |
||
867 | |||
868 | if self._label_data is not None: |
||
869 | for (x, y), text_item in zip(self.embedding, self._label_data): |
||
870 | self.plot.addItem(text_item) |
||
871 | text_item.setPos(x, y) |
||
872 | |||
873 | self._legend_item = LegendItem() |
||
874 | self._legend_item.setParentItem(self.plot.getViewBox()) |
||
875 | self._legend_item.anchor(*self.legend_anchor) |
||
876 | |||
877 | color_var = shape_var = None |
||
878 | color_index = self.cb_color_value.currentIndex() |
||
879 | if have_data and 1 <= color_index < len(self.colorvar_model): |
||
880 | color_var = self.colorvar_model[color_index] |
||
881 | assert isinstance(color_var, Orange.data.Variable) |
||
882 | shape_index = self.cb_shape_value.currentIndex() |
||
883 | if have_data and 1 <= shape_index < len(self.shapevar_model): |
||
884 | shape_var = self.shapevar_model[shape_index] |
||
885 | assert isinstance(shape_var, Orange.data.Variable) |
||
886 | |||
887 | if shape_var is not None or \ |
||
888 | (color_var is not None and color_var.is_discrete): |
||
889 | |||
890 | legend_data = mdsplotutils.legend_data( |
||
891 | color_var, shape_var, plotstyle=plotstyle) |
||
892 | |||
893 | for color, symbol, text in legend_data: |
||
894 | self._legend_item.addItem( |
||
895 | ScatterPlotItem(pen=color, brush=color, symbol=symbol, |
||
896 | size=10), |
||
897 | escape(text) |
||
898 | ) |
||
899 | else: |
||
900 | self._legend_item.hide() |
||
901 | |||
902 | def commit(self): |
||
903 | if self.embedding is not None: |
||
904 | output = embedding = Orange.data.Table.from_numpy( |
||
905 | Orange.data.Domain([Orange.data.ContinuousVariable("X"), |
||
906 | Orange.data.ContinuousVariable("Y")]), |
||
907 | self.embedding |
||
908 | ) |
||
909 | else: |
||
910 | output = embedding = None |
||
911 | |||
912 | if self.embedding is not None and self.data is not None: |
||
913 | domain = self.data.domain |
||
914 | attrs = domain.attributes |
||
915 | class_vars = domain.class_vars |
||
916 | metas = domain.metas |
||
917 | |||
918 | if self.output_embedding_role == OWMDS.AttrRole: |
||
919 | attrs = embedding.domain.attributes |
||
920 | elif self.output_embedding_role == OWMDS.AddAttrRole: |
||
921 | attrs = domain.attributes + embedding.domain.attributes |
||
922 | elif self.output_embedding_role == OWMDS.MetaRole: |
||
923 | metas += embedding.domain.attributes |
||
924 | |||
925 | domain = Orange.data.Domain(attrs, class_vars, metas) |
||
926 | output = Orange.data.Table.from_table(domain, self.data) |
||
927 | |||
928 | if self.output_embedding_role == OWMDS.AttrRole: |
||
929 | output.X[:] = embedding.X |
||
930 | if self.output_embedding_role == OWMDS.AddAttrRole: |
||
931 | output.X[:, -2:] = embedding.X |
||
932 | elif self.output_embedding_role == OWMDS.MetaRole: |
||
933 | output.metas[:, -2:] = embedding.X |
||
934 | |||
935 | self.send("Data", output) |
||
936 | if output is not None and self._selection_mask is not None and \ |
||
937 | numpy.any(self._selection_mask): |
||
938 | subset = output[self._selection_mask] |
||
939 | else: |
||
940 | subset = None |
||
941 | self.send("Selected Data", subset) |
||
942 | |||
943 | def onDeleteWidget(self): |
||
944 | super().onDeleteWidget() |
||
945 | self._clear_plot() |
||
946 | self._clear() |
||
947 | |||
948 | def __selection_end(self, path): |
||
949 | self.select(path) |
||
950 | self._pen_data = None |
||
951 | self._update_plot() |
||
952 | self._invalidate_output() |
||
953 | |||
954 | def select(self, region): |
||
955 | item = self._scatter_item |
||
956 | if item is None: |
||
957 | return |
||
958 | |||
959 | indices = numpy.array( |
||
960 | [spot.data() for spot in item.points() |
||
961 | if region.contains(spot.pos())], |
||
962 | dtype=int) |
||
963 | |||
964 | if not QtGui.QApplication.keyboardModifiers(): |
||
965 | self._selection_mask = None |
||
966 | |||
967 | self.select_indices(indices, QtGui.QApplication.keyboardModifiers()) |
||
968 | |||
969 | def select_indices(self, indices, modifiers=Qt.NoModifier): |
||
970 | if self.data is None: |
||
971 | return |
||
972 | |||
973 | if self._selection_mask is None or \ |
||
974 | not modifiers & (Qt.ControlModifier | Qt.ShiftModifier | |
||
975 | Qt.AltModifier): |
||
976 | self._selection_mask = numpy.zeros(len(self.data), dtype=bool) |
||
977 | |||
978 | if modifiers & Qt.AltModifier: |
||
979 | self._selection_mask[indices] = False |
||
980 | elif modifiers & Qt.ControlModifier: |
||
981 | self._selection_mask[indices] = ~self._selection_mask[indices] |
||
982 | else: |
||
983 | self._selection_mask[indices] = True |
||
984 | |||
985 | def save_graph(self): |
||
986 | from Orange.widgets.data.owsave import OWSave |
||
987 | |||
988 | save_img = OWSave(parent=self, data=self.plot.plotItem, |
||
989 | file_formats=FileFormats.img_writers) |
||
990 | save_img.exec_() |
||
991 | |||
1285 |
This can be caused by one of the following:
1. Missing Dependencies
This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.
2. Missing __init__.py files
This error could also result from missing
__init__.py
files in your module folders. Make sure that you place one file in each sub-folder.