source: orange/Orange/OrangeWidgets/Data/OWFeatureConstructor.py @ 11713:d2503813af19

Revision 11713:d2503813af19, 10.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Cleanup the doc string indentation for use in tooltips.

RevLine 
[7870]1"""
2<name>Feature Constructor</name>
3<description>Construct a new continuous attribute computed from existing attributes.</description>
[11096]4<icon>icons/FeatureConstructor.svg</icon>
[7870]5<priority>3100</priority>
6<contact>Janez Demsar (janez.demsar(@at@)fri.uni-lj.si)</contact>
7"""
8
9from OWWidget import *
10import OWGUI, math, re
11import random
[11713]12import inspect
[7870]13
14re_identifier = re.compile(r'([a-zA-Z_]\w*)|("[^"]+")')
15
16class IdentifierReplacer:
17    def __init__(self, reinserted, attributes):
18        self.reinserted = reinserted
19        self.attributes = attributes
20
21    def __call__(self, id):
22        id = id.group()
23        if id in self.reinserted:
24            return "(%s)" % self.reinserted[id]
25        if (id[0] == id[-1] == '"') and (id[1:-1] in self.attributes):
26            return "_ex[%s]" % id
27        if id in self.attributes:
28            return "_ex['%s']" % id
29        return id
30
31class AttrComputer:
32    FUNCTIONS = dict([(key, val) for key, val in math.__dict__.items() if not key.startswith("_")] +\
33                      {"normalvariate": random.normalvariate,
34                       "gauss": random.gauss,
35                       "expovariate": random.expovariate,
36                       "gammavariate": random.gammavariate,
37                       "betavariate": random.betavariate,
38                       "lognormvariate": random.lognormvariate,
39                       "paretovariate": random.paretovariate,
40                       "vonmisesvariate": random.vonmisesvariate,
41                       "weibullvariate": random.weibullvariate,
42                       "triangular": random.triangular,
43                       "uniform": random.uniform}.items())
44    def __init__(self, expression):
45        self.expression = expression
46
47    def __call__(self, ex, weight):
48        try:
49            return float(eval(self.expression, self.FUNCTIONS, {"_ex": ex}))
50        except Exception, ex:
51            return "?"
52
53class OWFeatureConstructor(OWWidget):
54    contextHandlers = {"": PerfectDomainContextHandler()}
55
56    def __init__(self,parent=None, signalManager = None):
57        OWWidget.__init__(self, parent, signalManager, "FeatureConstructor")
58
[9546]59        self.inputs = [("Data", orange.ExampleTable, self.setData)]
60        self.outputs = [("Data", ExampleTable)]
[7870]61
62        self.expression = self.attrname = ""
63        self.selectedDef = []
64        self.defLabels = []
65        self.data = None
66        self.definitions = []
67
68        self.selectedAttr = 0
69        self.selectedFunc = 0
70        self.autosend = True
71        self.loadSettings()
72
73        db = OWGUI.widgetBox(self.controlArea, "Attribute definitions", addSpace = True)
74
75        hb = OWGUI.widgetBox(db, None, "horizontal")
76        hbv = OWGUI.widgetBox(hb)
77        self.leAttrName = OWGUI.lineEdit(hbv, self, "attrname", "New attribute")
78        OWGUI.rubber(hbv)
79        vb = OWGUI.widgetBox(hb, None, "vertical", addSpace=True)
80        self.leExpression = OWGUI.lineEdit(vb, self, "expression", "Expression")
81        hhb = OWGUI.widgetBox(vb, None, "horizontal")
82        self.cbAttrs = OWGUI.comboBox(hhb, self, "selectedAttr", items = ["(all attributes)"], callback = self.attrListSelected)
83        sortedFuncs = sorted(m for m in AttrComputer.FUNCTIONS.keys())
84        self.cbFuncs = OWGUI.comboBox(hhb, self, "selectedFunc", items = ["(all functions)"] + sortedFuncs, callback = self.funcListSelected)
85        model = self.cbFuncs.model()
86        for i, func in enumerate(sortedFuncs):
[11713]87            model.item(i + 1).setToolTip(
88                inspect.getdoc(AttrComputer.FUNCTIONS[func])
89            )
[7870]90       
91        hb = OWGUI.widgetBox(db, None, "horizontal", addSpace=True)
92        OWGUI.button(hb, self, "Add", callback = self.addAttr, autoDefault=True)
93        OWGUI.button(hb, self, "Update", callback = self.updateAttr)
94        OWGUI.button(hb, self, "Remove", callback = self.removeAttr)
95        OWGUI.button(hb, self, "Remove All", callback = self.removeAllAttr)
96
97        self.lbDefinitions = OWGUI.listBox(db, self, "selectedDef", "defLabels", callback=self.selectAttr)
98        self.lbDefinitions.setFixedHeight(160)
99
100        hb = OWGUI.widgetBox(self.controlArea, "Apply", "horizontal")
101        OWGUI.button(hb, self, "Apply", callback = self.apply)
102        cb = OWGUI.checkBox(hb, self, "autosend", "Apply automatically", callback=self.enableAuto)
103        cb.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
104
105        self.adjustSize()
106
107    def settingsFromWidgetCallback(self, handler, context):
108        context.definitions = self.definitions
109
110    def settingsToWidgetCallback(self, handler, context):
111        self.definitions = getattr(context, "definitions", [])
112        self.defLabels = ["%s := %s" % t for t in self.definitions]
113        self.selectedDef = []
114
115    def setData(self, data):
116        self.closeContext()
117        self.data = data
118        self.cbAttrs.clear()
119        self.cbAttrs.addItem("(all attributes)")
120        if self.data:
121            self.cbAttrs.addItems([attr.name for attr in self.data.domain])
122        self.removeAllAttr()
123        self.openContext("", data)
124        self.apply()
125
126    def getAttrExpression(self, thisRow=-1):
127        attrname = self.attrname.strip()
128        if not attrname:
129            self.leAttrName.setFocus()
130            return
131        for row, (attr, expr) in enumerate(self.definitions):
132            if row!=thisRow and attr==attrname:
133                QMessageBox.critical(self, "Duplicate name", "Attribute with that name already exists.\nPlease choose a different name.")
134                self.leAttrName.setFocus()
135                return
136        expression = self.expression.strip()
137        if not expression:
138            self.leExpression.setFocus()
139            return
140        return attrname, expression
141       
142    def addAttr(self):
143        attrexpr = self.getAttrExpression()
144        if not attrexpr:
145            return
146        self.defLabels = self.defLabels + ["%s := %s" % attrexpr] # should be like this to update the listbox
147        self.definitions.append(attrexpr)
148        self.expression = self.attrname = ""
149        self.applyIf()
150
151    def updateAttr(self):
152        if self.selectedDef:
153            selected = self.selectedDef[0]
154            attrexpr = self.getAttrExpression(selected)
155            if not attrexpr:
156                return
157             # should be like this to reset the listbox
158            self.defLabels = self.defLabels[:selected] + ["%s := %s" % attrexpr] + self.defLabels[selected+1:]
159            self.definitions[selected] = attrexpr
160            self.applyIf()
161
162    def removeAttr(self):
163        if self.selectedDef:
164            selected = self.selectedDef[0]
165            if 0 <= selected < self.lbDefinitions.count():
166                self.defLabels = self.defLabels[:selected] + self.defLabels[selected+1:]
167                del self.definitions[selected]
168                self.applyIf()
169
170    def removeAllAttr(self):
171        self.defLabels = []
172        self.definitions = []
173        self.selectedDef = []
174        self.expression = ""
175        self.applyIf()
176
177    def selectAttr(self):
178        if self.selectedDef:
179            self.attrname, self.expression = self.definitions[self.selectedDef[0]]
180        else:
181            self.attrname = self.expression = ""
182
183    def insertIntoExpression(self, what):
184        # Doesn't work: clicking the listbox removes the selection
185        if self.leExpression.hasSelectedText():
186            self.leExpression.del_()
187        cp = self.leExpression.cursorPosition()
188        self.expression = self.expression[:cp] + what + self.expression[cp:]
189        self.leExpression.setFocus()
190
191    def attrListSelected(self):
192        if self.selectedAttr:
193            attr = str(self.cbAttrs.itemText(self.selectedAttr))
194            mo = re_identifier.match(attr)
195            if not mo or mo.span()[1] != len(attr):
196                attr = '"%s"' % attr
197            self.insertIntoExpression(attr)
198            self.selectedAttr = 0
199
200    def funcListSelected(self):
201        if self.selectedFunc:
202            print self.selectedFunc
203            func = str(self.cbFuncs.itemText(self.selectedFunc))
204            if func in ["atan2", "fmod", "ldexp", "log", "pow", "normalvariate",
205                        "gauss", "lognormvariate", "betavariate", "gammavariate",
206                        "triangular", "uniform", "vonmisesvariate", "weibullvariate"]:
207                self.insertIntoExpression(func + "(,)")
208                self.leExpression.cursorBackward(False, 2)
209            elif func in ["e", "pi"]:
210                self.insertIntoExpression(func)
211            else:
212                self.insertIntoExpression(func + "()")
213                self.leExpression.cursorBackward(False)
214            self.selectedFunc = 0
215
216    def applyIf(self):
217        self.dataChanged = True
218        if self.autosend:
219            self.apply()
220
221    def enableAuto(self):
222        if self.dataChanged:
223            self.apply()
224
225    def apply(self):
226        self.dataChanged = False
227        if not self.data:
[9546]228            self.send("Data", None)
[7870]229            return
230
231        oldDomain = self.data.domain
232
233        names = [d[0] for d in self.definitions]
234        for name in names:
235            if names.count(name)>1 or name in oldDomain > 1:
236                self.error(1, "Multiple attributes with the same name (%s)" % name)
[9546]237                self.send("Data", None)
[7870]238                return
239
240        unknown = [[name, exp, set([id[0] or id[1] for id in re_identifier.findall(exp) if id[0] in names or id[1][1:-1] in names])] for name, exp in self.definitions]
241        reinserted = {}
242        replacer = IdentifierReplacer(reinserted, [n.name for n in oldDomain])
243        while unknown:
244            solved = set()
245            for i, (name, exp, unk_attrs) in enumerate(unknown):
246                if not unk_attrs:
247                    reinserted[name] = re_identifier.sub(replacer, exp)
248                    del unknown[i]
249                    solved.add(name)
250            if not solved:
251                self.error(1, "Circular attribute definitions (%s)" % ", ".join([x[0] for x in unknown]))
[9546]252                self.send("Data", None)
[7870]253                return
254            for name, exp, unk_attrs in unknown:
255                unk_attrs -= solved
256
257        self.error(1)
258
259        newDomain = orange.Domain(oldDomain.attributes + [orange.FloatVariable(str(attrname), getValueFrom = AttrComputer(reinserted[attrname])) for attrname in names], oldDomain.classVar)
260        newDomain.addmetas(oldDomain.getmetas())
[9546]261        self.send("Data", orange.ExampleTable(newDomain, self.data))
Note: See TracBrowser for help on using the repository browser.