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,447 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from sklearn.linear_model import LogisticRegression
|
|
5
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
6
|
+
|
|
7
|
+
from .beam_search import beam_search_K1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogisticRuleRegression(BaseEstimator, ClassifierMixin):
|
|
11
|
+
"""Logistic Rule Regression is a directly interpretable supervised learning
|
|
12
|
+
method that performs logistic regression on rule-based features.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self,
|
|
15
|
+
lambda0=0.05,
|
|
16
|
+
lambda1=0.01,
|
|
17
|
+
useOrd=False,
|
|
18
|
+
debias=True,
|
|
19
|
+
init0=False,
|
|
20
|
+
K=1,
|
|
21
|
+
iterMax=200,
|
|
22
|
+
B=1,
|
|
23
|
+
wLB=0.5,
|
|
24
|
+
stopEarly=False,
|
|
25
|
+
eps=1e-6,
|
|
26
|
+
maxSolverIter=100):
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
Args:
|
|
30
|
+
lambda0 (float, optional): Regularization - fixed cost of each rule
|
|
31
|
+
lambda1 (float, optional): Regularization - additional cost of each literal in rule
|
|
32
|
+
useOrd (bool, optional): Also use standardized numerical features
|
|
33
|
+
debias (bool, optional): Re-fit final solution without regularization
|
|
34
|
+
init0 (bool, optional): Initialize with no features
|
|
35
|
+
K (int, optional): Column generation - maximum number of columns generated per iteration
|
|
36
|
+
iterMax (int, optional): Column generation - maximum number of iterations
|
|
37
|
+
B (int, optional): Column generation - beam search width
|
|
38
|
+
wLB (float, optional): Column generation - weight on lower bound in evaluating nodes
|
|
39
|
+
stopEarly (bool, optional): Column generation - stop after current degree once improving column found
|
|
40
|
+
eps (float, optional): Numerical tolerance on comparisons
|
|
41
|
+
maxSolverIter: Maximum number of logistic regression solver iterations
|
|
42
|
+
"""
|
|
43
|
+
# Use standardized ordinal features
|
|
44
|
+
self.useOrd = useOrd
|
|
45
|
+
# Initialize with no features
|
|
46
|
+
self.init0 = init0
|
|
47
|
+
# Regularization parameters
|
|
48
|
+
self.lambda0 = lambda0 # fixed cost of term
|
|
49
|
+
self.lambda1 = lambda1 # cost per literal
|
|
50
|
+
self.debias = debias # re-fit final solution without regularization
|
|
51
|
+
# Column generation parameters
|
|
52
|
+
self.K = K # maximum number of columns generated per iteration
|
|
53
|
+
self.iterMax = iterMax # maximum number of iterations
|
|
54
|
+
self.B = B # beam search width
|
|
55
|
+
self.wLB = wLB # weight on lower bound in evaluating nodes
|
|
56
|
+
self.stopEarly = stopEarly # stop after current degree once improving column found
|
|
57
|
+
# Numerical tolerance on comparisons
|
|
58
|
+
self.eps = eps
|
|
59
|
+
# Maximum logistic solver iterations
|
|
60
|
+
self.maxSolverIter = maxSolverIter
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def fit(self, X, y, Xstd=None):
|
|
64
|
+
"""Fit model to training data.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
X (DataFrame): Binarized features with MultiIndex column labels
|
|
68
|
+
y (array): Target variable
|
|
69
|
+
Xstd (DataFrame, optional): Standardized numerical features
|
|
70
|
+
Returns:
|
|
71
|
+
LogisticRuleRegression: Self
|
|
72
|
+
"""
|
|
73
|
+
# Initialization
|
|
74
|
+
# Number of samples
|
|
75
|
+
n = X.shape[0]
|
|
76
|
+
if self.init0:
|
|
77
|
+
# Initialize with empty feature indicator and conjunction matrices
|
|
78
|
+
z = pd.DataFrame([], index=X.columns)
|
|
79
|
+
A = np.empty((X.shape[0], 0))
|
|
80
|
+
else:
|
|
81
|
+
# Initialize with X itself i.e. singleton conjunctions
|
|
82
|
+
# Feature indicator and conjunction matrices
|
|
83
|
+
z = pd.DataFrame(np.eye(X.shape[1], dtype=int), index=X.columns)
|
|
84
|
+
# Remove negations
|
|
85
|
+
indPos = X.columns.get_level_values(1).isin(['', '<=', '=='])
|
|
86
|
+
z = z.loc[:,indPos]
|
|
87
|
+
A = X.loc[:,indPos].values
|
|
88
|
+
# Scale conjunction matrix to account for non-uniform penalties
|
|
89
|
+
A = A * self.lambda0 / (self.lambda0 + self.lambda1 * z.sum().values)
|
|
90
|
+
if self.useOrd:
|
|
91
|
+
self.namesOrd = Xstd.columns
|
|
92
|
+
numOrd = Xstd.shape[1]
|
|
93
|
+
# Scale ordinal features to have similar std as "average" binary feature
|
|
94
|
+
Astd = 0.4 * Xstd.values
|
|
95
|
+
# Iteration counter
|
|
96
|
+
self.it = 0
|
|
97
|
+
# Logistic regression object
|
|
98
|
+
lr = LogisticRegression(
|
|
99
|
+
penalty='l1',
|
|
100
|
+
C=1/(n*self.lambda0),
|
|
101
|
+
solver='saga',
|
|
102
|
+
max_iter=self.maxSolverIter)
|
|
103
|
+
|
|
104
|
+
self.p = y.mean()
|
|
105
|
+
if self.init0:
|
|
106
|
+
# Initial residual
|
|
107
|
+
r = (self.p - y) / n
|
|
108
|
+
# Derivative w.r.t. intercept term
|
|
109
|
+
UB = min(r.sum(), 0)
|
|
110
|
+
else:
|
|
111
|
+
# Fit logistic regression model
|
|
112
|
+
if self.useOrd:
|
|
113
|
+
B = np.concatenate((Astd, A), axis=1)
|
|
114
|
+
lr.fit(B, y)
|
|
115
|
+
# Initial residual
|
|
116
|
+
r = (lr.predict_proba(B)[:,1] - y) / n
|
|
117
|
+
else:
|
|
118
|
+
lr.fit(A, y)
|
|
119
|
+
# Initial residual
|
|
120
|
+
r = (lr.predict_proba(A)[:,1] - y) / n
|
|
121
|
+
# Most "negative" subderivative among current variables (undo scaling)
|
|
122
|
+
UB = -np.abs(np.dot(r, A))
|
|
123
|
+
UB *= (self.lambda0 + self.lambda1 * z.sum().values) / self.lambda0
|
|
124
|
+
UB += self.lambda0 + self.lambda1 * z.sum().values
|
|
125
|
+
UB = min(UB.min(), 0)
|
|
126
|
+
|
|
127
|
+
# Beam search for conjunctions with subdifferentials that exclude zero
|
|
128
|
+
vp, zp, Ap = beam_search_K1(r, X, self.lambda0, self.lambda1,
|
|
129
|
+
UB=UB, B=self.B, wLB=self.wLB, eps=self.eps, stopEarly=self.stopEarly)
|
|
130
|
+
vn, zn, An = beam_search_K1(-r, X, self.lambda0, self.lambda1,
|
|
131
|
+
UB=UB, B=self.B, wLB=self.wLB, eps=self.eps, stopEarly=self.stopEarly)
|
|
132
|
+
v = np.append(vp, vn)
|
|
133
|
+
|
|
134
|
+
while (v < UB).any() and (self.it < self.iterMax):
|
|
135
|
+
# Subdifferentials excluding zero exist, continue
|
|
136
|
+
self.it += 1
|
|
137
|
+
zNew = pd.concat([zp, zn], axis=1, ignore_index=True)
|
|
138
|
+
Anew = np.concatenate((Ap, An), axis=1)
|
|
139
|
+
|
|
140
|
+
# K conjunctions with largest subderivatives in absolute value
|
|
141
|
+
idxLargest = np.argsort(v)[:self.K]
|
|
142
|
+
v = v[idxLargest]
|
|
143
|
+
zNew = zNew.iloc[:,idxLargest]
|
|
144
|
+
Anew = Anew[:,idxLargest]
|
|
145
|
+
# Scale new conjunction matrix to account for non-uniform penalties
|
|
146
|
+
Anew = Anew * self.lambda0 / (self.lambda0 + self.lambda1 * zNew.sum().values)
|
|
147
|
+
|
|
148
|
+
# Add to existing conjunctions
|
|
149
|
+
z = pd.concat([z, zNew], axis=1, ignore_index=True)
|
|
150
|
+
A = np.concatenate((A, Anew), axis=1)
|
|
151
|
+
# Fit logistic regression model
|
|
152
|
+
if self.useOrd:
|
|
153
|
+
B = np.concatenate((Astd, A), axis=1)
|
|
154
|
+
lr.fit(B, y)
|
|
155
|
+
# Residual
|
|
156
|
+
r = (lr.predict_proba(B)[:,1] - y) / n
|
|
157
|
+
else:
|
|
158
|
+
lr.fit(A, y)
|
|
159
|
+
# Residual
|
|
160
|
+
r = (lr.predict_proba(A)[:,1] - y) / n
|
|
161
|
+
|
|
162
|
+
# Most "negative" subderivative among current variables (undo scaling)
|
|
163
|
+
UB = -np.abs(np.dot(r, A))
|
|
164
|
+
UB *= (self.lambda0 + self.lambda1 * z.sum().values) / self.lambda0
|
|
165
|
+
UB += self.lambda0 + self.lambda1 * z.sum().values
|
|
166
|
+
UB = min(UB.min(), 0)
|
|
167
|
+
|
|
168
|
+
# Beam search for conjunctions with subdifferentials that exclude zero
|
|
169
|
+
vp, zp, Ap = beam_search_K1(r, X, self.lambda0, self.lambda1,
|
|
170
|
+
UB=UB, B=self.B, wLB=self.wLB, eps=self.eps, stopEarly=self.stopEarly)
|
|
171
|
+
vn, zn, An = beam_search_K1(-r, X, self.lambda0, self.lambda1,
|
|
172
|
+
UB=UB, B=self.B, wLB=self.wLB, eps=self.eps, stopEarly=self.stopEarly)
|
|
173
|
+
v = np.append(vp, vn)
|
|
174
|
+
|
|
175
|
+
# Restrict model to conjunctions with nonzero coefficients
|
|
176
|
+
try:
|
|
177
|
+
idxNonzero = np.where(np.abs(lr.coef_) > self.eps)[1]
|
|
178
|
+
if self.useOrd:
|
|
179
|
+
# Nonzero indices of standardized and rule features
|
|
180
|
+
self.idxNonzeroOrd = idxNonzero[idxNonzero < numOrd]
|
|
181
|
+
nnzOrd = len(self.idxNonzeroOrd)
|
|
182
|
+
idxNonzeroRules = idxNonzero[idxNonzero >= numOrd] - numOrd
|
|
183
|
+
if self.debias and len(idxNonzero):
|
|
184
|
+
# Re-fit logistic regression model with effectively no regularization
|
|
185
|
+
z = z.iloc[:,idxNonzeroRules]
|
|
186
|
+
lr.C = 1 / self.eps
|
|
187
|
+
lr.fit(B[:,idxNonzero], y)
|
|
188
|
+
idxNonzero = np.where(np.abs(lr.coef_) > self.eps)[1]
|
|
189
|
+
# Nonzero indices of standardized and rule features
|
|
190
|
+
idxNonzeroOrd2 = idxNonzero[idxNonzero < nnzOrd]
|
|
191
|
+
self.idxNonzeroOrd = self.idxNonzeroOrd[idxNonzeroOrd2]
|
|
192
|
+
idxNonzeroRules = idxNonzero[idxNonzero >= nnzOrd] - nnzOrd
|
|
193
|
+
self.z = z.iloc[:,idxNonzeroRules]
|
|
194
|
+
lr.coef_ = lr.coef_[:,idxNonzero]
|
|
195
|
+
else:
|
|
196
|
+
if self.debias and len(idxNonzero):
|
|
197
|
+
# Re-fit logistic regression model with effectively no regularization
|
|
198
|
+
z = z.iloc[:,idxNonzero]
|
|
199
|
+
lr.C = 1 / self.eps
|
|
200
|
+
lr.fit(A[:,idxNonzero], y)
|
|
201
|
+
idxNonzero = np.where(np.abs(lr.coef_) > self.eps)[1]
|
|
202
|
+
self.z = z.iloc[:,idxNonzero]
|
|
203
|
+
lr.coef_ = lr.coef_[:,idxNonzero]
|
|
204
|
+
except AttributeError:
|
|
205
|
+
# Model has no coefficients except intercept
|
|
206
|
+
self.z = z
|
|
207
|
+
self.lr = lr
|
|
208
|
+
|
|
209
|
+
def compute_conjunctions(self, X):
|
|
210
|
+
"""Compute conjunctions of features as specified in self.z.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
X (DataFrame): Binarized features with MultiIndex column labels
|
|
214
|
+
Returns:
|
|
215
|
+
array: A -- Feature conjunction values, shape (X.shape[0], self.z.shape[1])
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
A = 1 - (((1 - X) @ self.z) > 0)
|
|
219
|
+
except AttributeError:
|
|
220
|
+
print("Attribute 'z' does not exist, please fit model first.")
|
|
221
|
+
# Scale conjunctions as done in training
|
|
222
|
+
A = A * self.lambda0 / (self.lambda0 + self.lambda1 * self.z.sum().values)
|
|
223
|
+
return A
|
|
224
|
+
|
|
225
|
+
def predict_proba(self, X, Xstd=None):
|
|
226
|
+
"""Predict probabilities of Y=1.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
X (DataFrame): Binarized features with MultiIndex column labels
|
|
230
|
+
Xstd (DataFrame, optional): Standardized numerical features
|
|
231
|
+
Returns:
|
|
232
|
+
array: p -- Predicted probabilities
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
if self.lr.coef_.shape[1]:
|
|
236
|
+
# Compute conjunctions of features
|
|
237
|
+
A = self.compute_conjunctions(X)
|
|
238
|
+
if self.useOrd:
|
|
239
|
+
# Selected ordinal features scaled as in training
|
|
240
|
+
Astd = 0.4 * Xstd.values[:,self.idxNonzeroOrd]
|
|
241
|
+
# Predict probabilities
|
|
242
|
+
B = np.concatenate((Astd, A), axis=1)
|
|
243
|
+
return self.lr.predict_proba(B)[:,1]
|
|
244
|
+
else:
|
|
245
|
+
# Predict probabilities
|
|
246
|
+
return self.lr.predict_proba(A)[:,1]
|
|
247
|
+
else:
|
|
248
|
+
return np.full(X.shape[0], self.p)
|
|
249
|
+
except AttributeError:
|
|
250
|
+
return np.full(X.shape[0], self.p)
|
|
251
|
+
|
|
252
|
+
def predict(self, X, Xstd=None):
|
|
253
|
+
"""Predict class labels.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
X (DataFrame): Binarized features with MultiIndex column labels
|
|
257
|
+
Xstd (DataFrame, optional): Standardized numerical features
|
|
258
|
+
Returns:
|
|
259
|
+
array: yhat -- Predicted labels
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
if self.lr.coef_.shape[1]:
|
|
263
|
+
# Compute conjunctions of features
|
|
264
|
+
A = self.compute_conjunctions(X)
|
|
265
|
+
if self.useOrd:
|
|
266
|
+
# Selected ordinal features scaled as in training
|
|
267
|
+
Astd = 0.4 * Xstd.values[:,self.idxNonzeroOrd]
|
|
268
|
+
# Predict probabilities
|
|
269
|
+
B = np.concatenate((Astd, A), axis=1)
|
|
270
|
+
return self.lr.predict(B)
|
|
271
|
+
else:
|
|
272
|
+
# Predict labels
|
|
273
|
+
return self.lr.predict(A)
|
|
274
|
+
else:
|
|
275
|
+
return np.full(X.shape[0], self.p > 0.5, dtype=int)
|
|
276
|
+
except AttributeError:
|
|
277
|
+
return np.full(X.shape[0], self.p > 0.5, dtype=int)
|
|
278
|
+
|
|
279
|
+
def explain(self, maxCoeffs=None, highDegOnly=False, prec=2):
|
|
280
|
+
"""Return DataFrame holding model features and their coefficients.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
maxCoeffs (int, optional): Maximum number of rules/numerical features to show
|
|
284
|
+
highDegOnly (bool, optional): Only show higher-degree rules
|
|
285
|
+
prec (int, optional): Number of decimal places to show for floating-value thresholds
|
|
286
|
+
Returns:
|
|
287
|
+
DataFrame: dfExpl -- Rules/numerical features and their coefficients
|
|
288
|
+
"""
|
|
289
|
+
# Number of ordinal features used
|
|
290
|
+
if self.useOrd:
|
|
291
|
+
nnzOrd = len(self.idxNonzeroOrd)
|
|
292
|
+
else:
|
|
293
|
+
nnzOrd = 0
|
|
294
|
+
if highDegOnly:
|
|
295
|
+
# Restrict to higher-degree rules
|
|
296
|
+
coeffs = self.lr.coef_[0, nnzOrd:][self.z.sum() > 1]
|
|
297
|
+
nnzOrd = 0
|
|
298
|
+
z = self.z.loc[:, self.z.sum() > 1]
|
|
299
|
+
else:
|
|
300
|
+
coeffs = self.lr.coef_[0,:]
|
|
301
|
+
z = self.z
|
|
302
|
+
# Initialize DataFrame to be printed
|
|
303
|
+
truncate = (maxCoeffs is not None) and (len(coeffs) > maxCoeffs)
|
|
304
|
+
nRows = maxCoeffs + 1 if truncate else len(coeffs) + 1
|
|
305
|
+
dfExpl = pd.DataFrame(index=range(nRows), columns=['rule','coefficient'])
|
|
306
|
+
|
|
307
|
+
# Intercept term
|
|
308
|
+
dfExpl.at[0, 'rule'] = '(intercept)'
|
|
309
|
+
dfExpl.at[0, 'coefficient'] = self.lr.intercept_[0]
|
|
310
|
+
# Sort coefficients from largest to smallest
|
|
311
|
+
idxSort = np.abs(coeffs).argsort()[:-nRows:-1]
|
|
312
|
+
dfExpl.loc[1:, 'coefficient'] = coeffs[idxSort]
|
|
313
|
+
# Iterate over sorted coefficients
|
|
314
|
+
for (row, i) in enumerate(idxSort):
|
|
315
|
+
if i < nnzOrd:
|
|
316
|
+
# Ordinal feature
|
|
317
|
+
dfExpl.at[row+1, 'rule'] = self.namesOrd[self.idxNonzeroOrd[i]]
|
|
318
|
+
else:
|
|
319
|
+
# MultiIndex of features participating in rule i
|
|
320
|
+
idxFeat = z.index[z.iloc[:,i - nnzOrd] > 0]
|
|
321
|
+
# String representations of features
|
|
322
|
+
strFeat = idxFeat.get_level_values(0) + ' ' + idxFeat.get_level_values(1)\
|
|
323
|
+
+ ' ' + idxFeat.get_level_values(2).to_series()\
|
|
324
|
+
.apply(lambda x: ('{:.' + str(prec) + 'f}').format(x) if type(x) is float else str(x))
|
|
325
|
+
# String representation of rule
|
|
326
|
+
dfExpl.at[row+1, 'rule'] = strFeat.str.cat(sep=' AND ')
|
|
327
|
+
|
|
328
|
+
# Prepare and return dataframe with model features and coefficients
|
|
329
|
+
if nnzOrd:
|
|
330
|
+
dfExpl.rename(columns={'rule': 'rule/numerical feature'}, inplace=True)
|
|
331
|
+
|
|
332
|
+
return dfExpl
|
|
333
|
+
|
|
334
|
+
def visualize(self, Xorig, fb, features=None):
|
|
335
|
+
"""Plot generalized additive model component, which includes first-degree rules
|
|
336
|
+
and linear functions of unbinarized ordinal features but excludes higher-degree rules.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
Xorig (DataFrame): Original unbinarized features
|
|
340
|
+
fb: FeatureBinarizer object used to binarize features
|
|
341
|
+
features (list, optional): Subset of features to be plotted
|
|
342
|
+
"""
|
|
343
|
+
# Number of ordinal features used
|
|
344
|
+
if self.useOrd:
|
|
345
|
+
nnzOrd = len(self.idxNonzeroOrd)
|
|
346
|
+
else:
|
|
347
|
+
nnzOrd = 0
|
|
348
|
+
|
|
349
|
+
# Initialize terms Series and x values for plots
|
|
350
|
+
terms = pd.Series(index=pd.MultiIndex.from_arrays([[],[],[]], names=self.z.index.names))
|
|
351
|
+
xPlot = {}
|
|
352
|
+
|
|
353
|
+
# Iterate over ordinal features
|
|
354
|
+
for i in range(nnzOrd):
|
|
355
|
+
# Restrict to specified features
|
|
356
|
+
f = self.namesOrd[self.idxNonzeroOrd[i]]
|
|
357
|
+
if (features is not None) and (f not in features):
|
|
358
|
+
continue
|
|
359
|
+
# Append term
|
|
360
|
+
terms = pd.concat([terms, pd.Series(self.lr.coef_[0,i], index=[(f,'','')])])
|
|
361
|
+
# Initialize x values with min and max
|
|
362
|
+
xPlot[f] = [Xorig[f].min(), Xorig[f].max()]
|
|
363
|
+
|
|
364
|
+
# Iterate over first-degree rules
|
|
365
|
+
for i in range(self.z.shape[1]):
|
|
366
|
+
if self.z.iloc[:,i].sum() > 1:
|
|
367
|
+
continue
|
|
368
|
+
# MultiIndex of rule
|
|
369
|
+
idxTerm = self.z.index[self.z.iloc[:,i] > 0]
|
|
370
|
+
(f, o, v) = idxTerm[0]
|
|
371
|
+
# Restrict to specified features
|
|
372
|
+
if (features is not None) and (f not in features):
|
|
373
|
+
continue
|
|
374
|
+
# Append new term
|
|
375
|
+
terms = pd.concat([terms, pd.Series(self.lr.coef_[0,i+nnzOrd], index=idxTerm)])
|
|
376
|
+
# Update x values
|
|
377
|
+
if f not in xPlot:
|
|
378
|
+
if o in ['<=','>']:
|
|
379
|
+
# Ordinal feature, initialize with min and max
|
|
380
|
+
xPlot[f] = [Xorig[f].min(), Xorig[f].max()]
|
|
381
|
+
else:
|
|
382
|
+
# Categorical feature, use all values
|
|
383
|
+
xPlot[f] = np.sort(Xorig[f].unique())
|
|
384
|
+
if o in ['<=','>']:
|
|
385
|
+
# Append values around threshold
|
|
386
|
+
xPlot[f].extend([v-self.eps, v+self.eps])
|
|
387
|
+
|
|
388
|
+
# Initialize y values for plots and variance calculation
|
|
389
|
+
yPlot = {}
|
|
390
|
+
yVar = pd.DataFrame(0., index=Xorig.index, columns=xPlot.keys())
|
|
391
|
+
plotLine = pd.Series(False, index=xPlot.keys())
|
|
392
|
+
# Iterate over GAM features
|
|
393
|
+
for f in xPlot.keys():
|
|
394
|
+
# Sort x values
|
|
395
|
+
xPlot[f] = np.sort(np.array(xPlot[f]))
|
|
396
|
+
yPlot[f] = np.zeros_like(xPlot[f], dtype=float)
|
|
397
|
+
# Iterate over terms involving feature
|
|
398
|
+
for ((o,v), c) in terms[f].items():
|
|
399
|
+
if o == '':
|
|
400
|
+
if self.useOrd and (f in fb.ordinal):
|
|
401
|
+
# Add linear function of standardized feature with same factor of 0.4
|
|
402
|
+
idxf = fb.ordinal.index(f)
|
|
403
|
+
yPlot[f] += 0.4 * c * (xPlot[f] - fb.scaler.mean_[idxf]) / fb.scaler.scale_[idxf]
|
|
404
|
+
yVar[f] += 0.4 * c * (Xorig[f] - fb.scaler.mean_[idxf]) / fb.scaler.scale_[idxf]
|
|
405
|
+
plotLine[f] = True
|
|
406
|
+
else:
|
|
407
|
+
# Binary feature, add indicator function
|
|
408
|
+
yPlot[f] += c * (xPlot[f] == fb.maps[f].index[1])
|
|
409
|
+
yVar[f] += c * (Xorig[f] == fb.maps[f].index[1])
|
|
410
|
+
elif o == '<=':
|
|
411
|
+
# Add step function
|
|
412
|
+
yPlot[f] += c * (xPlot[f] <= v)
|
|
413
|
+
yVar[f] += c * (Xorig[f] <= v)
|
|
414
|
+
plotLine[f] = True
|
|
415
|
+
elif o == '>':
|
|
416
|
+
# Add step function
|
|
417
|
+
yPlot[f] += c * (xPlot[f] > v)
|
|
418
|
+
yVar[f] += c * (Xorig[f] > v)
|
|
419
|
+
plotLine[f] = True
|
|
420
|
+
elif o == '==':
|
|
421
|
+
# Add indicator function
|
|
422
|
+
yPlot[f] += c * (xPlot[f].astype(str) == v)
|
|
423
|
+
yVar[f] += c * (Xorig[f].astype(str) == v)
|
|
424
|
+
elif o == '!=':
|
|
425
|
+
# Add indicator function
|
|
426
|
+
yPlot[f] += c * (xPlot[f].astype(str) != v)
|
|
427
|
+
yVar[f] += c * (Xorig[f].astype(str) != v)
|
|
428
|
+
elif o == 'not':
|
|
429
|
+
# Binary feature, add indicator function
|
|
430
|
+
yPlot[f] += c * (xPlot[f] == fb.maps[f].index[0])
|
|
431
|
+
yVar[f] += c * (Xorig[f] == fb.maps[f].index[0])
|
|
432
|
+
|
|
433
|
+
# Plot in order of descending variance
|
|
434
|
+
figs = {}
|
|
435
|
+
yVar2 = yVar.var().sort_values(ascending=False)
|
|
436
|
+
for f in yVar2.index:
|
|
437
|
+
figs[f] = plt.figure()
|
|
438
|
+
ax = figs[f].add_subplot(111)
|
|
439
|
+
if plotLine[f]:
|
|
440
|
+
ax.plot(xPlot[f], yPlot[f])
|
|
441
|
+
else:
|
|
442
|
+
ax.bar(xPlot[f], yPlot[f])
|
|
443
|
+
ax.xaxis.set_ticks(xPlot[f])
|
|
444
|
+
plt.xlabel(f)
|
|
445
|
+
plt.ylabel('contribution to log-odds of Y=1')
|
|
446
|
+
|
|
447
|
+
return figs, yVar2
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
from rulelearn_ri.trxf.core.predicate import Relation
|
|
3
|
+
|
|
4
|
+
OPERATOR_MAPS = {
|
|
5
|
+
'>=': Relation.GE,
|
|
6
|
+
'>' : Relation.GT,
|
|
7
|
+
'<=': Relation.LE,
|
|
8
|
+
'<' : Relation.LT,
|
|
9
|
+
'==': Relation.EQ,
|
|
10
|
+
'!=': Relation.NEQ,
|
|
11
|
+
'not': Relation.NEQ, # For boolean columns
|
|
12
|
+
'': Relation.EQ, # For boolean columns
|
|
13
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pandas import DataFrame
|
|
5
|
+
from pandas.api.types import is_object_dtype, is_signed_integer_dtype, is_string_dtype
|
|
6
|
+
|
|
7
|
+
from sklearn.model_selection import train_test_split
|
|
8
|
+
from sklearn.preprocessing import LabelEncoder
|
|
9
|
+
|
|
10
|
+
from rulelearn_ri.algorithms.ripper.binding import EQ, LE, GE
|
|
11
|
+
|
|
12
|
+
DEFAULT_ENCODING_VALUE = -sys.maxsize - 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _encoding_for_parallel(
|
|
16
|
+
data: DataFrame,
|
|
17
|
+
column_name_index_map
|
|
18
|
+
):
|
|
19
|
+
# each condition can be indexed from those four list
|
|
20
|
+
name_list = []
|
|
21
|
+
op_list = []
|
|
22
|
+
num_val_list = []
|
|
23
|
+
nom_val_list = []
|
|
24
|
+
|
|
25
|
+
for name in data.columns.values:
|
|
26
|
+
if np.issubdtype(data.dtypes[name], np.signedinteger):
|
|
27
|
+
for val in np.unique(data.loc[:, name]):
|
|
28
|
+
name_list.append(column_name_index_map[name]), op_list.append(EQ), num_val_list.append(
|
|
29
|
+
-1), nom_val_list.append(val)
|
|
30
|
+
|
|
31
|
+
elif np.issubdtype(data.dtypes[name], np.floating):
|
|
32
|
+
for val in np.unique(data.loc[:, name]):
|
|
33
|
+
name_list.append(column_name_index_map[name]), op_list.append(GE), num_val_list.append(
|
|
34
|
+
val), nom_val_list.append(-1)
|
|
35
|
+
name_list.append(column_name_index_map[name]), op_list.append(LE), num_val_list.append(
|
|
36
|
+
val), nom_val_list.append(-1)
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError('unsupported type {type}'.format(type=data.dtypes[name]))
|
|
39
|
+
|
|
40
|
+
return [np.array(name_list, dtype=np.int64),
|
|
41
|
+
np.array(op_list, dtype=np.uint8),
|
|
42
|
+
np.array(num_val_list, dtype=np.float64),
|
|
43
|
+
np.array(nom_val_list, dtype=np.int64)]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def init_encoder(
|
|
47
|
+
data,
|
|
48
|
+
):
|
|
49
|
+
label_encoders = dict()
|
|
50
|
+
|
|
51
|
+
for name in data.columns.values:
|
|
52
|
+
# pandas 3 reads text into a StringDtype, which np.issubdtype cannot interpret
|
|
53
|
+
dtype = data.dtypes[name]
|
|
54
|
+
if is_object_dtype(dtype) or is_string_dtype(dtype) or is_signed_integer_dtype(dtype):
|
|
55
|
+
label_encoders[name] = LabelEncoder()
|
|
56
|
+
label_encoders[name].fit(data[name])
|
|
57
|
+
|
|
58
|
+
return label_encoders
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def encode_nominal(
|
|
62
|
+
label_encoders,
|
|
63
|
+
data,
|
|
64
|
+
):
|
|
65
|
+
for col in label_encoders.keys():
|
|
66
|
+
label_map = {val: label for label, val in enumerate(label_encoders[col].classes_)}
|
|
67
|
+
data[col] = data[col].map(label_map)
|
|
68
|
+
# fillna and convert to int
|
|
69
|
+
data[col] = data[col].fillna(DEFAULT_ENCODING_VALUE).astype(np.int64)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _split_instances(
|
|
73
|
+
pos: np.ndarray,
|
|
74
|
+
neg: np.ndarray,
|
|
75
|
+
ratio,
|
|
76
|
+
random_state
|
|
77
|
+
):
|
|
78
|
+
pos_train, pos_prune = train_test_split(pos, random_state=random_state, train_size=ratio)
|
|
79
|
+
neg_train, neg_prune = train_test_split(neg, random_state=random_state, train_size=ratio)
|
|
80
|
+
|
|
81
|
+
return pos_train, pos_prune, neg_train, neg_prune
|