source: orange-bioinformatics/widgets/OWMAPlot.py @ 1357:999499ff1e4c

Revision 1357:999499ff1e4c, 17.8 KB checked in by ales_erjavec <ales.erjavec@…>, 3 years ago (diff)

Show an error message when input data has no class variable and is missing attribute labels.

Line 
1"""<name>MA Plot</name>
2<description>Normalize expression array data on a MA - plot</description>
3<icon>icons/Normalize.png</icons>
4"""
5
6from OWWidget import *
7from OWGraph import *
8import OWGUI
9import numpy
10
11import obiExpression
12       
13import OWConcurrent
14
15from functools import partial
16
17class ProgressBarDiscard(QObject):
18    def __init__(self, parent, redirect):
19        QObject.__init__(self, parent)
20        self.redirect = redirect
21        self._delay = False
22       
23    @pyqtSignature("progressBarSet(float)")
24    def progressBarSet(self, value):
25        # Discard OWBaseWidget.progressBarSet call, because it calls qApp.processEvents
26        #which can result in 'event queue climbing' and max. recursion error if GUI thread
27        #gets another advance signal before it finishes with this one
28        if not self._delay:
29            try:
30                self._delay = True
31                self.redirect.progressBarSet(value)
32            finally:
33                self._delay = False
34       
35       
36class OWMAPlot(OWWidget):
37    settingsList = ["appendZScore", "appendRIValues"]
38    contextHandlers = {"": DomainContextHandler("", ["selectedGroup", "zCutoff"])}
39   
40    CENTER_METHODS = [("Average", obiExpression.MA_center_average),
41                      ("Lowess (fast - interpolated)", obiExpression.MA_center_lowess_fast),
42                      ("Lowess", obiExpression.MA_center_lowess)]
43   
44    MERGE_METHODS = [("Average", numpy.ma.average),
45                     ("Median", numpy.ma.median),
46                     ("Geometric mean", obiExpression.geometric_mean)]
47   
48    def __init__(self, parent=None, signalManager=None, name="Normalize Expression Array"):
49        OWWidget.__init__(self, parent, signalManager, name, wantGraph=True)
50       
51        self.inputs = [("Expression array", ExampleTable, self.setData)]
52        self.outputs = [("Normalized expression array", ExampleTable, Default), ("Filtered expression array", ExampleTable)]
53       
54        self.selectedGroup = 0
55        self.selectedCenterMethod = 0
56        self.selectedMergeMethod = 0
57        self.zCutoff = 1.96
58        self.appendZScore = False
59        self.appendRIValues = False
60        self.autoCommit = False
61       
62        self.loadSettings()
63        ## GUI
64        self.infoBox = OWGUI.widgetLabel(OWGUI.widgetBox(self.controlArea, "Info", addSpace=True),
65                                         "No data on input.")
66       
67        box = OWGUI.widgetBox(self.controlArea, "Split by", addSpace=True)
68        self.groupCombo = OWGUI.comboBox(box, self, "selectedGroup", 
69                                         callback=self.onGroupSelection
70                                         )
71       
72        self.centerCombo = OWGUI.comboBox(self.controlArea, self, "selectedCenterMethod",
73                                          box="Center Fold-change Using",
74                                          items=[name for name, _ in self.CENTER_METHODS],
75                                          callback=self.onCenterMethodChange,
76                                          addSpace=True
77                                          )
78       
79        self.mergeCombo = OWGUI.comboBox(self.controlArea, self, "selectedMergeMethod",
80                                         box="Merge Replicates",
81                                         items=[name for name, _ in self.MERGE_METHODS],
82                                         tooltip="Select the method for replicate merging",
83                                         callback=self.onMergeMethodChange,
84                                         addSpace=True
85                                         )
86       
87        box = OWGUI.doubleSpin(self.controlArea, self, "zCutoff", 0.0, 3.0, 0.01,
88                               box="Z-Score Cutoff",
89                               callback=[self.replotMA, self.commitIf])
90       
91        OWGUI.separator(self.controlArea)
92       
93        box = OWGUI.widgetBox(self.controlArea, "Ouput")
94        OWGUI.checkBox(box, self, "appendZScore", "Append Z-Scores",
95                       tooltip="Append calculated Z-Scores to output",
96                       callback=self.commitIf
97                       )
98       
99        OWGUI.checkBox(box, self, "appendRIValues", "Append Log Ratio and Intensity values",
100                       tooltip="Append calculated Log Ratio and Intensity values to output data",
101                       callback=self.commitIf
102                       )
103       
104        cb = OWGUI.checkBox(box, self, "autoCommit", "Commit on change",
105                       tooltip="Commit data on any change",
106                       callback=self.commitIf
107                       )
108       
109        b = OWGUI.button(box, self, "Commit", callback=self.commit)
110        OWGUI.setStopper(self, b, cb, "changedFlag", callback=self.commit)
111       
112        self.connect(self.graphButton, SIGNAL("clicked()"), self.saveGraph)
113       
114        OWGUI.rubber(self.controlArea)
115        self.graph = OWGraph(self.mainArea)
116        self.graph.setAxisTitle(QwtPlot.xBottom, "Intensity: log<sub>10</sub>(R*G)")
117        self.graph.setAxisTitle(QwtPlot.yLeft, "Log ratio: log<sub>2</sub>(R/G)")
118        self.graph.showFilledSymbols = True
119        self.mainArea.layout().addWidget(self.graph)
120        self.groups = []
121        self.split_data = None, None
122        self.merged_splits = None, None
123        self.centered = None, None
124        self.changedFlag = False
125        self.data = None
126       
127        self.resize(800, 600)
128       
129    def onFinished(self, status):
130        self.setEnabled(True)
131   
132    def onUnhandledException(self, ex_info):
133        self.setEnabled(True)
134        print >> sys.stderr, "Unhandled exception in non GUI thread"
135       
136        ex_type, ex_val, tb = ex_info
137        if ex_type == numpy.linalg.LinAlgError and False:
138            self.error(0, "Linear algebra error: %s" % repr(ex_val))
139        else:
140            sys.excepthook(*ex_info)
141   
142    def onGroupSelection(self):
143        if self.data:
144            self.updateInfoBox()
145            self.splitData()
146            self.runNormalization()
147       
148    def onCenterMethodChange(self):
149        if self.data:
150            self.runNormalization()
151       
152    def onMergeMethodChange(self):
153        if self.data:
154            self.splitData()
155            self.runNormalization()
156       
157    def proposeGroups(self, data):
158        col_labels = [attr.attributes.items() for attr in data.domain.attributes]
159        col_labels = sorted(reduce(set.union, col_labels, set()))
160        col_labels = [(key, value, 1) for key, value in col_labels]
161       
162        attrs = [attr for attr in data.domain.variables + data.domain.getmetas().values() \
163                 if attr.varType == orange.VarTypes.Discrete]
164       
165        row_labels = [(attr.name, value, 0) for attr in attrs for value in attr.values]
166       
167        def filterSingleValues(labels):
168            ret = []
169            for name, value, axis in labels:
170                match = [(n, v, a) for n, v, a in labels if n == name]
171                if len(match) > 1:
172                    ret.append((name, value, axis))
173            return ret
174           
175        col_labels = filterSingleValues(col_labels)
176        row_labels = filterSingleValues(row_labels)
177       
178        return col_labels + row_labels
179   
180    def setData(self, data):
181        self.closeContext("")
182        self.data = data
183        self.error([0 ,1])
184        if data is not None:
185            self.infoBox.setText("%i genes on input" % len(data))
186            self.groups = self.proposeGroups(data)
187            self.groupCombo.clear()
188            self.groupCombo.addItems(["%s: %s" % (key, value) for key, value, axis in self.groups])
189           
190            if not self.groups:
191                self.error(1, "Input data has no class attribute or attribute labels!")
192                self.clear()
193                return
194           
195            self.openContext("", data)
196            self.selectedGroup = min(self.selectedGroup, len(self.groups) - 1)
197           
198            self.updateInfoBox()
199            self.splitData()
200            self.runNormalization()
201        else:
202            self.clear()
203       
204    def clear(self):
205        self.groups = []
206        self.data = None
207        self.centered = None, None
208        self.split_data = None, None
209        self.merged_splits = None, None
210        self.graph.removeDrawingCurves()
211        self.infoBox.setText("No data on input")
212        self.send("Normalized expression array", None)
213        self.send("Filtered expression array", None)
214       
215    def updateInfoBox(self):
216        genes = self.getGeneNames()
217        self.infoBox.setText("%i genes on input" % len(self.data))
218       
219    def getSelectedGroup(self):
220        return self.groups[self.selectedGroup]
221   
222    def getSelectedGroupSplit(self):
223        key, value, axis = self.getSelectedGroup()
224        other_values = [v for k, v, a in self.groups if k == key and a == axis and v != value]
225        return [(key, value), (key, other_values)], axis
226   
227    def getGeneNames(self):
228        key, value, axis = self.getSelectedGroup()
229        if axis == 0:
230            genes = [str(ex[key]) for ex in self.data]
231        else:
232            genes = [attr.name for attr in self.data.domain.attributes]
233           
234        return genes
235   
236    def splitData(self): 
237        groups, axis = self.getSelectedGroupSplit()
238        self.split_ind = [obiExpression.select_indices(self.data, key, value, axis) for key, value in groups]
239        self.split_data = obiExpression.split_data(self.data, groups, axis)
240       
241    def getMerged(self):
242        split1, split2 = self.split_data
243        (array1, _, _), (array2, _, _) = split1.toNumpyMA(), split2.toNumpyMA()
244       
245        _, _, axis = self.getSelectedGroup()
246        merge_function = self.MERGE_METHODS[self.selectedMergeMethod][1]
247       
248        merged1 = obiExpression.merge_replicates(array1, axis, merge_function=merge_function)
249        merged2 = obiExpression.merge_replicates(array2, axis, merge_function=merge_function)
250        self.merged_splits = merged1, merged2
251       
252        return self.merged_splits
253       
254    def runNormalization(self):
255        self.progressBarInit()
256        self.progressBarSet(0.0)
257        G, R = self.getMerged()
258        self.progressBarSet(5.0)
259       
260        center_method = self.CENTER_METHODS[self.selectedCenterMethod][1]
261       
262        # TODO: progess bar , lowess can take a long time
263        if self.selectedCenterMethod in [1, 2]: #Lowess
264            Gc, Rc = center_method(G, R, f = 1./min(500., len(G)/100), iter=1)
265        else:
266            Gc, Rc = center_method(G, R)
267        self.progressBarSet(70.0)
268        self.centered = Gc, Rc
269        self.z_scores = obiExpression.MA_zscore(Gc, Rc, 1./3.)
270        self.progressBarSet(100.0)
271        self.plotMA(Gc, Rc, self.z_scores, self.zCutoff)
272        self.progressBarFinished()
273       
274    def runNormalizationAsync(self):
275        """ Run MA centering and z_score estimation in a separate thread
276        """
277        self.error(0)
278        self.progressBarInit()
279        self.progressBarSet(0.0)
280        G, R = self.getMerged()
281#        self.progressBarSet(5.0)
282       
283        center_method = self.CENTER_METHODS[self.selectedCenterMethod][1]
284        use_lowess = self.selectedCenterMethod in [1, 2]
285       
286        def run(progressCallback = lambda value: None): # the function to run in a thread
287#            progressCallback(5.0)
288            if use_lowess:
289                Gc, Rc = center_method(G, R, f=2./3., iter=1, progressCallback=lambda val: progressCallback(val/2))
290            else:
291                Gc, Rc = center_method(G, R)
292            progressCallback(50)
293            z_scores = obiExpression.MA_zscore(Gc, Rc, 1./3., progressCallback= lambda val: progressCallback(50 + val/2))
294           
295            return Gc, Rc, z_scores
296       
297        self.progressDiscard = ProgressBarDiscard(self, self)
298         
299        async = self.asyncCall(run, name="Normalization",
300                               onResult=self.onResults,
301                               onError=self.onUnhandledException)
302        self.connect(async, SIGNAL("progressChanged(float)"), self.progressDiscard.progressBarSet, Qt.QueuedConnection)
303        self.setEnabled(False)
304        async.__call__(progressCallback=async.emitProgressChanged)
305           
306    ## comment out this line if threading creates any problems
307    runNormalization = runNormalizationAsync
308   
309    def onResults(self, (Gc, Rc, z_scores)):
310        """ Handle the results of centering and z-scoring
311        """
312        assert(QThread.currentThread() is self.thread())
313        self.setEnabled(True)
314        qApp.processEvents()
315        self.progressBarFinished()
316        self.centered = Gc, Rc
317        self.z_scores = z_scores
318        self.plotMA(Gc, Rc, z_scores, self.zCutoff)
319        self.commit()
320       
321   
322    def plotMA(self, G, R, z_scores, z_cuttof):
323        ratio, intensity = obiExpression.ratio_intensity(G, R)
324       
325        filter = numpy.isfinite(ratio) & numpy.isfinite(intensity) & numpy.isfinite(z_scores)
326        for array in [ratio, intensity, z_scores]:
327            if numpy.ma.is_masked(array):
328                filter &= -array.mask
329       
330        filtered_ind = numpy.ma.where(filter)
331        ratio = numpy.take(ratio, filtered_ind)
332        intensity = numpy.take(intensity, filtered_ind)
333        z_scores = numpy.take(z_scores, filtered_ind)
334       
335        red_ind = numpy.ma.where(numpy.ma.abs(z_scores) >= z_cuttof)
336        blue_ind = numpy.ma.where(numpy.ma.abs(z_scores) < z_cuttof)
337       
338        red_xdata, red_ydata = intensity[red_ind], ratio[red_ind]
339        blue_xdata, blue_ydata = intensity[blue_ind], ratio[blue_ind]
340        self.graph.removeDrawingCurves()
341       
342        c = self.graph.addCurve("", Qt.black, Qt.black, xData=[0.0, 1.0], yData=[0.0, 0.0], style=QwtPlotCurve.Lines, symbol=QwtSymbol.NoSymbol)
343        c.setAxis(QwtPlot.xTop, QwtPlot.yLeft)
344       
345        self.graph.addCurve("Z >= %.2f" % z_cuttof, QColor(Qt.red), Qt.red, brushAlpha=100, size=6, enableLegend=True, xData=list(red_xdata), yData=list(red_ydata), autoScale=True)
346        self.graph.addCurve("Z < %.2f" % z_cuttof, QColor(Qt.blue), Qt.blue, brushAlpha=100, size=6, enableLegend=True, xData=list(blue_xdata), yData=list(blue_ydata), autoScale=True)
347       
348        self.graph.setAxisScale(QwtPlot.xTop, 0.0, 1.0)
349         
350        self.graph.replot()
351       
352       
353    def replotMA(self):
354        if self.data and self.centered:
355            Gc, Rc = self.centered
356            self.plotMA(Gc, Rc, self.z_scores, self.zCutoff)
357       
358       
359    def commitIf(self):
360        if self.autoCommit and self.changedFlag:
361            self.commit()
362        else:
363            self.changedFlag = True
364           
365           
366    def commit(self):
367        if not self.data:
368            return
369       
370        G, R = self.merged_splits
371        Gc, Rc = self.centered
372        ind1, ind2 = self.split_ind
373       
374        gfactor = Gc / G
375       
376        domain = orange.Domain(self.data.domain.attributes, self.data.domain.classVar)
377        domain.addmetas(self.data.domain.getmetas())
378       
379        _, _, axis = self.getSelectedGroup()
380        if self.appendZScore and axis == 1:
381            attr = orange.FloatVariable("Z-Score")
382            if not hasattr(self, "z_score_mid"):
383                self.z_score_mid = orange.newmetaid()
384            mid = self.z_score_mid
385            domain.addmeta(mid, attr)
386           
387        if self.appendRIValues and axis == 1:
388            r_attr = orange.FloatVariable("Log Ratio")
389            i_attr = orange.FloatVariable("Intensity")
390            if not hasattr(self, "_r_mid"):
391                self._r_mid = orange.newmetaid()
392                self._i_mid = orange.newmetaid()
393            r_mid, i_mid = self._r_mid, self._i_mid
394            domain.addmeta(r_mid, r_attr)
395            domain.addmeta(i_mid, i_attr)
396           
397        data = orange.ExampleTable(domain, self.data)
398       
399        def finite_nonmasked(g):
400            return numpy.isfinite(g) and g is not numpy.ma.masked
401       
402        if axis == 0:
403            for i, gf in zip(ind1, gfactor):
404                for attr in domain.attributes:
405                    if not data[i][attr].isSpecial() and finite_nonmasked(gf):
406                        data[i][attr] = data[i][attr] * gf
407        else:
408            ratio, intensity = obiExpression.ratio_intensity(*self.centered) #*self.merged_splits)
409            for ex, gf, r, inten, z in zip(data, gfactor, ratio, intensity, self.z_scores):
410                for i in ind1:
411                    if not ex[i].isSpecial() and finite_nonmasked(gf):
412                        ex[i] = float(ex[i]) * gf
413                if self.appendZScore:
414                    ex[attr] = z if finite_nonmasked(z) else "?"
415                   
416                if self.appendRIValues:
417                    ex[r_attr] = r if finite_nonmasked(r) else "?"
418                    ex[i_attr] = inten if finite_nonmasked(inten) else "?"
419       
420        filtered_ind = list(numpy.ma.abs(self.z_scores.filled(0)) >= self.zCutoff)
421        if axis == 0:
422            attrs = [attr for attr, filtered in zip(domain.attributes, filtered_ind) if filtered]
423            filtered_domain = orange.Domain(attrs, domain.classVar)
424            filtered_domain.addmetas(domain.getmetas())
425            filtered_data = orange.ExampleTable(filtered_domain, data)
426        else:
427            filtered_data = data.select([int(b) for b in filtered_ind])
428           
429        self.send("Normalized expression array", data)
430        self.send("Filtered expression array", filtered_data)
431       
432       
433    def saveGraph(self):
434        from OWDlgs import OWChooseImageSizeDlg
435        dlg = OWChooseImageSizeDlg(self.graph, parent=self)
436        dlg.exec_()
437       
438       
439if __name__ == "__main__":
440    app = QApplication(sys.argv)
441    w= OWMAPlot()
442    data = orange.ExampleTable(os.path.expanduser("~/GDS1210.tab"))
443#    data = orange.ExampleTable(os.path.expanduser("../../../doc/datasets/brown-selected.tab"))
444    w.setData(data)
445    w.show()
446    app.exec_()
447       
448       
Note: See TracBrowser for help on using the repository browser.