source: orange-bioinformatics/orangecontrib/bio/widgets/OWQualityControl.py @ 1873:0810c5708cc5

Revision 1873:0810c5708cc5, 24.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Moved '_bioinformatics' into orangecontrib namespace.

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