source: orange-bioinformatics/widgets/OWQualityControl.py @ 1573:f7cad4491971

Revision 1573:f7cad4491971, 24.1 KB checked in by markotoplak, 2 years ago (diff)

Deduplicated code from obiExperiments in OWGenotypeDistances. OWQualityControl: python 2.6 support.

Line 
1"""
2<name>Quality Control</name>
3<description>Experiment quality control</description>
4
5"""
6
7import sys
8
9import Orange
10
11from OWWidget import *
12from OWItemModels import PyListModel, safe_text
13from OWGraphics import GraphicsSimpleTextLayoutItem
14import OWGUI
15
16from OWGenotypeDistances import SetContextHandler
17
18import obiExperiments as exp
19import numpy
20
21from collections import defaultdict
22from contextlib import contextmanager
23from pprint import pprint
24
25DEBUG = False
26
27
28@contextmanager
29def widget_disable(widget):
30    """A context to disable the widget (enabled property) 
31    """
32    widget.setEnabled(False)
33    try:
34        yield
35    finally:
36        widget.setEnabled(True)
37   
38   
39@contextmanager
40def disable_updates(widget):
41    """
42    A context that sets '_disable_updates' member to True, and
43    then restores it.
44   
45    """
46    widget._disable_updates = True
47    try:
48        yield
49    finally:
50        widget._disable_updates = False
51
52
53def group_label(splits, groups):
54    """Return group label.
55    """
56    labels = ["%s=%s" % (split, group) \
57              for split, group in zip(splits, groups)]
58    return " | ".join(labels)
59
60
61def sort_label(sort, attr):
62    """Return within group sorted items label for attribute.
63    """
64    items = [(key, attr.attributes.get(key, "?")) \
65             for key in sort]
66    labels = ["%s=%s" % tuple(item) for item in items]
67    return " | ".join(labels)
68
69
70def float_if_posible(val):
71    """Return val as float if possible otherwise return the value unchanged.
72   
73    """
74    try:
75        return float(val)
76    except ValueError:
77        return val
78   
79   
80def experiment_description(feature):
81    """Return experiment description from ``feature.attributes``.
82    """
83    text = ""
84    if feature.attributes:
85        items = feature.attributes.items()
86        items = [(safe_text(key), safe_text(value)) for key, value in items]
87        labels = map("%s = %s".__mod__, items)
88        text += "<b>%s</b><br/>" % safe_text(feature.name)
89        text += "<br/>".join(labels)
90    return text
91
92
93class OWQualityControl(OWWidget):
94    contextHandlers = {"": SetContextHandler("")}
95    settingsList = ["selected_distance_index"]
96   
97    DISTANCE_FUNCTIONS = [("Distance from Pearson correlation",
98                           exp.dist_pcorr),
99                          ("Euclidean distance", 
100                           exp.dist_eucl),
101                          ("Distance from Spearman correlation", 
102                           exp.dist_spearman)]
103
104    def __init__(self, parent=None, signalManager=None,
105                 title="Quality Control"):
106        OWWidget.__init__(self, parent, signalManager, title,
107                          wantGraph=True)
108
109        self.inputs = [("Experiment Data", Orange.data.Table, self.set_data)]
110
111        ## Settings
112        self.selected_distance_index = 0
113
114        ## Attributes
115        self.data = None
116        self.distances = None
117        self.groups = None
118        self.unique_pos = None
119        self.base_group_index = 0
120
121        ## GUI
122        box = OWGUI.widgetBox(self.controlArea, "Info")
123        self.info_box = OWGUI.widgetLabel(box, "\n")
124
125        ## Separate By box
126        box = OWGUI.widgetBox(self.controlArea, "Separate By")
127        self.split_by_model = PyListModel()
128        self.split_by_view = QListView()
129        self.split_by_view.setSelectionMode(QListView.ExtendedSelection)
130        self.split_by_view.setModel(self.split_by_model)
131        box.layout().addWidget(self.split_by_view)
132
133        self.connect(self.split_by_view.selectionModel(),
134                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
135                     self.on_split_key_changed)
136
137        ## Sort By box
138        box = OWGUI.widgetBox(self.controlArea, "Sort By")
139        self.sort_by_model = PyListModel()
140        self.sort_by_view = QListView()
141        self.sort_by_view.setSelectionMode(QListView.ExtendedSelection)
142        self.sort_by_view.setModel(self.sort_by_model)
143        box.layout().addWidget(self.sort_by_view)
144       
145        self.connect(self.sort_by_view.selectionModel(),
146                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
147                     self.on_sort_key_changed)
148
149        ## Distance box
150        box = OWGUI.widgetBox(self.controlArea, "Distance Measure")
151        OWGUI.comboBox(box, self, "selected_distance_index",
152                       items=[t[0] for t in self.DISTANCE_FUNCTIONS],
153                       callback=self.on_distance_measure_changed)
154
155        self.connect(self.graphButton,
156                     SIGNAL("clicked()"),
157                     self.save_graph)
158       
159        self.scene = QGraphicsScene()
160        self.scene_view = QualityGraphicsView(self.scene)
161        self.scene_view.setRenderHints(QPainter.Antialiasing)
162        self.mainArea.layout().addWidget(self.scene_view)
163       
164        self.connect(self.scene_view,
165                     SIGNAL("view_size_changed(QSize)"),
166                     self.on_view_resize)
167
168        self._disable_updates = False
169        self._cached_distances = {}
170        self._base_index_hints = {}
171        self.main_widget = None
172       
173        self.resize(800, 600)
174
175    def clear(self):
176        """Clear the widget state.
177        """
178        self.data = None
179        self.distances = None
180        self.groups = None
181        self.unique_pos = None
182       
183        with disable_updates(self):
184            self.split_by_model[:] = []
185            self.sort_by_model[:] = []
186
187        self.scene.clear()
188        self.info_box.setText("\n")
189        self._cached_distances = {}
190
191    def set_data(self, data=None):
192        """Set input experiment data.
193        """
194        self.clear()
195       
196        self.error(0)
197        self.warning(0)
198       
199        if data is not None:
200            keys = self.get_suitable_keys(data)
201            if not keys:
202                self.error(0, "Data has no suitable feature labels.")
203                data = None
204               
205        self.data = data
206
207    def handleNewSignals(self):
208        """Called after all signals have been set.
209        """
210        if self.data:
211            self.on_new_data()
212        else:
213            self.closeContext("")
214            self.clear()
215
216    def update_label_candidates(self):
217        """Update the label candidates selection GUI
218        (Group/Sort By views).
219       
220        """
221        keys = self.get_suitable_keys(self.data)
222        with disable_updates(self):
223            self.split_by_model[:] = keys
224            self.sort_by_model[:] = keys
225       
226    def get_suitable_keys(self, data):
227        """ Return suitable attr label keys from the data where
228        the key has at least two unique values in the data.
229       
230        """
231        attrs = [attr.attributes.items() for attr in data.domain.attributes]
232        attrs  = reduce(list.__add__, attrs, [])
233        # in case someone put non string values in attributes dict
234        attrs = [(str(key), str(value)) for key, value in attrs]
235        attrs = set(attrs)
236        values = defaultdict(set)
237        for key, value in attrs:
238            values[key].add(value)
239        keys = [key for key in values if len(values[key]) > 1]
240        return keys
241
242    def selected_split_by_labels(self):
243        """Return the current selected split labels.
244        """
245        sel_m = self.split_by_view.selectionModel()
246        indices = [r.row() for r in sel_m.selectedRows()]
247        return [self.sort_by_model[i] for i in indices]
248
249    def selected_sort_by_labels(self):
250        """Return the current selected sort labels
251        """
252        sel_m = self.sort_by_view.selectionModel()
253        indices = [r.row() for r in sel_m.selectedRows()]
254        return [self.sort_by_model[i] for i in indices]
255
256    def selected_distance(self):
257        """Return the selected distance function.
258        """
259        return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1]
260   
261    def selected_base_group_index(self):
262        """Return the selected base group index
263        """
264        return self.base_group_index
265   
266    def selected_base_indices(self, base_group_index=None):
267        indices = []
268        for g, ind in self.groups:
269            if base_group_index is None:
270                label = group_label(self.selected_split_by_labels(), g)
271                ind = [i for i in ind if i is not None]
272                i = self._base_index_hints.get(label, ind[0] if ind else None)
273            else:
274                i = ind[base_group_index]
275            indices.append(i)
276        return indices
277
278    def on_new_data(self):
279        """We have new data and need to recompute all.
280        """
281        self.closeContext("")
282       
283        self.update_label_candidates()
284        self.info_box.setText("%s genes \n%s experiments" % (
285                                len(self.data), 
286                                len(self.data.domain.attributes)
287                                )
288                              )
289       
290        self.base_group_index = 0
291       
292        keys = self.get_suitable_keys(self.data)
293        self.openContext("", keys)
294       
295        ## Restore saved context
296        context = self.currentContexts[""]
297        split_by_labels = getattr(context, "split_by_labels", set())
298        sort_by_labels = getattr(context, "sort_by_labels", set())
299       
300        def select(model, selection_model, selected_items):
301            """Select items in a Qt item model view
302            """
303            all_items = list(model)
304            try:
305                indices = [all_items.index(item) for item in selected_items]
306            except:
307                indices = []
308            for ind in indices:
309                selection_model.select(model.index(ind), 
310                                       QItemSelectionModel.Select)
311               
312        with disable_updates(self):
313            select(self.split_by_view.model(),
314                   self.split_by_view.selectionModel(),
315                   split_by_labels)
316           
317            select(self.sort_by_view.model(),
318                   self.sort_by_view.selectionModel(),
319                   sort_by_labels)
320       
321        with widget_disable(self):
322            self.split_and_update()
323       
324    def on_split_key_changed(self, *args):
325        """Split key has changed
326        """
327        with widget_disable(self):
328            if not self._disable_updates:
329                self.base_group_index = 0
330                context = self.currentContexts[""]
331                context.split_by_labels = self.selected_split_by_labels()
332                self.split_and_update()
333   
334    def on_sort_key_changed(self, *args):
335        """Sort key has changed
336        """
337        with widget_disable(self):
338            if not self._disable_updates:
339                self.base_group_index = 0
340                context = self.currentContexts[""]
341                context.sort_by_labels = self.selected_sort_by_labels()
342                self.split_and_update()
343       
344    def on_distance_measure_changed(self):
345        """Distance measure has changed
346        """
347        with widget_disable(self):
348            self.update_distances()
349            self.replot_experiments()
350       
351    def on_view_resize(self, size):
352        """The view with the quality plot has changed
353        """
354        if self.main_widget:
355            current = self.main_widget.size()
356            self.main_widget.resize(size.width() - 2, 
357                                    current.height())
358           
359            self.scene.setSceneRect(self.scene.itemsBoundingRect())
360       
361    def on_rug_item_clicked(self, item):
362        """An ``item`` in the quality plot has been clicked.
363        """
364        update = False
365        sort_by_labels = self.selected_sort_by_labels()
366        if sort_by_labels and item.in_group:
367            ## The item is part of the group
368            if item.group_index != self.base_group_index:
369                self.base_group_index = item.group_index
370                update = True
371           
372        else:
373            if sort_by_labels:
374                # If the user clicked on an background item it
375                # invalidates the sorted labels selection
376                with disable_updates(self):
377                    self.sort_by_view.selectionModel().clear()
378                    update = True
379                   
380            index = item.index
381            group = item.group
382            label = group_label(self.selected_split_by_labels(), group)
383           
384            if self._base_index_hints.get(label, 0) != index:
385                self._base_index_hints[label] = index
386                update = True
387           
388        if update:
389            with widget_disable(self):
390                self.split_and_update()
391       
392    def split_and_update(self):
393        """
394        Split the data based on the selected sort/split labels
395        and update the quality plot.
396       
397        """
398        split_labels = self.selected_split_by_labels()
399        sort_labels = self.selected_sort_by_labels()
400       
401        self.warning(0)
402        if not split_labels:
403            self.warning(0, "No separate by label selected.")
404           
405        self.groups, self.unique_pos = \
406                exp.separate_by(self.data, split_labels,
407                                consider=sort_labels,
408                                add_empty=True)
409       
410       
411        self.groups = sorted(self.groups.items(),
412                             key=lambda t: map(float_if_posible, t[0]))
413        self.unique_pos = sorted(self.unique_pos.items(),
414                                 key=lambda t: map(float_if_posible, t[0]))
415       
416#        pprint(self.groups)
417#        pprint(self.unique_pos)
418       
419        if self.groups:
420            if sort_labels:
421                group_base = self.selected_base_group_index()
422                base_indices = self.selected_base_indices(group_base)
423            else:
424                base_indices = self.selected_base_indices()
425            self.update_distances(base_indices)
426            self.replot_experiments()
427
428    def get_cached_distances(self, measure):
429        if measure not in self._cached_distances:
430            attrs = self.data.domain.attributes
431            mat = Orange.misc.SymMatrix(len(attrs))
432            self._cached_distances[measure] = \
433                (mat, set(zip(range(len(attrs)), range(len(attrs)))))
434           
435        return self._cached_distances[measure]
436       
437    def get_cached_distance(self, measure, i, j):
438        matrix, computed = self.get_cached_distances(measure)
439        key = (i, j) if i < j else (j, i) 
440        if key in computed:
441            return matrix[i, j]
442        else:
443            return None
444       
445    def get_distance(self, measure, i, j):
446        d = self.get_cached_distance(measure, i, j)
447        if d is None:
448            vec_i = exp.linearize(self.data, [i])
449            vec_j = exp.linearize(self.data, [j])
450            d = distance(vec_i, vec_j)
451            mat, computed = self.get_cached_distances(measure)
452            mat[i, j] = d
453            key = key = (i, j) if i < j else (j, i)
454            computed.add(key)
455        return d
456   
457    def store_distance(self, measure, i, j, dist):
458        matrix, computed = self.get_cached_distances(measure)
459        key = key = (i, j) if i < j else (j, i)
460        matrix[i, j] = dist
461        computed.add(key)
462       
463    def update_distances(self, base_indices=()):
464        """Recompute the experiment distances.
465        """
466        distance = self.selected_distance()
467        if base_indices == ():
468            base_group_index = self.selected_base_group_index()
469            base_indices = [ind[base_group_index] \
470                            for _, ind in self.groups]
471           
472        assert(len(base_indices) == len(self.groups)) 
473       
474        base_distances = []
475        attributes = self.data.domain.attributes
476        pb = OWGUI.ProgressBar(self, len(self.groups) * \
477                               len(attributes))
478       
479        cached_distances, filled_set = self.get_cached_distances(distance)
480       
481        for (group, indices), base_index in zip(self.groups, base_indices):
482            # Base column of the group
483            if base_index is not None:
484                base_vec = exp.linearize(self.data, [base_index])
485                distances = []
486                # Compute the distances between base column
487                # and all the rest data columns.
488                for i in range(len(attributes)):
489                    if i == base_index:
490                        distances.append(0.0)
491                    elif self.get_cached_distance(distance, i, base_index) is not None:
492                        distances.append(self.get_cached_distance(distance, i, base_index))
493                    else:
494                        vec_i = exp.linearize(self.data, [i])
495                        dist = distance(base_vec, vec_i)
496                        self.store_distance(distance, i, base_index, dist)
497                        distances.append(dist)
498                    pb.advance()
499                   
500                base_distances.append(distances)
501            else:
502                base_distances.append(None)
503               
504        pb.finish()
505        self.distances = base_distances
506
507    def replot_experiments(self):
508        """Replot the whole quality plot.
509        """
510        self.scene.clear()
511        labels = []
512       
513        max_dist = numpy.max(filter(None, self.distances))
514        rug_widgets = []
515       
516        group_pen = QPen(QColor(0, 0, 0))
517        group_pen.setWidth(2)
518        group_pen.setCapStyle(Qt.RoundCap)
519        background_pen = QPen(QColor(0, 0, 250, 150))
520        background_pen.setWidth(1)
521        background_pen.setCapStyle(Qt.RoundCap)
522       
523        main_widget = QualityControlWidget()
524        layout = QGraphicsGridLayout()
525        split_by = self.selected_split_by_labels()
526        sort_by = self.selected_sort_by_labels()
527        attributes = self.data.domain.attributes
528        if self.data:
529            for (group, indices), dist_vec in zip(self.groups, self.distances):
530                indices_set = set(indices)
531                rug_items = []
532                if dist_vec is not None:
533                    for i, attr in enumerate(attributes):
534                        # Is this a within group distance or background
535                        in_group = i in indices_set
536                        if in_group:
537                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
538                                           1.0, self.on_rug_item_clicked)
539                            rug_item.setPen(group_pen)
540                            tooltip = experiment_description(attr)
541                            rug_item.setToolTip(tooltip)
542                            rug_item.group_index = indices.index(i)
543                        else:
544                            rug_item = ClickableRugItem(dist_vec[i] / max_dist,
545                                           0.85, self.on_rug_item_clicked)
546                            rug_item.setPen(background_pen)
547                            tooltip = experiment_description(attr)
548                            rug_item.setToolTip(tooltip)
549                           
550                        rug_item.group = group
551                        rug_item.index = i
552                        rug_item.in_group = in_group
553                       
554                        rug_items.append(rug_item)
555                   
556                rug_widget = RugGraphicsWidget()
557                rug_widget.set_rug(rug_items)
558               
559                rug_widgets.append(rug_widget)
560               
561                label = group_label(self.selected_split_by_labels(), group)
562                label_item = QGraphicsSimpleTextItem(label, main_widget)
563                label_item = GraphicsSimpleTextLayoutItem(label_item)
564                label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
565                labels.append(label_item)
566       
567        for i, (label, w) in enumerate(zip(labels, rug_widgets)):
568            layout.addItem(label, i, 0, Qt.AlignVCenter)
569            layout.addItem(w, i, 1)
570            layout.setRowMaximumHeight(i, 30)
571           
572        main_widget.setLayout(layout)
573        self.scene.addItem(main_widget)
574        main_widget.show()
575        self.main_widget = main_widget
576        self.rug_widgets = rug_widgets
577        self.labels = labels
578        self.on_view_resize(self.scene_view.size())
579       
580    def save_graph(self):
581        from OWDlgs import OWChooseImageSizeDlg
582        dlg = OWChooseImageSizeDlg(self.scene, parent=self)
583        dlg.exec_()
584
585
586class RugGraphicsWidget(QGraphicsWidget):
587    def __init__(self, parent=None, rug=None):
588        QGraphicsWidget.__init__(self, parent)
589        self.rug_items = []
590        self.set_rug(rug)
591        self.setMaximumHeight(30)
592        self.setMinimumHeight(30)
593       
594    def clear(self):
595        """
596        Clear all rug items from this widget and remove them
597        from the scene.
598         
599        """
600        for item in self.rug_items:
601            item.setParent(None)
602            if self.scene() is not None:
603                self.scene().removeItem(item)
604
605    def set_rug(self, rug):
606        """
607        Set the rug items.
608       
609        ``rug`` must be a list of floats or already initialized
610        instances of RugItem. The widget takes ownership of all
611        items.
612         
613        """
614        rug = rug if rug is not None else []
615        self.clear()
616        self.add_rug(rug)
617
618    def add_rug(self, rug):
619        """
620        Add rug items.
621       
622        See :obj:`set_rug`
623       
624        """
625        items = []
626        for item in rug:
627            if isinstance(item, float):
628                item = RugItem(value=item)
629                items.append(item)
630            elif isinstance(item, RugItem):
631                items.append(item)
632
633        for item in items:
634            item.setParentItem(self)
635
636        self.rug_items.extend(items)
637
638        self.update_rug_geometry()
639       
640    def update_rug_geometry(self):
641        """Recompute the rug items positions within this widget.
642        """
643        size = self.size()
644        height = size.height()
645        width = size.width()
646       
647        for item in self.rug_items:
648            offset = (1.0 - item.height) * height / 2.0
649            item.setPos(width * item.value, 0)
650            item.setLine(0., offset, 0., height - offset)
651
652    def resizeEvent(self, event):
653        """Reimplemented from QGraphicsWidget
654        """
655        QGraphicsWidget.resizeEvent(self, event)
656        self.update_rug_geometry()
657
658    def setGeometry(self, geom):
659        """Reimplemented from QGraphicsWidget
660        """
661        QGraphicsWidget.setGeometry(self, geom)
662
663
664class RugItem(QGraphicsLineItem):
665    def __init__(self, value, height):
666        QGraphicsLineItem.__init__(self)
667        self.value = value
668        self.height = height
669
670    def set_height(self, height):
671        """Set the height of this item (in ratio of the rug height)
672        """
673        self.height = height
674       
675class ClickableRugItem(RugItem):
676    def __init__(self, value, height, on_pressed):
677        RugItem.__init__(self, value, height)
678        self.on_pressed = on_pressed
679        self.setAcceptedMouseButtons(Qt.LeftButton)
680        self.setAcceptHoverEvents(True)
681       
682    def mousePressEvent(self, event):
683        if event.button() == Qt.LeftButton and self.on_pressed:
684            self.on_pressed(self)
685           
686    def hoverEnterEvent(self, event):
687        pen = QPen(self.pen())
688        pen.setWidthF(3)
689        self.setPen(pen)
690        return RugItem.hoverEnterEvent(self, event)
691   
692    def hoverLeaveEvent(self, event):
693        pen = QPen(self.pen())
694        pen.setWidth(2)
695        self.setPen(pen)
696        return RugItem.hoverLeaveEvent(self, event)
697
698
699class QualityGraphicsView(QGraphicsView):
700    def resizeEvent(self, event):
701        QGraphicsView.resizeEvent(self, event)
702        self.emit(SIGNAL("view_size_changed(QSize)"),
703                  event.size())
704
705
706class QualityControlWidget(QGraphicsWidget):
707    if DEBUG:
708        def paint(self, painter, options, widget=0):
709            rect =  self.geometry()
710            rect.translate(-self.pos())
711            painter.drawRect(rect)
712           
713if __name__ == "__main__":
714    app = QApplication(sys.argv)
715    w = OWQualityControl()
716#    data = Orange.data.Table("doc:dicty-abc-sample.tab")
717    data = Orange.data.Table("doc:pipa.tab")
718
719    w.set_data(data)
720    w.show()
721    w.handleNewSignals()
722    app.exec_()
723    w.set_data(None)
724    w.handleNewSignals()
725    w.saveSettings()
Note: See TracBrowser for help on using the repository browser.