source: orange/Orange/OrangeWidgets/Prototypes/OWPCA.py @ 10832:c101ce95dd16

Revision 10832:c101ce95dd16, 15.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Added "All" special value to the max components spin box.

Line 
1"""
2<name>PCA</name>
3<description>Perform Principal Component Analysis</description>
4<contact>ales.erjavec(@ at @)fri.uni-lj.si</contact>
5<icons>icons/PCA.png</icons>
6<tags>pca,principal,component,projection</tags>
7
8"""
9import Orange
10import Orange.utils.addons
11
12from OWWidget import *
13import OWGUI
14
15import Orange
16import Orange.projection.linear as plinear
17
18import numpy as np
19import sys
20
21from plot.owplot import OWPlot
22from plot.owcurve import OWCurve
23from plot import owaxis
24
25
26class ScreePlot(OWPlot):
27    def __init__(self, parent=None, name="Scree Plot"):
28        OWPlot.__init__(self, parent, name=name)
29        self.cutoff_curve = CutoffCurve([0.0, 0.0], [0.0, 1.0],
30                x_axis_key=owaxis.xBottom, y_axis_key=owaxis.yLeft)
31        self.cutoff_curve.setVisible(False)
32        self.cutoff_curve.set_style(OWCurve.Lines)
33        self.add_custom_curve(self.cutoff_curve)
34
35    def is_cutoff_enabled(self):
36        return self.cutoff_curve and self.cutoff_curve.isVisible()
37
38    def set_cutoff_curve_enabled(self, state):
39        self.cutoff_curve.setVisible(state)
40
41    def set_cutoff_value(self, value):
42        xmin, xmax = self.x_scale()
43        x = min(max(value, xmin), xmax)
44        self.cutoff_curve.set_data([x, x], [0.0, 1.0])
45
46    def mousePressEvent(self, event):
47        if self.isLegendEvent(event, QGraphicsView.mousePressEvent):
48            return
49
50        if self.is_cutoff_enabled() and event.buttons() & Qt.LeftButton:
51            pos = self.mapToScene(event.pos())
52            x, _ = self.map_from_graph(pos)
53            xmin, xmax = self.x_scale()
54            if x >= xmin - 0.1 and x <= xmax + 0.1:
55                x = min(max(x, xmin), xmax)
56                self.cutoff_curve.set_data([x, x], [0.0, 1.0])
57                self.emit_cutoff_moved(x)
58        return QGraphicsView.mousePressEvent(self, event)
59
60    def mouseMoveEvent(self, event):
61        if self.isLegendEvent(event, QGraphicsView.mouseMoveEvent):
62            return
63
64        if self.is_cutoff_enabled() and event.buttons() & Qt.LeftButton:
65            pos = self.mapToScene(event.pos())
66            x, _ = self.map_from_graph(pos)
67            xmin, xmax = self.x_scale()
68            if x >= xmin - 0.5 and x <= xmax + 0.5:
69                x = min(max(x, xmin), xmax)
70                self.cutoff_curve.set_data([x, x], [0.0, 1.0])
71                self.emit_cutoff_moved(x)
72        return QGraphicsView.mouseMoveEvent(self, event)
73
74    def mouseReleaseEvene(self, event):
75        return QGraphicsView.mouseReleaseEvent(self, event)
76
77    def x_scale(self):
78        ax = self.axes[owaxis.xBottom]
79        if ax.labels:
80            return 0, len(ax.labels) - 1
81        elif ax.scale:
82            return ax.scale[0], ax.scale[1]
83        else:
84            raise ValueError
85
86    def emit_cutoff_moved(self, x):
87        self.emit(SIGNAL("cutoff_moved(double)"), x)
88
89    def set_axis_labels(self, *args):
90        OWPlot.set_axis_labels(self, *args)
91        self.map_transform = self.transform_for_axes()
92
93
94class CutoffCurve(OWCurve):
95    def __init__(self, *args, **kwargs):
96        OWCurve.__init__(self, *args, **kwargs)
97        self.setAcceptHoverEvents(True)
98        self.setCursor(Qt.SizeHorCursor)
99
100
101class OWPCA(OWWidget):
102    settingsList = ["standardize", "max_components", "variance_covered",
103                    "use_generalized_eigenvectors", "auto_commit"]
104
105    def __init__(self, parent=None, signalManager=None, title="PCA"):
106        OWWidget.__init__(self, parent, signalManager, title, wantGraph=True)
107
108        self.inputs = [("Input Data", Orange.data.Table, self.set_data)]
109        self.outputs = [("Transformed Data", Orange.data.Table, Default),
110                        ("Eigen Vectors", Orange.data.Table)]
111
112        self.standardize = True
113        self.max_components = 0
114        self.variance_covered = 100.0
115        self.use_generalized_eigenvectors = False
116        self.auto_commit = False
117
118        self.loadSettings()
119
120        self.data = None
121        self.changed_flag = False
122
123        #####
124        # GUI
125        #####
126        grid = QGridLayout()
127        box = OWGUI.widgetBox(self.controlArea, "Components Selection",
128                              orientation=grid)
129
130        label1 = QLabel("Max components", box)
131        grid.addWidget(label1, 1, 0)
132
133        sb1 = OWGUI.spin(box, self, "max_components", 0, 1000,
134                         tooltip="Maximum number of components",
135                         callback=self.on_update,
136                         addToLayout=False,
137                         keyboardTracking=False
138                         )
139        self.max_components_spin = sb1.control
140        self.max_components_spin.setSpecialValueText("All")
141        grid.addWidget(sb1.control, 1, 1)
142
143        label2 = QLabel("Variance covered", box)
144        grid.addWidget(label2, 2, 0)
145
146        sb2 = OWGUI.doubleSpin(box, self, "variance_covered", 1.0, 100.0, 1.0,
147                               tooltip="Percent of variance covered.",
148                               callback=self.on_update,
149                               decimals=1,
150                               addToLayout=False,
151                               keyboardTracking=False
152                               )
153        sb2.control.setSuffix("%")
154        grid.addWidget(sb2.control, 2, 1)
155
156        OWGUI.rubber(self.controlArea)
157
158        box = OWGUI.widgetBox(self.controlArea, "Commit")
159        cb = OWGUI.checkBox(box, self, "auto_commit", "Commit on any change")
160        b = OWGUI.button(box, self, "Commit",
161                         callback=self.update_components)
162        OWGUI.setStopper(self, b, cb, "changed_flag", self.update_components)
163
164        self.scree_plot = ScreePlot(self)
165#        self.scree_plot.set_main_title("Scree Plot")
166#        self.scree_plot.set_show_main_title(True)
167        self.scree_plot.set_axis_title(owaxis.xBottom, "Principal Components")
168        self.scree_plot.set_show_axis_title(owaxis.xBottom, 1)
169        self.scree_plot.set_axis_title(owaxis.yLeft, "Proportion of Variance")
170        self.scree_plot.set_show_axis_title(owaxis.yLeft, 1)
171
172        self.variance_curve = self.scree_plot.add_curve(
173                        "Variance",
174                        Qt.red, Qt.red, 2,
175                        xData=[],
176                        yData=[],
177                        style=OWCurve.Lines,
178                        enableLegend=True,
179                        lineWidth=2,
180                        autoScale=1,
181                        x_axis_key=owaxis.xBottom,
182                        y_axis_key=owaxis.yLeft,
183                        )
184
185        self.cumulative_variance_curve = self.scree_plot.add_curve(
186                        "Cumulative Variance",
187                        Qt.darkYellow, Qt.darkYellow, 2,
188                        xData=[],
189                        yData=[],
190                        style=OWCurve.Lines,
191                        enableLegend=True,
192                        lineWidth=2,
193                        autoScale=1,
194                        x_axis_key=owaxis.xBottom,
195                        y_axis_key=owaxis.yLeft,
196                        )
197
198        self.mainArea.layout().addWidget(self.scree_plot)
199        self.connect(self.scree_plot,
200                     SIGNAL("cutoff_moved(double)"),
201                     self.on_cutoff_moved
202                     )
203
204        self.connect(self.graphButton,
205                     SIGNAL("clicked()"),
206                     self.scree_plot.save_to_file)
207
208        self.components = None
209        self.variances = None
210        self.variances_sum = None
211        self.projector_full = None
212        self.currently_selected = 0
213
214        self.resize(800, 400)
215
216    def clear(self):
217        """Clear widget state
218        """
219        self.data = None
220        self.scree_plot.set_cutoff_curve_enabled(False)
221        self.clear_cached()
222        self.variance_curve.setVisible(False)
223        self.cumulative_variance_curve.setVisible(False)
224
225    def clear_cached(self):
226        """Clear cached components
227        """
228        self.components = None
229        self.variances = None
230        self.variances_cumsum = None
231        self.projector_full = None
232        self.currently_selected = 0
233
234    def set_data(self, data=None):
235        """Set the widget input data.
236        """
237        self.clear()
238        if data is not None:
239            self.data = data
240            self.on_change()
241        else:
242            self.send("Transformed Data", None)
243            self.send("Eigen Vectors", None)
244
245    def on_change(self):
246        """Data has changed and we need to recompute the projection.
247        """
248        if self.data is None:
249            return
250        self.clear_cached()
251        self.apply()
252
253    def on_update(self):
254        """Component selection was changed by the user.
255        """
256        if self.data is None:
257            return
258        self.update_cutoff_curve()
259        if self.currently_selected != self.number_of_selected_components():
260            self.update_components_if()
261
262    def construct_pca_all_comp(self):
263        pca = plinear.PCA(standardize=self.standardize,
264                          max_components=0,
265                          variance_covered=1,
266                          use_generalized_eigenvectors=self.use_generalized_eigenvectors
267                          )
268        return pca
269
270    def construct_pca(self):
271        max_components = self.max_components
272        variance_covered = self.variance_covered
273        pca = plinear.PCA(standardize=self.standardize,
274                          max_components=max_components,
275                          variance_covered=variance_covered / 100.0,
276                          use_generalized_eigenvectors=self.use_generalized_eigenvectors
277                          )
278        return pca
279
280    def apply(self):
281        """Apply PCA on input data, caching the full projection,
282        then updating the selected components.
283       
284        """
285        pca = self.construct_pca_all_comp()
286        self.projector_full = projector = pca(self.data)
287
288        self.variances = self.projector_full.variances
289        self.variances /= np.sum(self.variances)
290        self.variances_cumsum = np.cumsum(self.variances)
291
292        self.max_components_spin.setRange(0, len(self.variances))
293        self.update_scree_plot()
294        self.update_cutoff_curve()
295        self.update_components_if()
296
297    def update_components_if(self):
298        if self.auto_commit:
299            self.update_components()
300        else:
301            self.changed_flag = True
302
303    def update_components(self):
304        """Update the output components.
305        """
306        if self.data is None:
307            return
308
309        scale = self.projector_full.scale
310        center = self.projector_full.center
311        components = self.projector_full.projection
312        input_domain = self.projector_full.input_domain
313        variances = self.projector_full.variances
314        variance_sum = self.projector_full.variance_sum
315
316        # Get selected components (based on max_components and
317        # variance_coverd)
318        pca = self.construct_pca()
319        variances, components, variance_sum = pca._select_components(variances, components)
320
321        projector = plinear.PcaProjector(input_domain=input_domain,
322                                         standardize=self.standardize,
323                                         scale=scale,
324                                         center=center,
325                                         projection=components,
326                                         variances=variances,
327                                         variance_sum=variance_sum)
328        projected_data = projector(self.data)
329        eigenvectors = self.eigenvectors_as_table(components)
330
331        self.currently_selected = self.number_of_selected_components()
332
333        self.send("Transformed Data", projected_data)
334        self.send("Eigen Vectors", eigenvectors)
335
336        self.changed_flag = False
337
338    def eigenvectors_as_table(self, U):
339        features = [Orange.feature.Continuous("C%i" % i) \
340                    for i in range(1, U.shape[1] + 1)]
341        domain = Orange.data.Domain(features, False)
342        return Orange.data.Table(domain, [list(v) for v in U])
343
344    def update_scree_plot(self):
345        x_space = np.arange(0, len(self.variances))
346        self.scree_plot.set_axis_enabled(owaxis.xBottom, True)
347        self.scree_plot.set_axis_enabled(owaxis.yLeft, True)
348        self.scree_plot.set_axis_labels(owaxis.xBottom,
349                                        ["PC" + str(i + 1) for i in x_space])
350
351        self.variance_curve.set_data(x_space, self.variances)
352        self.cumulative_variance_curve.set_data(x_space, self.variances_cumsum)
353        self.variance_curve.setVisible(True)
354        self.cumulative_variance_curve.setVisible(True)
355
356        self.scree_plot.set_cutoff_curve_enabled(True)
357        self.scree_plot.replot()
358
359    def on_cutoff_moved(self, value):
360        """Cutoff curve was moved by the user.
361        """
362        components = int(np.floor(value)) + 1
363        # Did the number of components actually change
364        self.max_components = components
365        self.variance_covered = self.variances_cumsum[components - 1] * 100
366        if self.currently_selected != self.number_of_selected_components():
367            self.update_components_if()
368
369    def update_cutoff_curve(self):
370        """Update cutoff curve from 'Components Selection' control box.
371        """
372        if self.max_components == 0:
373            # Special "All" value
374            max_components = len(self.variances_cumsum)
375        else:
376            max_components = self.max_components
377
378        variance = self.variances_cumsum[max_components - 1] * 100.0
379        if variance < self.variance_covered:
380            cutoff = float(max_components - 1)
381        else:
382            cutoff = np.searchsorted(self.variances_cumsum,
383                                     self.variance_covered / 100.0)
384        self.scree_plot.set_cutoff_value(cutoff + 0.5)
385
386    def number_of_selected_components(self):
387        """How many components are selected.
388        """
389        if self.data is None:
390            return 0
391
392        variance_components = np.searchsorted(self.variances_cumsum,
393                                              self.variance_covered / 100.0)
394        if self.max_components == 0:
395            # Special "All" value
396            max_components = len(self.variances_cumsum)
397        else:
398            max_components = self.max_components
399        return min(variance_components + 1, max_components)
400
401    def sendReport(self):
402        self.reportSettings("PCA Settings",
403                            [("Max. components", self.max_components),
404                             ("Variance covered", "%i%%" % self.variance_covered),
405                             ])
406        if self.data is not None and self.projector_full:
407            output_domain = self.projector_full.output_domain
408            st_dev = np.sqrt(self.projector_full.variances)
409            summary = [[""] + [a.name for a in output_domain.attributes],
410                       ["Std. deviation"] + ["%.3f" % sd for sd in st_dev],
411                       ["Proportion Var"] + ["%.3f" % v for v in self.variances * 100.0],
412                       ["Cumulative Var"] + ["%.3f" % v for v in self.variances_cumsum * 100.0]
413                       ]
414
415            th = "<th>%s</th>".__mod__
416            header = "".join(map(th, summary[0]))
417            td = "<td>%s</td>".__mod__
418            summary = ["".join(map(td, row)) for row in summary[1:]]
419            tr = "<tr>%s</tr>".__mod__
420            summary = "\n".join(map(tr, [header] + summary))
421            summary = "<table>\n%s\n</table>" % summary
422
423            self.reportSection("Summary")
424            self.reportRaw(summary)
425
426            self.reportSection("Scree Plot")
427            self.reportImage(self.scree_plot.save_to_file_direct)
428
429
430if __name__ == "__main__":
431    app = QApplication(sys.argv)
432    w = OWPCA()
433    data = Orange.data.Table("iris")
434    w.set_data(data)
435    w.show()
436    app.exec_()
Note: See TracBrowser for help on using the repository browser.