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,425 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pandas import DataFrame, Series
|
|
3
|
+
|
|
4
|
+
from rulelearn_ri.algorithms.ripper.base import _encoding_for_parallel, init_encoder, encode_nominal, \
|
|
5
|
+
_split_instances
|
|
6
|
+
from rulelearn_ri.algorithms.ripper.binding import _find_best_literal, Literal, LE, GE, EQ, _bind_literal, \
|
|
7
|
+
_filter_contradicted_instances, append_literal, _bind_rule
|
|
8
|
+
from rulelearn_ri.algorithms.ripper.pruning import _pruning_irep
|
|
9
|
+
|
|
10
|
+
from rulelearn_ri.trxf.core import Conjunction
|
|
11
|
+
from rulelearn_ri.trxf.core import Feature
|
|
12
|
+
from rulelearn_ri.trxf.core import Predicate, Relation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def optimize_single_rule(
|
|
16
|
+
train: DataFrame,
|
|
17
|
+
y: Series,
|
|
18
|
+
target_label,
|
|
19
|
+
fixed_attributes=None,
|
|
20
|
+
fixed_predicates=None,
|
|
21
|
+
forbidden_attributes=None,
|
|
22
|
+
pruning_threshold: int = 20,
|
|
23
|
+
random_state: int = 0,
|
|
24
|
+
ratio: float = 2 / 3,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Optimize a single conjunctive rule for the given target label.
|
|
28
|
+
|
|
29
|
+
Optionally, certain features can be forced to appear in the rule via ``fixed_attributes``,
|
|
30
|
+
and/or certain whole predicates (with fixed boundaries) can be given via ``fixed_predicates``.
|
|
31
|
+
Both can be specified together; attributes already covered by fixed_predicates are skipped.
|
|
32
|
+
Conversely, ``forbidden_attributes`` lists features that must NOT appear in the rule; these
|
|
33
|
+
must not overlap with the forced attributes or predicates, otherwise a ValueError is raised.
|
|
34
|
+
The algorithm grows additional literals and prunes to produce a single optimized rule.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
train (pd.DataFrame): Training features
|
|
38
|
+
y (pd.Series): Training labels
|
|
39
|
+
target_label: The positive class label for the rule; must be one of the values in ``y``
|
|
40
|
+
fixed_attributes (list of str, optional): Column names that must appear in the rule
|
|
41
|
+
fixed_predicates (list of Predicate, optional): Predicates that must be included as-is
|
|
42
|
+
forbidden_attributes (list of str, optional): Column names that must not appear in the rule
|
|
43
|
+
pruning_threshold (int): Minimum instances for grow/prune split. Defaults to 20.
|
|
44
|
+
random_state (int): Random seed for data splitting. Defaults to 0.
|
|
45
|
+
ratio (float): Fraction of data for growing. Defaults to 2/3.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Conjunction: The optimized rule as a TRXF Conjunction
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If ``target_label`` does not occur in ``y`` (e.g. no positive instances, or a label
|
|
52
|
+
of the wrong type such as ``'True'`` for a boolean ``y``), mirroring ``RipperExplainer.fit``
|
|
53
|
+
"""
|
|
54
|
+
labels = np.unique(y).tolist()
|
|
55
|
+
if target_label not in labels:
|
|
56
|
+
raise ValueError('target_label {!r} not found in y (values: {})'.format(target_label, labels))
|
|
57
|
+
|
|
58
|
+
train = train.copy()
|
|
59
|
+
y = y.copy()
|
|
60
|
+
|
|
61
|
+
nominal_encoders = init_encoder(train)
|
|
62
|
+
encode_nominal(nominal_encoders, train)
|
|
63
|
+
column_name_list = train.columns.values
|
|
64
|
+
column_name_index_map = {name: idx for idx, name in enumerate(column_name_list)}
|
|
65
|
+
condition_mat = _encoding_for_parallel(train, column_name_index_map)
|
|
66
|
+
|
|
67
|
+
# Build the unified forced feature set and optional predefined rule
|
|
68
|
+
forced_literals = []
|
|
69
|
+
forced_features = set()
|
|
70
|
+
if fixed_predicates is not None:
|
|
71
|
+
forced_literals = _predicates_to_literals(
|
|
72
|
+
fixed_predicates, column_name_index_map, nominal_encoders)
|
|
73
|
+
forced_features = set(lit.name for lit in forced_literals)
|
|
74
|
+
|
|
75
|
+
required_features = set()
|
|
76
|
+
if fixed_attributes is not None:
|
|
77
|
+
required_features = set(column_name_index_map[attr]
|
|
78
|
+
for attr in fixed_attributes)
|
|
79
|
+
|
|
80
|
+
forbidden_features = set()
|
|
81
|
+
if forbidden_attributes is not None:
|
|
82
|
+
forbidden_features = set(column_name_index_map[attr]
|
|
83
|
+
for attr in forbidden_attributes)
|
|
84
|
+
|
|
85
|
+
_validate_forbidden_clash(column_name_list, forbidden_features,
|
|
86
|
+
required_features | forced_features)
|
|
87
|
+
|
|
88
|
+
train_arr = train.to_numpy(dtype=np.float64)
|
|
89
|
+
y_arr = y.values if isinstance(y, Series) else np.array(y)
|
|
90
|
+
|
|
91
|
+
pos = train_arr[y_arr == target_label]
|
|
92
|
+
neg = train_arr[y_arr != target_label]
|
|
93
|
+
|
|
94
|
+
neg = _filter_contradicted_instances(pos, neg)
|
|
95
|
+
|
|
96
|
+
if len(pos) == 0:
|
|
97
|
+
return Conjunction([])
|
|
98
|
+
|
|
99
|
+
# fixed_predicates already cover some attributes -- skip those
|
|
100
|
+
remaining_required_features = required_features - forced_features
|
|
101
|
+
|
|
102
|
+
has_predicates = len(forced_literals) > 0
|
|
103
|
+
has_remaining_attrs = len(remaining_required_features) > 0
|
|
104
|
+
|
|
105
|
+
if len(pos) > pruning_threshold:
|
|
106
|
+
pos_grow, pos_prune, neg_grow, neg_prune = _split_instances(
|
|
107
|
+
pos, neg, ratio, random_state)
|
|
108
|
+
else:
|
|
109
|
+
pos_grow = pos_prune = pos
|
|
110
|
+
neg_grow = neg_prune = neg
|
|
111
|
+
|
|
112
|
+
if has_predicates:
|
|
113
|
+
# Grow from fixed predicates first
|
|
114
|
+
rule = _grow_rule_standalone(
|
|
115
|
+
pos_grow, neg_grow, condition_mat, predefined_rule=forced_literals,
|
|
116
|
+
forbidden_features=forbidden_features)
|
|
117
|
+
|
|
118
|
+
if has_remaining_attrs:
|
|
119
|
+
# Bind data to the rule so far, then force-include remaining attributes
|
|
120
|
+
pos_bound = pos_grow
|
|
121
|
+
neg_bound = neg_grow
|
|
122
|
+
for lit in rule:
|
|
123
|
+
pos_bound = _bind_literal(pos_bound, lit.name, lit.op,
|
|
124
|
+
lit.num_val, lit.nom_val)
|
|
125
|
+
neg_bound = _bind_literal(neg_bound, lit.name, lit.op,
|
|
126
|
+
lit.num_val, lit.nom_val)
|
|
127
|
+
rule = _grow_rule_with_required_features(
|
|
128
|
+
pos_bound, neg_bound, remaining_required_features,
|
|
129
|
+
condition_mat, learned_rule=rule,
|
|
130
|
+
forbidden_features=forbidden_features)
|
|
131
|
+
elif has_remaining_attrs:
|
|
132
|
+
# Attributes only — grow from scratch forcing required features
|
|
133
|
+
rule = _grow_rule_with_required_features(
|
|
134
|
+
pos_grow, neg_grow, required_features, condition_mat,
|
|
135
|
+
forbidden_features=forbidden_features)
|
|
136
|
+
else:
|
|
137
|
+
# Unconstrained — standard grow
|
|
138
|
+
rule = _grow_rule_standalone(
|
|
139
|
+
pos_grow, neg_grow, condition_mat,
|
|
140
|
+
forbidden_features=forbidden_features)
|
|
141
|
+
|
|
142
|
+
# All forced features (from both predicates and attributes)
|
|
143
|
+
all_forced = forced_features | required_features
|
|
144
|
+
|
|
145
|
+
if len(all_forced) > 0:
|
|
146
|
+
rule = _prune_keeping_forced(pos_prune, neg_prune, rule, all_forced)
|
|
147
|
+
else:
|
|
148
|
+
rule = _pruning_irep(pos_prune, neg_prune, rule)
|
|
149
|
+
|
|
150
|
+
return _rule_to_trxf_conjunction(rule, column_name_list, nominal_encoders)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _predicates_to_literals(predicates, column_name_index_map, nominal_encoders):
|
|
154
|
+
"""
|
|
155
|
+
Convert TRXF Predicate objects to internal Literal objects.
|
|
156
|
+
"""
|
|
157
|
+
literals = []
|
|
158
|
+
for pred in predicates:
|
|
159
|
+
name = pred.feature.variable_names[0]
|
|
160
|
+
col_idx = column_name_index_map[name]
|
|
161
|
+
relation = pred.relation
|
|
162
|
+
|
|
163
|
+
if relation == Relation.EQ:
|
|
164
|
+
if name in nominal_encoders:
|
|
165
|
+
label_map = {val: label for label, val in
|
|
166
|
+
enumerate(nominal_encoders[name].classes_)}
|
|
167
|
+
nom_val = label_map[pred.value]
|
|
168
|
+
else:
|
|
169
|
+
nom_val = int(pred.value)
|
|
170
|
+
literals.append(Literal(col_idx, EQ, -1, nom_val))
|
|
171
|
+
elif relation == Relation.LE:
|
|
172
|
+
literals.append(Literal(col_idx, LE, pred.value, -1))
|
|
173
|
+
elif relation == Relation.GE:
|
|
174
|
+
literals.append(Literal(col_idx, GE, pred.value, -1))
|
|
175
|
+
else:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
'Unsupported relation {} for fixed_predicates. '
|
|
178
|
+
'Only EQ, LE, GE are supported.'.format(relation))
|
|
179
|
+
return literals
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _validate_forbidden_clash(column_name_list, forbidden_features, forced_features):
|
|
183
|
+
"""
|
|
184
|
+
Ensure that forbidden features do not overlap with features that are
|
|
185
|
+
forced via fixed_attributes or fixed_predicates.
|
|
186
|
+
"""
|
|
187
|
+
clash = forbidden_features & forced_features
|
|
188
|
+
if clash:
|
|
189
|
+
names = [column_name_list[idx] for idx in sorted(clash)]
|
|
190
|
+
raise ValueError(
|
|
191
|
+
'forbidden_attributes must not overlap with fixed_attributes or '
|
|
192
|
+
'fixed_predicates. Conflicting column(s): {}'.format(', '.join(names)))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _exclude_features(condition_mat, forbidden_features):
|
|
196
|
+
"""
|
|
197
|
+
Return the condition matrices with literals of forbidden features removed.
|
|
198
|
+
Returns None when no allowed literals remain.
|
|
199
|
+
"""
|
|
200
|
+
if not forbidden_features:
|
|
201
|
+
return condition_mat
|
|
202
|
+
mask = ~np.isin(condition_mat[0], list(forbidden_features))
|
|
203
|
+
if not mask.any():
|
|
204
|
+
return None
|
|
205
|
+
return [arr[mask] for arr in condition_mat]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _grow_rule_standalone(pos, neg, condition_mat, predefined_rule=None,
|
|
209
|
+
forbidden_features=None):
|
|
210
|
+
"""
|
|
211
|
+
Standalone version of RIPPER's grow rule, parameterized by condition_mat.
|
|
212
|
+
"""
|
|
213
|
+
if len(pos) == 0:
|
|
214
|
+
raise AssertionError('pos must not be empty')
|
|
215
|
+
|
|
216
|
+
neg = _filter_contradicted_instances(pos, neg)
|
|
217
|
+
|
|
218
|
+
if predefined_rule is not None:
|
|
219
|
+
pos = _bind_rule(pos, predefined_rule)
|
|
220
|
+
neg = _bind_rule(neg, predefined_rule)
|
|
221
|
+
if len(pos) == 0 or len(neg) == 0:
|
|
222
|
+
return list(predefined_rule)
|
|
223
|
+
learned_rule = list(predefined_rule)
|
|
224
|
+
else:
|
|
225
|
+
learned_rule = []
|
|
226
|
+
|
|
227
|
+
grow_mat = _exclude_features(condition_mat, forbidden_features)
|
|
228
|
+
|
|
229
|
+
while len(neg) > 0:
|
|
230
|
+
if grow_mat is None or len(pos) == 0:
|
|
231
|
+
break
|
|
232
|
+
pos = np.asfortranarray(pos)
|
|
233
|
+
neg = np.asfortranarray(neg)
|
|
234
|
+
prev_pos_len = len(pos)
|
|
235
|
+
prev_neg_len = len(neg)
|
|
236
|
+
|
|
237
|
+
best_literal_index = _find_best_literal(
|
|
238
|
+
pos, neg,
|
|
239
|
+
grow_mat[0], grow_mat[1],
|
|
240
|
+
grow_mat[2], grow_mat[3]
|
|
241
|
+
)
|
|
242
|
+
literal = Literal(
|
|
243
|
+
grow_mat[0][best_literal_index],
|
|
244
|
+
grow_mat[1][best_literal_index],
|
|
245
|
+
grow_mat[2][best_literal_index],
|
|
246
|
+
grow_mat[3][best_literal_index]
|
|
247
|
+
)
|
|
248
|
+
new_pos = _bind_literal(pos, literal.name, literal.op,
|
|
249
|
+
literal.num_val, literal.nom_val)
|
|
250
|
+
new_neg = _bind_literal(neg, literal.name, literal.op,
|
|
251
|
+
literal.num_val, literal.nom_val)
|
|
252
|
+
|
|
253
|
+
# a literal that filters neither set only restricts unseen data
|
|
254
|
+
if len(new_pos) == len(pos) and len(new_neg) == len(neg):
|
|
255
|
+
break
|
|
256
|
+
append_literal(learned_rule, literal)
|
|
257
|
+
pos, neg = new_pos, new_neg
|
|
258
|
+
|
|
259
|
+
if len(pos) == prev_pos_len and len(neg) == prev_neg_len:
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
return learned_rule
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _grow_rule_with_required_features(pos, neg, required_features, condition_mat,
|
|
266
|
+
learned_rule=None, forbidden_features=None):
|
|
267
|
+
"""
|
|
268
|
+
Grow a rule ensuring that every feature in ``required_features``
|
|
269
|
+
(a set of column indices) appears in the rule.
|
|
270
|
+
|
|
271
|
+
When ``learned_rule`` is given, literals are appended to it and
|
|
272
|
+
filtering of contradicting instances is skipped (caller is responsible).
|
|
273
|
+
|
|
274
|
+
Phase 1: restrict literal selection to remaining required features.
|
|
275
|
+
Phase 2: grow freely to eliminate remaining negatives, excluding any
|
|
276
|
+
forbidden features.
|
|
277
|
+
"""
|
|
278
|
+
if len(pos) == 0:
|
|
279
|
+
raise AssertionError('pos must not be empty')
|
|
280
|
+
|
|
281
|
+
if learned_rule is None:
|
|
282
|
+
neg = _filter_contradicted_instances(pos, neg)
|
|
283
|
+
learned_rule = []
|
|
284
|
+
remaining = set(required_features)
|
|
285
|
+
|
|
286
|
+
# Phase 1: force-include required features (one literal each)
|
|
287
|
+
while remaining and len(neg) > 0:
|
|
288
|
+
mask = np.isin(condition_mat[0], list(remaining))
|
|
289
|
+
if not mask.any():
|
|
290
|
+
break
|
|
291
|
+
filtered_mat = [arr[mask] for arr in condition_mat]
|
|
292
|
+
|
|
293
|
+
pos_f = np.asfortranarray(pos)
|
|
294
|
+
neg_f = np.asfortranarray(neg)
|
|
295
|
+
best_idx = _find_best_literal(
|
|
296
|
+
pos_f, neg_f,
|
|
297
|
+
filtered_mat[0], filtered_mat[1],
|
|
298
|
+
filtered_mat[2], filtered_mat[3]
|
|
299
|
+
)
|
|
300
|
+
literal = Literal(
|
|
301
|
+
filtered_mat[0][best_idx],
|
|
302
|
+
filtered_mat[1][best_idx],
|
|
303
|
+
filtered_mat[2][best_idx],
|
|
304
|
+
filtered_mat[3][best_idx]
|
|
305
|
+
)
|
|
306
|
+
append_literal(learned_rule, literal)
|
|
307
|
+
remaining.discard(literal.name)
|
|
308
|
+
|
|
309
|
+
pos = _bind_literal(pos, literal.name, literal.op,
|
|
310
|
+
literal.num_val, literal.nom_val)
|
|
311
|
+
neg = _bind_literal(neg, literal.name, literal.op,
|
|
312
|
+
literal.num_val, literal.nom_val)
|
|
313
|
+
|
|
314
|
+
if len(pos) == 0:
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
# Phase 1b: force-include any remaining required features even if
|
|
318
|
+
# negatives are already eliminated (user explicitly requested them)
|
|
319
|
+
for feat_idx in sorted(remaining):
|
|
320
|
+
mask = condition_mat[0] == feat_idx
|
|
321
|
+
if not mask.any():
|
|
322
|
+
continue
|
|
323
|
+
filtered_mat = [arr[mask] for arr in condition_mat]
|
|
324
|
+
|
|
325
|
+
pos_f = np.asfortranarray(pos)
|
|
326
|
+
neg_f = np.asfortranarray(neg)
|
|
327
|
+
best_idx = _find_best_literal(
|
|
328
|
+
pos_f, neg_f,
|
|
329
|
+
filtered_mat[0], filtered_mat[1],
|
|
330
|
+
filtered_mat[2], filtered_mat[3]
|
|
331
|
+
)
|
|
332
|
+
literal = Literal(
|
|
333
|
+
filtered_mat[0][best_idx],
|
|
334
|
+
filtered_mat[1][best_idx],
|
|
335
|
+
filtered_mat[2][best_idx],
|
|
336
|
+
filtered_mat[3][best_idx]
|
|
337
|
+
)
|
|
338
|
+
append_literal(learned_rule, literal)
|
|
339
|
+
|
|
340
|
+
pos = _bind_literal(pos, literal.name, literal.op,
|
|
341
|
+
literal.num_val, literal.nom_val)
|
|
342
|
+
neg = _bind_literal(neg, literal.name, literal.op,
|
|
343
|
+
literal.num_val, literal.nom_val)
|
|
344
|
+
|
|
345
|
+
# Phase 2: grow freely
|
|
346
|
+
grow_mat = _exclude_features(condition_mat, forbidden_features)
|
|
347
|
+
|
|
348
|
+
while len(neg) > 0:
|
|
349
|
+
if grow_mat is None or len(pos) == 0:
|
|
350
|
+
break
|
|
351
|
+
pos = np.asfortranarray(pos)
|
|
352
|
+
neg = np.asfortranarray(neg)
|
|
353
|
+
prev_pos_len = len(pos)
|
|
354
|
+
prev_neg_len = len(neg)
|
|
355
|
+
|
|
356
|
+
best_idx = _find_best_literal(
|
|
357
|
+
pos, neg,
|
|
358
|
+
grow_mat[0], grow_mat[1],
|
|
359
|
+
grow_mat[2], grow_mat[3]
|
|
360
|
+
)
|
|
361
|
+
literal = Literal(
|
|
362
|
+
grow_mat[0][best_idx],
|
|
363
|
+
grow_mat[1][best_idx],
|
|
364
|
+
grow_mat[2][best_idx],
|
|
365
|
+
grow_mat[3][best_idx]
|
|
366
|
+
)
|
|
367
|
+
new_pos = _bind_literal(pos, literal.name, literal.op,
|
|
368
|
+
literal.num_val, literal.nom_val)
|
|
369
|
+
new_neg = _bind_literal(neg, literal.name, literal.op,
|
|
370
|
+
literal.num_val, literal.nom_val)
|
|
371
|
+
|
|
372
|
+
# a literal that filters neither set only restricts unseen data
|
|
373
|
+
if len(new_pos) == len(pos) and len(new_neg) == len(neg):
|
|
374
|
+
break
|
|
375
|
+
append_literal(learned_rule, literal)
|
|
376
|
+
pos, neg = new_pos, new_neg
|
|
377
|
+
|
|
378
|
+
if len(pos) == prev_pos_len and len(neg) == prev_neg_len:
|
|
379
|
+
break
|
|
380
|
+
|
|
381
|
+
return learned_rule
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _prune_keeping_forced(pos_prune, neg_prune, rule, forced_features):
|
|
385
|
+
"""
|
|
386
|
+
Prune only the non-forced literals. Forced literals are bound first
|
|
387
|
+
to restrict the data, then pruning applies to the free suffix.
|
|
388
|
+
"""
|
|
389
|
+
forced = [lit for lit in rule if lit.name in forced_features]
|
|
390
|
+
free = [lit for lit in rule if lit.name not in forced_features]
|
|
391
|
+
|
|
392
|
+
bound_pos = pos_prune
|
|
393
|
+
bound_neg = neg_prune
|
|
394
|
+
for lit in forced:
|
|
395
|
+
bound_pos = _bind_literal(bound_pos, lit.name, lit.op,
|
|
396
|
+
lit.num_val, lit.nom_val)
|
|
397
|
+
bound_neg = _bind_literal(bound_neg, lit.name, lit.op,
|
|
398
|
+
lit.num_val, lit.nom_val)
|
|
399
|
+
|
|
400
|
+
if len(free) == 0:
|
|
401
|
+
return forced
|
|
402
|
+
|
|
403
|
+
pruned_free = _pruning_irep(bound_pos, bound_neg, free)
|
|
404
|
+
return forced + pruned_free
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _rule_to_trxf_conjunction(rule, column_name_list, nominal_encoders):
|
|
408
|
+
"""
|
|
409
|
+
Convert a list of Literal objects to a TRXF Conjunction.
|
|
410
|
+
"""
|
|
411
|
+
conjunction = Conjunction([])
|
|
412
|
+
for lit in rule:
|
|
413
|
+
name = column_name_list[lit.name]
|
|
414
|
+
feature = Feature(name)
|
|
415
|
+
if lit.op == EQ:
|
|
416
|
+
relation = Relation.EQ
|
|
417
|
+
value = nominal_encoders[name].classes_[lit.nom_val]
|
|
418
|
+
elif lit.op == LE:
|
|
419
|
+
relation = Relation.LE
|
|
420
|
+
value = lit.num_val
|
|
421
|
+
else:
|
|
422
|
+
relation = Relation.GE
|
|
423
|
+
value = lit.num_val
|
|
424
|
+
conjunction.add_predicate(Predicate(feature, relation, value))
|
|
425
|
+
return conjunction
|
rulelearn_ri/dise.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Directly interpretable supervised explainers
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
# Ensure compatibility with Python 2/3
|
|
7
|
+
if sys.version_info >= (3, 4):
|
|
8
|
+
ABC = abc.ABC
|
|
9
|
+
else:
|
|
10
|
+
ABC = abc.ABCMeta(str('ABC'), (), {})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DISExplainer(ABC):
|
|
14
|
+
"""
|
|
15
|
+
DISExplainer is the base class for Directly Interpretable Supervised Explainers (DISE).
|
|
16
|
+
Such explainers attempt to train explainable models directly from data.
|
|
17
|
+
Examples include Boolean Decision Rules via Column Generation[#1]_, Generalized Linear Rule Models[#2]_,
|
|
18
|
+
TED: Teaching AI to Explain its Decisions[#3]_, etc.
|
|
19
|
+
|
|
20
|
+
References:
|
|
21
|
+
.. [#1] Boolean Decision Rules via Column Generation, NIPS 2018.
|
|
22
|
+
Dash, Gunluk, Wei. https://arxiv.org/abs/1805.09901
|
|
23
|
+
.. [#2] Generalized Linear Rule Models, ICML 2019.
|
|
24
|
+
Dennis Wei, Sanjeeb Dash, Tian Gao, Oktay Gunluk.
|
|
25
|
+
https://arxiv.org/abs/1906.01761
|
|
26
|
+
.. [#3] TED: Teaching AI to Explain its Decisions, AIES 2019.
|
|
27
|
+
Michael Hind, Dennis Wei, Murray Campbell, Noel C. F. Codella, Amit Dhurandhar,
|
|
28
|
+
Aleksandra Mojsilovic, Karthikeyan Natesan Ramamurthy, Kush R. Varshney.
|
|
29
|
+
https://arxiv.org/abs/1811.04896
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *argv, **kwargs):
|
|
33
|
+
"""
|
|
34
|
+
Initialize a DISExplainer object.
|
|
35
|
+
ToDo: check common steps that need to be distilled here.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def set_params(self, *argv, **kwargs):
|
|
40
|
+
"""
|
|
41
|
+
Set parameters for the explainer.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def explain(self, *argv, **kwargs):
|
|
47
|
+
"""
|
|
48
|
+
Explain the model
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def fit(self, *argv, **kwargs):
|
|
54
|
+
"""
|
|
55
|
+
Fit a model on data.
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
@abc.abstractmethod
|
|
60
|
+
def predict(self, *argv, **kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Prediction for a batch of inputs (numpy array)
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from rulelearn_ri.trxf.core.dnf_ruleset import DnfRuleSet
|
|
8
|
+
from rulelearn_ri.trxf.metrics import compute_rule_metrics
|
|
9
|
+
|
|
10
|
+
Rule = namedtuple('Rule', ['conjunction', 'label', 'record_count', 'nb_correct', 'confidence', 'weight'])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RuleSelectionMethod(enum.Enum):
|
|
14
|
+
FIRST_HIT = enum.auto()
|
|
15
|
+
WEIGHTED_MAX = enum.auto()
|
|
16
|
+
WEIGHTED_SUM = enum.auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfidenceMetric(enum.Enum):
|
|
20
|
+
LAPLACE = enum.auto()
|
|
21
|
+
ACCURACY = enum.auto()
|
|
22
|
+
CONSTANT = enum.auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WeightMetric(enum.Enum):
|
|
26
|
+
CONFIDENCE = enum.auto()
|
|
27
|
+
CONSTANT = enum.auto()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RuleSetClassifier:
|
|
31
|
+
"""
|
|
32
|
+
This class implements a simple classifier constructed from a list of DNF rule sets.
|
|
33
|
+
Based on the specified RuleSelectionMethod, this classifier predicts the label for a given input.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self,
|
|
37
|
+
rule_sets: List[DnfRuleSet],
|
|
38
|
+
rule_selection_method: RuleSelectionMethod,
|
|
39
|
+
confidence_metric: ConfidenceMetric = None,
|
|
40
|
+
weight_metric: WeightMetric = None,
|
|
41
|
+
default_label: Any = None):
|
|
42
|
+
"""
|
|
43
|
+
@param rule_sets: A list of dnf rule sets
|
|
44
|
+
@param rule_selection_method: Conflict resolution strategy for multiple fired rules
|
|
45
|
+
@param confidence_metric: Rule confidence definition to use
|
|
46
|
+
@param weight_metric: Rule weight definition to use depending on RuleSelectionMethod
|
|
47
|
+
@param default_label: Optional default label to be returned when no rule set matches the data input.
|
|
48
|
+
"""
|
|
49
|
+
self._rule_sets = rule_sets
|
|
50
|
+
self._rule_selection_method = rule_selection_method
|
|
51
|
+
self._default_label = default_label
|
|
52
|
+
self._confidence_metric = confidence_metric
|
|
53
|
+
self._weight_metric = weight_metric
|
|
54
|
+
self._rules = [Rule(conjunction=c,
|
|
55
|
+
label=ruleset.then_part,
|
|
56
|
+
record_count=None,
|
|
57
|
+
nb_correct=None,
|
|
58
|
+
confidence=None,
|
|
59
|
+
weight=None)
|
|
60
|
+
for ruleset in self._rule_sets for c in ruleset.conjunctions]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def rules(self):
|
|
64
|
+
return self._rules
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def rule_selection_method(self):
|
|
68
|
+
return self._rule_selection_method
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def default_label(self):
|
|
72
|
+
return self._default_label
|
|
73
|
+
|
|
74
|
+
def update_rules_with_metrics(self, X: pd.DataFrame, y: pd.Series) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Compute rule level metrics for the provided input, and update the Rules with them
|
|
77
|
+
@param X: pandas data frame to compute required confidence score for PMML exported rules
|
|
78
|
+
@param y: pandas series to hold labels to compute required confidence score for PMML exported rules
|
|
79
|
+
"""
|
|
80
|
+
new_rules = []
|
|
81
|
+
for rule_set in self._rule_sets:
|
|
82
|
+
metric_list = compute_rule_metrics(rule_set, X, y)
|
|
83
|
+
for rule in metric_list:
|
|
84
|
+
confidence = self._compute_confidence(rule)
|
|
85
|
+
weight = self._compute_weight(rule)
|
|
86
|
+
new_rules.append(
|
|
87
|
+
Rule(conjunction=rule.conjunction, label=rule_set.then_part, record_count=rule.n_records,
|
|
88
|
+
nb_correct=rule.n_correct, confidence=confidence, weight=weight))
|
|
89
|
+
self._rules = new_rules
|
|
90
|
+
|
|
91
|
+
def predict(self, assignment: Dict[str, Any]) -> Any:
|
|
92
|
+
"""
|
|
93
|
+
Predict the label for the given assignment
|
|
94
|
+
@param assignment: A valuation of the variables that occur in the rule sets
|
|
95
|
+
"""
|
|
96
|
+
if self.rule_selection_method == RuleSelectionMethod.FIRST_HIT:
|
|
97
|
+
for rule in self.rules:
|
|
98
|
+
if rule.conjunction.evaluate(assignment):
|
|
99
|
+
return rule.label
|
|
100
|
+
return self.default_label
|
|
101
|
+
elif self.rule_selection_method == RuleSelectionMethod.WEIGHTED_SUM:
|
|
102
|
+
assert self.rules[0].weight is not None
|
|
103
|
+
label_weights = {}
|
|
104
|
+
for rule in self.rules:
|
|
105
|
+
if rule.label not in label_weights:
|
|
106
|
+
label_weights[rule.label] = rule.weight
|
|
107
|
+
else:
|
|
108
|
+
label_weights[rule.label] += rule.weight
|
|
109
|
+
return max(label_weights, key=label_weights.get)
|
|
110
|
+
elif self.rule_selection_method == RuleSelectionMethod.WEIGHTED_MAX:
|
|
111
|
+
assert self.rules[0].weight is not None
|
|
112
|
+
label_weights = {}
|
|
113
|
+
for rule in self.rules:
|
|
114
|
+
if rule.label not in label_weights:
|
|
115
|
+
label_weights[rule.label] = rule.weight
|
|
116
|
+
else:
|
|
117
|
+
label_weights[rule.label] = max(rule.weight, label_weights[rule.label])
|
|
118
|
+
return max(label_weights, key=label_weights.get)
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError("Unknown rule selection method: {}".format(self.rule_selection_method))
|
|
121
|
+
|
|
122
|
+
def _compute_confidence(self, rule):
|
|
123
|
+
if self._confidence_metric is None:
|
|
124
|
+
confidence = None
|
|
125
|
+
elif self._confidence_metric == ConfidenceMetric.LAPLACE:
|
|
126
|
+
confidence = rule.laplace_estimate
|
|
127
|
+
elif self._confidence_metric == ConfidenceMetric.ACCURACY:
|
|
128
|
+
confidence = rule.n_correct / rule.n_records
|
|
129
|
+
elif self._confidence_metric == ConfidenceMetric.CONSTANT:
|
|
130
|
+
confidence = 1
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError("Unknown confidence metric: {}".format(self._confidence_metric))
|
|
133
|
+
return confidence
|
|
134
|
+
|
|
135
|
+
def _compute_weight(self, rule):
|
|
136
|
+
if self._weight_metric == WeightMetric.CONFIDENCE:
|
|
137
|
+
weight = self._compute_confidence(rule)
|
|
138
|
+
elif self._weight_metric == WeightMetric.CONSTANT:
|
|
139
|
+
weight = 1
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError("Unknown weight metric: {}".format(self._weight_metric))
|
|
142
|
+
return weight
|
|
143
|
+
|
|
144
|
+
def __repr__(self):
|
|
145
|
+
return '%s(%r, %r)' % (self.__class__, self._rule_sets, self._default_label)
|
|
146
|
+
|
|
147
|
+
def __str__(self):
|
|
148
|
+
body = '\n'.join([str(rule_set) for rule_set in self._rule_sets])
|
|
149
|
+
else_clause = 'else ' + str(self.default_label)
|
|
150
|
+
return '\n'.join([body, else_clause])
|
|
151
|
+
|
|
152
|
+
def __eq__(self, other):
|
|
153
|
+
return (self._rule_sets == other.rule_sets) and (self.default_label == other.default_label)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BooleanEvaluator(metaclass=abc.ABCMeta):
|
|
6
|
+
@classmethod
|
|
7
|
+
def __subclasshook__(cls, subclass):
|
|
8
|
+
return callable(subclass.evaluate)
|
|
9
|
+
|
|
10
|
+
@abc.abstractmethod
|
|
11
|
+
def evaluate(self, assignment: Dict[str, Any]) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Evaluate the truth value w.r.t. the variable assignment
|
|
14
|
+
"""
|
|
15
|
+
raise NotImplementedError
|