source: orange/Orange/OrangeWidgets/Prototypes/OWPCA.py @ 10807:1c740d845fae

Revision 10807:1c740d845fae, 13.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Added standard commit box/button

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