source: orange/Orange/OrangeWidgets/Data/OWMultiMergeData.py @ 11748:467f952c108d

Revision 11748:467f952c108d, 23.2 KB checked in by blaz <blaz.zupan@…>, 6 months ago (diff)

Changes in headers, widget descriptions text.

Line 
1import Orange
2from OWWidget import *
3from OWItemModels import PyListModel, VariableListModel
4
5NAME = "Multi-Key Merge Data"
6DESCRIPTION = "Merges data sets based on values of selected groups of features."
7LONG_DESCRIPTION = ""
8ICON = "icons/MergeData.svg"
9PRIORITY = 100
10MAINTAINER = "Peter Husen"
11MAINTAINER_EMAIL = "phusen(@at@)bmb.sdu.dk"
12INPUTS = [("Data A", Orange.data.Table, "set_dataA", Default),
13          ("Data B", Orange.data.Table, "set_dataB")]
14OUTPUTS = [("Merged Data A+B", Orange.data.Table, Default),
15           ("Merged Data B+A", Orange.data.Table, )]
16
17
18def slices(indices):
19    """ Group the given integer indices into slices
20    """
21    indices = list(sorted(indices))
22    if indices:
23        first = last = indices[0]
24        for i in indices[1:]:
25            if i == last + 1:
26                last = i
27            else:
28                yield first, last + 1
29                first = last = i
30        yield first, last + 1
31
32
33def delslice(model, start, end):
34    """ Delete the start, end slice (rows) from the model.
35    """
36    if isinstance(model, PyListModel):
37        del model[start: end]
38    elif isinstance(model, QAbstractItemModel):
39        model.removeRows(start, end-start)
40    else:
41        raise TypeError(type(model))
42
43
44class VariablesListItemModel(VariableListModel):
45    """ An Qt item model for for list of orange.Variable objects.
46    Supports drag operations
47    """
48       
49    def flags(self, index):
50        flags = VariableListModel.flags(self, index)
51        if index.isValid():
52            flags |= Qt.ItemIsDragEnabled
53        else:
54            flags |= Qt.ItemIsDropEnabled
55        return flags
56   
57    ###########
58    # Drag/Drop
59    ###########
60   
61    MIME_TYPE = "application/x-Orange-VariableListModelData"
62   
63    def supportedDropActions(self):
64        return Qt.MoveAction
65   
66    def supportedDragActions(self):
67        return Qt.MoveAction
68   
69    def mimeTypes(self):
70        return [self.MIME_TYPE]
71   
72    def mimeData(self, indexlist):
73        descriptors = []
74        vars = []
75        item_data = []
76        for index in indexlist:
77            var = self[index.row()]
78            descriptors.append((var.name, var.varType))
79            vars.append(var)
80            item_data.append(self.itemData(index))
81       
82        mime = QMimeData()
83        mime.setData(self.MIME_TYPE, QByteArray(str(descriptors)))
84        mime._vars = vars
85        mime._item_data = item_data
86        return mime
87   
88    def dropMimeData(self, mime, action, row, column, parent):
89        if action == Qt.IgnoreAction:
90            return True
91   
92        vars, item_data = self.items_from_mime_data(mime)
93        if vars is None:
94            return False
95       
96        if row == -1:
97            row = len(self)
98           
99        self[row: row] = vars
100       
101        for i, data in enumerate(item_data):
102            self.setItemData(self.index(row + i), data)
103           
104        return True
105   
106    def items_from_mime_data(self, mime):
107        if not mime.hasFormat(self.MIME_TYPE):
108            return None, None
109       
110        if hasattr(mime, "_vars"):
111            vars = mime._vars
112            item_data = mime._item_data
113            return vars, item_data
114        else:
115            #TODO: get vars from orange.Variable.getExisting
116            return None, None
117
118
119class VariablesListItemView(QListView):
120    """ A Simple QListView subclass initialized for displaying
121    variables.
122    """
123    def __init__(self, parent=None):
124        QListView.__init__(self, parent)
125        self.setSelectionMode(QListView.ExtendedSelection)
126        self.setAcceptDrops(True)
127        self.setDragEnabled(True)
128        self.setDropIndicatorShown(True)
129        self.setDragDropMode(QListView.DragDrop)
130        if hasattr(self, "setDefaultDropAction"):
131            # For compatibility with Qt version < 4.6
132            self.setDefaultDropAction(Qt.MoveAction)
133        self.setDragDropOverwriteMode(False)
134        self.viewport().setAcceptDrops(True)
135   
136    def startDrag(self, supported_actions):
137        indices = self.selectionModel().selectedIndexes()
138        indices = [i for i in indices if i.flags() & Qt.ItemIsDragEnabled]
139        if indices:
140            data = self.model().mimeData(indices)
141            if not data:
142                return
143            # rect = QRect()
144           
145            drag = QDrag(self)
146            drag.setMimeData(data)
147           
148            default_action = Qt.IgnoreAction
149            if hasattr(self, "defaultDropAction") and \
150                    self.defaultDropAction() != Qt.IgnoreAction and \
151                    supported_actions & self.defaultDropAction():
152                default_action = self.defaultDropAction()
153            elif supported_actions & Qt.CopyAction and dragDropMode() != QListView.InternalMove:
154                default_action = Qt.CopyAction
155           
156            res = drag.exec_(supported_actions, default_action)
157               
158            if res == Qt.MoveAction:
159                selected = self.selectionModel().selectedIndexes()
160                rows = map(QModelIndex.row, selected)
161               
162                for s1, s2 in reversed(list(slices(rows))):
163                    delslice(self.model(), s1, s2)
164   
165    def render_to_pixmap(self, indices):
166        pass
167
168
169from functools import partial
170class OWMultiMergeData(OWWidget):
171    contextHandlers = { "A": DomainContextHandler("A", [ContextField("domainA_role_hints")]),
172                        "B": DomainContextHandler("B", [ContextField("domainB_role_hints")]) }
173
174    def __init__(self, parent = None, signalManager = None, name = "Merge data"):
175        OWWidget.__init__(self, parent, signalManager, name, wantMainArea = 0)  #initialize base class
176
177        # set channels
178        self.inputs = [("Data A", ExampleTable, self.set_dataA),
179                       ("Data B", ExampleTable, self.set_dataB)]
180       
181        self.outputs = [("Merged Data A+B", ExampleTable),
182                        ("Merged Data B+A", ExampleTable)]
183
184        self.domainA_role_hints = {}
185        self.domainB_role_hints = {}
186       
187        # data
188        self.dataA = None
189        self.dataB = None
190
191        # load settings
192        self.loadSettings()
193
194        # ####
195        # GUI
196        # ####
197       
198        import sip
199        sip.delete(self.controlArea.layout())
200       
201        layout = QGridLayout()
202        layout.setMargin(0)
203
204        # Available A attributes
205        box = OWGUI.widgetBox(self.controlArea, "Features in data set A", addToLayout=False)
206        self.available_attrsA = VariablesListItemModel()
207        self.available_attrsA_view = VariablesListItemView()
208        self.available_attrsA_view.setModel(self.available_attrsA)
209       
210        self.connect(self.available_attrsA_view.selectionModel(),
211                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
212                     partial(self.update_interface_state,
213                             self.available_attrsA_view))
214       
215        box.layout().addWidget(self.available_attrsA_view)
216        layout.addWidget(box, 0, 0, 2, 1)
217
218
219        # Used A Attributes
220        box = OWGUI.widgetBox(self.controlArea, "Used features from A", addToLayout=False)
221        self.used_attrsA = VariablesListItemModel()
222        self.used_attrsA_view = VariablesListItemView()
223        self.used_attrsA_view.setModel(self.used_attrsA)
224        self.connect(self.used_attrsA_view.selectionModel(),
225                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
226                     partial(self.update_interface_state,
227                             self.used_attrsA_view))
228       
229        box.layout().addWidget(self.used_attrsA_view)
230        layout.addWidget(box, 0, 2, 1, 1)
231
232
233        # Data A info
234        box = OWGUI.widgetBox(self, 'Data A', orientation = "vertical", addToLayout=0)
235        self.lblDataAExamples = OWGUI.widgetLabel(box, "#data instances")
236        self.lblDataAAttributes = OWGUI.widgetLabel(box, "#features")
237        layout.addWidget(box, 1, 1, 1, 2)
238
239
240        # Available B attributes
241        box = OWGUI.widgetBox(self.controlArea, "Features in data set B", addToLayout=False)
242        self.available_attrsB = VariablesListItemModel()
243        self.available_attrsB_view = VariablesListItemView()
244        self.available_attrsB_view.setModel(self.available_attrsB)
245       
246        self.connect(self.available_attrsB_view.selectionModel(),
247                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
248                     partial(self.update_interface_state,
249                             self.available_attrsB_view))
250       
251        box.layout().addWidget(self.available_attrsB_view)
252        layout.addWidget(box, 2, 0, 2, 1)
253
254
255        # Used B Attributes
256        box = OWGUI.widgetBox(self.controlArea, "Used features from A", addToLayout=False)
257        self.used_attrsB = VariablesListItemModel()
258        self.used_attrsB_view = VariablesListItemView()
259        self.used_attrsB_view.setModel(self.used_attrsB)
260        self.connect(self.used_attrsB_view.selectionModel(),
261                     SIGNAL("selectionChanged(QItemSelection, QItemSelection)"),
262                     partial(self.update_interface_state,
263                             self.used_attrsB_view))
264
265
266        # Data B info
267        box.layout().addWidget(self.used_attrsB_view)
268        layout.addWidget(box, 2, 2, 1, 1)
269
270        box = OWGUI.widgetBox(self, 'Data B', orientation = "vertical", addToLayout=0)
271        self.lblDataBExamples = OWGUI.widgetLabel(box, "#data instances")
272        self.lblDataBAttributes = OWGUI.widgetLabel(box, "#features")
273        layout.addWidget(box, 3, 1, 1, 2)
274
275
276        # A buttons
277        bbox = OWGUI.widgetBox(self.controlArea, addToLayout=False, margin=0)
278        layout.addWidget(bbox, 0, 1, 1, 1)
279       
280        self.up_attrA_button = OWGUI.button(bbox, self, "Up", 
281                    callback = partial(self.move_up, self.used_attrsA_view))
282        self.move_attrA_button = OWGUI.button(bbox, self, ">",
283                    callback = partial(self.move_selected, self.used_attrsA_view))
284        self.down_attrA_button = OWGUI.button(bbox, self, "Down",
285                    callback = partial(self.move_down, self.used_attrsA_view))
286
287
288        # B buttons
289        bbox = OWGUI.widgetBox(self.controlArea, addToLayout=False, margin=0)
290        layout.addWidget(bbox, 2, 1, 1, 1)
291
292        self.up_attrB_button = OWGUI.button(bbox, self, "Up",
293                    callback = partial(self.move_up, self.used_attrsB_view))
294        self.move_attrB_button = OWGUI.button(bbox, self, ">",
295                    callback = partial(self.move_selected, self.used_attrsB_view))
296        self.down_attrB_button = OWGUI.button(bbox, self, "Down",
297                    callback = partial(self.move_down, self.used_attrsB_view))
298
299
300        # Apply / reset
301        bbox = OWGUI.widgetBox(self.controlArea, orientation="horizontal", addToLayout=False, margin=0)
302        applyButton = OWGUI.button(bbox, self, "Apply", callback=self.commit)
303        resetButton = OWGUI.button(bbox, self, "Reset", callback=self.reset)
304       
305        layout.addWidget(bbox, 4, 0, 1, 3)
306       
307        layout.setHorizontalSpacing(0)
308        self.controlArea.setLayout(layout)
309       
310        self.data = None
311        self.output_report = None
312
313        self.resize(500, 600)
314       
315        # For automatic widget testing using
316        self._guiElements.extend( \
317                  [(QListView, self.available_attrsA_view),
318                   (QListView, self.used_attrsA_view),
319                   (QListView, self.available_attrsB_view),
320                   (QListView, self.used_attrsB_view),
321                  ])
322
323
324    ############################################################################################################################################################
325    ## Data input and output management
326    ############################################################################################################################################################
327   
328   
329    def set_dataA(self, data=None):
330        self.update_domainA_role_hints()
331        self.closeContext("A")
332        self.dataA = data
333        if data is not None:
334            self.openContext("A", data)
335            all_vars = data.domain.variables + data.domain.getmetas().values()
336           
337            var_sig = lambda attr: (attr.name, attr.varType)
338           
339            domain_hints = dict([(var_sig(attr), ("available", i)) \
340                            for i, attr in enumerate(data.domain.attributes)])
341           
342            domain_hints.update(dict([(var_sig(attr), ("available", i)) \
343                for i, attr in enumerate(data.domain.getmetas().values())]))
344           
345            if data.domain.class_var:
346                domain_hints[var_sig(data.domain.class_var)] = ("available", 0)
347                   
348            domain_hints.update(self.domainA_role_hints) # update the hints from context settings
349           
350            attrs_for_role = lambda role: [(domain_hints[var_sig(attr)][1], attr) \
351                    for attr in all_vars if domain_hints[var_sig(attr)][0] == role]
352           
353            available = [attr for place, attr in sorted(attrs_for_role("available"))]
354            used = [attr for place, attr in sorted(attrs_for_role("used"))]
355           
356            self.available_attrsA[:] = available
357            self.used_attrsA[:] = used
358        else:
359            self.available_attrsA[:] = []
360            self.used_attrsA[:] = []
361       
362        self.updateInfoA()
363        self.commit()
364
365
366    def set_dataB(self, data=None):
367        self.update_domainB_role_hints()
368        self.closeContext("B")
369        self.dataB = data
370        if data is not None:
371            self.openContext("B", data)
372            all_vars = data.domain.variables + data.domain.getmetas().values()
373           
374            var_sig = lambda attr: (attr.name, attr.varType)
375           
376            domain_hints = dict([(var_sig(attr), ("available", i)) \
377                            for i, attr in enumerate(data.domain.attributes)])
378           
379            domain_hints.update(dict([(var_sig(attr), ("available", i)) \
380                for i, attr in enumerate(data.domain.getmetas().values())]))
381           
382            if data.domain.class_var:
383                domain_hints[var_sig(data.domain.class_var)] = ("available", 0)
384                   
385            domain_hints.update(self.domainB_role_hints) # update the hints from context settings
386           
387            attrs_for_role = lambda role: [(domain_hints[var_sig(attr)][1], attr) \
388                    for attr in all_vars if domain_hints[var_sig(attr)][0] == role]
389           
390            available = [attr for place, attr in sorted(attrs_for_role("available"))]
391            used = [attr for place, attr in sorted(attrs_for_role("used"))]
392           
393            self.available_attrsB[:] = available
394            self.used_attrsB[:] = used
395        else:
396            self.available_attrsB[:] = []
397            self.used_attrsB[:] = []
398
399        self.updateInfoB()
400        self.commit()
401
402
403    def updateInfoA(self):
404        """Updates data A info box.
405        """
406        if self.dataA:
407            self.lblDataAExamples.setText("%s example%s" % self._sp(self.dataA))
408            self.lblDataAAttributes.setText("%s attribute%s" % self._sp(self.available_attrsA[:] + self.used_attrsA[:]))
409        else:
410            self.lblDataAExamples.setText("No data on input A.")
411            self.lblDataAAttributes.setText("")
412
413
414    def updateInfoB(self):
415        """Updates data B info box.
416        """
417        if self.dataB:
418            self.lblDataBExamples.setText("%s example%s" % self._sp(self.dataB))
419            self.lblDataBAttributes.setText("%s attribute%s" % self._sp(self.available_attrsB[:] + self.used_attrsB[:]))
420        else:
421            self.lblDataBExamples.setText("No data on input B.")
422            self.lblDataBAttributes.setText("")
423
424
425    def update_domainA_role_hints(self):
426        """ Update the domain hints to be stored in the widgets settings.
427        """
428        hints_from_model = lambda role, model: \
429                [((attr.name, attr.varType), (role, i)) \
430                 for i, attr in enumerate(model)]
431       
432        hints = dict(hints_from_model("available", self.available_attrsA))
433        hints.update(hints_from_model("used", self.used_attrsA))
434        self.domainA_role_hints = hints
435
436    def update_domainB_role_hints(self):
437        """ Update the domain hints to be stored in the widgets settings.
438        """
439        hints_from_model = lambda role, model: \
440                [((attr.name, attr.varType), (role, i)) \
441                 for i, attr in enumerate(model)]
442       
443        hints = dict(hints_from_model("available", self.available_attrsB))
444        hints.update(hints_from_model("used", self.used_attrsB))
445        self.domainB_role_hints = hints
446
447    def move_rows(self, view, rows, offset):
448        model = view.model()
449        newrows = [min(max(0, row + offset), len(model) - 1) for row in rows]
450       
451        for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0):
452            model[row], model[newrow] = model[newrow], model[row]
453           
454        selection = QItemSelection()
455        for nrow in newrows:
456            index = model.index(nrow, 0)
457            selection.select(index, index)
458        view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
459
460    def move_up(self, view):
461        selected = self.selected_rows(view)
462        self.move_rows(view, selected, -1)
463   
464    def move_down(self, view):
465        selected = self.selected_rows(view)
466        self.move_rows(view, selected, 1)
467       
468    def selected_rows(self, view):
469        """ Return the selected rows in the view.
470        """
471        rows = view.selectionModel().selectedRows()
472        model = view.model()
473        return [r.row() for r in rows]
474
475    def move_selected(self, view):
476        fromto = {
477            self.available_attrsA_view: self.used_attrsA_view,
478            self.available_attrsB_view: self.used_attrsB_view,
479            self.used_attrsA_view: self.available_attrsA_view,
480            self.used_attrsB_view: self.available_attrsB_view
481        }
482        if self.selected_rows(view):
483            self.move_selected_from_to(view, fromto[view])
484        else:
485            self.move_selected_from_to(fromto[view], view)
486   
487    def move_selected_from_to(self, src, dst):
488        self.move_from_to(src, dst, self.selected_rows(src))
489       
490    def move_from_to(self, src, dst, rows):
491        src_model = src.model()
492        attrs = [src_model[r] for r in rows]
493
494        for s1, s2 in reversed(list(slices(rows))):
495            del src_model[s1:s2]
496
497        dst_model = dst.model()
498        dst_model.extend(attrs)
499
500
501    def reset(self):
502        if self.dataA is not None:
503            meta_attrsA = self.dataA.domain.getmetas().values()
504            self.available_attrsA[:] = self.dataA.domain.variables + meta_attrsA
505            self.used_attrsA[:] = []
506            self.update_domainA_role_hints()
507        if self.dataB is not None:
508            meta_attrsB = self.dataB.domain.getmetas().values()
509            self.available_attrsB[:] = self.dataB.domain.variables + meta_attrsB
510            self.used_attrsB[:] = []
511            self.update_domainB_role_hints()
512
513
514    def update_interface_state(self, focus=None, selected=None, deselected=None):
515        for view in [self.available_attrsA_view, self.used_attrsA_view,
516                     self.available_attrsB_view, self.used_attrsB_view]:
517            if view is not focus and not view.hasFocus():
518                view.selectionModel().clear()
519               
520        availableA_selected = bool(self.available_attrsA_view.selectionModel().selectedRows())
521        availableB_selected = bool(self.available_attrsB_view.selectionModel().selectedRows())
522                              #bool(self.selected_rows(self.available_attrs_view))
523                                 
524        move_attrA_enabled = bool(availableA_selected or \
525                                  self.used_attrsA_view.selectionModel().selectedRows())
526        move_attrB_enabled = bool(availableB_selected or \
527                                  self.used_attrsB_view.selectionModel().selectedRows())
528
529        self.move_attrA_button.setEnabled(move_attrA_enabled)
530        self.move_attrB_button.setEnabled(move_attrB_enabled)
531        if move_attrA_enabled:
532            self.move_attrA_button.setText(">" if availableA_selected else "<")
533        if move_attrB_enabled:
534            self.move_attrB_button.setText(">" if availableB_selected else "<")
535
536    def commit(self):
537        if self.dataA: self.update_domainA_role_hints()
538        if self.dataB: self.update_domainB_role_hints()
539        self.error(0)
540        if self.dataA and self.dataB and list(self.used_attrsA) and list(self.used_attrsB):
541            try:
542                self.send("Merged Data A+B", self.merge(self.dataA, self.dataB, self.used_attrsA, self.used_attrsB))
543                self.send("Merged Data B+A", self.merge(self.dataB, self.dataA, self.used_attrsB, self.used_attrsA))
544            except Orange.core.KernelException, ex:
545                self.error(0, "Cannot merge the two tables (%r)" % str(ex))
546        else:
547            self.send("Merged Data A+B", None)
548            self.send("Merged Data B+A", None)
549
550    ############################################################################################################################################################
551    ## Utility functions
552    ############################################################################################################################################################
553
554    def _sp(self, l, capitalize=True):
555        """Input: list; returns tuple (str(len(l)), "s"/"")
556        """
557        n = len(l)
558        if n == 0:
559            if capitalize:
560                return "No", "s"
561            else:
562                return "no", "s"
563        elif n == 1:
564            return str(n), ''
565        else:
566            return str(n), 's'
567
568    def merge(self, dataA, dataB, varsA, varsB):
569        """ Merge two tables
570        """
571       
572        vkey = lambda row, vs : tuple( row[v].native() for v in vs )
573       
574        val2idx = dict([( vkey(e,varsB) , i) for i, e in reversed(list(enumerate(dataB)))])
575       
576        #for key in ["?", "~", ""]:
577        #    if key in val2idx:
578        #        val2idx.pop(key)
579
580        metasA = dataA.domain.getmetas().items()
581        metasB = dataB.domain.getmetas().items()
582       
583        includedAttsB = [attrB for attrB in dataB.domain if attrB not in dataA.domain]
584        includedMetaB = [(mid, meta) for mid, meta in metasB if (mid, meta) not in metasA]
585        includedClassVarB = dataB.domain.classVar and dataB.domain.classVar not in dataA.domain
586       
587        reducedDomainB = Orange.data.Domain(includedAttsB, includedClassVarB)
588        reducedDomainB.addmetas(dict(includedMetaB))
589       
590       
591        mergingB = Orange.data.Table(reducedDomainB)
592       
593        for ex in dataA:
594            ind = val2idx.get( vkey(ex,varsA), None)
595            if ind is not None:
596                mergingB.append(Orange.data.Instance(reducedDomainB, dataB[ind]))
597               
598            else:
599                mergingB.append(Orange.data.Instance(reducedDomainB, ["?"] * len(reducedDomainB)))
600               
601        return Orange.data.Table([dataA, mergingB])
602   
603if __name__=="__main__":
604    import sys
605    a=QApplication(sys.argv)
606    ow=OWMultiMergeData()
607    ow.show()
608    data = Orange.data.Table("iris.tab")
609    data2 = Orange.data.Table("iris.tab")
610    ow.set_dataA(data)
611    ow.set_dataB(data2)
612    a.exec_()
613
614
Note: See TracBrowser for help on using the repository browser.