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,544 @@
|
|
|
1
|
+
from typing import Optional, Tuple, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from numpy import ndarray
|
|
6
|
+
from pandas import DataFrame, Series
|
|
7
|
+
from pandas.api.types import is_float_dtype, is_integer_dtype, is_object_dtype
|
|
8
|
+
from sklearn.base import TransformerMixin
|
|
9
|
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
|
10
|
+
from sklearn.tree import DecisionTreeClassifier
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_text_dtype(dtype) -> bool:
|
|
14
|
+
# pandas >= 3 stores strings as StringDtype rather than object
|
|
15
|
+
return is_object_dtype(dtype) or isinstance(dtype, pd.StringDtype)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_ordinal_dtype(dtype) -> bool:
|
|
19
|
+
return is_integer_dtype(dtype) or is_float_dtype(dtype)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# noinspection PyPep8Naming
|
|
23
|
+
class FeatureBinarizer(TransformerMixin):
|
|
24
|
+
"""Transformer for binarizing categorical and ordinal features.
|
|
25
|
+
|
|
26
|
+
For use with BooleanRuleCG, LogisticRuleRegression and LinearRuleRegression
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self, colCateg=[], numThresh=9, negations=False, threshStr=False, returnOrd=False, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
Args:
|
|
31
|
+
colCateg (list): Categorical features ('object' dtype automatically treated as categorical)
|
|
32
|
+
numThresh (int): Number of quantile thresholds used to binarize ordinal variables
|
|
33
|
+
negations (bool): Append negations
|
|
34
|
+
threshStr (bool): Convert thresholds on ordinal features to strings
|
|
35
|
+
returnOrd (bool): Also return standardized ordinal features
|
|
36
|
+
"""
|
|
37
|
+
# List of categorical columns
|
|
38
|
+
if type(colCateg) is pd.Series:
|
|
39
|
+
self.colCateg = colCateg.tolist()
|
|
40
|
+
elif type(colCateg) is not list:
|
|
41
|
+
self.colCateg = [colCateg]
|
|
42
|
+
else:
|
|
43
|
+
self.colCateg = colCateg
|
|
44
|
+
# Number of quantile thresholds used to binarize ordinal features
|
|
45
|
+
self.numThresh = numThresh
|
|
46
|
+
self.thresh = {}
|
|
47
|
+
# whether to append negations
|
|
48
|
+
self.negations = negations
|
|
49
|
+
# whether to convert thresholds on ordinal features to strings
|
|
50
|
+
self.threshStr = threshStr
|
|
51
|
+
# Also return standardized ordinal features
|
|
52
|
+
self.returnOrd = returnOrd
|
|
53
|
+
|
|
54
|
+
def fit(self, X):
|
|
55
|
+
"""Fit FeatureBinarizer to data
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
X (DataFrame): Original features
|
|
59
|
+
Returns:
|
|
60
|
+
FeatureBinarizer: Self
|
|
61
|
+
self.maps (dict): Mappings for unary/binary columns
|
|
62
|
+
self.enc (dict): OneHotEncoders for categorical columns
|
|
63
|
+
self.thresh (dict(array)): Thresholds for ordinal columns
|
|
64
|
+
self.NaN (list): Ordinal columns containing NaN values
|
|
65
|
+
self.ordinal (list): Ordinal columns
|
|
66
|
+
self.scaler (StandardScaler): StandardScaler for ordinal columns
|
|
67
|
+
"""
|
|
68
|
+
data = X
|
|
69
|
+
# Quantile probabilities
|
|
70
|
+
quantProb = np.linspace(1. / (self.numThresh + 1.), self.numThresh / (self.numThresh + 1.), self.numThresh)
|
|
71
|
+
# Initialize
|
|
72
|
+
maps = {}
|
|
73
|
+
enc = {}
|
|
74
|
+
thresh = {}
|
|
75
|
+
NaN = []
|
|
76
|
+
if self.returnOrd:
|
|
77
|
+
ordinal = []
|
|
78
|
+
|
|
79
|
+
# Iterate over columns
|
|
80
|
+
for c in data:
|
|
81
|
+
# number of unique values
|
|
82
|
+
valUniq = data[c].nunique()
|
|
83
|
+
|
|
84
|
+
# Constant or binary column
|
|
85
|
+
if valUniq <= 2:
|
|
86
|
+
# Mapping to 0, 1
|
|
87
|
+
maps[c] = pd.Series(range(valUniq), index=np.sort(data[c].dropna().unique()))
|
|
88
|
+
|
|
89
|
+
# Categorical column
|
|
90
|
+
elif (c in self.colCateg) or _is_text_dtype(data[c].dtype):
|
|
91
|
+
# OneHotEncoder object
|
|
92
|
+
enc[c] = OneHotEncoder(sparse_output=False, dtype=int, handle_unknown='ignore')
|
|
93
|
+
# Fit to observed categories
|
|
94
|
+
enc[c].fit(data[[c]])
|
|
95
|
+
|
|
96
|
+
# Ordinal column
|
|
97
|
+
elif _is_ordinal_dtype(data[c].dtype):
|
|
98
|
+
# Few unique values
|
|
99
|
+
if valUniq <= self.numThresh + 1:
|
|
100
|
+
# Thresholds are sorted unique values excluding maximum
|
|
101
|
+
thresh[c] = np.sort(data[c].unique())[:-1]
|
|
102
|
+
# Many unique values
|
|
103
|
+
else:
|
|
104
|
+
# Thresholds are quantiles excluding repetitions
|
|
105
|
+
thresh[c] = data[c].quantile(q=quantProb).unique()
|
|
106
|
+
if data[c].isnull().any():
|
|
107
|
+
# Contains NaN values
|
|
108
|
+
NaN.append(c)
|
|
109
|
+
if self.returnOrd:
|
|
110
|
+
ordinal.append(c)
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
print(("Skipping column '" + str(c) + "': data type cannot be handled"))
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
self.maps = maps
|
|
117
|
+
self.enc = enc
|
|
118
|
+
self.thresh = thresh
|
|
119
|
+
self.NaN = NaN
|
|
120
|
+
if self.returnOrd:
|
|
121
|
+
self.ordinal = ordinal
|
|
122
|
+
# Fit StandardScaler to ordinal features
|
|
123
|
+
self.scaler = StandardScaler().fit(data[ordinal])
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def transform(self, X):
|
|
127
|
+
"""Binarize features
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
X (DataFrame): Original features
|
|
131
|
+
Returns:
|
|
132
|
+
A (DataFrame): Binarized features with MultiIndex column labels
|
|
133
|
+
Xstd (DataFrame, optional): Standardized ordinal features
|
|
134
|
+
"""
|
|
135
|
+
data = X
|
|
136
|
+
maps = self.maps
|
|
137
|
+
enc = self.enc
|
|
138
|
+
thresh = self.thresh
|
|
139
|
+
NaN = self.NaN
|
|
140
|
+
|
|
141
|
+
# Initialize dataframe
|
|
142
|
+
A = pd.DataFrame(index=data.index,
|
|
143
|
+
columns=pd.MultiIndex.from_arrays([[], [], []], names=['feature', 'operation', 'value']))
|
|
144
|
+
|
|
145
|
+
# Iterate over columns
|
|
146
|
+
for c in data:
|
|
147
|
+
# Constant or binary column
|
|
148
|
+
if c in maps:
|
|
149
|
+
# Rename values to 0, 1
|
|
150
|
+
colName = (str(c), '!=', str(maps[c].index[0])) if len(maps[c]) == 1 else (str(c), '==', str(maps[c].index[1]))
|
|
151
|
+
A[colName] = data[c].map(maps[c]).astype(int)
|
|
152
|
+
if self.negations:
|
|
153
|
+
A[(str(c), '==', str(maps[c].index[0]))] = 1 - A[colName]
|
|
154
|
+
|
|
155
|
+
# Categorical column
|
|
156
|
+
elif c in enc:
|
|
157
|
+
# Apply OneHotEncoder
|
|
158
|
+
Anew = enc[c].transform(data[[c]])
|
|
159
|
+
Anew = pd.DataFrame(Anew, index=data.index, columns=enc[c].categories_[0].astype(str))
|
|
160
|
+
if self.negations:
|
|
161
|
+
# Append negations
|
|
162
|
+
Anew = pd.concat([Anew, 1 - Anew], axis=1, keys=[(str(c), '=='), (str(c), '!=')])
|
|
163
|
+
else:
|
|
164
|
+
Anew.columns = pd.MultiIndex.from_product([[str(c)], ['=='], Anew.columns],
|
|
165
|
+
names=['feature', 'operation', 'value'])
|
|
166
|
+
# Concatenate
|
|
167
|
+
A = pd.concat([A, Anew], axis=1)
|
|
168
|
+
|
|
169
|
+
# Ordinal column
|
|
170
|
+
elif c in thresh:
|
|
171
|
+
# Threshold values to produce binary arrays
|
|
172
|
+
Anew = (data[c].values[:, np.newaxis] <= thresh[c]).astype(int)
|
|
173
|
+
if self.negations:
|
|
174
|
+
# Append negations
|
|
175
|
+
Anew = np.concatenate((Anew, 1 - Anew), axis=1)
|
|
176
|
+
ops = ['<=', '>']
|
|
177
|
+
else:
|
|
178
|
+
ops = ['<=']
|
|
179
|
+
# Convert to dataframe with column labels
|
|
180
|
+
if self.threshStr:
|
|
181
|
+
Anew = pd.DataFrame(Anew, index=data.index,
|
|
182
|
+
columns=pd.MultiIndex.from_product([[str(c)], ops, thresh[c].astype(str)]))
|
|
183
|
+
else:
|
|
184
|
+
Anew = pd.DataFrame(Anew, index=data.index,
|
|
185
|
+
columns=pd.MultiIndex.from_product([[str(c)], ops, thresh[c]]))
|
|
186
|
+
if c in NaN:
|
|
187
|
+
# Ensure that rows corresponding to NaN values are zeroed out
|
|
188
|
+
indNull = data[c].isnull()
|
|
189
|
+
Anew.loc[indNull] = 0
|
|
190
|
+
# Add NaN indicator column
|
|
191
|
+
Anew[(str(c), '==', 'NaN')] = indNull.astype(int)
|
|
192
|
+
if self.negations:
|
|
193
|
+
Anew[(str(c), '!=', 'NaN')] = (~indNull).astype(int)
|
|
194
|
+
# Concatenate
|
|
195
|
+
A = pd.concat([A, Anew], axis=1)
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
print(("Skipping column '" + str(c) + "': data type cannot be handled"))
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if self.returnOrd:
|
|
202
|
+
# Standardize ordinal features
|
|
203
|
+
Xstd = self.scaler.transform(data[self.ordinal])
|
|
204
|
+
Xstd = pd.DataFrame(Xstd, index=data.index, columns=self.ordinal)
|
|
205
|
+
# Fill NaN with mean (which is now zero)
|
|
206
|
+
Xstd.fillna(0, inplace=True)
|
|
207
|
+
return A, Xstd
|
|
208
|
+
else:
|
|
209
|
+
return A
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# noinspection PyPep8Naming
|
|
213
|
+
class FeatureBinarizerFromTrees(TransformerMixin):
|
|
214
|
+
"""Transformer for binarizing categorical and ordinal features.
|
|
215
|
+
|
|
216
|
+
For use with BooleanRuleCG, LogisticRuleRegression, and LinearRuleRegression. This transformer generates binary
|
|
217
|
+
features using splits in decision trees. Compared to `FeatureBinarizer`, this approach reduces the number of
|
|
218
|
+
features required to produce an accurate model. The smaller feature space shortens training time and often
|
|
219
|
+
simplifies rule sets.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self,
|
|
223
|
+
colCateg: list = None,
|
|
224
|
+
treeNum: int = 1,
|
|
225
|
+
treeDepth: Optional[int] = 4,
|
|
226
|
+
treeFeatureSelection: Union[str, float, None] = None,
|
|
227
|
+
treeKwargs: dict = None,
|
|
228
|
+
threshRound: Optional[int] = 6,
|
|
229
|
+
threshStr: bool = False,
|
|
230
|
+
returnOrd: bool = False,
|
|
231
|
+
randomState: int = None,
|
|
232
|
+
**kwargs):
|
|
233
|
+
"""
|
|
234
|
+
Args:
|
|
235
|
+
colCateg (list): Categorical features ('object' dtype automatically treated as categorical). These features
|
|
236
|
+
are one-hot-encoded.
|
|
237
|
+
treeNum (int): Number of trees to fit. Setting 'treeNum' to a value greater than one usually produces a
|
|
238
|
+
larger variety of output features.
|
|
239
|
+
treeDepth (int): The maximum depth of the tree. Setting 'treeDepth=None' grows a tree without limit.
|
|
240
|
+
Larger depth values produce more output features. Corresponds to parameter 'max_depth' in
|
|
241
|
+
DecisionTreeClassifier.
|
|
242
|
+
treeFeatureSelection (float, str): When building a tree, the input features are randomly permuted at
|
|
243
|
+
each split. This parameter specifies how many input features are considered at each split. By default,
|
|
244
|
+
this parameter is set to 'None' which indicates that all features should be considered at every split.
|
|
245
|
+
Other possible values are 'sqrt', 'log2', or a float that indicates the proportion of features to
|
|
246
|
+
select at every split (e.g. 0.5 would randomly select half of the input features at every split).
|
|
247
|
+
To create a wide variety of output features, or to sift through a very large number of features,
|
|
248
|
+
increase 'treeNum' and set 'treeFeatureSelection="sqrt"'. Corresponds to 'max_features' in
|
|
249
|
+
DecisionTreeClassifier.
|
|
250
|
+
treeKwargs (dict): A dictionary of parameters to pass to the scikit-learn DecisionTreeClassifier during
|
|
251
|
+
fitting.
|
|
252
|
+
threshRound (int): Round threshold values by this number of decimal places. This parameter can be used
|
|
253
|
+
to prevent similar thresholds from generating separate binarized features. E.g., if 'threshRound=2',
|
|
254
|
+
only one binarized feature will be generated for thresholds 0.009 and 0.01. Setting 'threshRound=None'
|
|
255
|
+
will disable rounding.
|
|
256
|
+
threshStr (bool): Convert threshold values to strings, including categorical values, in transformed
|
|
257
|
+
data frame's index.
|
|
258
|
+
returnOrd (bool): Return a standardized data frame for ordinal features (both discrete and continuous)
|
|
259
|
+
during transformation in addition to the binarized data frame.
|
|
260
|
+
randomState (int): Random state for decision tree.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
# Categorical columns
|
|
264
|
+
if colCateg is None:
|
|
265
|
+
self.colCateg = []
|
|
266
|
+
elif type(colCateg) is Series:
|
|
267
|
+
self.colCateg = colCateg.to_list()
|
|
268
|
+
elif type(colCateg) is not list:
|
|
269
|
+
self.colCateg = [colCateg]
|
|
270
|
+
else:
|
|
271
|
+
self.colCateg = colCateg
|
|
272
|
+
|
|
273
|
+
# Number of trees
|
|
274
|
+
if (treeNum is None) or (treeNum < 1) or (int(treeNum) != treeNum):
|
|
275
|
+
raise ValueError('The value for \'treeNum\' must be an integer value greater than zero.')
|
|
276
|
+
self.treeNum = int(treeNum)
|
|
277
|
+
|
|
278
|
+
# Tree kwargs
|
|
279
|
+
if treeKwargs is None:
|
|
280
|
+
treeKwargs = dict(max_features=None)
|
|
281
|
+
elif 'max_features' not in treeKwargs:
|
|
282
|
+
treeKwargs['max_features'] = None
|
|
283
|
+
|
|
284
|
+
# Tree depth
|
|
285
|
+
if treeDepth is not None:
|
|
286
|
+
if (treeDepth < 1) or (int(treeDepth) != treeDepth):
|
|
287
|
+
raise ValueError('The value for \'treeDepth\' must be None or an integer value greater than zero.')
|
|
288
|
+
treeKwargs['max_depth'] = treeDepth
|
|
289
|
+
elif 'max_depth' in treeKwargs:
|
|
290
|
+
treeDepth = treeKwargs['max_depth']
|
|
291
|
+
self.treeDepth = treeDepth
|
|
292
|
+
|
|
293
|
+
# Tree feature selection
|
|
294
|
+
if treeFeatureSelection is not None:
|
|
295
|
+
if isinstance(treeFeatureSelection, str):
|
|
296
|
+
error = treeFeatureSelection not in ('auto', 'sqrt', 'log2')
|
|
297
|
+
elif isinstance(treeFeatureSelection, (float, int)):
|
|
298
|
+
error = (treeFeatureSelection <= 0.0) or (treeFeatureSelection > 1.0)
|
|
299
|
+
else:
|
|
300
|
+
error = True
|
|
301
|
+
if error:
|
|
302
|
+
raise ValueError('Valid values for \'treeFeatureSelection\' are None, \'auto\', \'sqrt\', \'log2\', or '
|
|
303
|
+
'a value in interval (0, 1].')
|
|
304
|
+
treeKwargs['max_features'] = treeFeatureSelection
|
|
305
|
+
elif 'max_features' in treeKwargs:
|
|
306
|
+
treeFeatureSelection = treeKwargs['max_features']
|
|
307
|
+
self.treeFeatureSelection = treeFeatureSelection
|
|
308
|
+
|
|
309
|
+
# Tree kwargs
|
|
310
|
+
self.treeKwargs = treeKwargs
|
|
311
|
+
|
|
312
|
+
# Random state
|
|
313
|
+
self.randomState = randomState
|
|
314
|
+
|
|
315
|
+
# Rounding for ordinal values
|
|
316
|
+
if (threshRound is not None) and (threshRound < 0):
|
|
317
|
+
raise ValueError('The value for \'threshRound\' must be None, or zero, or greater than zero.')
|
|
318
|
+
self.threshRound = threshRound
|
|
319
|
+
|
|
320
|
+
# Whether to convert thresholds on ordinal features to strings
|
|
321
|
+
self.threshStr = threshStr
|
|
322
|
+
|
|
323
|
+
# Whether to convert thresholds on ordinal features to strings. Also return standardized ordinal features
|
|
324
|
+
# during transformation
|
|
325
|
+
self.returnOrd = returnOrd
|
|
326
|
+
|
|
327
|
+
def _fit_transform_like_feature_binarizer(self, X: DataFrame) -> DataFrame:
|
|
328
|
+
# Initialize
|
|
329
|
+
maps = {}
|
|
330
|
+
enc = {}
|
|
331
|
+
thresh = {}
|
|
332
|
+
ordinal = []
|
|
333
|
+
A = DataFrame(index=X.index,
|
|
334
|
+
columns=pd.MultiIndex.from_arrays([[], [], []], names=['feature', 'operation', 'value']))
|
|
335
|
+
|
|
336
|
+
# Iterate over columns
|
|
337
|
+
for c in X:
|
|
338
|
+
# number of unique values
|
|
339
|
+
valUniq = X[c].nunique()
|
|
340
|
+
|
|
341
|
+
# Constant or binary column
|
|
342
|
+
if valUniq <= 2:
|
|
343
|
+
# Mapping to 0, 1
|
|
344
|
+
maps[c] = pd.Series(range(valUniq), index=np.sort(X[c].dropna().unique()))
|
|
345
|
+
A[(str(c), '', '')] = X[c].map(maps[c])
|
|
346
|
+
|
|
347
|
+
# Categorical column
|
|
348
|
+
elif (c in self.colCateg) or _is_text_dtype(X[c].dtype):
|
|
349
|
+
# In the past, OneHotEncoder did not support NaN. Now it does, but it doesn't logically work for this
|
|
350
|
+
# scenario. Check for NaNs and throw the same exception manually.
|
|
351
|
+
if X[c].isna().any():
|
|
352
|
+
raise ValueError('Categorical input contains NaN.')
|
|
353
|
+
# OneHotEncoder object
|
|
354
|
+
enc[c] = OneHotEncoder(sparse_output=False, dtype=int, handle_unknown='ignore')
|
|
355
|
+
# Fit to observed categories
|
|
356
|
+
enc[c].fit(X[[c]])
|
|
357
|
+
# Apply OneHotEncoder
|
|
358
|
+
Anew = enc[c].transform(X[[c]])
|
|
359
|
+
# Original FeatureBinarizer converts all values to str. This class preserves type to be used
|
|
360
|
+
# during transform.
|
|
361
|
+
Anew = DataFrame(Anew, index=X.index, columns=enc[c].categories_[0])
|
|
362
|
+
Anew.columns = pd.MultiIndex.from_product([[str(c)], ['=='], Anew.columns],
|
|
363
|
+
names=['feature', 'operation', 'value'])
|
|
364
|
+
# Concatenate
|
|
365
|
+
A = pd.concat([A, Anew], axis=1)
|
|
366
|
+
|
|
367
|
+
# Ordinal column
|
|
368
|
+
elif _is_ordinal_dtype(X[c].dtype):
|
|
369
|
+
# Unlike FeatureBinarizer, just append the original ordinal column. It will be fit by the
|
|
370
|
+
# DecisionTreeClassifier.
|
|
371
|
+
Anew = DataFrame(
|
|
372
|
+
X[c].to_numpy(), # Required
|
|
373
|
+
columns=pd.MultiIndex.from_arrays([[c], ['<='], [0.0]], names=['feature', 'operation', 'value']),
|
|
374
|
+
index=X.index
|
|
375
|
+
)
|
|
376
|
+
A = pd.concat([A, Anew], axis=1)
|
|
377
|
+
ordinal.append(c)
|
|
378
|
+
|
|
379
|
+
else:
|
|
380
|
+
print(("Skipping column '" + str(c) + "': data type cannot be handled"))
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
self.maps = maps
|
|
384
|
+
self.enc = enc
|
|
385
|
+
self.thresh = thresh
|
|
386
|
+
self.ordinal = ordinal
|
|
387
|
+
|
|
388
|
+
return A
|
|
389
|
+
|
|
390
|
+
def fit(self, X: DataFrame, y: Union[ndarray, DataFrame, Series, list] = None):
|
|
391
|
+
"""Fit transformer. NaN/None values are not permitted for X or y.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
X (DataFrame): Original features
|
|
395
|
+
y (Iterable): Target
|
|
396
|
+
Returns:
|
|
397
|
+
FeatureBinarizerFromTrees: Self
|
|
398
|
+
self.enc (dict): OneHotEncoders for categorical columns
|
|
399
|
+
self.features (MultiIndex): Pandas MultiIndex of feature names, operations, and values
|
|
400
|
+
self.maps (dict): Mappings for unary/binary columns
|
|
401
|
+
self.ordinal (list): Ordinal columns
|
|
402
|
+
self.scaler (StandardScaler): StandardScaler for ordinal columns
|
|
403
|
+
self.thresh (dict(array)): Thresholds for ordinal columns
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
# The decision tree will also throw an exception, but it is cryptic.
|
|
407
|
+
if y is None:
|
|
408
|
+
raise ValueError('The parameter \'y\' is required.')
|
|
409
|
+
# DecisionTreeClassifier accepts NaN since scikit-learn 1.3, so it no longer rejects them for us.
|
|
410
|
+
if X.isna().any().any():
|
|
411
|
+
raise ValueError('Input contains NaN.')
|
|
412
|
+
|
|
413
|
+
# Binarize unary/binary/categorical features according to the FeatureBinarizer style. Ordinal columns
|
|
414
|
+
# are not binarized: i.e., they are included as-is in the data frame with '<=' operations in the index.
|
|
415
|
+
# They will be binarized by extracting splits from the decision tree.
|
|
416
|
+
Xfb = self._fit_transform_like_feature_binarizer(X)
|
|
417
|
+
|
|
418
|
+
# Save resulting MultiIndex separately and reset columns. scikit-learn will not support them in version 1.2
|
|
419
|
+
# during fit().
|
|
420
|
+
XfbMultiIndex = Xfb.columns.copy()
|
|
421
|
+
Xfb.columns = range(0, Xfb.shape[1])
|
|
422
|
+
|
|
423
|
+
# Fit decision trees.
|
|
424
|
+
featuresIdx = np.empty((0,), int)
|
|
425
|
+
thresholds = np.empty((0,), int)
|
|
426
|
+
|
|
427
|
+
randomState = self.randomState
|
|
428
|
+
for i in range(self.treeNum):
|
|
429
|
+
if randomState is not None:
|
|
430
|
+
randomState += i
|
|
431
|
+
tree = DecisionTreeClassifier(random_state=randomState, **self.treeKwargs)
|
|
432
|
+
tree.fit(Xfb, y)
|
|
433
|
+
featuresIdx = np.hstack((featuresIdx, tree.tree_.feature))
|
|
434
|
+
thresholds = np.hstack((thresholds, tree.tree_.threshold))
|
|
435
|
+
|
|
436
|
+
# See `Understanding the decision tree structure`
|
|
437
|
+
# https://scikit-learn.org/stable/auto_examples/tree/plot_unveil_tree_structure.html
|
|
438
|
+
select = featuresIdx != -2 # Indicates leaf nodes
|
|
439
|
+
featuresIdx = featuresIdx[select]
|
|
440
|
+
thresholds = thresholds[select]
|
|
441
|
+
|
|
442
|
+
# Important to do rounding before dropping duplicates because rounding can create duplicates.
|
|
443
|
+
if (self.threshRound is not None) and len(self.ordinal):
|
|
444
|
+
thresholds = thresholds.round(self.threshRound)
|
|
445
|
+
|
|
446
|
+
# Create frame from column index which contains the relevant features
|
|
447
|
+
features: DataFrame = XfbMultiIndex[featuresIdx].to_frame(False)
|
|
448
|
+
|
|
449
|
+
# Set thresholds for ordinal values
|
|
450
|
+
if len(self.ordinal):
|
|
451
|
+
select = (features['operation'] == '<=').to_numpy()
|
|
452
|
+
features.loc[select, 'value'] = thresholds[select]
|
|
453
|
+
|
|
454
|
+
# Drop duplicate features.
|
|
455
|
+
features.drop_duplicates(inplace=True)
|
|
456
|
+
|
|
457
|
+
# Create rule pairs for each feature
|
|
458
|
+
temp: DataFrame = features.copy(True)
|
|
459
|
+
temp['operation'] = temp['operation'].replace({'<=': '>', '==': '!=', '': 'not'})
|
|
460
|
+
features = pd.concat((features, temp))
|
|
461
|
+
|
|
462
|
+
# Create/sort multi-index that will be used during transformation.
|
|
463
|
+
self.features = pd.MultiIndex.from_frame(features)
|
|
464
|
+
self.features = self.features.sortlevel((0, 1, 2))[0]
|
|
465
|
+
|
|
466
|
+
# Update respective self attributes based on the selected features.
|
|
467
|
+
if '==' in self.features.levels[1]:
|
|
468
|
+
names = self.features.get_loc_level('==', 'operation')[1].get_level_values('feature').unique()
|
|
469
|
+
self.enc = {k: self.enc[k] for k in names}
|
|
470
|
+
else:
|
|
471
|
+
self.enc = {}
|
|
472
|
+
|
|
473
|
+
if '' in self.features.levels[1]:
|
|
474
|
+
names = self.features.get_loc_level('', 'operation')[1].get_level_values('feature').unique()
|
|
475
|
+
self.maps = {k: self.maps[k] for k in names}
|
|
476
|
+
else:
|
|
477
|
+
self.maps = {}
|
|
478
|
+
|
|
479
|
+
if '<=' in self.features.levels[1]:
|
|
480
|
+
names = self.features.get_loc_level('<=', 'operation')[1].get_level_values('feature').unique()
|
|
481
|
+
self.thresh = \
|
|
482
|
+
{k: self.features
|
|
483
|
+
.get_loc_level([k, '<='], ['feature', 'operation'])[1]
|
|
484
|
+
.get_level_values('value')
|
|
485
|
+
.to_numpy(dtype=float)
|
|
486
|
+
for k in names}
|
|
487
|
+
self.ordinal = names.to_list()
|
|
488
|
+
if self.returnOrd:
|
|
489
|
+
self.scaler = StandardScaler().fit(X[names])
|
|
490
|
+
else:
|
|
491
|
+
self.thresh = {}
|
|
492
|
+
self.ordinal = []
|
|
493
|
+
self.scaler = None
|
|
494
|
+
|
|
495
|
+
return self
|
|
496
|
+
|
|
497
|
+
def transform(self, X: DataFrame) -> Union[DataFrame, Tuple[DataFrame, DataFrame]]:
|
|
498
|
+
"""Binarize features. Binary features are sorted name-operation-value.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
X (DataFrame): Original features
|
|
502
|
+
Returns:
|
|
503
|
+
A (DataFrame): Binarized features with MultiIndex column labels
|
|
504
|
+
Xstd (DataFrame, optional): Standardized ordinal features
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
result: DataFrame = DataFrame(
|
|
508
|
+
np.zeros((X.shape[0], len(self.features)), dtype=int), # Type consistent with original FeatureBinarizer
|
|
509
|
+
columns=self.features,
|
|
510
|
+
index=X.index
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Using to_numpy() speeds up the overall transform by more than 2x because indices aren't created/aligned.
|
|
514
|
+
# Futhermore, using numpy raises warnings for NaN/None values which is what we want here.
|
|
515
|
+
for feature, test, value in result.columns:
|
|
516
|
+
if test == '<=':
|
|
517
|
+
v = X[feature].to_numpy() <= value
|
|
518
|
+
elif test == '>':
|
|
519
|
+
v = X[feature].to_numpy() > value
|
|
520
|
+
elif test == '==':
|
|
521
|
+
v = X[feature].to_numpy() == value
|
|
522
|
+
elif test == '!=':
|
|
523
|
+
v = X[feature].to_numpy() != value
|
|
524
|
+
elif test == '':
|
|
525
|
+
v = X[feature].to_numpy() == self.maps[feature].index[1]
|
|
526
|
+
elif test == 'not':
|
|
527
|
+
v = X[feature].to_numpy() == self.maps[feature].index[0]
|
|
528
|
+
else:
|
|
529
|
+
raise RuntimeError(f'Test operation \'{test}\' not supported.')
|
|
530
|
+
|
|
531
|
+
# Faster to replace column with numpy because there is no index to align/join
|
|
532
|
+
result[(feature, test, value)] = v.astype(int)
|
|
533
|
+
|
|
534
|
+
if self.threshStr:
|
|
535
|
+
result.columns = result.columns.set_levels(levels=result.columns.levels[2].astype(str), level='value')
|
|
536
|
+
|
|
537
|
+
# This is taken from FeatureBinarizer.
|
|
538
|
+
if self.returnOrd:
|
|
539
|
+
# Standardize ordinal features
|
|
540
|
+
Xstd = self.scaler.transform(X[self.ordinal])
|
|
541
|
+
Xstd = DataFrame(Xstd, index=X.index, columns=self.ordinal)
|
|
542
|
+
return result, Xstd
|
|
543
|
+
else:
|
|
544
|
+
return result
|