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

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

Changes in headers, widget descriptions text.

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