source: orange-bioinformatics/orangecontrib/bio/widgets/OWQualityControl.py @ 1892:5c6f87657211

Revision 1892:5c6f87657211, 24.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 6 months ago (diff)

Replaced use of safe_text with xml.sax.saxutils.escape

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