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.
Files changed (73) hide show
  1. rulelearn_ri/__init__.py +6 -0
  2. rulelearn_ri/algorithms/__init__.py +0 -0
  3. rulelearn_ri/algorithms/r2n/__init__.py +0 -0
  4. rulelearn_ri/algorithms/r2n/base.py +28 -0
  5. rulelearn_ri/algorithms/r2n/functions.py +25 -0
  6. rulelearn_ri/algorithms/r2n/mask.py +58 -0
  7. rulelearn_ri/algorithms/r2n/preprocessing_layers.py +192 -0
  8. rulelearn_ri/algorithms/r2n/r2n_algo.py +287 -0
  9. rulelearn_ri/algorithms/r2n/simple_rulenet.py +75 -0
  10. rulelearn_ri/algorithms/r2n/training.py +34 -0
  11. rulelearn_ri/algorithms/r2n/utilities.py +117 -0
  12. rulelearn_ri/algorithms/rbm/__init__.py +0 -0
  13. rulelearn_ri/algorithms/rbm/boolean_rule_cg.py +65 -0
  14. rulelearn_ri/algorithms/rbm/rbm/BRCG.py +69 -0
  15. rulelearn_ri/algorithms/rbm/rbm/GLRM.py +98 -0
  16. rulelearn_ri/algorithms/rbm/rbm/__init__.py +6 -0
  17. rulelearn_ri/algorithms/rbm/rbm/beam_search.py +302 -0
  18. rulelearn_ri/algorithms/rbm/rbm/boolean_rule_cg.py +245 -0
  19. rulelearn_ri/algorithms/rbm/rbm/features.py +544 -0
  20. rulelearn_ri/algorithms/rbm/rbm/linear_regression.py +393 -0
  21. rulelearn_ri/algorithms/rbm/rbm/logistic_regression.py +447 -0
  22. rulelearn_ri/algorithms/rbm/utils.py +13 -0
  23. rulelearn_ri/algorithms/ripper/__init__.py +10 -0
  24. rulelearn_ri/algorithms/ripper/base.py +81 -0
  25. rulelearn_ri/algorithms/ripper/binding.py +541 -0
  26. rulelearn_ri/algorithms/ripper/mdl.py +97 -0
  27. rulelearn_ri/algorithms/ripper/pruning.py +212 -0
  28. rulelearn_ri/algorithms/ripper/ripper.py +600 -0
  29. rulelearn_ri/algorithms/ripper/ripper_ruleset_generator.py +26 -0
  30. rulelearn_ri/algorithms/ripper/single_rule.py +425 -0
  31. rulelearn_ri/dise.py +64 -0
  32. rulelearn_ri/trxf/__init__.py +0 -0
  33. rulelearn_ri/trxf/classifier/__init__.py +0 -0
  34. rulelearn_ri/trxf/classifier/ruleset_classifier.py +153 -0
  35. rulelearn_ri/trxf/core/__init__.py +5 -0
  36. rulelearn_ri/trxf/core/boolean_evaluator.py +15 -0
  37. rulelearn_ri/trxf/core/conjunction.py +50 -0
  38. rulelearn_ri/trxf/core/dnf_ruleset.py +64 -0
  39. rulelearn_ri/trxf/core/feature.py +155 -0
  40. rulelearn_ri/trxf/core/predicate.py +98 -0
  41. rulelearn_ri/trxf/core/ruleset_generator.py +25 -0
  42. rulelearn_ri/trxf/core/utils.py +40 -0
  43. rulelearn_ri/trxf/metrics.py +98 -0
  44. rulelearn_ri/trxf/pmml_export/__init__.py +4 -0
  45. rulelearn_ri/trxf/pmml_export/__version__.py +1 -0
  46. rulelearn_ri/trxf/pmml_export/models/__init__.py +24 -0
  47. rulelearn_ri/trxf/pmml_export/models/attribute.py +24 -0
  48. rulelearn_ri/trxf/pmml_export/models/characteristics.py +20 -0
  49. rulelearn_ri/trxf/pmml_export/models/data_dictionary.py +53 -0
  50. rulelearn_ri/trxf/pmml_export/models/mining_schema.py +25 -0
  51. rulelearn_ri/trxf/pmml_export/models/pmml_ruleset_model.py +16 -0
  52. rulelearn_ri/trxf/pmml_export/models/predicate.py +42 -0
  53. rulelearn_ri/trxf/pmml_export/models/rule.py +26 -0
  54. rulelearn_ri/trxf/pmml_export/models/ruleset.py +28 -0
  55. rulelearn_ri/trxf/pmml_export/models/ruleset_model.py +16 -0
  56. rulelearn_ri/trxf/pmml_export/models/scorecard.py +35 -0
  57. rulelearn_ri/trxf/pmml_export/pmml_exporter.py +18 -0
  58. rulelearn_ri/trxf/pmml_export/reader/__init__.py +2 -0
  59. rulelearn_ri/trxf/pmml_export/reader/abstract_reader.py +16 -0
  60. rulelearn_ri/trxf/pmml_export/reader/trxf_reader.py +113 -0
  61. rulelearn_ri/trxf/pmml_export/serializer/__init__.py +2 -0
  62. rulelearn_ri/trxf/pmml_export/serializer/abstract_serializer.py +11 -0
  63. rulelearn_ri/trxf/pmml_export/serializer/nyoka_serializer.py +162 -0
  64. rulelearn_ri/trxf/scorecard/__init__.py +3 -0
  65. rulelearn_ri/trxf/scorecard/bins.py +253 -0
  66. rulelearn_ri/trxf/scorecard/partition.py +71 -0
  67. rulelearn_ri/trxf/scorecard/scorecard.py +78 -0
  68. rulelearn_ripper_interactive-0.2.3.dist-info/METADATA +164 -0
  69. rulelearn_ripper_interactive-0.2.3.dist-info/RECORD +73 -0
  70. rulelearn_ripper_interactive-0.2.3.dist-info/WHEEL +5 -0
  71. rulelearn_ripper_interactive-0.2.3.dist-info/licenses/LICENSE +201 -0
  72. rulelearn_ripper_interactive-0.2.3.dist-info/licenses/NOTICE +11 -0
  73. 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,5 @@
1
+ from .conjunction import Conjunction
2
+ from .predicate import Relation, Predicate
3
+ from .feature import Feature
4
+ from .dnf_ruleset import DnfRuleSet
5
+ from .ruleset_generator import RuleSetGenerator
@@ -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