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