#
source:
orange/Orange/OrangeWidgets/Unsupervised/OWMDS.py
@
11379:6bf8920120ac

Revision 11379:6bf8920120ac, 29.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff) |
---|

Rev | Line | |
---|---|---|

[8042] | 1 | """ |

2 | <name>MDS</name> | |

3 | <description>Multi dimensional scaling</description> | |

[11217] | 4 | <icon>icons/MDS.svg</icon> |

[8042] | 5 | <contact>Ales Erjavec (ales.erjavec(@at@)fri.uni-lj.si)</contact> |

6 | <priority>2500</priority> | |

7 | """ | |

[11379] | 8 | |

9 | import sys | |

10 | import math | |

11 | ||

12 | import numpy | |

13 | ||

[8042] | 14 | from OWWidget import * |

15 | import orange | |

16 | import orngMDS | |

17 | import OWGUI | |

[11379] | 18 | |

[8042] | 19 | import OWColorPalette |

20 | import OWToolbars | |

21 | from OWGraph import * | |

22 | from PyQt4.Qwt5 import * | |

[11379] | 23 | |

[8042] | 24 | from random import random |

25 | ||

26 | ||

27 | class OWMDS(OWWidget): | |

[11379] | 28 | settingsList = [ |

29 | "graph.PointSize", "graph.proportionGraphed", "graph.ColorAttr", | |

30 | "graph.SizeAttr", "graph.ShapeAttr", "graph.NameAttr", | |

31 | "graph.ShowStress", "graph.NumStressLines", "graph.ShowName", | |

32 | "graph.differentWidths", "graph.stressByTransparency", | |

33 | "graph.useAntialiasing" "StressFunc", "applyLSMT", | |

34 | "toolbarSelection", "autoSendSelection", "selectionOptions", | |

35 | "computeStress", "RefreshMode" | |

36 | ] | |

37 | ||

38 | contextHandlers = { | |

39 | "": DomainContextHandler( | |

40 | "", | |

41 | [ContextField("graph.ColorAttr", DomainContextHandler.Optional), | |

42 | ContextField("graph.SizeAttr", DomainContextHandler.Optional), | |

43 | ContextField("graph.ShapeAttr", DomainContextHandler.Optional), | |

44 | ContextField("graph.NameAttr", DomainContextHandler.Optional), | |

45 | ContextField("graph.ShowName", DomainContextHandler.Optional)] | |

46 | ) | |

47 | } | |

48 | ||

49 | def __init__(self, parent=None, signalManager=None, | |

50 | name="Multi Dimensional Scaling"): | |

[8042] | 51 | OWWidget.__init__(self, parent, signalManager, name, wantGraph=True) |

52 | ||

[11379] | 53 | self.inputs = [("Distances", orange.SymMatrix, self.cmatrix), |

54 | ("Data Subset", ExampleTable, self.cselected)] | |

55 | self.outputs = [("Data", ExampleTable)] | |

56 | ||

57 | self.StressFunc = 3 | |

58 | self.minStressDelta = 5e-5 | |

59 | self.maxIterations = 5000 | |

60 | self.maxImprovment = 10 | |

61 | self.autoSendSelection = 0 | |

62 | self.toolbarSelection = 0 | |

63 | self.selectionOptions = 0 | |

64 | self.computeStress = 1 | |

65 | self.ReDraw = 1 | |

66 | self.NumIter = 1 | |

67 | self.RefreshMode = 0 | |

[8042] | 68 | self.applyLSMT = 0 |

69 | ||

[11379] | 70 | self.stressFunc = [("Kruskal stress", orngMDS.KruskalStress), |

71 | ("Sammon stress", orngMDS.SammonStress), | |

72 | ("Signed Sammon stress", orngMDS.SgnSammonStress), | |

73 | ("Signed relative stress", orngMDS.SgnRelStress)] | |

74 | ||

75 | self.graph = MDSGraph(self.mainArea) | |

[8042] | 76 | self.mainArea.layout().addWidget(self.graph) |

[11379] | 77 | |

[8042] | 78 | self.loadSettings() |

79 | ||

[11379] | 80 | tabs = OWGUI.tabWidget(self.controlArea) |

[8042] | 81 | |

[11379] | 82 | mds = OWGUI.createTabPage(tabs, "MDS") |

83 | graph = OWGUI.createTabPage(tabs, "Graph") | |

84 | ||

85 | # MDS Tab | |

86 | init = OWGUI.widgetBox(mds, "Initialization") | |

[8042] | 87 | OWGUI.button(init, self, "Randomize", self.randomize) |

88 | OWGUI.button(init, self, "Jitter", self.jitter) | |

89 | OWGUI.button(init, self, "Torgerson", self.torgerson) | |

90 | ||

[11379] | 91 | opt = OWGUI.widgetBox(mds, "Optimization") |

92 | ||

93 | self.startButton = OWGUI.button(opt, self, "Optimize", self.testStart) | |

[8042] | 94 | OWGUI.button(opt, self, "Single Step", self.smacofStep) |

95 | box = OWGUI.widgetBox(opt, "Stress Function") | |

[11379] | 96 | OWGUI.comboBox(box, self, "StressFunc", |

97 | items=[a[0] for a in self.stressFunc], | |

98 | callback=self.updateStress) | |

[8042] | 99 | |

[11379] | 100 | OWGUI.radioButtonsInBox(opt, self, "RefreshMode", |

101 | ["Every step", "Every 10 steps", | |

102 | "Every 100 steps"], | |

103 | "Refresh During Optimization") | |

104 | ||

105 | self.stopping = OWGUI.widgetBox(opt, "Stopping Conditions") | |

106 | ||

107 | OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Min. stress change", | |

108 | flat=True), | |

109 | self, "minStressDelta", minValue=5e-5, maxValue=1e-2, | |

110 | step=5e-5, labelFormat="%.5f", intOnly=0) | |

111 | ||

112 | OWGUI.hSlider(OWGUI.widgetBox(self.stopping, "Max. number of steps", | |

113 | flat=True), | |

114 | self, "maxIterations", minValue=10, maxValue=5000, | |

115 | step=10, labelFormat="%i") | |

116 | ||

117 | # Graph Tab | |

118 | OWGUI.hSlider(graph, self, "graph.PointSize", box="Point Size", | |

119 | minValue=1, maxValue=20, callback=self.graph.updateData) | |

120 | ||

121 | self.colorCombo = OWGUI.comboBox(graph, self, "graph.ColorAttr", | |

122 | box="Color", | |

123 | callback=self.graph.updateData) | |

124 | self.sizeCombo = OWGUI.comboBox(graph, self, "graph.SizeAttr", | |

125 | box="Size", | |

126 | callback=self.graph.updateData) | |

127 | self.shapeCombo = OWGUI.comboBox(graph, self, "graph.ShapeAttr", | |

128 | box="Shape", | |

129 | callback=self.graph.updateData) | |

130 | self.nameCombo = OWGUI.comboBox(graph, self, "graph.NameAttr", | |

131 | box="Label", | |

132 | callback=self.graph.updateData) | |

133 | ||

[8042] | 134 | box = OWGUI.widgetBox(graph, "Distances & Stress") |

[11379] | 135 | |

136 | OWGUI.checkBox(box, self, "graph.ShowStress", "Show similar pairs", | |

137 | callback=self.graph.updateLinesRepaint) | |

[8042] | 138 | b2 = OWGUI.widgetBox(box) |

139 | OWGUI.widgetLabel(b2, "Proportion of connected pairs") | |

140 | OWGUI.separator(b2, height=3) | |

[11379] | 141 | OWGUI.hSlider(b2, self, "graph.proportionGraphed", minValue=0, |

142 | maxValue=20, | |

143 | callback=self.graph.updateLinesRepaint, | |

144 | tooltip=("Proportion of connected pairs (Maximum of " | |

145 | "1000 lines will be drawn")) | |

146 | OWGUI.checkBox(box, self, "graph.differentWidths", | |

147 | "Show distance by line width", | |

148 | callback=self.graph.updateLinesRepaint) | |

149 | OWGUI.checkBox(box, self, "graph.stressByTransparency", | |

150 | "Show stress by transparency", | |

151 | callback=self.graph.updateData) | |

152 | OWGUI.checkBox(box, self, "graph.stressBySize", | |

153 | "Show stress by symbol size", | |

154 | callback=self.updateStressBySize) | |

[8042] | 155 | self.updateStressBySize(True) |

156 | ||

[11379] | 157 | OWGUI.checkBox(graph, self, "graph.useAntialiasing", |

158 | label="Use antialiasing", | |

159 | box="Antialiasing", | |

160 | tooltip="Use antialiasing for beter quality graphics", | |

161 | callback=self.graph.updateData) | |

162 | ||

163 | self.zoomToolbar = OWToolbars.ZoomSelectToolbar( | |

164 | self, graph, self.graph, self.autoSendSelection | |

165 | ) | |

166 | ||

167 | self.connect(self.zoomToolbar.buttonSendSelections, | |

168 | SIGNAL("clicked()"), | |

169 | self.sendSelections) | |

170 | self.graph.autoSendSelectionCallback = \ | |

171 | lambda: self.autoSendSelection and self.sendSelections() | |

[8042] | 172 | |

173 | OWGUI.checkBox(graph, self, "autoSendSelection", "Auto send selected") | |

[11379] | 174 | OWGUI.radioButtonsInBox(graph, self, "selectionOptions", |

175 | ["Don't append", "Append coordinates", | |

176 | "Append coordinates as meta"], | |

177 | box="Append coordinates", | |

178 | callback=self.sendIf) | |

[8042] | 179 | |

[11379] | 180 | mds.setSizePolicy( |

181 | QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) | |

182 | ) | |

183 | graph.setSizePolicy( | |

184 | QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) | |

185 | ) | |

186 | ||

[8042] | 187 | self.controlArea.setMinimumWidth(250) |

[11379] | 188 | |

[8042] | 189 | OWGUI.rubber(mds) |

190 | ||

[11379] | 191 | infoBox = OWGUI.widgetBox(mds, "Info") |

192 | self.infoA = OWGUI.widgetLabel(infoBox, "Avg. stress:") | |

193 | self.infoB = OWGUI.widgetLabel(infoBox, "Num. steps") | |

194 | ||

195 | self.connect(self.graphButton, | |

196 | SIGNAL("clicked()"), | |

197 | self.graph.saveToFile) | |

198 | ||

199 | self.resize(900, 630) | |

200 | ||

201 | self.done = True | |

202 | self.data = None | |

203 | self.selectedInputExamples = [] | |

204 | self.selectedInput = [] | |

[8042] | 205 | |

206 | def cmatrix(self, matrix=None): | |

207 | self.closeContext() | |

[11379] | 208 | self.origMatrix = matrix |

209 | self.data = data = None | |

[8042] | 210 | if matrix: |

[11379] | 211 | self.data = data = getattr(matrix, "items") |

212 | matrix.matrixType = orange.SymMatrix.Symmetric | |

213 | ||

214 | self.graph.ColorAttr = 0 | |

215 | self.graph.SizeAttr = 0 | |

216 | self.graph.ShapeAttr = 0 | |

217 | self.graph.NameAttr = 0 | |

[8042] | 218 | self.graph.closestPairs = None |

[11379] | 219 | |

[8042] | 220 | if isinstance(data, orange.ExampleTable): |

221 | self.setExampleTable(data) | |

222 | elif isinstance(data, orange.VarList): | |

223 | self.setVarList(data) | |

[11379] | 224 | |

[8042] | 225 | if matrix: |

[11379] | 226 | self.mds = orngMDS.MDS(matrix) |

227 | self.mds.points = numpy.random.random( | |

228 | size=[self.mds.n, self.mds.dim] | |

229 | ) | |

230 | ||

[8042] | 231 | self.mds.getStress() |

[11379] | 232 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

[8042] | 233 | if data and type(data) == orange.ExampleTable: |

[11379] | 234 | self.openContext("", self.data) |

235 | self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, | |

236 | self.names, self.selectedInput) | |

[8042] | 237 | else: |

238 | self.graph.clear() | |

239 | ||

240 | def cselected(self, selected=[]): | |

[11379] | 241 | self.selectedInputExamples = selected and selected or[] |

242 | if self.data and type(self.data) == orange.ExampleTable: | |

[8042] | 243 | self.setExampleTable(self.data) |

[11379] | 244 | self.graph.setData(self.mds, self.colors, self.sizes, self.shapes, |

245 | self.names, self.selectedInput) | |

[8042] | 246 | |

247 | def setExampleTable(self, data): | |

248 | self.colorCombo.clear() | |

249 | self.sizeCombo.clear() | |

250 | self.shapeCombo.clear() | |

251 | self.nameCombo.clear() | |

[11379] | 252 | attributes = list(data.domain.variables) + \ |

253 | data.domain.getmetas().values() | |

254 | discAttributes = filter( | |

255 | lambda a: a.varType == orange.VarTypes.Discrete, | |

256 | attributes | |

257 | ) | |

258 | contAttributes = filter( | |

259 | lambda a: a.varType == orange.VarTypes.Continuous, | |

260 | attributes | |

261 | ) | |

262 | attrName = [attr.name for attr in attributes] | |

263 | for name in ["Same color"] + attrName: | |

[8042] | 264 | self.colorCombo.addItem(name) |

[11379] | 265 | for name in ["Same size"] + map(lambda a: a.name, contAttributes): |

[8042] | 266 | self.sizeCombo.addItem(name) |

[11379] | 267 | for name in ["Same shape"] + map(lambda a: a.name, discAttributes): |

[8042] | 268 | self.shapeCombo.addItem(name) |

[11379] | 269 | for name in ["No name"] + attrName: |

[8042] | 270 | self.nameCombo.addItem(name) |

271 | ||

272 | try: | |

[11379] | 273 | self.graph.NameAttr = \ |

274 | 1 + [name.lower() for name in attrName].index("name") | |

[8042] | 275 | except: |

276 | pass | |

277 | ||

[11379] | 278 | self.attributes = attributes |

279 | self.discAttributes = discAttributes | |

280 | self.contAttributes = contAttributes | |

281 | ||

282 | self.colors = [[Qt.black] * (len(attributes) + 1) | |

283 | for i in range(len(data))] | |

284 | self.shapes = [[QwtSymbol.Ellipse] * (len(discAttributes) + 1) | |

285 | for i in range(len(data))] | |

286 | self.sizes = [[5] * (len(contAttributes) + 1) | |

287 | for i in range(len(data))] | |

288 | self.names = [[""] * (len(attributes) + 1) | |

289 | for i in range(len(data))] | |

[8042] | 290 | try: |

[11379] | 291 | selectedInput = self.selectedInputExamples.select(data.domain) |

[8042] | 292 | except: |

[11379] | 293 | selectedInput = [] |

294 | self.selectedInput = map( | |

295 | lambda d: selectedInput and (d in selectedInput) or not selectedInput, | |

296 | data | |

297 | ) | |

298 | ||

299 | contI = discI = attrI = 1 | |

300 | ||

[8042] | 301 | def check(ex, a): |

302 | try: | |

303 | ex[a] | |

[11379] | 304 | except Exception: |

[8042] | 305 | return False |

306 | return not ex[a].isSpecial() | |

[11379] | 307 | |

[8042] | 308 | for j, attr in enumerate(attributes): |

[11379] | 309 | if attr.varType == orange.VarTypes.Discrete: |

310 | c = OWColorPalette.ColorPaletteHSV(len(attr.values)) | |

[8042] | 311 | for i in range(len(data)): |

[11379] | 312 | if check(data[i], attr): |

313 | self.colors[i][attrI] = c[int(data[i][attr])] | |

314 | self.shapes[i][discI] = self.graph.shapeList[int(data[i][attr]) % len(self.graph.shapeList)] | |

315 | self.names[i][attrI] = " " + str(data[i][attr]) | |

316 | else: | |

317 | self.colors[i][attrI] = Qt.black | |

318 | self.shapes[i][discI] = self.graph.shapeList[0] | |

319 | self.names[i][attrI] = "" | |

320 | attrI += 1 | |

321 | discI += 1 | |

322 | elif attr.varType == orange.VarTypes.Continuous: | |

323 | c = OWColorPalette.ColorPaletteBW(-1) | |

324 | ||

325 | val = [e[attr] for e in data if check(e, attr)] | |

326 | minVal = min(val or [0]) | |

327 | maxVal = max(val or [1]) | |

328 | span = max(maxVal - minVal, 1e-6) | |

[8042] | 329 | for i in range(len(data)): |

[11379] | 330 | if check(data[i], attr): |

331 | self.colors[i][attrI] = c.getColor((data[i][attr] - minVal) / span) | |

332 | self.names[i][attrI] = " " + str(data[i][attr]) | |

333 | self.sizes[i][contI] = int(self.data[i][attr] / maxVal * 9) + 1 | |

334 | else: | |

335 | self.colors[i][attrI] = Qt.black | |

336 | self.names[i][attrI] = "" | |

337 | self.sizes[i][contI] = 5 | |

338 | contI += 1 | |

339 | attrI += 1 | |

[8042] | 340 | else: |

341 | for i in range(len(data)): | |

[11379] | 342 | self.colors[i][attrI] = Qt.black |

343 | if check(data[i], attr): | |

344 | self.names[i][attrI] = " " + str(data[i][attr]) | |

345 | else: | |

346 | self.names[i][attrI] = "" | |

347 | ||

348 | attrI += 1 | |

349 | ||

[8042] | 350 | if data and data.domain.classVar: |

351 | if data.domain.classVar.varType == orange.VarTypes.Discrete: | |

[11379] | 352 | self.graph.ColorAttr = len(self.colors[0]) - 1 # index 0 is Same color! |

[8042] | 353 | elif data.domain.classVar.varType == orange.VarTypes.Continuous: |

[11379] | 354 | self.graph.SizeAttr = len(self.sizes[0]) - 1 # index 0 is Same color! |

[8042] | 355 | |

356 | def setVarList(self, data): | |

357 | self.colorCombo.clear() | |

358 | self.sizeCombo.clear() | |

359 | self.shapeCombo.clear() | |

360 | self.nameCombo.clear() | |

361 | for name in ["Same color", "Variable"]: | |

362 | self.colorCombo.addItem(name) | |

363 | for name in ["No name", "Var name"]: | |

364 | self.nameCombo.addItem(name) | |

[11379] | 365 | self.colors = [[Qt.black] * 3 for i in range(len(data))] |

366 | self.shapes = [[QwtSymbol.Ellipse] for i in range(len(data))] | |

367 | self.sizes = [[5] for i in range(len(data))] | |

368 | self.names = [[""] * 4 for i in range(len(data))] | |

369 | self.selectedInput = [False] * len(data) | |

[8042] | 370 | try: |

[11379] | 371 | c = OWColorPalette.ColorPaletteHSV(len(data)) |

[8042] | 372 | for i, d in enumerate(data): |

[11379] | 373 | self.colors[i][1] = c[i] |

374 | self.names[i][1] = " " + str(d.name) | |

[8042] | 375 | except Exception, val: |

376 | print val | |

377 | ||

[11379] | 378 | def updateStressBySize(self, noRepaint=False): |

[8042] | 379 | self.sizeCombo.setDisabled(self.graph.stressBySize) |

380 | if not noRepaint: | |

[9066] | 381 | self.graph.updateData() |

[11379] | 382 | |

[8042] | 383 | def smacofStep(self): |

384 | if not getattr(self, "mds", None): | |

385 | return | |

386 | for i in range(self.NumIter): | |

387 | self.mds.SMACOFstep() | |

388 | if self.computeStress: | |

389 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

[11379] | 390 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

391 | ||

[8042] | 392 | if self.ReDraw: |

393 | self.graph.updateData() | |

394 | ||

395 | ## I (Janez) disabled LSMT because it is implemented as it never should be: | |

396 | # orngMDS.LSMT transforms the distance matrix itself (indeed there is | |

397 | # the original stored, too), and from that point on there is no way the | |

398 | # user can "untransform" it, except for resending the signal | |

399 | # Since the basic problem is in bad design of orngMDS, I removed the option | |

[11379] | 400 | # from the widget. If somebody has time to fix orngMDS first, he's welcome. |

[8042] | 401 | def LSMT(self): |

402 | if not getattr(self, "mds", None): | |

403 | return | |

404 | self.mds.LSMT() | |

405 | if self.computeStress: | |

406 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

[11379] | 407 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

[8042] | 408 | if self.ReDraw: |

409 | self.graph.updateData() | |

410 | ||

411 | def torgerson(self): | |

412 | if not getattr(self, "mds", None): | |

413 | return | |

414 | self.mds.Torgerson() | |

415 | if self.computeStress: | |

416 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

[11379] | 417 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

[8042] | 418 | self.graph.updateData() |

419 | ||

420 | def randomize(self): | |

421 | if not getattr(self, "mds", None): | |

422 | return | |

[11379] | 423 | self.mds.points = numpy.random.random(size=[self.mds.n, 2]) |

[8042] | 424 | if self.computeStress: |

425 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

[11379] | 426 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

[8042] | 427 | self.graph.updateData() |

428 | ||

429 | def jitter(self): | |

430 | if not getattr(self, "mds", None): | |

431 | return | |

[9066] | 432 | mi = numpy.min(self.mds.points, axis=0) |

433 | ma = numpy.max(self.mds.points, axis=0) | |

434 | st = 0.05 * (ma - mi) | |

[8042] | 435 | for i in range(self.mds.n): |

436 | for j in range(2): | |

[11379] | 437 | self.mds.points[i][j] += st[j] * (random() - 0.5) |

[8042] | 438 | if self.computeStress: |

439 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

[11379] | 440 | self.stress = self.getAvgStress(self.stressFunc[self.StressFunc][1]) |

[8042] | 441 | self.graph.updateData() |

442 | ||

443 | def testStart(self): | |

444 | if not getattr(self, "mds", None): | |

445 | return | |

[11379] | 446 | if self.done == False: |

447 | self.done = True | |

[8042] | 448 | return |

[11379] | 449 | self.done = False |

[8042] | 450 | self.startButton.setText("Stop Optimization") |

451 | self.stopping.setDisabled(1) | |

452 | self.progressBarInit() | |

[11379] | 453 | self.iterNum = 0 |

454 | self.mds.progressCallback = self.callback | |

455 | # The name mangling for orange2.5 does not seem to work for | |

456 | # orangeom.MDS so I set this explicitly | |

457 | self.mds.progress_callback = self.callback | |

458 | ||

459 | self.mds.mds.optimize(self.maxIterations, | |

460 | self.stressFunc[self.StressFunc][1], | |

461 | self.minStressDelta) | |

462 | if self.iterNum % (math.pow(10, self.RefreshMode)): | |

[8042] | 463 | self.graph.updateData() |

464 | self.startButton.setText("Optimize") | |

465 | self.stopping.setDisabled(0) | |

466 | self.progressBarFinished() | |

[11379] | 467 | self.done = True |

[8042] | 468 | |

[11379] | 469 | def callback(self, a, b=None): |

470 | if not self.iterNum % (math.pow(10, self.RefreshMode)): | |

[8042] | 471 | self.graph.updateData() |

[11379] | 472 | self.iterNum += 1 |

[8042] | 473 | self.infoB.setText("Num. steps: %i" % self.iterNum) |

474 | self.infoA.setText("Avg. Stress: %f" % self.mds.avgStress) | |

[11379] | 475 | self.progressBarSet(int(a * 100)) |

[8042] | 476 | if self.done: |

477 | return 0 | |

478 | else: | |

479 | return 1 | |

480 | ||

481 | def getAvgStress(self, stressf=orngMDS.SgnRelStress): | |

482 | return self.mds.avgStress | |

483 | ||

484 | def sendIf(self, i=-1): | |

485 | if self.autoSendSelection: | |

486 | self.sendSelections() | |

[11379] | 487 | |

[8042] | 488 | def sendSelections(self): |

489 | if not getattr(self, "mds", None): | |

490 | return | |

[11379] | 491 | selectedInd = [] |

492 | for i, (x, y) in enumerate(self.mds.points): | |

493 | if self.graph.isPointSelected(x, y): | |

494 | selectedInd += [i] | |

495 | if type(self.data) == orange.ExampleTable: | |

[8042] | 496 | self.sendExampleTable(selectedInd) |

497 | ||

498 | def sendExampleTable(self, selectedInd): | |

[11379] | 499 | if self.selectionOptions == 0: |

[9546] | 500 | self.send("Data", orange.ExampleTable(self.data.getitems(selectedInd))) |

[8042] | 501 | else: |

[11379] | 502 | xAttr = orange.FloatVariable("X") |

503 | yAttr = orange.FloatVariable("Y") | |

504 | if self.selectionOptions == 1: | |

505 | domain = orange.Domain([xAttr, yAttr] + | |

506 | [v for v in self.data.domain.variables]) | |

[8042] | 507 | domain.addmetas(self.data.domain.getmetas()) |

508 | else: | |

[11379] | 509 | domain = orange.Domain(self.data.domain) |

[8042] | 510 | domain.addmeta(orange.newmetaid(), xAttr) |

511 | domain.addmeta(orange.newmetaid(), yAttr) | |

[11379] | 512 | selection = orange.ExampleTable(domain) |

[8042] | 513 | selection.extend(self.data.getitems(selectedInd)) |

514 | for i in range(len(selectedInd)): | |

[11379] | 515 | selection[i][xAttr] = self.mds.points[selectedInd[i]][0] |

516 | selection[i][yAttr] = self.mds.points[selectedInd[i]][1] | |

[9546] | 517 | self.send("Data", selection) |

[8042] | 518 | |

519 | def updateStress(self): | |

520 | if not getattr(self, "mds", None): | |

521 | return | |

522 | self.mds.getStress(self.stressFunc[self.StressFunc][1]) | |

523 | self.graph.replot() | |

524 | ||

525 | def sendReport(self): | |

[11379] | 526 | self.reportSettings( |

527 | "Optimization", | |

528 | [("Stress function", self.stressFunc[self.StressFunc][0]), | |

529 | ("Minimal stress change", self.minStressDelta), | |

530 | ("Maximal number of steps", self.maxIterations)] | |

531 | ) | |

532 | ||

533 | if self.graph.ColorAttr or self.graph.stressBySize or \ | |

534 | self.graph.SizeAttr or self.graph.ShapeAttr or \ | |

535 | self.graph.NameAttr or self.graph.ShowStress: | |

536 | self.reportSettings( | |

537 | "Visual settings", | |

538 | [self.graph.ColorAttr and ("Point color", self.colorCombo.currentText()), | |

539 | self.graph.stressBySize and ("Point size", "<stress>") | |

540 | or self.graph.SizeAttr and ("Point size", self.sizeCombo.currentText()), | |

541 | self.graph.ShapeAttr and ("Point shape", self.shapeCombo.currentText()), | |

542 | self.graph.NameAttr and ("Labels", self.nameCombo.currentText()), | |

543 | self.graph.ShowStress and ("Proportion of connected pairs", self.graph.proportionGraphed)] | |

544 | ) | |

545 | ||

[8042] | 546 | self.reportSection("Chart") |

547 | self.reportImage(self.graph.saveToFileDirect) | |

[11379] | 548 | |

[8042] | 549 | |

550 | class MDSGraph(OWGraph): | |

551 | def __init__(self, parent=None, name=None): | |

552 | OWGraph.__init__(self, parent, name) | |

553 | self.data = None | |

554 | self.mds = None | |

555 | self.PointSize = 5 | |

556 | self.ColorAttr = 0 | |

557 | self.SizeAttr = 0 | |

558 | self.ShapeAttr = 0 | |

559 | self.NameAttr = 0 | |

560 | self.ShowStress = False | |

561 | self.differentWidths = True | |

562 | self.stressByTransparency = True | |

563 | self.stressBySize = False | |

564 | self.NumStressLines = 10 | |

565 | self.proportionGraphed = 20 | |

566 | self.ShowName = True | |

[11379] | 567 | # self.curveKeys=[] |

[8042] | 568 | self.pointKeys = [] |

569 | self.points = [] | |

570 | self.lines = [] | |

571 | self.lineKeys = [] | |

572 | self.distanceLineKeys = [] | |

573 | self.colors = [] | |

574 | self.sizes = [] | |

575 | self.closestPairs = None | |

576 | self.shapeList = [QwtSymbol.Ellipse, | |

[11379] | 577 | QwtSymbol.Rect, |

578 | QwtSymbol.Diamond, | |

579 | QwtSymbol.Triangle, | |

580 | QwtSymbol.DTriangle, | |

581 | QwtSymbol.UTriangle, | |

582 | QwtSymbol.LTriangle, | |

583 | QwtSymbol.RTriangle, | |

584 | QwtSymbol.Cross, | |

585 | QwtSymbol.XCross] | |

586 | ||

[9066] | 587 | self.axis_margin = 10 |

[8042] | 588 | |

589 | def setData(self, mds, colors, sizes, shapes, names, showFilled): | |

590 | self.mds = mds | |

[11379] | 591 | # self.data=data |

[8042] | 592 | self.colors = colors |

593 | self.sizes = sizes | |

594 | self.shapes = shapes | |

595 | self.names = names | |

[11379] | 596 | self.showFilled = showFilled # map(lambda d: not d, showFilled) |

[8042] | 597 | self.updateData() |

598 | ||

599 | def updateData(self): | |

600 | self.clear() | |

601 | self.distanceLineKeys = [] | |

602 | if self.ShowStress: | |

603 | self.updateDistanceLines() | |

604 | self.setPoints() | |

605 | self.updateAxes() | |

606 | self.replot() | |

607 | ||

608 | def updateDistanceLines(self): | |

609 | if not self.mds: | |

610 | return | |

611 | N = len(self.mds.points) | |

[11379] | 612 | # draw maximum of 1000 closest pairs |

613 | np = min(int(N * (N - 1) / 2. * self.proportionGraphed / 100.), 1000) | |

614 | needlines = int(math.ceil((1 + math.sqrt(1 + 8 * np)) / 2)) | |

[8042] | 615 | |

616 | if self.closestPairs is None or len(self.closestPairs) < np: | |

617 | import heapq | |

618 | m = self.mds.originalDistances | |

619 | self.closestPairs = sorted(heapq.nsmallest(np, ((m[i, j], i, j) for i in range(m.dim) for j in range(i)))) | |

[11379] | 620 | |

[8042] | 621 | for c in self.distanceLineKeys: |

622 | try: | |

623 | c.detach() | |

[11379] | 624 | except RuntimeError: |

625 | # underlying C/C++ object has been deleted | |

[8042] | 626 | pass |

627 | self.distanceLineKeys = [] | |

[11379] | 628 | |

[8042] | 629 | hdist = self.closestPairs[:np] |

630 | if not hdist: | |

631 | return | |

[11379] | 632 | |

633 | black = QColor(192, 192, 192) | |

[8042] | 634 | if self.differentWidths: |

635 | mindist = hdist[0][0] | |

636 | maxdist = hdist[-1][0] | |

637 | else: | |

638 | mindist = maxdist = 0 | |

639 | if maxdist != mindist: | |

[11379] | 640 | k = 3 / (maxdist - mindist) ** 2 |

[8042] | 641 | for dist, i, j in hdist: |

642 | pti, ptj = self.mds.points[i], self.mds.points[j] | |

[11379] | 643 | c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, |

644 | xData=[pti[0], ptj[0]], yData=[pti[1], ptj[1]], | |

645 | lineWidth=max(1, (maxdist - dist) ** 2 * k)) | |

[8042] | 646 | c.setZ(10) |

647 | self.distanceLineKeys.append(c) | |

648 | else: | |

649 | for dist, i, j in hdist: | |

650 | pti, ptj = self.mds.points[i], self.mds.points[j] | |

[11379] | 651 | c = self.addCurve("A", black, black, 0, QwtPlotCurve.Lines, |

652 | xData=[pti[0], ptj[0]], yData=[pti[1], ptj[1]], | |

653 | lineWidth=2) | |

[8042] | 654 | c.setZ(10) |

655 | self.distanceLineKeys.append(c) | |

[11379] | 656 | |

[8042] | 657 | def updateLinesRepaint(self): |

658 | if self.mds: | |

659 | if self.ShowStress: | |

660 | self.updateDistanceLines() | |

661 | else: | |

662 | for c in self.distanceLineKeys: | |

663 | try: | |

664 | c.detach() | |

[11379] | 665 | except RuntimeError: |

666 | # underlying C/C++ object has been deleted | |

667 | pass | |

[8042] | 668 | self.distanceLineKeys = [] |

669 | self.replot() | |

670 | ||

671 | def setPoints(self): | |

672 | if not self.mds: | |

[11379] | 673 | return |

674 | if self.ShapeAttr == 0 and self.SizeAttr == 0 and \ | |

675 | self.NameAttr == 0 and not self.stressBySize and \ | |

676 | not self.stressByTransparency: | |

677 | colors = [c[self.ColorAttr] for c in self.colors] | |

[8042] | 678 | |

[11379] | 679 | set = [] |

[8042] | 680 | for c in colors: |

[11379] | 681 | if c not in set: |

[8042] | 682 | set.append(c) |

683 | ||

[11379] | 684 | dict = {} |

[8042] | 685 | for i in range(len(self.colors)): |

686 | hsv = QColor(self.colors[i][self.ColorAttr]).getHsv() | |

687 | if dict.has_key(hsv): | |

688 | dict[hsv].append(i) | |

689 | else: | |

[11379] | 690 | dict[hsv] = [i] |

691 | ||

[8042] | 692 | for color in set: |

[11379] | 693 | X = [self.mds.points[i][0] for i in dict[QColor(color).getHsv()] |

694 | if self.showFilled[i]] | |

695 | Y = [self.mds.points[i][1] for i in dict[QColor(color).getHsv()] | |

696 | if self.showFilled[i]] | |

697 | c = self.addCurve("A", color, color, self.PointSize, | |

698 | symbol=QwtSymbol.Ellipse, xData=X, yData=Y) | |

[8042] | 699 | c.setZ(100) |

[11379] | 700 | |

701 | X = [self.mds.points[i][0] for i in dict[QColor(color).getHsv()] | |

702 | if not self.showFilled[i]] | |

703 | Y = [self.mds.points[i][1] for i in dict[QColor(color).getHsv()] | |

704 | if not self.showFilled[i]] | |

705 | c = self.addCurve("A", color, color, self.PointSize, | |

706 | symbol=QwtSymbol.Ellipse, xData=X, yData=Y, | |

707 | showFilledSymbols=False) | |

[8042] | 708 | c.setZ(100) |

709 | else: | |

710 | if self.stressBySize or self.stressByTransparency: | |

711 | stresses = map(sum, self.mds.stress) | |

712 | mins, maxs = min(stresses), max(stresses) | |

[11379] | 713 | ks = self.PointSize / max(1, maxs - mins) |

714 | cs = 1 / max(1., maxs - mins) | |

[8042] | 715 | for i in range(len(self.colors)): |

716 | cq = QColor(self.colors[i][self.ColorAttr]) | |

717 | if self.stressByTransparency: | |

718 | cq.setAlpha(255 * (1 - cs * (stresses[i] - mins))) | |

719 | c = self.addCurve("a", cq, self.colors[i][self.ColorAttr], | |

[11379] | 720 | (max(5, ks * (1 + maxs - stresses[i])) |

721 | if self.stressBySize else | |

722 | self.sizes[i][self.SizeAttr] * 1.0 / 5 * self.PointSize), | |

723 | symbol=self.shapes[i][self.ShapeAttr], | |

724 | xData=[self.mds.points[i][0]], | |

725 | yData=[self.mds.points[i][1]], | |

726 | showFilledSymbols=self.showFilled[i]) | |

727 | ||

[8042] | 728 | c.setZ(100) |

[11379] | 729 | if self.NameAttr != 0: |

730 | c = self.addMarker(self.names[i][self.NameAttr], | |

731 | self.mds.points[i][0], | |

732 | self.mds.points[i][1], | |

733 | Qt.AlignBottom) | |

[8042] | 734 | c.setZ(100) |

735 | ||

[11379] | 736 | if len(self.mds.points) > 0: |

[8042] | 737 | X = [point[0] for point in self.mds.points] |

738 | Y = [point[1] for point in self.mds.points] | |

[9051] | 739 | max_x, min_x = max(X), min(X) |

740 | max_y, min_y = max(Y), min(Y) | |

741 | span_x = max_x - min_x | |

742 | span_y = max_y - min_y | |

[11379] | 743 | self.setAxisScale(QwtPlot.xBottom, |

744 | min_x - 0.05 * span_x, max_x + 0.05 * span_x) | |

745 | self.setAxisScale(QwtPlot.yLeft, | |

746 | min_y - 0.05 * span_y, max_y + 0.05 * span_y) | |

[8042] | 747 | |

748 | def sendData(self, *args): | |

749 | pass | |

750 | ||

[11379] | 751 | |

752 | if __name__ == "__main__": | |

753 | app = QApplication(sys.argv) | |

754 | w = OWMDS() | |

[8042] | 755 | w.show() |

[11379] | 756 | data = orange.ExampleTable("iris") |

757 | ||

[8042] | 758 | matrix = orange.SymMatrix(len(data)) |

759 | dist = orange.ExamplesDistanceConstructor_Euclidean(data) | |

760 | matrix = orange.SymMatrix(len(data)) | |

761 | matrix.setattr('items', data) | |

762 | for i in range(len(data)): | |

[11379] | 763 | for j in range(i + 1): |

[8042] | 764 | matrix[i, j] = dist(data[i], data[j]) |

765 | ||

766 | w.cmatrix(matrix) | |

767 | app.exec_() |

**Note:**See TracBrowser for help on using the repository browser.