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

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

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

Line 
1"""<name>Genotype Distances</name>
2<description>Compute distances between expression profiles of different experimental factors.</description>
3<icon>icons/GenotypeDistances.png</icon>
4<priority>1050</priority>
5<contact>Ales Erjavec (ales.erjavec(@at@).fri.uni-lj.si)</contact>
6"""
7
8from OWWidget import *
9import OWGUI
10from OWItemModels import PyListModel
11import Orange
12
13from collections import defaultdict
14from operator import add
15import numpy
16import math
17
18from obiExperiments import separate_by, data_type, linearize, dist_pcorr, dist_eucl, dist_spearman
19
20def clone_attr(attr):
21    newattr = attr.clone()
22    def get_value_from(ex, w=None):
23        if attr in ex.domain:
24            v = str(ex[attr])
25        else:
26            v = "?"
27        return newattr(v)
28       
29    newattr.get_value_from = get_value_from
30    newattr.source_variable = attr
31    newattr.attributes.update(attr.attributes)
32    return newattr
33   
34def name_gen(a):
35    i = 1
36    while True:
37        yield a + " {0}".format(i)
38        i += 1
39       
40missing_name_gen = name_gen("!!missing")
41inactive_name_gen = name_gen("!!inactive")
42
43
44class MyHeaderView(QHeaderView):
45    def __init__(self, *args):
46        QHeaderView.__init__(self, *args)
47       
48    def mouseMoveEvent(self, event):
49        event.ignore()
50       
51    def wheelEvent(self, event):
52        event.ignore()
53       
54       
55class SetContextHandler(ContextHandler):
56    def match(self, context, imperfect, items):
57        items = set(items)
58        saved_items = set(getattr(context, "items", []))
59        if imperfect:
60            return float(len(items.intersection(saved_items)))/(len(items.union(saved_items)) or 1)
61        else:
62            return items == saved_items
63       
64    def findOrCreateContext(self, widget, items):       
65        index, context, score = self.findMatch(widget, self.findImperfect, items)
66        if context:
67            if index < 0:
68                self.addContext(widget, context)
69            else:
70                self.moveContextUp(widget, index)
71            return context, False
72        else:
73            context = self.newContext()
74            context.items = items
75            self.addContext(widget, context)
76            return context, True
77       
78class OWGenotypeDistances(OWWidget):
79    contextHandlers = {"": SetContextHandler("")}
80    settingsList = ["auto_commit"]
81   
82    DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr),
83                          ("Euclidean distance", dist_eucl),
84                          ("Distance from Spearman correlation", dist_spearman)]
85   
86    def __init__(self, parent=None, signalManager=None, title="Genotype Distances"):
87        OWWidget.__init__(self, parent, signalManager, title)
88       
89        self.inputs = [("Example Table", ExampleTable, self.set_data)]
90        self.outputs = [("Distances", Orange.core.SymMatrix), ("Sorted Example Table", ExampleTable)]
91       
92        self.distance_measure = 0
93        self.auto_commit = False
94        self.changed_flag = False
95       
96        self.loadSettings()
97       
98        ########
99        # GUI
100        ########
101       
102        self.info_box = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea, "Input",
103                                                         addSpace=True),
104                                         "No data on input\n")
105       
106        box = OWGUI.widgetBox(self.controlArea, "Separate By",
107                              addSpace=True)
108        self.separate_view = QListView()
109        self.separate_view.setSelectionMode(QListView.MultiSelection)
110        box.layout().addWidget(self.separate_view)
111       
112        box = OWGUI.widgetBox(self.controlArea, "Sort By",
113                              addSpace=True)
114        self.relevant_view = QListView()
115        self.relevant_view.setSelectionMode (QListView.MultiSelection)
116        box.layout().addWidget(self.relevant_view)
117       
118        self.distance_view = OWGUI.comboBox(self.controlArea, self, "distance_measure",
119                                            box="Distance Measure",
120                                            items=[d[0] for d in self.DISTANCE_FUNCTIONS])
121       
122        OWGUI.rubber(self.controlArea)
123       
124        box = OWGUI.widgetBox(self.controlArea, "Commit")
125        cb = OWGUI.checkBox(box, self, "auto_commit", "Commit on any change",
126                            tooltip="Compute and send the distances on any change.",
127                            callback=self.commit_if)
128       
129        b = OWGUI.button(box, self, "Commit",
130                         tooltip="Compute the distances and send the output signals.",
131                         callback=self.commit,
132                         default=True)
133       
134        OWGUI.setStopper(self, b, cb, "changed_flag", callback=self.commit)
135       
136        self.groups_box = OWGUI.widgetBox(self.mainArea, "Groups")
137        self.groups_scroll_area = QScrollArea()
138        self.groups_box.layout().addWidget(self.groups_scroll_area)
139       
140        self.data = None
141        self.partitions = []
142        self.matrix = None
143        self.split_groups = []
144        self._disable_updates = False
145       
146        self.resize(800, 600)
147       
148    def clear(self):
149        self.data = None
150        self.partitions = []
151        self.split_groups = []
152        self.matrix = None
153        self.send("Distances", None)
154       
155    def get_suitable_keys(self, data):
156        """ Return suitable attr label keys from the data where the key has at least
157        two unique values in the data.
158       
159        """
160        attrs = [attr.attributes.items() for attr in data.domain.attributes]
161        attrs  = reduce(list.__add__, attrs, [])
162        # in case someone put non string values in attributes dict
163        attrs = [(str(key), str(value)) for key, value in attrs]
164        attrs = set(attrs)
165        values = defaultdict(set)
166        for key, value in attrs:
167            values[key].add(value)
168        keys = [key for key in values if len(values[key]) > 1]
169        return keys
170       
171    def set_data(self, data=None):
172        """ Set the input example table.
173        """
174        self.closeContext()
175        self.clear()
176        self.data = data
177        self.error(0)
178        self.warning(0)
179        if data and not self.get_suitable_keys(data):
180            self.error(0, "Data has no suitable attribute labels.")
181            data = None
182           
183        if data:
184            self.info_box.setText("{0} genes\n{1} experiments".format(len(data), len(data.domain)))
185            self.update_control()
186            self.split_data()
187        else:
188            self.separate_view.setModel(PyListModel([]))
189            self.relevant_view.setModel(PyListModel([]))
190            self.groups_scroll_area.setWidget(QWidget())
191            self.info_box.setText("No data on input.\n")
192            self.commit()
193           
194    def update_control(self):
195        """ Update the control area of the widget. Populate the list
196        views with keys from attribute labels.
197       
198        """
199        keys = self.get_suitable_keys(self.data)
200         
201        model = PyListModel(keys)
202        self.separate_view.setModel(model)
203        self.connect(self.separate_view.selectionModel(),
204                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
205                     self.on_separate_key_changed)
206       
207        model = PyListModel(keys)
208        self.relevant_view.setModel(model)
209        self.connect(self.relevant_view.selectionModel(),
210                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
211                     self.on_relevant_key_changed)
212       
213        self.openContext("", keys)
214       
215        # Get the selected keys from the open context
216        context = self.currentContexts[""]
217        separate_keys = getattr(context, "separate_keys", set())
218        relevant_keys = getattr(context, "relevant_keys", set())
219       
220        def select(model, selection_model, selected_items):
221            all_items = list(model)
222            try:
223                indices = [all_items.index(item) for item in selected_items]
224            except:
225                indices = []
226            for ind in indices:
227                selection_model.select(model.index(ind), QItemSelectionModel.Select)
228               
229        self._disable_updates = True
230        try:
231            select(self.relevant_view.model(),
232                   self.relevant_view.selectionModel(),
233                   relevant_keys)
234           
235            select(self.separate_view.model(),
236                   self.separate_view.selectionModel(),
237                   separate_keys)
238        finally:
239            self._disable_updates = False
240       
241    def on_separate_key_changed(self, *args):
242        if not self._disable_updates:
243            context = self.currentContexts[""]
244            context.separate_keys = self.selected_separeate_by_keys()
245            self.split_data()
246   
247    def on_relevant_key_changed(self, *args):
248        if not self._disable_updates:
249            context = self.currentContexts[""]
250            context.relevant_keys = self.selected_relevant_keys()
251            self.split_data()
252       
253    def selected_separeate_by_keys(self):
254        """ Return the currently selected separate by keys
255        """
256        rows = self.separate_view.selectionModel().selectedRows()
257        rows = sorted([idx.row() for idx in rows])
258        keys = [self.separate_view.model()[row] for row in rows]
259        return keys
260       
261    def selected_relevant_keys(self):
262        """ Return the currently selected relevant keys
263        """
264        rows = self.relevant_view.selectionModel().selectedRows()
265        rows = sorted([idx.row() for idx in rows])
266        keys = [self.relevant_view.model()[row] for row in rows]
267        return keys
268   
269    def split_data(self):
270        """ Split the data and update the Groups widget
271        """
272        separate_keys = self.selected_separeate_by_keys()
273        relevant_keys = self.selected_relevant_keys()
274       
275        self.warning(0)
276        if not separate_keys:
277            self.warning(0, "No separate by attribute selected.")
278
279        partitions,uniquepos = separate_by(self.data, separate_keys, consider=relevant_keys)
280        partitions = partitions.items()
281
282        all_values = defaultdict(set)
283        for a in [ at.attributes for at in self.data.domain.attributes ]:
284            for k,v in a.iteritems():
285                all_values[k].add(v)
286
287        #sort groups
288        pkeys = [ key for key,_ in partitions ]
289        types = [ data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0])) ]
290
291        partitions = sorted(partitions, key=lambda x:
292                    tuple(types[i](v) for i,v in enumerate(x[0])))
293
294        split_groups = []
295       
296        # Collect relevant key value pairs for all columns
297        relevant_items = None
298
299        for keys, indices in partitions:
300            if relevant_items == None:
301                relevant_items = [ defaultdict(set) for _ in range(len(indices)) ]
302            for i, ind in enumerate(indices):
303                if ind is not None:
304                    attr = self.data.domain[ind]
305                    for key in relevant_keys:
306                        relevant_items[i][key].add(attr.attributes[key])
307
308        #those with different values between rows are not relevant
309        for d in relevant_items:
310            for k,s in d.items():
311                if len(s) > 1:
312                    del d[k]
313                else:
314                    d[k] = s.pop()
315
316        def get_attr(attr_index, i):
317            if attr_index is None:
318                attr = Orange.feature.Continuous(missing_name_gen.next())
319                attr.attributes.update(relevant_items[i])
320                return attr
321            else:
322                return self.data.domain[attr_index]
323       
324        for keys, indices in partitions:
325            attrs = [get_attr(attr_index, i) for i, attr_index in enumerate(indices)]
326            for attr in attrs:
327                attr.attributes.update(zip(separate_keys, keys))
328            domain = Orange.data.Domain(attrs, None)
329            domain.add_metas(self.data.domain.get_metas().items())
330#            newdata = Orange.data.Table(domain)
331            split_groups.append((keys, domain))
332         
333        self.set_groups(separate_keys, split_groups, relevant_keys, relevant_items, all_values, uniquepos)
334       
335        self.partitions = partitions
336        self.split_groups = split_groups
337       
338        self.commit_if()
339#        self.update_distances(separate_keys, partitions, self.data)
340       
341    def set_groups(self, keys, groups, relevant_keys, relevant_items, all_values, uniquepos):
342        """ Set the current data groups and update the Group widget
343        """
344        layout = QVBoxLayout()
345        header_widths = []
346        header_views = []
347        palette = self.palette()
348        all_values = all_values.keys()
349
350        def for_print(rd):
351            attrs = []
352            for d in rd:
353                attr = Orange.feature.Continuous(inactive_name_gen.next())
354                attr.attributes.update(d)
355                attrs.append(attr)
356            return Orange.data.Domain(attrs, None)
357
358        for separatev, domain in [ (None, for_print(relevant_items)) ] + groups:
359            label = None
360            if separatev != None:
361                ann_vals = " <b>|</b> ".join(["<b>{0}</ b> = {1}".format(key,val) \
362                     for key, val in zip(keys, separatev)])
363                label = QLabel(ann_vals)
364           
365            model = QStandardItemModel()
366            for i, attr in enumerate(domain.attributes):
367                item = QStandardItem()
368                if separatev != None:
369                    up = uniquepos[separatev][i]
370                else:
371                    up = False if False in [ a[i] for a in uniquepos.values() ] else True
372                if str(attr.name).startswith("!!missing "): ## TODO: Change this to not depend on name
373                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
374                                   for key in all_values if key not in relevant_items[i]]
375                    header_text = "\n".join(header_text) if header_text else "Empty"
376                    item.setData(QVariant(header_text), Qt.DisplayRole)
377                    item.setFlags(Qt.NoItemFlags)
378                    item.setData(QVariant(QColor(Qt.red)), Qt.ForegroundRole)
379                    item.setData(QVariant(palette.color(QPalette.Disabled, QPalette.Window)), Qt.BackgroundRole)
380                    item.setData(QVariant("Missing feature."), Qt.ToolTipRole)
381                elif str(attr.name).startswith("!!inactive "):
382                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
383                                   for key in all_values if key in relevant_items[i]]
384                    header_text = "\n".join(header_text) if header_text else "No descriptor"
385                    item.setData(QVariant(header_text), Qt.DisplayRole)
386                    item.setData(QVariant(palette.color(QPalette.Disabled, QPalette.Window)), Qt.BackgroundRole)
387                else:
388                    header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \
389                                   for key in all_values if key not in relevant_items[i]]
390                    header_text = "\n".join(header_text) if header_text else "Empty"
391                    item.setData(QVariant(header_text), Qt.DisplayRole)
392                    item.setData(QVariant(attr.name), Qt.ToolTipRole)
393
394                if up == False:
395                    item.setData(QVariant(QColor(Qt.red)), Qt.ForegroundRole)
396                   
397                model.setHorizontalHeaderItem(i, item)
398            attr_count = len(domain.attributes)
399            view = MyHeaderView(Qt.Horizontal)
400            view.setResizeMode(QHeaderView.Fixed)
401            view.setModel(model)
402            hint = view.sizeHint()
403            view.setMaximumHeight(hint.height())
404           
405            widths = [view.sectionSizeHint(i) for i in range(attr_count)]
406            header_widths.append(widths)
407            header_views.append(view)
408           
409            if label:
410                layout.addWidget(label)
411            layout.addWidget(view)
412            layout.addSpacing(8)
413           
414        # Make all header sections the same width
415        width_sum = 0
416        max_header_count = max([h.count() for h in header_views])
417        for i in range(max_header_count):
418            max_width = max([w[i] for w in header_widths if i < len(w)] or [0])
419            for view in header_views:
420                if i < view.count():
421                    view.resizeSection(i, max_width)
422            width_sum += max_width + 2
423               
424        for h in header_views:
425            h.setMinimumWidth(h.length() + 4)
426           
427        widget = QWidget()
428        widget.setLayout(layout)
429        widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum)
430        layout.activate()
431       
432        max_width = max(h.length() for h in header_views) + 20
433       
434        left, top, right, bottom  = self.getContentsMargins()
435        widget.setMinimumWidth(width_sum)
436        widget.setMinimumWidth(max_width + left + right)
437        self.groups_scroll_area.setWidget(widget)
438       
439    def compute_distances(self, separate_keys, partitions, data):
440        """ Compute the distances between genotypes.
441        """
442        if separate_keys and partitions:
443            self.progressBarInit()
444            matrix = Orange.core.SymMatrix(len(partitions))
445            profiles = [linearize(data, indices) for _, indices in partitions]
446            dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1]
447            from Orange.misc import progressBarMilestones
448            count = (len(profiles) * len(profiles) - 1) / 2
449            milestones = progressBarMilestones(count)
450            iter_count = 0
451            for i in range(len(profiles)):
452                for j in range(i + 1, len(profiles)):
453                    matrix[i, j] = dist_func(profiles[i], profiles[j])
454                    iter_count += 1
455                    if iter_count in milestones:
456                        self.progressBarSet(100.0 * iter_count / count)
457            self.progressBarFinished()
458           
459            items = [["{0}={1}".format(key, value) for key, value in zip(separate_keys, values)] \
460                      for values, _ in partitions]
461            items = [" | ".join(item) for item in items]
462            matrix.setattr("items", items)
463        else:
464            matrix = None
465           
466        self.matrix = matrix
467       
468    def commit_if(self):
469        if self.auto_commit and self.changed_flag:
470            self.commit()
471        else:
472            self.changed_flag = True
473           
474    def commit(self):
475        separate_keys = self.selected_separeate_by_keys()
476        self.compute_distances(separate_keys,
477                               self.partitions,
478                               self.data)
479       
480        if self.split_groups:
481            all_attrs = []
482            for group, domain in self.split_groups: 
483                attrs = []
484                group_name = " | ".join("{0}={1}".format(*item) for item in \
485                                        zip(separate_keys, group))
486                for attr in domain.attributes:
487                    newattr = clone_attr(attr)
488                    newattr.attributes["<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets.
489                    attrs.append(newattr)
490                   
491                all_attrs.extend(attrs)
492               
493            #all_attrs = reduce(add, [list(domain.attributes) for _, domain in self.split_groups], [])
494            domain = Orange.data.Domain(all_attrs, self.data.domain.class_var)
495            domain.add_metas(self.data.domain.get_metas().items())
496           
497            data = Orange.data.Table(domain, self.data)
498        else:
499            data = None
500        self.send("Sorted Example Table", data)
501        self.send("Distances", self.matrix)
502        self.changed_flag = False
503
504if __name__ == "__main__":
505    import os, sys
506    app = QApplication(sys.argv )
507    w = OWGenotypeDistances()
508#    data = Orange.data.Table(os.path.expanduser("~/Documents/dicty-express-sample.tab"))
509#    data = Orange.data.Table(os.path.expanduser("~/Downloads/tmp.tab"))
510    data = Orange.data.Table(os.path.expanduser("~/tgr.tab"))
511    w.set_data(data)
512    w.show()
513    app.exec_()
514    w.saveSettings()
515   
516#    data = Orange.data.Table("tmp.tab")
517#    partitions = separate_by(data, [ "genotype" ], consider=["tp", "replicate"]).items()
518#    print partitions
519#    l1 = linearize(data, partitions[0][1])
520#    l2 = linearize(data, partitions[1][1])
521#    print  dist_eucl(l1, l2)
522#    print  dist_pcorr(l1, l2)
523
524   
525
Note: See TracBrowser for help on using the repository browser.