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,600 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from rulelearn_ri.dise import DISExplainer
|
|
8
|
+
|
|
9
|
+
from rulelearn_ri.algorithms.ripper.base import _encoding_for_parallel, init_encoder, encode_nominal, _split_instances
|
|
10
|
+
from rulelearn_ri.algorithms.ripper.binding import append_literal, _bind_literal, _bind_rule, \
|
|
11
|
+
unbound_rule_list_index, _unbound, _unbound_rule_list, _rule_list_predict, LE, EQ, _find_best_literal, Literal, \
|
|
12
|
+
_filter_contradicted_instances
|
|
13
|
+
from rulelearn_ri.algorithms.ripper.mdl import _mdl
|
|
14
|
+
from rulelearn_ri.algorithms.ripper.pruning import _pruning_irep, _minimize_rule_list, _pruning_optimization
|
|
15
|
+
|
|
16
|
+
from rulelearn_ri.trxf.core import Conjunction
|
|
17
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
18
|
+
from rulelearn_ri.trxf.core import Feature
|
|
19
|
+
from rulelearn_ri.trxf.core import Predicate, Relation
|
|
20
|
+
|
|
21
|
+
from pandas import DataFrame, Series
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RipperExplainer(DISExplainer):
|
|
25
|
+
"""
|
|
26
|
+
RIPPER (Repeated Incremental Pruning to Produce Error Reduction) is a heuristic rule induction algorithm
|
|
27
|
+
based on separate-and-conquer. The explainer outputs a rule set in Disjunctive Normal Form (DNF) for a single
|
|
28
|
+
target concept.
|
|
29
|
+
|
|
30
|
+
References:
|
|
31
|
+
.. [#ML95] `William W Cohen, "Fast Effective Rule Induction"
|
|
32
|
+
Machine Learning: Proceedings of the Twelfth International Conference, 1995.
|
|
33
|
+
<http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.107.2612&rep=rep1&type=pdf>
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
d: int = 64,
|
|
39
|
+
k: int = 2,
|
|
40
|
+
pruning_threshold: int = 20,
|
|
41
|
+
random_state: int = 0,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Args:
|
|
45
|
+
d (int): The number of bits that a new rule need to gain. Defaults to 64.
|
|
46
|
+
k (int): The number of iterations for the optimization loop. Defaults to 2.
|
|
47
|
+
pruning_threshold (int): The minimum number of instances for splitting. Defaults to 20.
|
|
48
|
+
random_state (int): The random seed for the splitting function. Defaults to 0.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.d = d
|
|
53
|
+
self.k = k
|
|
54
|
+
self.pruning_threshold = pruning_threshold
|
|
55
|
+
self.random_state = random_state
|
|
56
|
+
self._target_label = None
|
|
57
|
+
self.default_label = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def target_label(self):
|
|
61
|
+
"""
|
|
62
|
+
The latest positive value RIPPER has been fitted for.
|
|
63
|
+
"""
|
|
64
|
+
return self._target_label
|
|
65
|
+
|
|
66
|
+
def set_params(self, **kwargs):
|
|
67
|
+
self.d = kwargs.get('d', 64)
|
|
68
|
+
self.k = kwargs.get('k', 2)
|
|
69
|
+
self.pruning_threshold = kwargs.get('pruning_threshold', 20)
|
|
70
|
+
self.random_state = kwargs.get('random_state', 0)
|
|
71
|
+
|
|
72
|
+
def fit(self, train: DataFrame, y: Series, target_label=None):
|
|
73
|
+
"""
|
|
74
|
+
The fit function for RIPPER algorithm. Its implementation is limited to DataFrame and Series because the RIPPER
|
|
75
|
+
algorithm needs the information of feature name and have to support nominal data type. Only `float` dtypes are
|
|
76
|
+
considered numerical features. All others (including int) are treated as nominal.
|
|
77
|
+
|
|
78
|
+
If target_label is specified, binary classification is assumed and asserted, and training uses target_label as selection\
|
|
79
|
+
of positive examples.
|
|
80
|
+
|
|
81
|
+
The induction of rules is deterministic by default as all random choices are initialized with self.random_state,
|
|
82
|
+
which is 0 by default.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
train (pd.DataFrame): The features of the training set
|
|
86
|
+
y (pd.Series): The labels of the training set
|
|
87
|
+
target_label (Any): The target label to learn for binary classification, among the unique values of y. \
|
|
88
|
+
If not provided, Ripper will induce a native ordered ruleset with multiple labels/conclusions.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
self
|
|
92
|
+
"""
|
|
93
|
+
logger = logging.getLogger(__name__)
|
|
94
|
+
|
|
95
|
+
self._target_label = target_label
|
|
96
|
+
|
|
97
|
+
self._labels = None
|
|
98
|
+
self._rule_map = None
|
|
99
|
+
|
|
100
|
+
self._nominal_column_encoders = None
|
|
101
|
+
self._column_name_list = None
|
|
102
|
+
self._column_name_index_map = None
|
|
103
|
+
self._condition_mat = None
|
|
104
|
+
|
|
105
|
+
self._gain_n = 0
|
|
106
|
+
|
|
107
|
+
# Check that X and y have correct shape
|
|
108
|
+
assert train.shape[0] == y.shape[0], 'X, y should have same length'
|
|
109
|
+
|
|
110
|
+
self._nominal_column_encoders = init_encoder(train)
|
|
111
|
+
encode_nominal(self._nominal_column_encoders, train)
|
|
112
|
+
self._column_name_list = train.columns.values
|
|
113
|
+
|
|
114
|
+
self._column_name_index_map = {name: index for index, name in enumerate(self._column_name_list)}
|
|
115
|
+
|
|
116
|
+
unique_labels, counts = np.unique(y, return_counts=True)
|
|
117
|
+
label_count_dict = dict(zip(unique_labels, counts))
|
|
118
|
+
|
|
119
|
+
assert len(unique_labels) > 1, 'label has only one value'
|
|
120
|
+
|
|
121
|
+
# Check whenever target_label is given that label has only two values and target_label is one of them
|
|
122
|
+
assert (target_label is None) or (len(unique_labels) == 2), 'Positive label value given but label not binary.'
|
|
123
|
+
assert (target_label is None) or (
|
|
124
|
+
target_label in unique_labels), 'Positive label value given is not a label value.'
|
|
125
|
+
|
|
126
|
+
self._labels = sorted(unique_labels, key=lambda e: label_count_dict[e],
|
|
127
|
+
reverse=False) # returns list from ndarray;
|
|
128
|
+
# the label values are sorted according to their occurrence; least frequent value first
|
|
129
|
+
# Set reverse=True for reversing that order
|
|
130
|
+
if (target_label is not None) and (self._labels[0] != target_label):
|
|
131
|
+
# reverse value order for binary classification when target_label is not the least frequent
|
|
132
|
+
self._labels.reverse()
|
|
133
|
+
self.default_label = self._labels[-1] # the default value is the last label in the sorting order
|
|
134
|
+
logger.debug('Label value ordering: ' + str(self._labels))
|
|
135
|
+
|
|
136
|
+
self._condition_mat = _encoding_for_parallel(train, self._column_name_index_map)
|
|
137
|
+
self._gain_n = self._condition_mat[0].shape[0]
|
|
138
|
+
|
|
139
|
+
# copy=True: under pandas 3 copy-on-write these arrays come back read-only,
|
|
140
|
+
# and the numba kernels in binding.py only accept writable ones
|
|
141
|
+
train = train.to_numpy(dtype=np.dtype(float), copy=True)
|
|
142
|
+
y = y.to_numpy(copy=True)
|
|
143
|
+
|
|
144
|
+
x_irep_list = train
|
|
145
|
+
y_irep = y
|
|
146
|
+
|
|
147
|
+
self._rule_map = OrderedDict()
|
|
148
|
+
|
|
149
|
+
for i in range(len(self._labels) - 1):
|
|
150
|
+
pos = self._labels[i]
|
|
151
|
+
neg = self._labels[i + 1:]
|
|
152
|
+
|
|
153
|
+
indexer_pos = y_irep == pos
|
|
154
|
+
indexer_neg = np.isin(y_irep, neg)
|
|
155
|
+
|
|
156
|
+
train_pos = x_irep_list[indexer_pos]
|
|
157
|
+
train_neg = x_irep_list[indexer_neg]
|
|
158
|
+
|
|
159
|
+
irep_res = self._irep_plus_outer_loop(
|
|
160
|
+
train_pos,
|
|
161
|
+
train_neg,
|
|
162
|
+
d=self.d,
|
|
163
|
+
ratio=2 / 3,
|
|
164
|
+
pruning_threshold=self.pruning_threshold
|
|
165
|
+
)
|
|
166
|
+
if len(irep_res) > 0:
|
|
167
|
+
self._rule_map[pos] = irep_res
|
|
168
|
+
bool_vec_index = unbound_rule_list_index(x_irep_list, self._rule_map[pos])
|
|
169
|
+
x_irep_list = x_irep_list[bool_vec_index]
|
|
170
|
+
y_irep = y_irep[bool_vec_index]
|
|
171
|
+
|
|
172
|
+
logger.debug('# begin optimization')
|
|
173
|
+
|
|
174
|
+
for _ in range(self.k):
|
|
175
|
+
x_optimize = train
|
|
176
|
+
y_optimise = y
|
|
177
|
+
for i in range(len(self._labels) - 1):
|
|
178
|
+
if not self._rule_map.get(self._labels[i]):
|
|
179
|
+
continue
|
|
180
|
+
pos = self._labels[i]
|
|
181
|
+
neg = self._labels[i + 1:]
|
|
182
|
+
|
|
183
|
+
indexer_pos = y_optimise == pos
|
|
184
|
+
indexer_neg = np.isin(y_optimise, neg)
|
|
185
|
+
|
|
186
|
+
train_pos = x_optimize[indexer_pos]
|
|
187
|
+
train_neg = x_optimize[indexer_neg]
|
|
188
|
+
|
|
189
|
+
self._rule_map[pos] = self._optimize(
|
|
190
|
+
train_pos,
|
|
191
|
+
train_neg,
|
|
192
|
+
ratio=2 / 3,
|
|
193
|
+
rules_input=self._rule_map[pos]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
bool_vec_index = unbound_rule_list_index(x_optimize, self._rule_map[pos])
|
|
197
|
+
x_optimize = x_optimize[bool_vec_index]
|
|
198
|
+
y_optimise = y_optimise[bool_vec_index]
|
|
199
|
+
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def predict(self, X: DataFrame) -> np.ndarray:
|
|
203
|
+
"""
|
|
204
|
+
The predict function for RIPPER algorithm. Its implementation is limited to DataFrame and Series because the
|
|
205
|
+
RIPPER algorithm needs the information of feature name and have to support nominal data type
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
X (pd.DataFrame): DataFrame of features
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
np.array: predicted labels
|
|
212
|
+
"""
|
|
213
|
+
test = X.copy()
|
|
214
|
+
encode_nominal(self._nominal_column_encoders, test)
|
|
215
|
+
result_vec = [None for _ in range(len(test))]
|
|
216
|
+
|
|
217
|
+
input_arr = test.to_numpy(dtype=np.dtype(float))
|
|
218
|
+
|
|
219
|
+
result = {label: _rule_list_predict(input_arr, self._rule_map[label]) for label in self._rule_map.keys()}
|
|
220
|
+
|
|
221
|
+
for label in self._rule_map.keys():
|
|
222
|
+
for i in range(len(result_vec)):
|
|
223
|
+
if result_vec[i] is None and result[label][i]:
|
|
224
|
+
result_vec[i] = label
|
|
225
|
+
|
|
226
|
+
return np.array([res if res is not None else self.default_label for res in result_vec])
|
|
227
|
+
|
|
228
|
+
def explain(self):
|
|
229
|
+
"""
|
|
230
|
+
Export rule set to technical interchange format trxf from internal representation
|
|
231
|
+
for the positive value (i.e. label value) it has been fitted for.
|
|
232
|
+
|
|
233
|
+
When the internal rule set is empty an empty dnf rule set with the internal pos value
|
|
234
|
+
is returned.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
trxf.DnfRuleSet
|
|
238
|
+
"""
|
|
239
|
+
assert (self.target_label is not None), 'Not fitted or not fitted for a specific pos value. Use export_rules ' \
|
|
240
|
+
'in the latter case. '
|
|
241
|
+
|
|
242
|
+
if len(self._rule_map.items()) == 0:
|
|
243
|
+
return DnfRuleSet([], self.target_label)
|
|
244
|
+
for label, rules in self._rule_map.items():
|
|
245
|
+
if label == self.target_label:
|
|
246
|
+
return self._rules_to_trxf_dnf_ruleset(rules, label)
|
|
247
|
+
raise Exception('No rules found for label: ' + str(self.target_label))
|
|
248
|
+
|
|
249
|
+
def explain_multiclass(self):
|
|
250
|
+
"""
|
|
251
|
+
Export rules to technical interchange format trxf from internal representation
|
|
252
|
+
Returns a list of rule sets.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
list(trxf.DnfRuleSet): -- Ordered list of rulesets
|
|
256
|
+
"""
|
|
257
|
+
res = list()
|
|
258
|
+
if len(self._rule_map.items()) == 0:
|
|
259
|
+
return DnfRuleSet([], self.target_label)
|
|
260
|
+
for label, rules in self._rule_map.items():
|
|
261
|
+
dnf_ruleset = self._rules_to_trxf_dnf_ruleset(rules, label)
|
|
262
|
+
res.append(dnf_ruleset)
|
|
263
|
+
default_rule = DnfRuleSet([], self.default_label)
|
|
264
|
+
res.append(default_rule)
|
|
265
|
+
return res
|
|
266
|
+
|
|
267
|
+
def _irep_plus_outer_loop(
|
|
268
|
+
self,
|
|
269
|
+
pos: np.ndarray,
|
|
270
|
+
neg: np.ndarray,
|
|
271
|
+
d: int,
|
|
272
|
+
ratio: float = 2 / 3,
|
|
273
|
+
pruning_threshold: int = 20
|
|
274
|
+
):
|
|
275
|
+
"""
|
|
276
|
+
The learning phase of RIPPER.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
pos (np.ndarray): Positive instances
|
|
280
|
+
neg (np.ndarray): Negative instances
|
|
281
|
+
d (int): The number to bit that a new rule need to gain
|
|
282
|
+
ratio (float): The percentage of pruning data
|
|
283
|
+
pruning_threshold (int): The minimum number of instances for splitting
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
list: Rule list learned using IREP algorithm
|
|
287
|
+
"""
|
|
288
|
+
neg = _filter_contradicted_instances(pos, neg)
|
|
289
|
+
n = self._gain_n
|
|
290
|
+
|
|
291
|
+
rules = []
|
|
292
|
+
|
|
293
|
+
# TODO verify if the intent was to make a copy of the value instead of the reference
|
|
294
|
+
pos_original = pos
|
|
295
|
+
neg_original = neg
|
|
296
|
+
|
|
297
|
+
dl_min = _mdl(pos, neg, rules, n)
|
|
298
|
+
|
|
299
|
+
while len(pos) > 0:
|
|
300
|
+
|
|
301
|
+
if len(pos) > pruning_threshold:
|
|
302
|
+
pos_grow, pos_prune, neg_grow, neg_prune = _split_instances(pos, neg, ratio, self.random_state)
|
|
303
|
+
rule = self._grow_rule(pos_grow, neg_grow)
|
|
304
|
+
rule = _pruning_irep(pos_prune, neg_prune, rule)
|
|
305
|
+
else:
|
|
306
|
+
rule = self._grow_rule(pos, neg)
|
|
307
|
+
|
|
308
|
+
rules.append(rule)
|
|
309
|
+
dl_new = _mdl(pos, neg, rules, n)
|
|
310
|
+
|
|
311
|
+
if dl_new > dl_min + d:
|
|
312
|
+
return _minimize_rule_list(pos_original, neg_original, rules, n)
|
|
313
|
+
else:
|
|
314
|
+
if dl_new < dl_min:
|
|
315
|
+
dl_min = dl_new
|
|
316
|
+
pos = _unbound(pos, rule)
|
|
317
|
+
neg = _unbound(neg, rule)
|
|
318
|
+
|
|
319
|
+
return rules
|
|
320
|
+
|
|
321
|
+
def _replacement(
|
|
322
|
+
self,
|
|
323
|
+
pos: np.ndarray,
|
|
324
|
+
neg: np.ndarray,
|
|
325
|
+
rules,
|
|
326
|
+
index: int,
|
|
327
|
+
ratio: float = 2 / 3
|
|
328
|
+
):
|
|
329
|
+
"""
|
|
330
|
+
The replacement step for RIPPER optimization
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
pos: np.ndarray
|
|
335
|
+
Positive instances
|
|
336
|
+
neg: np.ndarray
|
|
337
|
+
Negative instances
|
|
338
|
+
rules : list
|
|
339
|
+
Rules that needs to be optimized
|
|
340
|
+
index : int
|
|
341
|
+
Index of selected rule
|
|
342
|
+
ratio : float
|
|
343
|
+
The percentage of pruning data
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
list
|
|
348
|
+
A replacement rule
|
|
349
|
+
"""
|
|
350
|
+
rest = rules[:index] + rules[index + 1:]
|
|
351
|
+
|
|
352
|
+
new_pos = _unbound_rule_list(pos, rest)
|
|
353
|
+
|
|
354
|
+
if len(new_pos) > 2:
|
|
355
|
+
|
|
356
|
+
pos_grow, pos_prune, neg_grow, neg_prune = _split_instances(new_pos, neg, ratio, self.random_state)
|
|
357
|
+
|
|
358
|
+
new_rule = self._grow_rule(pos=pos_grow, neg=neg_grow)
|
|
359
|
+
|
|
360
|
+
pruned_rule = _pruning_optimization(
|
|
361
|
+
pos_prune=pos_prune,
|
|
362
|
+
neg_prune=neg_prune,
|
|
363
|
+
rule=new_rule,
|
|
364
|
+
rules=rules,
|
|
365
|
+
index=index
|
|
366
|
+
)
|
|
367
|
+
return pruned_rule
|
|
368
|
+
else:
|
|
369
|
+
return rules[index]
|
|
370
|
+
|
|
371
|
+
def _revision(
|
|
372
|
+
self,
|
|
373
|
+
pos: np.ndarray,
|
|
374
|
+
neg: np.ndarray,
|
|
375
|
+
rules,
|
|
376
|
+
index: int,
|
|
377
|
+
ratio: float = 2 / 3
|
|
378
|
+
):
|
|
379
|
+
"""
|
|
380
|
+
The revision step for RIPPER optimization
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
pos: np.ndarray
|
|
385
|
+
Positive instances
|
|
386
|
+
neg: np.ndarray
|
|
387
|
+
Negative instances
|
|
388
|
+
rules : list
|
|
389
|
+
Rules that needs to be optimized
|
|
390
|
+
index : int
|
|
391
|
+
Index of selected rule
|
|
392
|
+
ratio : float
|
|
393
|
+
The percentage of pruning data
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
list
|
|
398
|
+
A revision rule
|
|
399
|
+
"""
|
|
400
|
+
rule = rules[index]
|
|
401
|
+
|
|
402
|
+
new_pos = _unbound_rule_list(pos, rules[:index] + rules[index + 1:])
|
|
403
|
+
|
|
404
|
+
if new_pos.shape[0] > 2:
|
|
405
|
+
|
|
406
|
+
pos_grow, pos_prune, neg_grow, neg_prune = _split_instances(new_pos, neg, ratio, self.random_state)
|
|
407
|
+
new_rule = self._grow_rule(pos_grow, neg_grow, predefined_rule=rule)
|
|
408
|
+
pruned_rule = _pruning_optimization(
|
|
409
|
+
pos_prune=pos_prune,
|
|
410
|
+
neg_prune=neg_prune,
|
|
411
|
+
rule=new_rule,
|
|
412
|
+
rules=rules,
|
|
413
|
+
index=index
|
|
414
|
+
)
|
|
415
|
+
return pruned_rule
|
|
416
|
+
else:
|
|
417
|
+
return rule
|
|
418
|
+
|
|
419
|
+
def _optimize(
|
|
420
|
+
self,
|
|
421
|
+
pos: np.ndarray,
|
|
422
|
+
neg: np.ndarray,
|
|
423
|
+
rules_input,
|
|
424
|
+
ratio: float = 2 / 3
|
|
425
|
+
):
|
|
426
|
+
"""
|
|
427
|
+
The optimization step for RIPPER
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
pos: np.ndarray
|
|
432
|
+
Positive instances
|
|
433
|
+
neg: np.ndarray
|
|
434
|
+
Negative instances
|
|
435
|
+
rules_input : list
|
|
436
|
+
Rules that needs to be optimized
|
|
437
|
+
ratio : float
|
|
438
|
+
The percentage of pruning data
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
list
|
|
443
|
+
Optimized rule list
|
|
444
|
+
"""
|
|
445
|
+
rules = rules_input.copy()
|
|
446
|
+
|
|
447
|
+
i = 0
|
|
448
|
+
while i < len(rules):
|
|
449
|
+
|
|
450
|
+
# ====================== origin ============================================================================
|
|
451
|
+
dl_origin = _mdl(pos, neg, rules, self._gain_n)
|
|
452
|
+
|
|
453
|
+
# ====================== replacement =======================================================================
|
|
454
|
+
replacement_rule = self._replacement(pos=pos, neg=neg, rules=rules, ratio=ratio, index=i)
|
|
455
|
+
if len(replacement_rule) != 0:
|
|
456
|
+
dl_replacement = _mdl(pos, neg, rules[:i] + [replacement_rule] + rules[i + 1:], self._gain_n)
|
|
457
|
+
else:
|
|
458
|
+
dl_replacement = _mdl(pos, neg, rules[:i] + rules[i + 1:], self._gain_n)
|
|
459
|
+
|
|
460
|
+
# ====================== revision ==========================================================================
|
|
461
|
+
revision_rule = self._revision(pos=pos, neg=neg, rules=rules, ratio=ratio, index=i)
|
|
462
|
+
|
|
463
|
+
dl_revision = _mdl(pos, neg, rules[:i] + [revision_rule] + rules[i + 1:], self._gain_n)
|
|
464
|
+
|
|
465
|
+
# ====================== result ============================================================================
|
|
466
|
+
if dl_origin <= dl_replacement and dl_origin <= dl_revision:
|
|
467
|
+
# if no improvement is made, then the original rule is chosen
|
|
468
|
+
pass
|
|
469
|
+
# secondly, replacement rule is tested
|
|
470
|
+
elif dl_replacement <= dl_origin and dl_replacement <= dl_revision:
|
|
471
|
+
if len(replacement_rule) != 0:
|
|
472
|
+
rules[i] = replacement_rule
|
|
473
|
+
else:
|
|
474
|
+
del rules[i]
|
|
475
|
+
continue
|
|
476
|
+
# finally, if the origin rule and the replacement rule are not satisfied, the revision rule is chosen
|
|
477
|
+
else:
|
|
478
|
+
rules[i] = revision_rule
|
|
479
|
+
i += 1
|
|
480
|
+
return rules
|
|
481
|
+
|
|
482
|
+
def _rule_to_trxf_conjunction(self, rule):
|
|
483
|
+
"""
|
|
484
|
+
Transform one rule to a trxf conjunction given its internal presentation
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
rule : list
|
|
489
|
+
Input rule
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
trxf conjunction
|
|
494
|
+
String representation of that rule
|
|
495
|
+
"""
|
|
496
|
+
# TODO code duplication with decode_rule should be refactored -> deprecate decode_rule as it should go
|
|
497
|
+
# through trxf
|
|
498
|
+
|
|
499
|
+
conjunction = Conjunction([])
|
|
500
|
+
|
|
501
|
+
for condition in rule:
|
|
502
|
+
name = self._column_name_list[condition.name]
|
|
503
|
+
feature = Feature(name)
|
|
504
|
+
if condition.op == EQ:
|
|
505
|
+
relation = Relation.EQ
|
|
506
|
+
value = self._nominal_column_encoders[name].classes_[condition.nom_val]
|
|
507
|
+
elif condition.op == LE:
|
|
508
|
+
relation = Relation.LE
|
|
509
|
+
value = condition.num_val
|
|
510
|
+
else:
|
|
511
|
+
relation = Relation.GE
|
|
512
|
+
value = condition.num_val
|
|
513
|
+
|
|
514
|
+
predicate = Predicate(feature, relation, value)
|
|
515
|
+
conjunction.add_predicate(predicate)
|
|
516
|
+
|
|
517
|
+
return conjunction
|
|
518
|
+
|
|
519
|
+
def _rules_to_trxf_dnf_ruleset(self, rules, label):
|
|
520
|
+
"""
|
|
521
|
+
Transform rules to trxf dnf_ruleset given their internal presentation and their label
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
rules : list
|
|
526
|
+
Rules for one target
|
|
527
|
+
label : str
|
|
528
|
+
The label of rules
|
|
529
|
+
|
|
530
|
+
Returns
|
|
531
|
+
-------
|
|
532
|
+
DnfRuleSet
|
|
533
|
+
"""
|
|
534
|
+
conjunctions = list()
|
|
535
|
+
for rule in rules:
|
|
536
|
+
conjunction = self._rule_to_trxf_conjunction(rule)
|
|
537
|
+
conjunctions.append(conjunction)
|
|
538
|
+
dnf_ruleset = DnfRuleSet(conjunctions, label)
|
|
539
|
+
return dnf_ruleset
|
|
540
|
+
|
|
541
|
+
def _grow_rule(self, pos, neg, predefined_rule=None):
|
|
542
|
+
|
|
543
|
+
"""
|
|
544
|
+
The grow function for IREP*
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
pos : np.ndarray
|
|
549
|
+
Positive instances
|
|
550
|
+
neg : np.ndarray
|
|
551
|
+
Negative instances
|
|
552
|
+
predefined_rule: const int64_t[::1]
|
|
553
|
+
Existing rules
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
list
|
|
558
|
+
Learned rule
|
|
559
|
+
"""
|
|
560
|
+
_validate_grow_rule_input(pos)
|
|
561
|
+
neg = _filter_contradicted_instances(pos, neg)
|
|
562
|
+
|
|
563
|
+
if predefined_rule is not None:
|
|
564
|
+
pos = _bind_rule(pos, predefined_rule)
|
|
565
|
+
neg = _bind_rule(neg, predefined_rule)
|
|
566
|
+
|
|
567
|
+
# the length of pos must be always positive
|
|
568
|
+
if len(pos) == 0 or len(neg) == 0:
|
|
569
|
+
return predefined_rule
|
|
570
|
+
|
|
571
|
+
learned_rule = list(predefined_rule)
|
|
572
|
+
else:
|
|
573
|
+
learned_rule = list()
|
|
574
|
+
|
|
575
|
+
while len(neg) > 0:
|
|
576
|
+
pos = np.asfortranarray(pos)
|
|
577
|
+
neg = np.asfortranarray(neg)
|
|
578
|
+
|
|
579
|
+
best_literal_index = _find_best_literal(pos,
|
|
580
|
+
neg,
|
|
581
|
+
self._condition_mat[0],
|
|
582
|
+
self._condition_mat[1],
|
|
583
|
+
self._condition_mat[2],
|
|
584
|
+
self._condition_mat[3]
|
|
585
|
+
)
|
|
586
|
+
literal = Literal(self._condition_mat[0][best_literal_index],
|
|
587
|
+
self._condition_mat[1][best_literal_index],
|
|
588
|
+
self._condition_mat[2][best_literal_index],
|
|
589
|
+
self._condition_mat[3][best_literal_index]
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
append_literal(learned_rule, literal)
|
|
593
|
+
pos = _bind_literal(pos, literal.name, literal.op, literal.num_val, literal.nom_val)
|
|
594
|
+
neg = _bind_literal(neg, literal.name, literal.op, literal.num_val, literal.nom_val)
|
|
595
|
+
return learned_rule
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _validate_grow_rule_input(pos):
|
|
599
|
+
if len(pos) == 0:
|
|
600
|
+
raise AssertionError('pos must not be empty')
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from rulelearn_ri.algorithms.ripper.ripper import RipperExplainer
|
|
6
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
7
|
+
from rulelearn_ri.trxf.core.ruleset_generator import RuleSetGenerator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RipperRuleSetGenerator(RuleSetGenerator):
|
|
11
|
+
def generate(self, X: pd.DataFrame, y: pd.Series, to_learn: Any, **kwargs) -> DnfRuleSet:
|
|
12
|
+
"""
|
|
13
|
+
Optional kwargs:
|
|
14
|
+
|
|
15
|
+
d : int
|
|
16
|
+
The number to bit that a new rule need to gain (default=64)
|
|
17
|
+
k : int
|
|
18
|
+
The number of optimization iteration (default=2)
|
|
19
|
+
pruning_threshold : int
|
|
20
|
+
The minimum number of instances for splitting (default=20)
|
|
21
|
+
random_state:
|
|
22
|
+
The random seed for grow/prune set splitting (default=0)
|
|
23
|
+
"""
|
|
24
|
+
ripper = RipperExplainer(**kwargs)
|
|
25
|
+
ripper.fit(X, y, to_learn)
|
|
26
|
+
return ripper.explain()
|