SKSurrogate 0.1.0__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.
@@ -0,0 +1,409 @@
1
+ """
2
+ DataProcess: A DataFrame preprocessing module for ML
3
+ ===========================================================
4
+
5
+ The module is designed to do some preprocessing task on a given DataFrame.
6
+ Currently, the module detects types of columns data e.g., numerical (integer, float), categorical, boolean, ordinal,
7
+ datetime, and labels. To impose a text column, the type of the column should be declared before passing the DataFrame.
8
+ This can be done by a simple code such as `df.astype({'txt_column':pandas.StringDtype()})`
9
+
10
+ The module performs ordinal encoding, one hot encoding, date range to float transformation, label encoding, and
11
+ missing data imputation.
12
+
13
+ To Do
14
+ ----------
15
+ Processing of Image data.
16
+
17
+ Code
18
+ ----------
19
+ """
20
+ import numpy as np
21
+
22
+ import numpy
23
+ import pandas
24
+ import pandas as pd
25
+ from copy import copy
26
+ import re
27
+ from sklearn.base import BaseEstimator, TransformerMixin
28
+ from sklearn.pipeline import Pipeline
29
+ from sklearn.experimental import enable_iterative_imputer
30
+ from sklearn.impute import IterativeImputer
31
+ from sklearn.feature_extraction.text import TfidfVectorizer
32
+
33
+
34
+ class DateTime2Num(TransformerMixin, BaseEstimator):
35
+ """
36
+ Converts a date range data to numeric (float) between `lower` and `upper`.
37
+
38
+ :param cols: default: `None`; column names for transformation.
39
+ :param unit: default: 'day`; the time unit.
40
+ :param lower: default: `0.0`; the lower bound of the transformation range.
41
+ :param upper: default: `2.0`; the upper bound of the transformation range.
42
+ """
43
+
44
+ def __init__(self, cols=None, unit='day', lower=0., upper=2.):
45
+ if unit not in ['day', 'hour', 'minute', 'second']:
46
+ raise ValueError("`unit` must be either 'day', 'hour', 'minute', or 'second'.")
47
+ self.cols = cols
48
+ self.unit = unit
49
+ self.lower = lower
50
+ self.upper = upper
51
+ self.coefs = dict()
52
+ self.scale = 1.
53
+ self.scale = numpy.timedelta64(1, 's')
54
+ if self.unit == 'minute':
55
+ self.scale = 60.
56
+ self.scale = numpy.timedelta64(1, 'm')
57
+ elif self.unit == 'hour':
58
+ self.scale = 60 * 60.
59
+ self.scale = numpy.timedelta64(1, 'h')
60
+ elif self.unit == 'day':
61
+ self.scale = 60 * 60 * 24.
62
+ self.scale = numpy.timedelta64(1, 'D')
63
+
64
+ def fit(self, X):
65
+ """
66
+ Fits and calculates the transformation parameters for date range transformation.
67
+
68
+ :param X: Input data
69
+ :return: `self`
70
+ """
71
+ if self.cols is None:
72
+ return self
73
+ n_length = self.upper - self.lower
74
+ for clmn in self.cols:
75
+ t_vec = X[clmn].dropna().values
76
+ min_date, max_date = t_vec.min(), t_vec.max()
77
+ # t_length = (max_date - min_date).seconds / self.scale
78
+ t_length = (max_date - min_date) / self.scale
79
+ trans_coef = n_length / t_length
80
+ intercept = self.lower
81
+ self.coefs[clmn] = (trans_coef, intercept, min_date)
82
+ return self
83
+
84
+ def transform(self, X):
85
+ """
86
+ Apply the calculated parameters to transform the datetime columns.
87
+
88
+ :param X: the input data
89
+ :return: transformed data
90
+ """
91
+ if self.cols is None:
92
+ return X
93
+ TX = X
94
+ for clmn in self.cols:
95
+ vect = numpy.array(
96
+ [self.coefs[clmn][1] + self.coefs[clmn][0] * (_ - self.coefs[clmn][2]) / self.scale for _ in
97
+ TX[clmn].values])
98
+ TX[clmn] = vect
99
+ return TX
100
+
101
+ def fit_transform(self, X, y=None, **fit_params):
102
+ """
103
+ Fits and transforms the input data.
104
+
105
+ :param X: input data;
106
+ :param y: target field; not required, but included to comply with scikit-learn's signature
107
+ :param fit_params: included to comply with scikit-learn's signature
108
+ :return: transformed DataFrame
109
+ """
110
+ self.fit(X)
111
+ return self.transform(X)
112
+
113
+
114
+ class DataPreprocess(object):
115
+ """
116
+ This class performs a few reprocessing task on the input DataFrame to provide a ML ready dataset.
117
+
118
+ :param df: the input raw DataFrame
119
+ :param bin_threshold: float between 0. and 1.; when the proportion of unique values to the total number of entities
120
+ falls below this number fo two valued columns, the type is deduced to be binary
121
+ :param cat_threshold: when the proportion of unique values to the total number of entities
122
+ falls below this number fo discrete columns, the type is deduced to be categorical or ordinal
123
+ :param force_threshold: boolean, indicate whether to force the deduction based on the provided thresholds or not
124
+ :param imputer: the imputer object; default `IterativeImputer` for missing values imputation
125
+ :param force_impute: boolean; default `True` to indicate the desire for missing value imputation
126
+ """
127
+
128
+ def __init__(self, df, bin_threshold=.1, cat_threshold=.1, force_threshold=False, imputer=None, force_impute=True,
129
+ text_processor=None):
130
+ self.df = copy(df)
131
+ self.bin_threshold = bin_threshold
132
+ self.cat_threshold = cat_threshold
133
+ self.force_threshold = force_threshold
134
+ self.columns = list(df.columns)
135
+ self.values = dict()
136
+ self.uniques = dict()
137
+ self.types = dict()
138
+ self.transforms = dict()
139
+ self.mapping = dict()
140
+ self.deduced_types = dict(float64=[], int64=[], datetime64=[], other=[], text=[],
141
+ binary=[], categorical=[], label=[], obsolete=[])
142
+ self.pivot_types = dict()
143
+ self.processed_frame = dict()
144
+ self.transformed_df = None
145
+ self.steps = []
146
+ for clmn in self.columns:
147
+ self.values[clmn] = self.df[clmn].dropna().values
148
+ self.uniques[clmn] = self.df[clmn].dropna().unique()
149
+ self.types[clmn] = self.df[clmn].dtypes.name
150
+ self.types_deduced = False
151
+ self.encoded = False
152
+ self.force_impute = force_impute
153
+ if imputer is None:
154
+ self.imputer = IterativeImputer()
155
+ else:
156
+ self.imputer = imputer
157
+ if text_processor is None:
158
+ self.txt_prcsr = TfidfVectorizer(sublinear_tf=True, max_features=1600, min_df=5, norm='l2',
159
+ ngram_range=(1, 3), stop_words='english')
160
+ else:
161
+ self.txt_prcsr = text_processor
162
+
163
+ @staticmethod
164
+ def numeric(val, typ='float64'):
165
+ trans_val = re.sub(r"[^\d\.]", "", str(val))
166
+ if typ == 'float64':
167
+ return float(trans_val)
168
+ elif typ == 'int64':
169
+ return int(trans_val)
170
+
171
+ def is_float(self, clmn):
172
+ """
173
+ Determines if the type of the data for the column `clmn` is float or not
174
+
175
+ :param clmn: the name of the column
176
+ :return: boolean
177
+ """
178
+ if self.types[clmn] == 'float64':
179
+ return True
180
+ return False
181
+
182
+ def is_int(self, clmn):
183
+ """
184
+ Determines if the type of the data for the column `clmn` is integer or not
185
+
186
+ :param clmn: the name of the column
187
+ :return: boolean
188
+ """
189
+ if self.types[clmn] == 'int64':
190
+ return True
191
+ return False
192
+
193
+ def is_object(self, clmn):
194
+ """
195
+ Determines if the type of the data for the column `clmn` is object (e.g., string, blob, etc.) or not
196
+
197
+ :param clmn: the name of the column
198
+ :return: boolean
199
+ """
200
+ if self.types[clmn] == 'object':
201
+ return True
202
+ return False
203
+
204
+ def is_datetime(self, clmn):
205
+ """
206
+ Determines if the type of the data for the column `clmn` is datetime or not
207
+
208
+ :param clmn: the name of the column
209
+ :return: boolean
210
+ """
211
+ if 'datetime64' in self.types[clmn]:
212
+ return True
213
+ return False
214
+
215
+ def is_text(self, clmn):
216
+ """
217
+ Determine if the type of the data for the column `clmn` is string/text or not. A string column is of type
218
+ `pandas.StringDtype()`.
219
+
220
+ :param clmn: the name of the column
221
+ :return: boolean
222
+ """
223
+ if 'string' in self.types[clmn]:
224
+ self.df[clmn].fillna("", inplace=True)
225
+ return True
226
+ return False
227
+
228
+ def is_obsolete(self, clmn):
229
+ """
230
+ Determines if the column is worth to be included in the analysis or not
231
+
232
+ :param clmn: the name of the column
233
+ :return: boolean; `True` if includes more than 1 value, `False` otherwise
234
+ """
235
+ if len(self.uniques[clmn]) < 2:
236
+ return True
237
+ return False
238
+
239
+ def is_bin(self, clmn):
240
+ """
241
+ Determines if the type of the data for the column `clmn` is binary or not
242
+
243
+ :param clmn: the name of the column
244
+ :return: boolean
245
+ """
246
+ ratio = len(self.uniques[clmn]) / len(self.values[clmn])
247
+ if self.force_threshold and (len(self.uniques[clmn]) == 2) and (ratio <= self.bin_threshold):
248
+ return True
249
+ if not self.force_threshold and (len(self.uniques[clmn]) == 2):
250
+ return True
251
+ return False
252
+
253
+ def is_categorical(self, clmn):
254
+ """
255
+ Determines if the type of the data for the column `clmn` is categorical or not
256
+
257
+ :param clmn: the name of the column
258
+ :return: boolean
259
+ """
260
+ ratio = len(self.uniques[clmn]) / len(self.values[clmn])
261
+ if self.force_threshold and (ratio <= self.cat_threshold):
262
+ return True
263
+ elif (ratio <= self.cat_threshold) and (self.types[clmn] == 'object'):
264
+ return True
265
+ else:
266
+ return False
267
+
268
+ def is_label(self, clmn):
269
+ """
270
+ Determines if the type of the data for the column `clmn` is label or not
271
+
272
+ :param clmn: the name of the column
273
+ :return: boolean
274
+ """
275
+ if (self.types[clmn] == 'object') and not self.is_categorical(clmn):
276
+ return True
277
+ return False
278
+
279
+ def set_type(self, clmn, typ):
280
+ if typ not in self.deduced_types:
281
+ raise "Unknown type."
282
+ self.deduced_types[typ].append(clmn)
283
+ self.pivot_types[clmn] = typ
284
+
285
+ def deduce_types(self):
286
+ """
287
+ Deduces the type of all columns.
288
+
289
+ :return: `None`
290
+ """
291
+ for clmn in self.columns:
292
+ if clmn in self.pivot_types:
293
+ continue
294
+ if self.is_label(clmn):
295
+ self.deduced_types['label'].append(clmn)
296
+ self.pivot_types[clmn] = 'label'
297
+ elif self.is_text(clmn):
298
+ self.deduced_types['text'].append(clmn)
299
+ self.pivot_types[clmn] = 'text'
300
+ elif self.is_bin(clmn):
301
+ self.deduced_types['binary'].append(clmn)
302
+ self.pivot_types[clmn] = 'binary'
303
+ elif self.is_categorical(clmn):
304
+ self.deduced_types['categorical'].append(clmn)
305
+ self.pivot_types[clmn] = 'categorical'
306
+ elif self.is_datetime(clmn):
307
+ self.deduced_types['datetime64'].append(clmn)
308
+ self.pivot_types[clmn] = 'datetime64'
309
+ elif self.is_object(clmn):
310
+ self.deduced_types['other'].append(clmn)
311
+ self.pivot_types[clmn] = 'other'
312
+ elif self.is_int(clmn):
313
+ self.deduced_types['int64'].append(clmn)
314
+ self.pivot_types[clmn] = 'int64'
315
+ elif self.is_float(clmn):
316
+ self.deduced_types['float64'].append(clmn)
317
+ self.pivot_types[clmn] = 'float64'
318
+ elif self.is_obsolete(clmn):
319
+ self.deduced_types['obsolete'].append(clmn)
320
+ self.pivot_types[clmn] = 'obsolete'
321
+ self.types_deduced = True
322
+
323
+ def transform_label_bin(self, clmn):
324
+ """
325
+ Transforms the determined label and binary columns.
326
+
327
+ :param clmn: the column's name
328
+ :return: `None`
329
+ """
330
+ lbls = numpy.sort(self.uniques[clmn])
331
+ self.transforms[clmn] = {_: lbls[_] for _ in range(lbls.shape[0])}
332
+ self.mapping[clmn] = {lbls[_]: _ for _ in range(lbls.shape[0])}
333
+
334
+ def encode(self):
335
+ """
336
+ Transforms the whole input DataFrame based on the deduced types and does missing data imputation if desired.
337
+ The resulted DataFrame will be stored in `transformed_df`
338
+
339
+ :return: Transformed and encoded dataframe
340
+ """
341
+ if not self.types_deduced:
342
+ self.deduce_types()
343
+ self.steps = []
344
+ txt_clmn_df = None
345
+ org_df = copy(self.df)
346
+ for clmn in self.deduced_types['int64']:
347
+ values = org_df[clmn].astype(str).str.replace(r"[^\d\.]", "", regex=True)
348
+ org_df[clmn] = pandas.to_numeric(values, errors='coerce')
349
+ for clmn in self.deduced_types['float64']:
350
+ values = org_df[clmn].astype(str).str.replace(r"[^\d\.]", "", regex=True)
351
+ org_df[clmn] = pandas.to_numeric(values, errors='coerce')
352
+ if self.deduced_types['text']:
353
+ txt_clmn_df = self.df[self.deduced_types['text']]
354
+ org_df.drop(self.deduced_types['text'], axis=1, inplace=True)
355
+ ordinal_columns = self.deduced_types['binary'] + self.deduced_types['label']
356
+ for clmn in ordinal_columns:
357
+ self.transform_label_bin(clmn)
358
+
359
+ working_df = org_df.copy()
360
+ for clmn in ordinal_columns:
361
+ working_df[clmn] = working_df[clmn].map(self.mapping[clmn]).astype(float)
362
+
363
+ for clmn in self.deduced_types['categorical']:
364
+ dummy = pandas.get_dummies(
365
+ working_df[clmn].astype(object).fillna("__MISSING__"),
366
+ prefix=str(clmn),
367
+ prefix_sep="_",
368
+ dummy_na=False,
369
+ )
370
+ working_df = pandas.concat([working_df.drop(columns=[clmn]), dummy], axis=1)
371
+
372
+ for clmn in self.deduced_types['datetime64']:
373
+ working_df[clmn] = pandas.to_datetime(working_df[clmn], errors='coerce')
374
+ working_df[clmn] = (
375
+ (working_df[clmn] - pandas.Timestamp("1970-01-01"))
376
+ .dt.total_seconds()
377
+ .astype(float)
378
+ )
379
+
380
+ dtn = DateTime2Num(cols=self.deduced_types['datetime64'])
381
+ self.steps.append(('Date2Num', dtn))
382
+ self.steps.append(('Impute', self.imputer))
383
+ trans = Pipeline(self.steps)
384
+
385
+ # Keep the numeric matrix consistent with the original API while avoiding the deprecated category_encoders path.
386
+ encoded_frame = pandas.DataFrame(
387
+ trans.fit_transform(working_df),
388
+ columns=working_df.columns,
389
+ )
390
+ self.transformed_df = encoded_frame.copy()
391
+
392
+ rounded_columns = set(ordinal_columns).union(self.deduced_types['categorical'])
393
+ for clmn in self.transformed_df.columns.intersection(rounded_columns):
394
+ rounded = pandas.to_numeric(self.transformed_df[clmn], errors='coerce').round(0)
395
+ if self.force_impute:
396
+ self.transformed_df[clmn] = rounded.astype(int)
397
+ else:
398
+ self.transformed_df[clmn] = rounded.astype(float)
399
+
400
+ for clmn in self.deduced_types['text']:
401
+ txt_array = txt_clmn_df[clmn].values
402
+ processed_txt = self.txt_prcsr.fit_transform(txt_array)
403
+ length = processed_txt.shape[1]
404
+ clmn_names = ["%s_%d" % (clmn, _) for _ in range(length)]
405
+ self.transformed_df = pandas.concat(
406
+ [self.transformed_df, pandas.DataFrame(processed_txt.toarray(), columns=clmn_names)], axis=1)
407
+
408
+ self.encoded = True
409
+ return self.transformed_df