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,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
|
+
}
|