source: orange/Orange/OrangeWidgets/Unsupervised/OWPCA.py @ 11217:adbdaf6efe02

Revision 11217:adbdaf6efe02, 16.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Addeed new widget icons by Peter Cuhalev and replaced existing ones with expanded paths and removed groups.

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