# source:orange/orange/orngABML.py@8740:b61c00c29d53

Revision 8740:b61c00c29d53, 17.3 KB checked in by martin <martin@…>, 3 years ago (diff)
Line
1# This module is used to handle argumented examples.
2
3import Orange.core
4import re
5import string
6import warnings
7
8import numpy
9import math
10
11# regular expressions
12# exppression for testing validity of a set of arguments:
13testVal = re.compile(r"""[" \s]*                              # remove any special characters at the beginning
14                     ~?                                       # argument could be negative (~) or positive (without ~)
15                     {                                        # left parenthesis of the argument
16                     \s*[\w\W]+                                # first attribute of the argument
17                     (\s*,\s*[\w\W]+)*                         # following attributes in the argument
18                     }                                        # right parenthesis of the argument
19                     (\s*,\s*~?{\s*[\w\W]+(\s*,\s*[\w\W]+)*})*  # following arguments
20                     [" \s]*"""                               # remove any special characters at the end
21                     , re.VERBOSE)
22
23# splitting regular expressions
24argRE = re.compile(r'[,\s]*(\{[^{}]+\})[,\s]*')
25argAt = re.compile(r'[{},]+')
26argCompare = re.compile(r'[(<=)(>=)<>]')
27
28def strSign(oper):
29    if oper == Orange.core.ValueFilter_continuous.Less:
30        return "<"
31    elif oper == Orange.core.ValueFilter_continuous.LessEqual:
32        return "<="
33    elif oper == Orange.core.ValueFilter_continuous.Greater:
34        return ">"
35    elif oper == Orange.core.ValueFilter_continuous.GreaterEqual:
36        return ">="
37    else: return "="
38
39def strArg(arg, domain, leave_ref):
40    if type(arg) == Orange.core.ValueFilter_discrete:
41        return str(domain[arg.position].name)
42    else:
43        if leave_ref:
44            return str(domain[arg.position].name)+strSign(arg.oper)
45        else:
46            return str(domain[arg.position].name)+strSign(arg.oper)+str(arg.ref)
47
48def listOfAttributeNames(rule, leave_ref = False):
49    if not rule.filter.conditions:
50        return ""
51    list = ""
52    for val in rule.filter.conditions[:-1]:
53        lr = leave_ref or val.unspecialized_condition
54        list += strArg(val, rule.filter.domain, lr) + ","
55    lr = leave_ref or rule.filter.conditions[-1].unspecialized_condition
56    list += strArg(rule.filter.conditions[-1], rule.filter.domain, lr)
57    return list
58
59
60class Argumentation:
61    """ Class that describes a set of positive and negative arguments
62    this class is used as a value for ArgumentationVariable. """
63    def __init__(self):
64        self.positive_arguments = Orange.core.RuleList()
65        self.negative_arguments = Orange.core.RuleList()
66        self.not_yet_computed_arguments = [] # Arguments that need the whole data set
67                                          # when processing are stored here
68
69    # add an argument that supports the class of the example
71        self.positive_arguments.append(argument)
72
73    # add an argument that opposes the class of the example
75        self.negative_arguments.append(argument)
76
77    def addNotYetComputed(self, argument, notyet, positive):
78        self.not_yet_computed_arguments.append((argument,notyet,positive))
79
80    def __str__(self):
81        retValue = ""
82        # iterate through positive arguments (rules) and
83        # write them down as a text list
84        if len(self.positive_arguments)>0:
85            for (i,pos) in enumerate(self.positive_arguments[:-1]):
86                retValue+="{"+listOfAttributeNames(pos)+"}"
87                retValue+=","
88            retValue+="{"+listOfAttributeNames(self.positive_arguments[-1])+"}"
89            # do the same thing for negative argument,
90            # just that this time use sign "~" in front of the list
91        if len(self.negative_arguments)>0:
92            if len(retValue)>0:
93                retValue += ","
94            for (i,neg) in enumerate(self.negative_arguments[:-1]):
95                retValue+="~"
96                retValue+="{"+listOfAttributeNames(neg,leave_ref=True)+"}"
97                retValue+=","
98            retValue+="~{"+listOfAttributeNames(self.negative_arguments[-1],leave_ref=True)+"}"
99        return retValue
100
101POSITIVE = True
102NEGATIVE = False
103class ArgumentVariable(Orange.core.PythonVariable):
104    """ For writing and parsing arguments in .tab files. """
105    def str2val(self, strV):
106        """ convert str to val - used for creating variables. """
107        return self.filestr2val(strV, None)
108
109    def filestr2val(self, strV, example=None):
110        """ write arguments (from string in file) to value - used also as a function for reading from data. """
111        mt = testVal.match(strV)
112        if not mt or not mt.end() == len(strV):
113            warnings.warn(strV+" is a badly formed argument.")
114            return Orange.core.PythonValueSpecial(2) # return special if argument doesnt match the formal form
115
116        if not example:
117            example = self.example
118        domain = example.domain
119
120        # get a set of arguments
121        splitedSet = filter(lambda x:x!='' and x!='"', argRE.split(strV))
122
123        # create an Argumentation object - an empty set of arguments
124        argumentation = Argumentation()
125        type = POSITIVE # type of argument - positive = True / negative = False
126        for sp in splitedSet:
127            # for each argument determine whether it is positive or negative
128            if sp == '~':
129                type = NEGATIVE
130                continue
131            argument = Orange.core.Rule(filter=Orange.core.Filter_values(domain=domain))
132            argument.setattr("unspecialized_argument",False)
133
134            reasonValues = filter(lambda x:x!='' and x!='"', argAt.split(sp)) # reasons in this argument
135            # iterate through argument names
136            for r in reasonValues:
137                r=string.strip(r)   # Remove all white characters on both sides
138                try:
139                    attribute = domain[r]
140                except:
141                    attribute = None
142                if attribute: # only attribute name is mentioned as a reason
143                    if domain.index(attribute)<0:
144                        warnings.warn("Meta attribute %s used in argument. Is this intentional?"%r)
145                        continue
146                    value = example[attribute]
147                    if attribute.varType == Orange.core.VarTypes.Discrete: # discrete argument
148                        argument.filter.conditions.append(Orange.core.ValueFilter_discrete(
149                                                            position = domain.attributes.index(attribute),
150                                                            values=[value],
151                                                            acceptSpecial = 0))
152                        argument.filter.conditions[-1].setattr("unspecialized_condition",False)
153                    else: # continuous but without reference point
154                        warnings.warn("Continous attributes (%s) in arguments should not be used without a comparison sign (<,<=,>,>=)"%r)
155
156                else: # attribute and something name is the reason, probably cont. attribute
157                    # one of four possible delimiters should be found, <,>,<=,>=
158                    splitReason = filter(lambda x:x!='' and x!='"', argCompare.split(r))
159                    if len(splitReason)>2 or len(splitReason)==0:
160                        warnings.warn("Reason %s is a badly formed part of an argument."%r)
161                        continue
162                    # get attribute name and continous reference value
163                    attributeName = string.strip(splitReason[0])
164                    if len(splitReason) > 1:
165                        refValue = string.strip(splitReason[1])
166                    else:
167                        refValue = ""
168                    if refValue:
169                        sign = r[len(attributeName):-len(refValue)]
170                    else:
171                        sign = r[len(attributeName):]
172
173                    # evaluate name and value
174                    try:
175                        attribute = domain[attributeName]
176                    except:
177                        warnings.warn("Attribute %s is not a part of the domain"%attributeName)
178                        continue
179                    if domain.index(attribute)<0:
180                        warnings.warn("Meta attribute %s used in argument. Is this intentional?"%r)
181                        continue
182                    if refValue:
183                        try:
184                            ref = eval(refValue)
185                        except:
186                            warnings.warn("Error occured while reading value by argument's reason. Argument: %s, value: %s"%(r,refValue))
187                            continue
188                    else:
189                        ref = 0.
190                    if sign == "<": oper = Orange.core.ValueFilter_continuous.Less
191                    elif sign == ">": oper = Orange.core.ValueFilter_continuous.Greater
192                    elif sign == "<=": oper = Orange.core.ValueFilter_continuous.LessEqual
193                    else: oper = Orange.core.ValueFilter_continuous.GreaterEqual
194                    argument.filter.conditions.append(Orange.core.ValueFilter_continuous(
195                                                position = domain.attributes.index(attribute),
196                                                oper=oper,
197                                                ref=ref,
198                                                acceptSpecial = 0))
199                    if not refValue and type == POSITIVE:
200                        argument.filter.conditions[-1].setattr("unspecialized_condition",True)
201                        argument.setattr("unspecialized_argument",True)
202                    else:
203                        argument.filter.conditions[-1].setattr("unspecialized_condition",False)
204
205            if example.domain.classVar:
206                argument.classifier = Orange.core.DefaultClassifier(defaultVal = example.getclass())
207            argument.complexity = len(argument.filter.conditions)
208
209            if type: # and len(argument.filter.conditions):
211            else: # len(argument.filter.conditions):
213            type = POSITIVE
214        return argumentation
215
216
217    # used for writing to data: specify output (string) presentation of arguments in tab. file
218    def val2filestr(self, val, example):
219        return str(val)
220
221    # used for writing to string
222    def val2str(self, val):
223        return str(val)
224
225
226class ArgumentFilter_hasSpecial:
227    def __call__(self, examples, attribute, target_class=-1, negate=0):
228        indices = [0]*len(examples)
229        for i in range(len(examples)):
230            if examples[i][attribute].isSpecial():
231                indices[i]=1
232            elif target_class>-1 and not int(examples[i].getclass()) == target_class:
233                indices[i]=1
234            elif len(examples[i][attribute].value.positive_arguments) == 0:
235                indices[i]=1
236        return examples.select(indices,0,negate=negate)
237
238def evaluateAndSortArguments(examples, argAtt, evaluateFunction = None, apriori = None):
239    """ Evaluate positive arguments and sort them by quality. """
240    if not apriori:
241        apriori = Orange.core.Distribution(examples.domain.classVar,examples)
242    if not evaluateFunction:
243        evaluateFunction = Orange.core.RuleEvaluator_Laplace()
244
245    for e in examples:
246        if not e[argAtt].isSpecial():
247            for r in e[argAtt].value.positive_arguments:
248                r.filterAndStore(examples, 0, e[examples.domain.classVar])
249                r.quality = evaluateFunction(r,examples,0,int(e[examples.domain.classVar]),apriori)
250            e[argAtt].value.positive_arguments.sort(lambda x,y: -cmp(x.quality, y.quality))
251
252def isGreater(oper):
253    if oper == Orange.core.ValueFilter_continuous.Greater or \
254       oper == Orange.core.ValueFilter_continuous.GreaterEqual:
255        return True
256    return False
257
258def isLess(oper):
259    if oper == Orange.core.ValueFilter_continuous.Less or \
260       oper == Orange.core.ValueFilter_continuous.LessEqual:
261        return True
262    return False
263
264class ConvertClass:
265    """ Converting class variables into dichotomous class variable. """
266    def __init__(self, classAtt, classValue, newClassAtt):
267        self.classAtt = classAtt
268        self.classValue = classValue
269        self.newClassAtt = newClassAtt
270
271    def __call__(self,example, returnWhat):
272        if example[self.classAtt] == self.classValue:
273            return Orange.core.Value(self.newClassAtt, self.classValue+"_")
274        else:
275            return Orange.core.Value(self.newClassAtt, "not " + self.classValue)
276
277def createDichotomousClass(domain, att, value, negate, removeAtt = None):
278    # create new variable
279    newClass = Orange.core.EnumVariable(att.name+"_", values = [str(value)+"_", "not " + str(value)])
280    positive = Orange.core.Value(newClass, str(value)+"_")
281    negative = Orange.core.Value(newClass, "not " + str(value))
282    newClass.getValueFrom = ConvertClass(att,str(value),newClass)
283
284    att = [a for a in domain.attributes]
285    newDomain = Orange.core.Domain(att+[newClass])
287    if negate==1:
288        return (newDomain, negative)
289    else:
290        return (newDomain, positive)
291
292
293class ConvertCont:
294    def __init__(self, position, value, oper, newAtt):
295        self.value = value
296        self.oper = oper
297        self.position = position
298        self.newAtt = newAtt
299
300    def __call__(self,example, returnWhat):
301        if example[self.position].isSpecial():
302            return example[self.position]
303        if isLess(self.oper):
304            if example[self.position]<self.value:
305                return Orange.core.Value(self.newAtt, self.value)
306            else:
307                return Orange.core.Value(self.newAtt, float(example[self.position]))
308        else:
309            if example[self.position]>self.value:
310                return Orange.core.Value(self.newAtt, self.value)
311            else:
312                return Orange.core.Value(self.newAtt, float(example[self.position]))
313
314
316    """ Main task of this function is to add probabilistic errors to examples."""
317    for ex_i, ex in enumerate(test_data):
318        (cl,prob) = classifier(ex,Orange.core.GetBoth)
319        ex.setmeta("ProbError", float(ex.getmeta("ProbError")) + 1.-prob[ex.getclass()])
320
321def nCrossValidation(data,learner,weightID=0,folds=5,n=4,gen=0,argument_id="Arguments"):
322    """ Function performs n x fold crossvalidation. For each classifier
323        test set is updated by calling function addErrors. """
324    acc = 0.0
325    rules = {}
326    for d in data:
327        rules[float(d["SerialNumberPE"])] = []
328    pick = Orange.core.MakeRandomIndicesCV(folds=folds, randseed=gen, stratified = Orange.core.MakeRandomIndices.StratifiedIfPossible)
329    for n_i in range(n):
330        pick.randseed = gen+10*n_i
331        selection = pick(data)
332        for folds_i in range(folds):
333            for data_i,e in enumerate(data):
334                try:
335                    if e[argument_id]: # examples with arguments do not need to be tested
336                        selection[data_i]=folds_i+1
337                except:
338                    pass
339            train_data = data.selectref(selection, folds_i,negate=1)
340            test_data = data.selectref(selection, folds_i,negate=0)
341            classifier = learner(train_data,weightID)
344            for d in test_data:
345                for r in classifier.rules:
346                    if r(d):
347                        rules[float(d["SerialNumberPE"])].append(r)
348    # normalize prob errors
349    for d in data:
350        d["ProbError"]=d["ProbError"]/n
351    return rules
352
353def findProb(learner,examples,weightID=0,folds=5,n=4,gen=0,thr=0.5,argument_id="Arguments"):
354    """ General method for calling to find problematic example.
355        It returns all critial examples along with average probabilistic errors that ought to be higher then thr.
356        Taking the one with highest error is the same as taking the most
357        problematic example. """
358
359    newDomain = Orange.core.Domain(examples.domain.attributes, examples.domain.classVar)
361    newExamples = Orange.core.ExampleTable(newDomain, examples)
362    if not newExamples.domain.hasmeta("ProbError"):
363        newId = Orange.core.newmetaid()
365        newExamples = Orange.core.ExampleTable(newDomain, examples)
366    if not newExamples.domain.hasmeta("SerialNumberPE"):
367        newId = Orange.core.newmetaid()
369        newExamples = Orange.core.ExampleTable(newDomain, examples)
370    for i in range(len(newExamples)):
371        newExamples[i]["SerialNumberPE"] = float(i)
372        newExamples[i]["ProbError"] = 0.
373
374    # it returns a list of examples now: (index of example-starting with 0, example, prob error, rules covering example
375    rules = nCrossValidation(newExamples,learner,weightID=weightID, folds=folds, n=n, gen=gen, argument_id=argument_id)
376    return [(ei, examples[ei], float(e["ProbError"]), rules[float(e["SerialNumberPE"])]) for ei, e in enumerate(newExamples) if e["ProbError"] > thr]
377
Note: See TracBrowser for help on using the repository browser.