source: orange/Orange/OrangeWidgets/Unsupervised/OWPCA.py @ 10835:162ece4df30c

Revision 10835:162ece4df30c, 15.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Fixed priority for PCA widget.

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