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,302 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ class PricingInstance(object):
6
+ """Instance of the pricing problem"""
7
+ def __init__(self, rp, rn, Xp, Xn, v0, z0):
8
+ self.rp = rp
9
+ self.rn = rn
10
+ self.Xp = Xp
11
+ self.Xn = Xn
12
+ self.v0 = v0
13
+ self.z0 = z0
14
+
15
+ def eval_singletons(self, lambda1):
16
+ """Evaluate all singleton solutions (adding one literal)"""
17
+ self.Rp = np.dot(self.rp, self.Xp)
18
+ self.Rn = np.dot(self.rn, self.Xn)
19
+ self.v1 = self.v0 - self.Rp - self.Rn + lambda1
20
+ self.v1 = pd.Series(self.v1, index=self.Xp.columns)
21
+
22
+ def compute_LB(self, lambda1):
23
+ """Compute lower bound on higher-order solutions"""
24
+ Rp0 = self.rp.sum()
25
+ if np.ndim(lambda1):
26
+ self.LB = np.array([])
27
+ else:
28
+ self.LB = np.minimum(np.cumsum(np.sort(self.Rp)[::-1])[1:], Rp0)
29
+ self.LB += np.sort(self.Rn)[-2::-1]
30
+ self.LB -= lambda1 * np.arange(2, len(self.Rp)+1)
31
+ self.LB = self.v0 - self.LB
32
+
33
+ # Lower bound specific to each singleton solution
34
+ self.LB1 = self.v1 + self.Rp - Rp0 + lambda1
35
+ if len(self.LB):
36
+ self.LB1[self.LB1 < self.LB.min()] = self.LB.min()
37
+
38
+
39
+ def beam_search_K1(r, X, lambda0, lambda1, UB=0, D=10, B=5, wLB=0.5, eps=1e-6, stopEarly=True):
40
+ """Beam search to generate SINGLE SOLUTION (K = 1) to pricing problem
41
+ Problem parameters:
42
+ r = cost vector (residuals)
43
+ X = binary feature DataFrame
44
+ lambda0 = fixed cost of a term
45
+ lambda1 = cost per literal
46
+
47
+ Algorithm parameters:
48
+ UB = initial upper bound on value of solutions
49
+ D = maximum degree
50
+ B = beam width
51
+ wLB = weight on lower bound in evaluating nodes
52
+ eps = numerical tolerance on comparisons
53
+ stopEarly = stop after current degree once solution is found
54
+ """
55
+
56
+ # Initialize output
57
+ vOut = np.array([])
58
+ zOut = pd.Series(index=X.columns)
59
+
60
+ # Initialize queue with root instance
61
+ # Separate data according to positive and negative residuals
62
+ rp = r[r > 0]
63
+ rn = r[r < 0]
64
+ Xp = 1 - X.loc[r > 0]
65
+ Xn = 1 - X.loc[r < 0]
66
+ instCurr = [PricingInstance(rp, rn, Xp, Xn, r.sum() + lambda0, pd.Series(0, index=zOut.index))]
67
+
68
+ # Iterate over increasing degree while queue is non-empty
69
+ deg = 0
70
+ while (not len(vOut) or not stopEarly) and len(instCurr) and deg < D:
71
+ deg += 1
72
+
73
+ # Initialize list of children to process
74
+ vNext = np.array([])
75
+ vNextMax = np.inf
76
+ zNext = pd.DataFrame([], index=X.columns)
77
+ idxInstNext = np.array([], dtype=int)
78
+ idxFeatNext = np.array([])
79
+
80
+ # Process instances in queue
81
+ for (idxInst, inst) in enumerate(instCurr):
82
+
83
+ # Evaluate all singleton solutions
84
+ inst.eval_singletons(lambda1)
85
+
86
+ # Solutions that improve on current output
87
+ vCand = inst.v1[inst.v1 < UB - eps]
88
+ if len(vCand):
89
+ # Update output with best of these solutions
90
+ idxMin = vCand.idxmin()
91
+ UB = vCand[idxMin]
92
+ vOut = np.array([UB])
93
+ zOut = inst.z0.copy()
94
+ zOut[idxMin] = 1
95
+
96
+ # Compute lower bounds on higher-degree solutions
97
+ inst.compute_LB(lambda1)
98
+ # Evaluate children using weighted average of their costs and LBs
99
+ vChild = (1 - wLB) * inst.v1 + wLB * inst.LB1
100
+
101
+ # Best children with potential to improve on current output and current candidates (allow for duplicate removal)
102
+ vChild = vChild[(inst.LB1 < UB - eps) & (vChild < vNextMax - eps)].sort_values()[:B+idxInst]
103
+ if len(vChild):
104
+ # Feature indicators of these best children
105
+ zChild = pd.DataFrame(zOut.index.values[:,np.newaxis] == vChild.index.values, index=zOut.index).astype(int)
106
+ zChild = zChild.add(inst.z0, axis=0)
107
+
108
+ # Append to current candidates
109
+ vNext = np.append(vNext, vChild.values)
110
+ zNext = pd.concat([zNext, zChild], axis=1, ignore_index=True)
111
+ idxInstNext = np.append(idxInstNext, np.full(B+idxInst, idxInst))
112
+ idxFeatNext = np.append(idxFeatNext, vChild.index.values)
113
+ # Remove duplicates
114
+ _, idxUniq = np.unique(zNext, return_index=True, axis=1)
115
+ vNext = vNext[idxUniq]
116
+ zNext = zNext.iloc[:,idxUniq]
117
+ idxInstNext = idxInstNext[idxUniq]
118
+ idxFeatNext = idxFeatNext[idxUniq]
119
+ # Update candidates
120
+ idxBest = np.argsort(vNext)[:B]
121
+ vNext = vNext[idxBest]
122
+ if len(vNext):
123
+ vNextMax = vNext[-1]
124
+ zNext = zNext.iloc[:,idxBest]
125
+ zNext.columns = range(zNext.shape[1])
126
+ idxInstNext = idxInstNext[idxBest]
127
+ idxFeatNext = idxFeatNext[idxBest]
128
+
129
+ # Instances to process in next iteration
130
+ instNext = []
131
+ for (idxInst, i, idxz) in zip(idxInstNext, idxFeatNext, zNext):
132
+ # Create pricing instance
133
+ # Remove covered rows
134
+ rowKeep = instCurr[idxInst].Xp[i] == 0
135
+ rp = instCurr[idxInst].rp[rowKeep]
136
+ Xp = instCurr[idxInst].Xp.loc[rowKeep]
137
+ rowKeep = instCurr[idxInst].Xn[i] == 0
138
+ rn = instCurr[idxInst].rn[rowKeep]
139
+ Xn = instCurr[idxInst].Xn.loc[rowKeep]
140
+ # Remove redundant features
141
+ if type(Xp.columns) is pd.MultiIndex:
142
+ colKeep = pd.Series(Xp.columns.get_level_values(0) != i[0], index=Xp.columns)
143
+ if i[1] == '<=':
144
+ thresh = Xp[i[0]].columns.get_level_values(1).to_series().replace('NaN', np.nan).values
145
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '>') & (thresh < i[2])
146
+ elif i[1] == '>':
147
+ thresh = Xp[i[0]].columns.get_level_values(1).to_series().replace('NaN', np.nan).values
148
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '<=') & (thresh > i[2])
149
+ elif i[1] == '!=':
150
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '!=') & (Xp[i[0]].columns.get_level_values(1) != i[2])
151
+ Xp = Xp.loc[:, colKeep]
152
+ Xn = Xn.loc[:, colKeep]
153
+ instNext.append(PricingInstance(rp, rn, Xp, Xn, instCurr[idxInst].v1[i], zNext[idxz]))
154
+
155
+ instCurr = instNext
156
+
157
+ # Conjunctions corresponding to solutions
158
+ if zOut.count():
159
+ zOut = pd.DataFrame(zOut)
160
+ else:
161
+ zOut = pd.DataFrame(index=X.columns)
162
+
163
+ aOut = 1 - (np.dot(1 - X, zOut) > 0)
164
+
165
+ return vOut, zOut, aOut
166
+
167
+
168
+ def beam_search(r, X, lambda0, lambda1, K=1, UB=0, D=10, B=5, wLB=0.5, eps=1e-6, stopEarly=False):
169
+ """Beam search to generate solutions to pricing problem
170
+ Problem parameters:
171
+ r = cost vector (residuals)
172
+ X = binary feature DataFrame
173
+ lambda0 = fixed cost of a term
174
+ lambda1 = cost per literal
175
+
176
+ Algorithm parameters:
177
+ K = maximum number of solutions returned
178
+ UB = initial upper bound on value of solutions
179
+ D = maximum degree
180
+ B = beam width
181
+ wLB = weight on lower bound in evaluating nodes
182
+ eps = numerical tolerance on comparisons
183
+ stopEarly = stop after current degree once solution is found
184
+ """
185
+
186
+ # Initialize output
187
+ vOut = np.array([])
188
+ zOut = pd.DataFrame(index=X.columns)
189
+
190
+ # Remove redundant rows by grouping by unique feature combinations and summing residual
191
+ X2 = X.copy()
192
+ r2 = r
193
+
194
+ # Initialize queue with root instance
195
+ # Separate data according to positive and negative residuals
196
+ rp = r2[r2 > 0]
197
+ rn = r2[r2 < 0]
198
+ Xp = 1 - X2.loc[r2 > 0]
199
+ Xn = 1 - X2.loc[r2 < 0]
200
+ instCurr = [PricingInstance(rp, rn, Xp, Xn, r2.sum() + lambda0, pd.Series(0, index=zOut.index))]
201
+
202
+ # Iterate over increasing degree while queue is non-empty
203
+ deg = 0
204
+ while (not len(vOut) or not stopEarly) and len(instCurr) and deg < D:
205
+ deg += 1
206
+
207
+ # Initialize list of children to process
208
+ vNext = np.array([])
209
+ vNextMax = np.inf
210
+ zNext = pd.DataFrame([], index=X2.columns)
211
+ idxInstNext = np.array([], dtype=int)
212
+ idxFeatNext = np.array([])
213
+
214
+ # Process instances in queue
215
+ for (idxInst, inst) in enumerate(instCurr):
216
+
217
+ # Evaluate all singleton solutions
218
+ inst.eval_singletons(lambda1)
219
+
220
+ # Best solutions that also improve on current output (allow for duplicate removal)
221
+ vCand = inst.v1[inst.v1 < UB - eps].sort_values()[:K+B]
222
+ if len(vCand):
223
+ zCand = pd.DataFrame(zOut.index.values[:,np.newaxis] == vCand.index.values, index=zOut.index).astype(int)
224
+ zCand = zCand.add(inst.z0, axis=0)
225
+ # Append to current output
226
+ vOut = np.append(vOut, vCand.values)
227
+ zOut = pd.concat([zOut, zCand], axis=1, ignore_index=True)
228
+ # Remove duplicates
229
+ _, idxUniq = np.unique(zOut, return_index=True, axis=1)
230
+ vOut = vOut[idxUniq]
231
+ zOut = zOut.iloc[:,idxUniq]
232
+ # Update output
233
+ indBest = np.argsort(vOut)[:K]
234
+ vOut = vOut[indBest]
235
+ UB = vOut.max()
236
+ zOut = zOut.iloc[:,indBest]
237
+ zOut.columns = range(zOut.shape[1])
238
+
239
+ # Compute lower bounds on higher-degree solutions
240
+ inst.compute_LB(lambda1)
241
+ # Evaluate children using weighted average of their costs and LBs
242
+ vChild = (1 - wLB) * inst.v1 + wLB * inst.LB1
243
+
244
+ # Best children with potential to improve on current output and current candidates (allow for duplicate removal)
245
+ vChild = vChild[(inst.LB1 < UB - eps) & (vChild < vNextMax - eps)].sort_values()[:B+idxInst]
246
+ if len(vChild):
247
+ # Feature indicators of these best children
248
+ zChild = pd.DataFrame(zOut.index.values[:,np.newaxis] == vChild.index.values, index=zOut.index).astype(int)
249
+ zChild = zChild.add(inst.z0, axis=0)
250
+
251
+ # Append to current candidates
252
+ vNext = np.append(vNext, vChild.values)
253
+ zNext = pd.concat([zNext, zChild], axis=1, ignore_index=True)
254
+ idxInstNext = np.append(idxInstNext, np.full(B+idxInst, idxInst))
255
+ idxFeatNext = np.append(idxFeatNext, vChild.index.values)
256
+ # Remove duplicates
257
+ _, idxUniq = np.unique(zNext, return_index=True, axis=1)
258
+ vNext = vNext[idxUniq]
259
+ zNext = zNext.iloc[:,idxUniq]
260
+ idxInstNext = idxInstNext[idxUniq]
261
+ idxFeatNext = idxFeatNext[idxUniq]
262
+ # Update candidates
263
+ idxBest = np.argsort(vNext)[:B]
264
+ vNext = vNext[idxBest]
265
+ if len(vNext):
266
+ vNextMax = vNext[-1]
267
+ zNext = zNext.iloc[:,idxBest]
268
+ zNext.columns = range(zNext.shape[1])
269
+ idxInstNext = idxInstNext[idxBest]
270
+ idxFeatNext = idxFeatNext[idxBest]
271
+
272
+ # Instances to process in next iteration
273
+ instNext = []
274
+ for (idxInst, i, idxz) in zip(idxInstNext, idxFeatNext, zNext):
275
+ # Create pricing instance
276
+ # Remove covered rows
277
+ rowKeep = instCurr[idxInst].Xp[i] == 0
278
+ rp = instCurr[idxInst].rp[rowKeep]
279
+ Xp = instCurr[idxInst].Xp.loc[rowKeep]
280
+ rowKeep = instCurr[idxInst].Xn[i] == 0
281
+ rn = instCurr[idxInst].rn[rowKeep]
282
+ Xn = instCurr[idxInst].Xn.loc[rowKeep]
283
+ # Remove redundant features
284
+ colKeep = pd.Series(Xp.columns.get_level_values(0) != i[0], index=Xp.columns)
285
+ if i[1] == '<=':
286
+ thresh = Xp[i[0]].columns.get_level_values(1).to_series().replace('NaN', np.nan).values
287
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '>') & (thresh < i[2])
288
+ elif i[1] == '>':
289
+ thresh = Xp[i[0]].columns.get_level_values(1).to_series().replace('NaN', np.nan).values
290
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '<=') & (thresh > i[2])
291
+ elif i[1] == '!=':
292
+ colKeep[i[0]] = (Xp[i[0]].columns.get_level_values(0) == '!=') & (Xp[i[0]].columns.get_level_values(1) != i[2])
293
+ Xp = Xp.loc[:, colKeep]
294
+ Xn = Xn.loc[:, colKeep]
295
+ instNext.append(PricingInstance(rp, rn, Xp, Xn, instCurr[idxInst].v1[i], zNext[idxz]))
296
+
297
+ instCurr = instNext
298
+
299
+ # Conjunctions corresponding to solutions
300
+ aOut = 1 - (np.dot(1 - X, zOut) > 0)
301
+
302
+ return vOut, zOut, aOut
@@ -0,0 +1,245 @@
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import cvxpy as cvx
5
+ from sklearn.metrics import confusion_matrix, accuracy_score
6
+ from sklearn.base import BaseEstimator, ClassifierMixin
7
+ import time
8
+
9
+ from .beam_search import beam_search, beam_search_K1
10
+
11
+
12
+ class BooleanRuleCG(BaseEstimator, ClassifierMixin):
13
+ """BooleanRuleCG is a directly interpretable supervised learning method
14
+ for binary classification that learns a Boolean rule in disjunctive
15
+ normal form (DNF) or conjunctive normal form (CNF) using column generation (CG).
16
+ AIX360 implements a heuristic beam search version of BRCG that is less
17
+ computationally intensive than the published integer programming version [#NeurIPS2018]_.
18
+
19
+ References:
20
+ .. [#NeurIPS2018] `S. Dash, O. Günlük, D. Wei, "Boolean decision rules via
21
+ column generation." Neural Information Processing Systems (NeurIPS), 2018.
22
+ <https://papers.nips.cc/paper/7716-boolean-decision-rules-via-column-generation.pdf>`_
23
+ """
24
+ def __init__(self,
25
+ lambda0=0.001,
26
+ lambda1=0.001,
27
+ CNF=False,
28
+ iterMax=100,
29
+ timeMax=100,
30
+ K=10,
31
+ D=10,
32
+ B=5,
33
+ eps=1e-6,
34
+ solver='ECOS',
35
+ verbose=False,
36
+ silent=False):
37
+ """
38
+ Args:
39
+ lambda0 (float, optional): Complexity - fixed cost of each clause
40
+ lambda1 (float, optional): Complexity - additional cost for each literal
41
+ CNF (bool, optional): CNF instead of DNF
42
+ iterMax (int, optional): Column generation - maximum number of iterations
43
+ timeMax (int, optional): Column generation - maximum runtime in seconds
44
+ K (int, optional): Column generation - maximum number of columns generated per iteration
45
+ D (int, optional): Column generation - maximum degree
46
+ B (int, optional): Column generation - beam search width
47
+ eps (float, optional): Numerical tolerance on comparisons
48
+ solver (str, optional): Linear programming - solver
49
+ verbose (bool, optional): Linear programming - verboseness
50
+ silent (bool, optional): Silence overall algorithm messages
51
+ """
52
+ # Complexity parameters
53
+ self.lambda0 = lambda0 # fixed cost of each clause
54
+ self.lambda1 = lambda1 # additional cost per literal
55
+ # CNF instead of DNF
56
+ self.CNF = CNF
57
+ # Column generation parameters
58
+ self.iterMax = iterMax # maximum number of iterations
59
+ self.timeMax = timeMax # maximum runtime in seconds
60
+ self.K = K # maximum number of columns generated per iteration
61
+ self.D = D # maximum degree
62
+ self.B = B # beam search width
63
+ # Numerical tolerance on comparisons
64
+ self.eps = eps
65
+ # Linear programming parameters
66
+ self.solver = solver # solver
67
+ self.verbose = verbose # verboseness
68
+ # Silence output
69
+ self.silent = silent
70
+
71
+ def fit(self, X, y):
72
+ """Fit model to training data.
73
+
74
+ Args:
75
+ X (DataFrame): Binarized features with MultiIndex column labels
76
+ y (array): Binary-valued target variable
77
+ Returns:
78
+ BooleanRuleCG: Self
79
+ """
80
+ if not self.silent:
81
+ print('Learning {} rule with complexity parameters lambda0={}, lambda1={}'\
82
+ .format('CNF' if self.CNF else 'DNF', self.lambda0, self.lambda1))
83
+ if self.CNF:
84
+ # Flip labels for CNF
85
+ y = 1 - y
86
+ # Positive (y = 1) and negative (y = 0) samples
87
+ P = np.where(y > 0.5)[0]
88
+ Z = np.where(y < 0.5)[0]
89
+ nP = len(P)
90
+ n = len(y)
91
+
92
+ # Initialize with empty and singleton conjunctions, i.e. X plus all-ones feature
93
+ # Feature indicator and conjunction matrices
94
+ z = pd.DataFrame(np.eye(X.shape[1], X.shape[1]+1, 1, dtype=int), index=X.columns)
95
+ A = np.hstack((np.ones((X.shape[0],1), dtype=int), X))
96
+ # Iteration counter
97
+ self.it = 0
98
+ # Start time
99
+ self.starttime = time.time()
100
+
101
+ # Formulate master LP
102
+ # Variables
103
+ w = cvx.Variable(A.shape[1], nonneg=True)
104
+ xi = cvx.Variable(nP, nonneg=True)
105
+ # Objective function (no penalty on empty conjunction)
106
+ cs = self.lambda0 + self.lambda1 * z.sum().values
107
+ cs[0] = 0
108
+ obj = cvx.Minimize(cvx.sum(xi) / n + cvx.sum(A[Z,:] * w) / n + cs * w)
109
+ # Constraints
110
+ constraints = [xi + A[P,:] * w >= 1]
111
+
112
+ # Solve problem
113
+ prob = cvx.Problem(obj, constraints)
114
+ prob.solve(solver=self.solver, verbose=self.verbose)
115
+ if not self.silent:
116
+ print('Initial LP solved')
117
+
118
+ # Extract dual variables
119
+ r = np.ones_like(y, dtype=float) / n
120
+ r[P] = -constraints[0].dual_value
121
+
122
+ # Beam search for conjunctions with negative reduced cost
123
+ # Most negative reduced cost among current variables
124
+ UB = np.dot(r, A) + cs
125
+ UB = min(UB.min(), 0)
126
+ v, zNew, Anew = beam_search(r, X, self.lambda0, self.lambda1,
127
+ K=self.K, UB=UB, D=self.D, B=self.B, eps=self.eps)
128
+
129
+ while (v < -self.eps).any() and (self.it < self.iterMax) and (time.time()-self.starttime < self.timeMax):
130
+ # Negative reduced costs found
131
+ self.it += 1
132
+ if not self.silent:
133
+ print('Iteration: {}, Objective: {:.4f}'.format(self.it, prob.value))
134
+
135
+ # Add to existing conjunctions
136
+ z = pd.concat([z, zNew], axis=1, ignore_index=True)
137
+ A = np.concatenate((A, Anew), axis=1)
138
+
139
+ # Reformulate master LP
140
+ # Variables
141
+ w = cvx.Variable(A.shape[1], nonneg=True)
142
+ # Objective function
143
+ cs = np.concatenate((cs, self.lambda0 + self.lambda1 * zNew.sum().values))
144
+ obj = cvx.Minimize(cvx.sum(xi) / n + cvx.sum(A[Z,:] * w) / n + cs * w)
145
+ # Constraints
146
+ constraints = [xi + A[P,:] * w >= 1]
147
+
148
+ # Solve problem
149
+ prob = cvx.Problem(obj, constraints)
150
+ prob.solve(solver=self.solver, verbose=self.verbose)
151
+
152
+ # Extract dual variables
153
+ r[P] = -constraints[0].dual_value
154
+
155
+ # Beam search for conjunctions with negative reduced cost
156
+ # Most negative reduced cost among current variables
157
+ UB = np.dot(r, A) + cs
158
+ UB = min(UB.min(), 0)
159
+ v, zNew, Anew = beam_search(r, X, self.lambda0, self.lambda1,
160
+ K=self.K, UB=UB, D=self.D, B=self.B, eps=self.eps)
161
+
162
+ # Save generated conjunctions and LP solution
163
+ self.z = z
164
+ self.wLP = w.value
165
+
166
+ r = np.full(nP, 1./n)
167
+ self.w = beam_search_K1(r, pd.DataFrame(1-A[P,:]), 0, A[Z,:].sum(axis=0) / n + cs,
168
+ UB=r.sum(), D=100, B=2*self.B, eps=self.eps, stopEarly=False)[1].values.ravel()
169
+ if len(self.w) == 0:
170
+ self.w = np.zeros_like(self.wLP, dtype=int)
171
+
172
+ def compute_conjunctions(self, X):
173
+ """Compute conjunctions of features as specified in self.z.
174
+
175
+ Args:
176
+ X (DataFrame): Binarized features with MultiIndex column labels
177
+ Returns:
178
+ array: A -- Conjunction values
179
+ """
180
+ try:
181
+ A = 1 - (np.dot(1 - X, self.z) > 0) # Changed matmul to dot, because failed on some machines
182
+ except AttributeError:
183
+ print("Attribute 'z' does not exist, please fit model first.")
184
+ return A
185
+
186
+ def predict(self, X):
187
+ """Predict class labels.
188
+
189
+ Args:
190
+ X (DataFrame): Binarized features with MultiIndex column labels
191
+ Returns:
192
+ array: y -- Predicted labels
193
+ """
194
+ # Compute conjunctions of features
195
+ A = self.compute_conjunctions(X)
196
+ # Predict labels
197
+ if self.CNF:
198
+ # Flip labels since model is actually a DNF for Y=0
199
+ return 1 - (np.dot(A, self.w) > 0)
200
+ else:
201
+ return (np.dot(A, self.w) > 0).astype(int)
202
+
203
+ def explain(self, maxConj=None, prec=2):
204
+ """Return rules comprising the model.
205
+
206
+ Args:
207
+ maxConj (int, optional): Maximum number of conjunctions to show
208
+ prec (int, optional): Number of decimal places to show for floating-value thresholds
209
+ Returns:
210
+ Dictionary containing
211
+
212
+ * isCNF (bool): flag signaling whether model is CNF or DNF
213
+ * rules (list): selected conjunctions formatted as strings
214
+ """
215
+ # Selected conjunctions
216
+ z = self.z.loc[:, self.w > 0.5]
217
+ truncate = (maxConj is not None) and (z.shape[1] > maxConj)
218
+ nConj = maxConj if truncate else z.shape[1]
219
+
220
+ """
221
+ if self.CNF:
222
+ print('Predict Y=0 if ANY of the following rules are satisfied, otherwise Y=1:')
223
+ else:
224
+ print('Predict Y=1 if ANY of the following rules are satisfied, otherwise Y=0:')
225
+ """
226
+
227
+ # Sort conjunctions by increasing order
228
+ idxSort = z.sum().sort_values().index[:nConj]
229
+ # Iterate over sorted conjunctions
230
+ conj = []
231
+ for i in idxSort:
232
+ # MultiIndex of features participating in rule i
233
+ idxFeat = z.index[z[i] > 0]
234
+ # String representations of features
235
+ strFeat = idxFeat.get_level_values(0) + ' ' + idxFeat.get_level_values(1)\
236
+ + ' ' + idxFeat.get_level_values(2).to_series()\
237
+ .apply(lambda x: ('{:.' + str(prec) + 'f}').format(x) if type(x) is float else str(x))
238
+ # String representation of rule
239
+ strFeat = strFeat.str.cat(sep=' AND ')
240
+ conj.append(strFeat)
241
+
242
+ return {
243
+ 'isCNF': self.CNF,
244
+ 'rules': conj
245
+ }