rulelearn-ripper-interactive 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rulelearn_ri/__init__.py +6 -0
- rulelearn_ri/algorithms/__init__.py +0 -0
- rulelearn_ri/algorithms/r2n/__init__.py +0 -0
- rulelearn_ri/algorithms/r2n/base.py +28 -0
- rulelearn_ri/algorithms/r2n/functions.py +25 -0
- rulelearn_ri/algorithms/r2n/mask.py +58 -0
- rulelearn_ri/algorithms/r2n/preprocessing_layers.py +192 -0
- rulelearn_ri/algorithms/r2n/r2n_algo.py +287 -0
- rulelearn_ri/algorithms/r2n/simple_rulenet.py +75 -0
- rulelearn_ri/algorithms/r2n/training.py +34 -0
- rulelearn_ri/algorithms/r2n/utilities.py +117 -0
- rulelearn_ri/algorithms/rbm/__init__.py +0 -0
- rulelearn_ri/algorithms/rbm/boolean_rule_cg.py +65 -0
- rulelearn_ri/algorithms/rbm/rbm/BRCG.py +69 -0
- rulelearn_ri/algorithms/rbm/rbm/GLRM.py +98 -0
- rulelearn_ri/algorithms/rbm/rbm/__init__.py +6 -0
- rulelearn_ri/algorithms/rbm/rbm/beam_search.py +302 -0
- rulelearn_ri/algorithms/rbm/rbm/boolean_rule_cg.py +245 -0
- rulelearn_ri/algorithms/rbm/rbm/features.py +544 -0
- rulelearn_ri/algorithms/rbm/rbm/linear_regression.py +393 -0
- rulelearn_ri/algorithms/rbm/rbm/logistic_regression.py +447 -0
- rulelearn_ri/algorithms/rbm/utils.py +13 -0
- rulelearn_ri/algorithms/ripper/__init__.py +10 -0
- rulelearn_ri/algorithms/ripper/base.py +81 -0
- rulelearn_ri/algorithms/ripper/binding.py +541 -0
- rulelearn_ri/algorithms/ripper/mdl.py +97 -0
- rulelearn_ri/algorithms/ripper/pruning.py +212 -0
- rulelearn_ri/algorithms/ripper/ripper.py +600 -0
- rulelearn_ri/algorithms/ripper/ripper_ruleset_generator.py +26 -0
- rulelearn_ri/algorithms/ripper/single_rule.py +425 -0
- rulelearn_ri/dise.py +64 -0
- rulelearn_ri/trxf/__init__.py +0 -0
- rulelearn_ri/trxf/classifier/__init__.py +0 -0
- rulelearn_ri/trxf/classifier/ruleset_classifier.py +153 -0
- rulelearn_ri/trxf/core/__init__.py +5 -0
- rulelearn_ri/trxf/core/boolean_evaluator.py +15 -0
- rulelearn_ri/trxf/core/conjunction.py +50 -0
- rulelearn_ri/trxf/core/dnf_ruleset.py +64 -0
- rulelearn_ri/trxf/core/feature.py +155 -0
- rulelearn_ri/trxf/core/predicate.py +98 -0
- rulelearn_ri/trxf/core/ruleset_generator.py +25 -0
- rulelearn_ri/trxf/core/utils.py +40 -0
- rulelearn_ri/trxf/metrics.py +98 -0
- rulelearn_ri/trxf/pmml_export/__init__.py +4 -0
- rulelearn_ri/trxf/pmml_export/__version__.py +1 -0
- rulelearn_ri/trxf/pmml_export/models/__init__.py +24 -0
- rulelearn_ri/trxf/pmml_export/models/attribute.py +24 -0
- rulelearn_ri/trxf/pmml_export/models/characteristics.py +20 -0
- rulelearn_ri/trxf/pmml_export/models/data_dictionary.py +53 -0
- rulelearn_ri/trxf/pmml_export/models/mining_schema.py +25 -0
- rulelearn_ri/trxf/pmml_export/models/pmml_ruleset_model.py +16 -0
- rulelearn_ri/trxf/pmml_export/models/predicate.py +42 -0
- rulelearn_ri/trxf/pmml_export/models/rule.py +26 -0
- rulelearn_ri/trxf/pmml_export/models/ruleset.py +28 -0
- rulelearn_ri/trxf/pmml_export/models/ruleset_model.py +16 -0
- rulelearn_ri/trxf/pmml_export/models/scorecard.py +35 -0
- rulelearn_ri/trxf/pmml_export/pmml_exporter.py +18 -0
- rulelearn_ri/trxf/pmml_export/reader/__init__.py +2 -0
- rulelearn_ri/trxf/pmml_export/reader/abstract_reader.py +16 -0
- rulelearn_ri/trxf/pmml_export/reader/trxf_reader.py +113 -0
- rulelearn_ri/trxf/pmml_export/serializer/__init__.py +2 -0
- rulelearn_ri/trxf/pmml_export/serializer/abstract_serializer.py +11 -0
- rulelearn_ri/trxf/pmml_export/serializer/nyoka_serializer.py +162 -0
- rulelearn_ri/trxf/scorecard/__init__.py +3 -0
- rulelearn_ri/trxf/scorecard/bins.py +253 -0
- rulelearn_ri/trxf/scorecard/partition.py +71 -0
- rulelearn_ri/trxf/scorecard/scorecard.py +78 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/METADATA +164 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/RECORD +73 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/WHEEL +5 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/licenses/LICENSE +201 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/licenses/NOTICE +11 -0
- rulelearn_ripper_interactive-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
|
|
3
|
+
from rulelearn_ri.trxf.core.boolean_evaluator import BooleanEvaluator
|
|
4
|
+
from rulelearn_ri.trxf.core.predicate import Predicate
|
|
5
|
+
from rulelearn_ri.trxf.core.utils import set_equality
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Conjunction(BooleanEvaluator):
|
|
9
|
+
def __init__(self, predicate_list: List[Predicate]):
|
|
10
|
+
"""
|
|
11
|
+
@param predicate_list: a list of predicates intrpreted as a conjunction
|
|
12
|
+
"""
|
|
13
|
+
self._predicates = predicate_list
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def predicates(self):
|
|
17
|
+
return self._predicates
|
|
18
|
+
|
|
19
|
+
def add_predicate(self, pred: Predicate):
|
|
20
|
+
self._predicates.append(pred)
|
|
21
|
+
|
|
22
|
+
def delete_predicate(self, pred: Predicate):
|
|
23
|
+
self._predicates.remove(pred)
|
|
24
|
+
|
|
25
|
+
def evaluate(self, assignment: Dict[str, Any]) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Evaluate the truth value of the conjunction w.r.t. the variable assignment
|
|
28
|
+
|
|
29
|
+
@param assignment: dict mapping variable name to value
|
|
30
|
+
@return: bool truth value of the predicate
|
|
31
|
+
"""
|
|
32
|
+
for pred in self.predicates:
|
|
33
|
+
if not pred.evaluate(assignment):
|
|
34
|
+
return False
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return '%s(%r)' %(self.__class__, self._predicates)
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
if len(self.predicates) == 0:
|
|
42
|
+
return 'true'
|
|
43
|
+
else:
|
|
44
|
+
return ' ^ '.join(['['+str(x)+']' for x in self._predicates])
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other):
|
|
47
|
+
return set_equality(self.predicates, other.predicates)
|
|
48
|
+
|
|
49
|
+
def __len__(self):
|
|
50
|
+
return len(self.predicates)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import Dict, List, Any
|
|
2
|
+
|
|
3
|
+
from rulelearn_ri.trxf.core.boolean_evaluator import BooleanEvaluator
|
|
4
|
+
from rulelearn_ri.trxf.core.conjunction import Conjunction
|
|
5
|
+
from rulelearn_ri.trxf.core.utils import set_equality
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DnfRuleSet(BooleanEvaluator):
|
|
9
|
+
"""
|
|
10
|
+
This class implements a ruleset in the disjunctive normal form: c_1 v c_2 v .... v c_k. Here, each c_i is a
|
|
11
|
+
conjunction of predicates, i.e., c_i = p_{i1} & p_{i2} & ... & p_{i n_i}. All the conjunctions share the same
|
|
12
|
+
conclusion, specified by the then_part of the ruleset.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, conjunctions: List[Conjunction], then_part: Any) -> None:
|
|
16
|
+
"""
|
|
17
|
+
@param conjunctions: A list containing conjunctions in this ruleset
|
|
18
|
+
@param then_part: The then part of this RuleSet.
|
|
19
|
+
"""
|
|
20
|
+
self._then_part = then_part
|
|
21
|
+
self._conjunctions = []
|
|
22
|
+
for conjunction in conjunctions:
|
|
23
|
+
if conjunction not in self._conjunctions:
|
|
24
|
+
self._conjunctions.append(conjunction)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def conjunctions(self):
|
|
28
|
+
return self._conjunctions
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def then_part(self):
|
|
32
|
+
return self._then_part
|
|
33
|
+
|
|
34
|
+
def add_conjunction(self, conjunction: Conjunction) -> None:
|
|
35
|
+
if conjunction not in self._conjunctions:
|
|
36
|
+
self._conjunctions.append(conjunction)
|
|
37
|
+
|
|
38
|
+
def remove_conjunction(self, conjunction: Conjunction) -> None:
|
|
39
|
+
self._conjunctions.remove(conjunction)
|
|
40
|
+
|
|
41
|
+
def list_conjunctions(self) -> List[Conjunction]:
|
|
42
|
+
return self.conjunctions
|
|
43
|
+
|
|
44
|
+
def evaluate(self, assignment: Dict[str, Any]) -> bool:
|
|
45
|
+
for conjunction in self.conjunctions:
|
|
46
|
+
if conjunction.evaluate(assignment):
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def __repr__(self):
|
|
51
|
+
return '%s(%r, %r)' % (self.__class__, self._conjunctions, self._then_part)
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
if len(self.conjunctions) == 0:
|
|
55
|
+
disjunction = 'false'
|
|
56
|
+
else:
|
|
57
|
+
disjunction = ' v\n'.join(['(' + str(x) + ')' for x in self._conjunctions])
|
|
58
|
+
return 'if\n' + disjunction + '\nthen\n' + str(self._then_part)
|
|
59
|
+
|
|
60
|
+
def __eq__(self, other):
|
|
61
|
+
return set_equality(self.conjunctions, other.conjunctions) and self.then_part == other.then_part
|
|
62
|
+
|
|
63
|
+
def __len__(self):
|
|
64
|
+
return len(self.conjunctions)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import numbers
|
|
2
|
+
import tokenize
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
OPERATORS = ['+', '-', '*', '/']
|
|
7
|
+
PARENTHESES = ['(', ')']
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_number(s):
|
|
11
|
+
try:
|
|
12
|
+
float(s)
|
|
13
|
+
return True
|
|
14
|
+
except ValueError:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Feature:
|
|
19
|
+
def __init__(self, expression: str):
|
|
20
|
+
"""
|
|
21
|
+
@param expression: Infix arithmetic expression encoded as utf-8 string involving constants, operators (+, -,
|
|
22
|
+
*, /), parentheses, and variable names. Variable names shall not have any of the above operators or
|
|
23
|
+
whitespace as part of the name. Care must be taken not to use the utf-8 long dash '\xe2' for the minus sign,
|
|
24
|
+
when copy-pasting.
|
|
25
|
+
"""
|
|
26
|
+
self._expression = expression
|
|
27
|
+
self._rpn = _shunting_yard(self._expression)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def variable_names(self):
|
|
31
|
+
"""
|
|
32
|
+
A list of variable names in the feature representation
|
|
33
|
+
"""
|
|
34
|
+
tokens = _get_tokens(self._expression)
|
|
35
|
+
return [x for x in tokens if x not in OPERATORS + ['(', ')'] and not is_number(x)]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def reverse_polish_notation(self):
|
|
39
|
+
"""
|
|
40
|
+
The Reverse Polish Notation (https://en.wikipedia.org/wiki/Reverse_Polish_notation) representation of the
|
|
41
|
+
feature as a list of token strings. This serves as the symbolic representation of the feature.
|
|
42
|
+
"""
|
|
43
|
+
return self._rpn
|
|
44
|
+
|
|
45
|
+
def evaluate(self, assignment: Dict[str, Any]) -> Any:
|
|
46
|
+
"""
|
|
47
|
+
Evaluate the value of the feature for the provided variable assignment
|
|
48
|
+
|
|
49
|
+
@param assignment: dict mapping variable name to value
|
|
50
|
+
@return: the feature value w.r.t. the assignment
|
|
51
|
+
"""
|
|
52
|
+
operand_stack = []
|
|
53
|
+
for token in self.reverse_polish_notation:
|
|
54
|
+
if token in OPERATORS:
|
|
55
|
+
rhs = operand_stack.pop()
|
|
56
|
+
_validate_number(rhs)
|
|
57
|
+
lhs = operand_stack.pop()
|
|
58
|
+
_validate_number(lhs)
|
|
59
|
+
expression = str(lhs) + token + str(rhs)
|
|
60
|
+
try:
|
|
61
|
+
operand_stack.append(eval(expression))
|
|
62
|
+
except NameError as e:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
'eval() failed for expression "{}"'.format(expression)) from e
|
|
65
|
+
else:
|
|
66
|
+
operand = assignment[token] if token in assignment else float(token)
|
|
67
|
+
operand_stack.append(operand)
|
|
68
|
+
assert len(operand_stack) == 1
|
|
69
|
+
|
|
70
|
+
result = operand_stack.pop()
|
|
71
|
+
try:
|
|
72
|
+
result = float(result)
|
|
73
|
+
except ValueError:
|
|
74
|
+
pass
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
return '%s(%r)' % (self.__class__, self.reverse_polish_notation)
|
|
79
|
+
|
|
80
|
+
def __str__(self):
|
|
81
|
+
return ' '.join(self._expression.split())
|
|
82
|
+
|
|
83
|
+
def __eq__(self, other):
|
|
84
|
+
return self.__repr__() == other.__repr__()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _shunting_yard(expression):
|
|
88
|
+
"""
|
|
89
|
+
Converts infix expression string into Reverse Polish Notation.
|
|
90
|
+
https://en.wikipedia.org/wiki/Shunting-yard_algorithm
|
|
91
|
+
"""
|
|
92
|
+
tokens = _get_tokens(expression)
|
|
93
|
+
if len(tokens) > 0 and (tokens[0] == '-' or tokens[0] == '+'):
|
|
94
|
+
tokens.insert(0, 0)
|
|
95
|
+
op_precedence = {'+': 1, '-': 1, '*': 2, '/': 2}
|
|
96
|
+
output = []
|
|
97
|
+
op_stack = []
|
|
98
|
+
for token in tokens:
|
|
99
|
+
if token in OPERATORS:
|
|
100
|
+
while len(op_stack) > 0 and op_stack[-1] != '(' and op_precedence[op_stack[-1]] >= op_precedence[token]:
|
|
101
|
+
output.append(op_stack.pop())
|
|
102
|
+
op_stack.append(token)
|
|
103
|
+
elif token == '(':
|
|
104
|
+
op_stack.append(token)
|
|
105
|
+
elif token == ')':
|
|
106
|
+
while len(op_stack) > 0 and op_stack[-1] != '(':
|
|
107
|
+
output.append(op_stack.pop())
|
|
108
|
+
if len(op_stack) == 0:
|
|
109
|
+
raise ValueError('Unmatched closing parenthesis in expression: ' + expression)
|
|
110
|
+
op_stack.pop()
|
|
111
|
+
else:
|
|
112
|
+
output.append(token)
|
|
113
|
+
while len(op_stack) > 0:
|
|
114
|
+
if op_stack[-1] == '(':
|
|
115
|
+
raise ValueError('Unmatched opening parenthesis in expression: ' + expression)
|
|
116
|
+
output.append(op_stack.pop())
|
|
117
|
+
|
|
118
|
+
_validate_rpn(output, expression)
|
|
119
|
+
return output
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_tokens(expression):
|
|
123
|
+
no_whitespace = ''.join(expression.split())
|
|
124
|
+
g = tokenize.tokenize(BytesIO(no_whitespace.encode('UTF-8')).readline)
|
|
125
|
+
assert next(g).string == 'utf-8'
|
|
126
|
+
tokens = []
|
|
127
|
+
buffer = ''
|
|
128
|
+
result = []
|
|
129
|
+
for _, v, _, _, _ in g:
|
|
130
|
+
tokens.append(v)
|
|
131
|
+
for token in tokens:
|
|
132
|
+
if token not in OPERATORS + PARENTHESES:
|
|
133
|
+
buffer += token
|
|
134
|
+
else:
|
|
135
|
+
result.append(buffer)
|
|
136
|
+
result.append(token)
|
|
137
|
+
buffer = ''
|
|
138
|
+
result.append(buffer)
|
|
139
|
+
return [x for x in result if x != '']
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_rpn(rpn, expression):
|
|
143
|
+
stack_size = 0
|
|
144
|
+
for token in rpn:
|
|
145
|
+
valence = 2 if token in OPERATORS else 0
|
|
146
|
+
stack_size += 1 - valence
|
|
147
|
+
if stack_size <= 0:
|
|
148
|
+
raise ValueError('Invalid RPN {} for expression "{}"'.format(rpn, expression))
|
|
149
|
+
if stack_size != 1:
|
|
150
|
+
raise ValueError('Invalid RPN {} for expression "{}"'.format(rpn, expression))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _validate_number(value):
|
|
154
|
+
if not isinstance(value, numbers.Number) or isinstance(value, bool):
|
|
155
|
+
raise ValueError('{} is not a number.'.format(value))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
from rulelearn_ri.trxf.core.boolean_evaluator import BooleanEvaluator
|
|
6
|
+
from rulelearn_ri.trxf.core.feature import Feature, is_number
|
|
7
|
+
|
|
8
|
+
ABS_TOL = 1e-5
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Relation(Enum):
|
|
12
|
+
NEQ = '!='
|
|
13
|
+
EQ = '=='
|
|
14
|
+
LT = '<'
|
|
15
|
+
LE = '<='
|
|
16
|
+
GT = '>'
|
|
17
|
+
GE = '>='
|
|
18
|
+
|
|
19
|
+
def __neg__(self):
|
|
20
|
+
neg_table = {
|
|
21
|
+
self.NEQ: self.EQ,
|
|
22
|
+
self.EQ: self.NEQ,
|
|
23
|
+
self.LT: self.GE,
|
|
24
|
+
self.GE: self.LT,
|
|
25
|
+
self.LE: self.GT,
|
|
26
|
+
self.GT: self.LE
|
|
27
|
+
}
|
|
28
|
+
return neg_table[self]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Predicate(BooleanEvaluator):
|
|
32
|
+
def __init__(self, feature: Feature, relation: Relation, value: Any):
|
|
33
|
+
"""
|
|
34
|
+
@param feature: the LHS of the relation
|
|
35
|
+
@param value: the RHS of the relation
|
|
36
|
+
"""
|
|
37
|
+
self._feature = feature
|
|
38
|
+
self._relation = relation
|
|
39
|
+
self._value = value
|
|
40
|
+
_validate_predicate(self._relation, self._value)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def feature(self):
|
|
44
|
+
return self._feature
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def relation(self):
|
|
48
|
+
return self._relation
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def value(self):
|
|
52
|
+
return self._value
|
|
53
|
+
|
|
54
|
+
def evaluate(self, assignment: Dict[str, Any]) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Evaluate the truth value of the predicate w.r.t. the variable assignment
|
|
57
|
+
|
|
58
|
+
@param assignment: dict mapping variable name to value
|
|
59
|
+
@return: bool truth value of the predicate
|
|
60
|
+
"""
|
|
61
|
+
feature_val = self.feature.evaluate(assignment)
|
|
62
|
+
if self.relation == Relation.EQ:
|
|
63
|
+
return math.isclose(float(feature_val), float(self.value), abs_tol=ABS_TOL) if is_number(self.value) \
|
|
64
|
+
else feature_val == self.value
|
|
65
|
+
elif self.relation == Relation.NEQ:
|
|
66
|
+
return feature_val != self.value
|
|
67
|
+
elif self.relation == Relation.GE:
|
|
68
|
+
return feature_val >= self.value
|
|
69
|
+
elif self.relation == Relation.GT:
|
|
70
|
+
return feature_val > self.value
|
|
71
|
+
elif self.relation == Relation.LE:
|
|
72
|
+
return feature_val <= self.value
|
|
73
|
+
elif self.relation == Relation.LT:
|
|
74
|
+
return feature_val < self.value
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError('Unknown relation operator {}'.format(self.relation))
|
|
77
|
+
|
|
78
|
+
def __neg__(self):
|
|
79
|
+
if self.value is True or self.value is False:
|
|
80
|
+
return Predicate(self.feature, self.relation, not self.value)
|
|
81
|
+
return Predicate(self.feature, -self.relation, self.value)
|
|
82
|
+
|
|
83
|
+
def __repr__(self):
|
|
84
|
+
return '%s(%r, %r, %r)' % (self.__class__, self.feature, self.relation, self.value)
|
|
85
|
+
|
|
86
|
+
def __str__(self):
|
|
87
|
+
return ' '.join([str(self.feature), str(self.relation.value), str(self.value)])
|
|
88
|
+
|
|
89
|
+
def __eq__(self, other):
|
|
90
|
+
return self.feature == other.feature and self.relation == other.relation and self.value == other.value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_predicate(relation, value):
|
|
94
|
+
if (isinstance(value, str) or isinstance(value, bool)) and relation != Relation.EQ and relation != Relation.NEQ:
|
|
95
|
+
raise ValueError('String or boolean value "{}" can only be compared through "==" or "!=" but was "{}"'
|
|
96
|
+
.format(value, relation.value))
|
|
97
|
+
|
|
98
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RuleSetGenerator(metaclass=abc.ABCMeta):
|
|
10
|
+
@classmethod
|
|
11
|
+
def __subclasshook__(cls, subclass):
|
|
12
|
+
return callable(subclass.generate)
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def generate(self, X: pd.DataFrame, y: pd.Series, to_learn: Any, **kwargs) -> DnfRuleSet:
|
|
16
|
+
"""
|
|
17
|
+
Train a rule induction algorithm and generate a trxf DnfRuleSet
|
|
18
|
+
|
|
19
|
+
@param X: pandas dataframe representing features
|
|
20
|
+
@param y: pandas series representing labels
|
|
21
|
+
@param to_learn: one of the unique values of y we want to learn, representing the positive class
|
|
22
|
+
@param kwargs: algorithm-specific parameters
|
|
23
|
+
@return: a trxf DnfRuleSet
|
|
24
|
+
"""
|
|
25
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from rulelearn_ri.trxf.core.boolean_evaluator import BooleanEvaluator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def set_equality(list1: list, list2: list) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Equivalent to set(list1) == set(list2) but checks the set inequality manually for lists containing elements that are
|
|
9
|
+
not hashable.
|
|
10
|
+
|
|
11
|
+
@param list1: The first list
|
|
12
|
+
@param list2: The second list
|
|
13
|
+
|
|
14
|
+
@return bool: Whether the two lists are equal when converted to sets
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
equality = True
|
|
18
|
+
for item in list1:
|
|
19
|
+
if item not in list2:
|
|
20
|
+
equality = False
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
for item in list2:
|
|
24
|
+
if item not in list1:
|
|
25
|
+
equality = False
|
|
26
|
+
break
|
|
27
|
+
|
|
28
|
+
return equality
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def batch_evaluate(evaluator: BooleanEvaluator, X: pd.DataFrame) -> pd.Series:
|
|
32
|
+
"""
|
|
33
|
+
Evaluate the truth value of the evaluator for each row of X, representing the assignments
|
|
34
|
+
|
|
35
|
+
@param evaluator: a BooleanEvaluator, e.g., DnfRuleSet, Conjunction, etc.
|
|
36
|
+
@param X: pandas dataframe where column names represent variable names used by the evaluator
|
|
37
|
+
@return: pandas series of boolean values representing
|
|
38
|
+
"""
|
|
39
|
+
assignments = X.to_dict('records')
|
|
40
|
+
return pd.Series(list(map(lambda assignment: evaluator.evaluate(assignment), assignments)))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from sklearn.metrics import confusion_matrix
|
|
8
|
+
|
|
9
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
10
|
+
from rulelearn_ri.trxf.core.utils import batch_evaluate
|
|
11
|
+
|
|
12
|
+
RuleContributionMetrics = namedtuple(
|
|
13
|
+
'RuleContributionMetrics',
|
|
14
|
+
'conjunction, tp_common, tp_unique, fp_common, fp_unique, n_correct, n_records, laplace_estimate')
|
|
15
|
+
"""
|
|
16
|
+
conjunction: a trxf Conjunction object
|
|
17
|
+
tp_common: count of true positives identified based on this and at least one other rule (large values indicate redundancy)
|
|
18
|
+
tp_unique: count of true positives correctly identifying a label based only on this rule (indicates strength of the rule)
|
|
19
|
+
fp_common: count of false positives based on this and at least one other rule
|
|
20
|
+
fp_unique: count of false positives, i.e. mis-classified instances, based on this rule alone
|
|
21
|
+
n_correct: count of true positives + true_negatives
|
|
22
|
+
n_records: total number of instances
|
|
23
|
+
laplace_estimate: metric of confidence (https://en.wikipedia.org/wiki/Additive_smoothing)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
RuleComplexityMetrics = namedtuple('RuleComplexityMetrics',
|
|
27
|
+
'num_rules, max_rule_length, sum_rule_length, num_distinct_predicates')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_ruleset_metrics(ruleset: DnfRuleSet, X: pd.DataFrame, y: pd.Series):
|
|
31
|
+
"""
|
|
32
|
+
Compute ruleset-level confusion matrix and accuracy when used as a binary classifier
|
|
33
|
+
|
|
34
|
+
@param X: features
|
|
35
|
+
@param y: labels
|
|
36
|
+
@return: a dict describing the confusion matrix and accuracy
|
|
37
|
+
"""
|
|
38
|
+
then_part = ruleset.then_part
|
|
39
|
+
y_true = (y == then_part)
|
|
40
|
+
y_pred = batch_evaluate(ruleset, X)
|
|
41
|
+
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
|
|
42
|
+
accuracy = (tn + tp) / len(y)
|
|
43
|
+
return {'tn': tn, 'fp': fp, 'fn': fn, 'tp': tp, 'accuracy': accuracy}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_rule_metrics(ruleset: DnfRuleSet, X: pd.DataFrame, y: pd.Series) -> List[RuleContributionMetrics]:
|
|
47
|
+
"""
|
|
48
|
+
Compute individual rule-level contribution to coverage and redundancy.
|
|
49
|
+
See https://ibm.ent.box.com/file/888961054325?sb=/activity/annotations/956283151925 for more context.
|
|
50
|
+
|
|
51
|
+
@param X: features
|
|
52
|
+
@param y: labels
|
|
53
|
+
@return: a dict mapping the string representation of conjunctions to a dict containing the contribution metrics
|
|
54
|
+
"""
|
|
55
|
+
then_part = ruleset.then_part
|
|
56
|
+
res = []
|
|
57
|
+
for conjunction in ruleset.conjunctions:
|
|
58
|
+
complement = deepcopy(ruleset)
|
|
59
|
+
complement.remove_conjunction(conjunction)
|
|
60
|
+
y_pred_compl = batch_evaluate(complement, X)
|
|
61
|
+
tp_compl, tn_compl, fp_compl, fn_compl = get_preaggregated_confusion_matrix(y_pred_compl, y, then_part)
|
|
62
|
+
y_pred_conj = batch_evaluate(conjunction, X)
|
|
63
|
+
tp_conj, tn_conj, fp_conj, fn_conj = get_preaggregated_confusion_matrix(y_pred_conj, y, then_part)
|
|
64
|
+
laplace_estimate = (len(tp_conj) + 1) / (len(tp_conj) + len(fp_conj) + 2)
|
|
65
|
+
|
|
66
|
+
tp_common = len(set(tp_conj).intersection(set(tp_compl)))
|
|
67
|
+
tp_unique = len(set(tp_conj).difference(set(tp_compl)))
|
|
68
|
+
fp_common = len(set(fp_conj).intersection(set(fp_compl)))
|
|
69
|
+
fp_unique = len(set(fp_conj).difference(set(fp_compl)))
|
|
70
|
+
n_correct = len(tn_conj) + len(tp_conj)
|
|
71
|
+
n_records = X.shape[0]
|
|
72
|
+
res.append(RuleContributionMetrics(
|
|
73
|
+
conjunction, tp_common, tp_unique, fp_common, fp_unique, n_correct, n_records, laplace_estimate))
|
|
74
|
+
return res
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_ruleset_complexity(ruleset: DnfRuleSet):
|
|
78
|
+
num_rules = len(ruleset)
|
|
79
|
+
rule_lengths = list(map(len, ruleset.conjunctions))
|
|
80
|
+
max_rule_length = max(rule_lengths)
|
|
81
|
+
sum_rule_length = sum(rule_lengths)
|
|
82
|
+
predicates = []
|
|
83
|
+
for c in ruleset.conjunctions:
|
|
84
|
+
predicates += [p for p in c.predicates if p not in predicates]
|
|
85
|
+
num_distinct_predicates = len(predicates)
|
|
86
|
+
return RuleComplexityMetrics(num_rules, max_rule_length, sum_rule_length, num_distinct_predicates)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_preaggregated_confusion_matrix(y_pred, y_true, pos_value):
|
|
90
|
+
y_p = y_pred.to_numpy()
|
|
91
|
+
y_t = (y_true.to_numpy() == pos_value)
|
|
92
|
+
|
|
93
|
+
tp = np.where(y_p & y_t)[0]
|
|
94
|
+
tn = np.where(np.logical_not(y_p) & np.logical_not(y_t))[0]
|
|
95
|
+
fp = np.where(y_p & np.logical_not(y_t))[0]
|
|
96
|
+
fn = np.where(y_t & np.logical_not(y_p))[0]
|
|
97
|
+
|
|
98
|
+
return tp, tn, fp, fn
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = '0.0.1'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .data_dictionary import DataDictionary
|
|
2
|
+
from .data_dictionary import DataField
|
|
3
|
+
from .data_dictionary import DataType
|
|
4
|
+
from .data_dictionary import OpType
|
|
5
|
+
from .data_dictionary import Value
|
|
6
|
+
from .data_dictionary import Restriction
|
|
7
|
+
from .mining_schema import MiningField
|
|
8
|
+
from .mining_schema import MiningFieldUsageType
|
|
9
|
+
from .mining_schema import MiningSchema
|
|
10
|
+
from .predicate import BooleanOperator
|
|
11
|
+
from .predicate import CompoundPredicate
|
|
12
|
+
from .predicate import Operator
|
|
13
|
+
from .predicate import SimplePredicate
|
|
14
|
+
from .predicate import TruePredicate
|
|
15
|
+
from .rule import SimpleRule
|
|
16
|
+
from .rule import DEFAULT_WEIGHT
|
|
17
|
+
from .rule import DEFAULT_CONFIDENCE
|
|
18
|
+
from .ruleset import RuleSelectionMethod
|
|
19
|
+
from .ruleset import RuleSet
|
|
20
|
+
from .ruleset_model import RuleSetModel
|
|
21
|
+
from .pmml_ruleset_model import SimplePMMLRuleSetModel
|
|
22
|
+
from .scorecard import Scorecard, Output, OutputField
|
|
23
|
+
from .characteristics import Characteristics, Characteristic
|
|
24
|
+
from .attribute import Attribute, ComplexPartialScore
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from . import predicate
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# python >= 3.7
|
|
7
|
+
from dataclasses import dataclass as dataclass
|
|
8
|
+
from dataclasses import field as field
|
|
9
|
+
except ImportError:
|
|
10
|
+
from attr import s as dataclass
|
|
11
|
+
from attr import ib as field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ComplexPartialScore:
|
|
16
|
+
feature_name: str = field()
|
|
17
|
+
multiplier: str = field(default=None)
|
|
18
|
+
constant: str = field(default=None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Attribute:
|
|
23
|
+
score: typing.Union[str, ComplexPartialScore]
|
|
24
|
+
predicate: typing.Union[predicate.SimplePredicate, predicate.CompoundPredicate, predicate.TruePredicate]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from . import attribute
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# python >= 3.7
|
|
7
|
+
from dataclasses import dataclass as dataclass
|
|
8
|
+
except ImportError:
|
|
9
|
+
from attr import s as dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Characteristic:
|
|
14
|
+
name: str
|
|
15
|
+
attributes: typing.List[attribute.Attribute]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Characteristics:
|
|
20
|
+
characteristics: typing.List[Characteristic]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
try:
|
|
2
|
+
# python >= 3.7
|
|
3
|
+
from dataclasses import dataclass as dataclass
|
|
4
|
+
from dataclasses import field as field
|
|
5
|
+
except ImportError:
|
|
6
|
+
from attr import s as dataclass
|
|
7
|
+
from attr import ib as field
|
|
8
|
+
import enum
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataType(enum.Enum):
|
|
13
|
+
string = 0
|
|
14
|
+
integer = 1
|
|
15
|
+
float = 2
|
|
16
|
+
double = 3
|
|
17
|
+
boolean = 4
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OpType(enum.Enum):
|
|
21
|
+
categorical = 0
|
|
22
|
+
ordinal = 1
|
|
23
|
+
continuous = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Restriction(enum.Enum):
|
|
27
|
+
valid = 0
|
|
28
|
+
invalid = 1
|
|
29
|
+
missing = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Value:
|
|
34
|
+
value: str = field()
|
|
35
|
+
property: Restriction = field(default=Restriction.valid)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class DataField:
|
|
40
|
+
name: str = field()
|
|
41
|
+
optype: OpType = field()
|
|
42
|
+
dataType: DataType = field()
|
|
43
|
+
values: typing.Optional[typing.List[Value]] = field(default=None)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
if self.values and \
|
|
47
|
+
(self.dataType is not DataType.string or self.optype not in (OpType.ordinal, OpType.categorical)):
|
|
48
|
+
raise ValueError
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class DataDictionary:
|
|
53
|
+
dataFields: typing.List[DataField] = field()
|