ripple-down-rules 0.0.0__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.
- ripple_down_rules/__init__.py +0 -0
- ripple_down_rules/datasets.py +148 -0
- ripple_down_rules/datastructures/__init__.py +4 -0
- ripple_down_rules/datastructures/callable_expression.py +237 -0
- ripple_down_rules/datastructures/dataclasses.py +76 -0
- ripple_down_rules/datastructures/enums.py +173 -0
- ripple_down_rules/datastructures/generated/__init__.py +0 -0
- ripple_down_rules/datastructures/generated/column/__init__.py +0 -0
- ripple_down_rules/datastructures/generated/row/__init__.py +0 -0
- ripple_down_rules/datastructures/table.py +544 -0
- ripple_down_rules/experts.py +281 -0
- ripple_down_rules/failures.py +10 -0
- ripple_down_rules/prompt.py +101 -0
- ripple_down_rules/rdr.py +687 -0
- ripple_down_rules/rules.py +260 -0
- ripple_down_rules/utils.py +463 -0
- ripple_down_rules-0.0.0.dist-info/METADATA +54 -0
- ripple_down_rules-0.0.0.dist-info/RECORD +20 -0
- ripple_down_rules-0.0.0.dist-info/WHEEL +5 -0
- ripple_down_rules-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
from anytree import NodeMixin
|
7
|
+
from typing_extensions import List, Optional, Self, Union, Dict, Any
|
8
|
+
|
9
|
+
from .datastructures import CallableExpression, Case, SQLTable
|
10
|
+
from .datastructures.enums import RDREdge, Stop
|
11
|
+
from .utils import SubclassJSONSerializer
|
12
|
+
|
13
|
+
|
14
|
+
class Rule(NodeMixin, SubclassJSONSerializer, ABC):
|
15
|
+
fired: Optional[bool] = None
|
16
|
+
"""
|
17
|
+
Whether the rule has fired or not.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, conditions: Optional[CallableExpression] = None,
|
21
|
+
conclusion: Optional[CallableExpression] = None,
|
22
|
+
parent: Optional[Rule] = None,
|
23
|
+
corner_case: Optional[Union[Case, SQLTable]] = None,
|
24
|
+
weight: Optional[str] = None):
|
25
|
+
"""
|
26
|
+
A rule in the ripple down rules classifier.
|
27
|
+
|
28
|
+
:param conditions: The conditions of the rule.
|
29
|
+
:param conclusion: The conclusion of the rule when the conditions are met.
|
30
|
+
:param parent: The parent rule of this rule.
|
31
|
+
:param corner_case: The corner case that this rule is based on/created from.
|
32
|
+
:param weight: The weight of the rule, which is the type of edge connecting the rule to its parent.
|
33
|
+
"""
|
34
|
+
super(Rule, self).__init__()
|
35
|
+
self.conclusion = conclusion
|
36
|
+
self.corner_case = corner_case
|
37
|
+
self.parent = parent
|
38
|
+
self.weight: Optional[str] = weight
|
39
|
+
self.conditions = conditions if conditions else None
|
40
|
+
self.json_serialization: Optional[Dict[str, Any]] = None
|
41
|
+
|
42
|
+
def _post_detach(self, parent):
|
43
|
+
"""
|
44
|
+
Called after this node is detached from the tree, useful when drawing the tree.
|
45
|
+
|
46
|
+
:param parent: The parent node from which this node was detached.
|
47
|
+
"""
|
48
|
+
self.weight = None
|
49
|
+
|
50
|
+
def __call__(self, x: Case) -> Self:
|
51
|
+
return self.evaluate(x)
|
52
|
+
|
53
|
+
def evaluate(self, x: Case) -> Rule:
|
54
|
+
"""
|
55
|
+
Check if the rule or its refinement or its alternative match the case,
|
56
|
+
by checking if the conditions are met, then return the rule that matches the case.
|
57
|
+
|
58
|
+
:param x: The case to evaluate the rule on.
|
59
|
+
:return: The rule that fired or the last evaluated rule if no rule fired.
|
60
|
+
"""
|
61
|
+
if not self.conditions:
|
62
|
+
raise ValueError("Rule has no conditions")
|
63
|
+
self.fired = self.conditions(x)
|
64
|
+
return self.evaluate_next_rule(x)
|
65
|
+
|
66
|
+
@abstractmethod
|
67
|
+
def evaluate_next_rule(self, x: Case):
|
68
|
+
"""
|
69
|
+
Evaluate the next rule after this rule is evaluated.
|
70
|
+
"""
|
71
|
+
pass
|
72
|
+
|
73
|
+
@property
|
74
|
+
def name(self):
|
75
|
+
"""
|
76
|
+
Get the name of the rule, which is the conditions and the conclusion.
|
77
|
+
"""
|
78
|
+
return self.__str__()
|
79
|
+
|
80
|
+
def __str__(self, sep="\n"):
|
81
|
+
"""
|
82
|
+
Get the string representation of the rule, which is the conditions and the conclusion.
|
83
|
+
"""
|
84
|
+
return f"{self.conditions}{sep}=> {self.conclusion}"
|
85
|
+
|
86
|
+
def __repr__(self):
|
87
|
+
return self.__str__()
|
88
|
+
|
89
|
+
|
90
|
+
class HasAlternativeRule:
|
91
|
+
"""
|
92
|
+
A mixin class for rules that have an alternative rule.
|
93
|
+
"""
|
94
|
+
_alternative: Optional[Rule] = None
|
95
|
+
"""
|
96
|
+
The alternative rule of the rule, which is evaluated when the rule doesn't fire.
|
97
|
+
"""
|
98
|
+
furthest_alternative: Optional[List[Rule]] = None
|
99
|
+
"""
|
100
|
+
The furthest alternative rule of the rule, which is the last alternative rule in the chain of alternative rules.
|
101
|
+
"""
|
102
|
+
all_alternatives: Optional[List[Rule]] = None
|
103
|
+
"""
|
104
|
+
All alternative rules of the rule, which is all the alternative rules in the chain of alternative rules.
|
105
|
+
"""
|
106
|
+
|
107
|
+
@property
|
108
|
+
def alternative(self) -> Optional[Rule]:
|
109
|
+
return self._alternative
|
110
|
+
|
111
|
+
@alternative.setter
|
112
|
+
def alternative(self, new_rule: Rule):
|
113
|
+
"""
|
114
|
+
Set the alternative rule of the rule. It is important that no rules should be retracted or changed,
|
115
|
+
only new rules should be added.
|
116
|
+
"""
|
117
|
+
if self.furthest_alternative:
|
118
|
+
self.furthest_alternative[-1].alternative = new_rule
|
119
|
+
else:
|
120
|
+
new_rule.parent = self
|
121
|
+
new_rule.weight = RDREdge.Alternative.value if not new_rule.weight else new_rule.weight
|
122
|
+
self._alternative = new_rule
|
123
|
+
self.furthest_alternative = [new_rule]
|
124
|
+
|
125
|
+
|
126
|
+
class HasRefinementRule:
|
127
|
+
_refinement: Optional[HasAlternativeRule] = None
|
128
|
+
"""
|
129
|
+
The refinement rule of the rule, which is evaluated when the rule fires.
|
130
|
+
"""
|
131
|
+
|
132
|
+
@property
|
133
|
+
def refinement(self) -> Optional[Rule]:
|
134
|
+
return self._refinement
|
135
|
+
|
136
|
+
@refinement.setter
|
137
|
+
def refinement(self, new_rule: Rule):
|
138
|
+
"""
|
139
|
+
Set the refinement rule of the rule. It is important that no rules should be retracted or changed,
|
140
|
+
only new rules should be added.
|
141
|
+
"""
|
142
|
+
new_rule.top_rule = self
|
143
|
+
if self.refinement:
|
144
|
+
self.refinement.alternative = new_rule
|
145
|
+
else:
|
146
|
+
new_rule.parent = self
|
147
|
+
new_rule.weight = RDREdge.Refinement.value
|
148
|
+
self._refinement = new_rule
|
149
|
+
|
150
|
+
|
151
|
+
class SingleClassRule(Rule, HasAlternativeRule, HasRefinementRule):
|
152
|
+
"""
|
153
|
+
A rule in the SingleClassRDR classifier, it can have a refinement or an alternative rule or both.
|
154
|
+
"""
|
155
|
+
|
156
|
+
def evaluate_next_rule(self, x: Case) -> SingleClassRule:
|
157
|
+
if self.fired:
|
158
|
+
returned_rule = self.refinement(x) if self.refinement else self
|
159
|
+
else:
|
160
|
+
returned_rule = self.alternative(x) if self.alternative else self
|
161
|
+
return returned_rule if returned_rule.fired else self
|
162
|
+
|
163
|
+
def fit_rule(self, x: Case, target: CallableExpression, conditions: CallableExpression):
|
164
|
+
new_rule = SingleClassRule(conditions, target,
|
165
|
+
corner_case=x, parent=self)
|
166
|
+
if self.fired:
|
167
|
+
self.refinement = new_rule
|
168
|
+
else:
|
169
|
+
self.alternative = new_rule
|
170
|
+
|
171
|
+
def write_conclusion_as_source_code(self, parent_indent: str = "") -> str:
|
172
|
+
"""
|
173
|
+
Get the source code representation of the conclusion of the rule.
|
174
|
+
|
175
|
+
:param parent_indent: The indentation of the parent rule.
|
176
|
+
"""
|
177
|
+
if isinstance(self.conclusion, CallableExpression):
|
178
|
+
conclusion = self.conclusion.parsed_user_input
|
179
|
+
elif isinstance(self.conclusion, Enum):
|
180
|
+
conclusion = str(self.conclusion)
|
181
|
+
else:
|
182
|
+
conclusion = self.conclusion
|
183
|
+
return f"{parent_indent}{' ' * 4}return {conclusion}\n"
|
184
|
+
|
185
|
+
def write_condition_as_source_code(self, parent_indent: str = "") -> str:
|
186
|
+
"""
|
187
|
+
Get the source code representation of the conditions of the rule.
|
188
|
+
|
189
|
+
:param parent_indent: The indentation of the parent rule.
|
190
|
+
"""
|
191
|
+
if_clause = "elif" if self.weight == RDREdge.Alternative.value else "if"
|
192
|
+
return f"{parent_indent}{if_clause} {self.conditions.parsed_user_input}:\n"
|
193
|
+
|
194
|
+
def to_json(self) -> Dict[str, Any]:
|
195
|
+
self.json_serialization = {**SubclassJSONSerializer.to_json(self),
|
196
|
+
"conditions": self.conditions.to_json(),
|
197
|
+
"conclusion": self.conclusion.to_json(),
|
198
|
+
"parent": self.parent.json_serialization if self.parent else None,
|
199
|
+
"corner_case": self.corner_case.to_json() if self.corner_case else None,
|
200
|
+
"weight": self.weight,
|
201
|
+
"refinement": self.refinement.to_json() if self.refinement is not None else None,
|
202
|
+
"alternative": self.alternative.to_json() if self.alternative is not None else None}
|
203
|
+
return self.json_serialization
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def _from_json(cls, data: Dict[str, Any]) -> SingleClassRule:
|
207
|
+
loaded_rule = cls(conditions=CallableExpression.from_json(data["conditions"]),
|
208
|
+
conclusion=CallableExpression.from_json(data["conclusion"]),
|
209
|
+
parent=SingleClassRule.from_json(data["parent"]),
|
210
|
+
corner_case=Case.from_json(data["corner_case"]),
|
211
|
+
weight=data["weight"])
|
212
|
+
loaded_rule.refinement = SingleClassRule.from_json(data["refinement"])
|
213
|
+
loaded_rule.alternative = SingleClassRule.from_json(data["alternative"])
|
214
|
+
return loaded_rule
|
215
|
+
|
216
|
+
|
217
|
+
class MultiClassStopRule(Rule, HasAlternativeRule):
|
218
|
+
"""
|
219
|
+
A rule in the MultiClassRDR classifier, it can have an alternative rule and a top rule,
|
220
|
+
the conclusion of the rule is a Stop category meant to stop the parent conclusion from being made.
|
221
|
+
"""
|
222
|
+
top_rule: Optional[MultiClassTopRule] = None
|
223
|
+
"""
|
224
|
+
The top rule of the rule, which is the nearest ancestor that fired and this rule is a refinement of.
|
225
|
+
"""
|
226
|
+
|
227
|
+
def __init__(self, *args, **kwargs):
|
228
|
+
super(MultiClassStopRule, self).__init__(*args, **kwargs)
|
229
|
+
self.conclusion = Stop.stop
|
230
|
+
|
231
|
+
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
|
232
|
+
if self.fired:
|
233
|
+
self.top_rule.fired = False
|
234
|
+
return self.top_rule.alternative
|
235
|
+
elif self.alternative:
|
236
|
+
return self.alternative(x)
|
237
|
+
else:
|
238
|
+
return self.top_rule.alternative
|
239
|
+
|
240
|
+
|
241
|
+
class MultiClassTopRule(Rule, HasRefinementRule, HasAlternativeRule):
|
242
|
+
"""
|
243
|
+
A rule in the MultiClassRDR classifier, it can have a refinement and a next rule.
|
244
|
+
"""
|
245
|
+
|
246
|
+
def __init__(self, *args, **kwargs):
|
247
|
+
super(MultiClassTopRule, self).__init__(*args, **kwargs)
|
248
|
+
self.weight = RDREdge.Next.value
|
249
|
+
|
250
|
+
def evaluate_next_rule(self, x: Case) -> Optional[Union[MultiClassStopRule, MultiClassTopRule]]:
|
251
|
+
if self.fired and self.refinement:
|
252
|
+
return self.refinement(x)
|
253
|
+
elif self.alternative: # Here alternative refers to next rule in MultiClassRDR
|
254
|
+
return self.alternative
|
255
|
+
|
256
|
+
def fit_rule(self, x: Case, target: CallableExpression, conditions: CallableExpression):
|
257
|
+
if self.fired and target != self.conclusion:
|
258
|
+
self.refinement = MultiClassStopRule(conditions, corner_case=x, parent=self)
|
259
|
+
elif not self.fired:
|
260
|
+
self.alternative = MultiClassTopRule(conditions, target, corner_case=x, parent=self)
|