PineBioML 1.2.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,393 @@
1
+ from sklearn import metrics
2
+ from sklearn.model_selection import StratifiedKFold
3
+ from sklearn.pipeline import Pipeline
4
+ from pandas import DataFrame, Series, concat
5
+
6
+
7
+ class sklearn_esitimator_wrapper():
8
+ """
9
+ A basic wrapper for sklearn_esitimator. It transfer the data pipeline of sklearn from numpy.array to pandas.DataFrame.
10
+ If you want to pass any model with api in sklearn style into Pine, you should wrap it in wrapper.
11
+ """
12
+
13
+ def __init__(self, kernel: object):
14
+ """
15
+
16
+ Args:
17
+ kernel (object): a sklearn esitimator. for example: sklearn.ensemble.RandomForestClassifier or sklearn.ensemble.RandomForestRegressor
18
+ """
19
+
20
+ self.kernel = kernel
21
+
22
+ def fit(self, x: DataFrame, y: Series, retune=True) -> object:
23
+ """
24
+ sklearn esitimator api: fit
25
+
26
+ Args:
27
+ x (DataFrame): feature
28
+ y (Series): label
29
+ retune (bool, optional): To retune the model or not. For sklearn_esitimator_wrapper, it is just a placeholder without acutual facility. Defaults to True.
30
+
31
+ Returns:
32
+ object: A sklearn_esitimator within pandas data flow.
33
+ """
34
+ self.label_name = y.name
35
+ self.kernel.fit(x, y)
36
+ return self
37
+
38
+ def predict(self, x: DataFrame) -> Series:
39
+ """
40
+ sklearn esitimator api: predict
41
+
42
+ Args:
43
+ x (DataFrame): feature
44
+
45
+ Returns:
46
+ Series: kernel prediction
47
+ """
48
+
49
+ return Series(self.kernel.predict(x),
50
+ index=x.index,
51
+ name=self.label_name)
52
+
53
+ def predict_proba(self, x: DataFrame) -> DataFrame:
54
+ """
55
+ sklearn esitimator api: predict_proba for classification
56
+
57
+ Args:
58
+ x (DataFrame): feature
59
+
60
+ Raises:
61
+ NotImplementedError: Regression has no attribute 'predict_proba'
62
+
63
+ Returns:
64
+ DataFrame: predicted probability with shape (n_sample, n_class)
65
+ """
66
+ if "predict_proba" in dir(self.kernel):
67
+ return DataFrame(self.kernel.predict_proba(x),
68
+ index=x.index,
69
+ columns=self.kernel.classes_)
70
+ else:
71
+ raise NotImplementedError(
72
+ "{} do not have attribute 'predict_proba'.".format(
73
+ self.kernel.__str__()))
74
+
75
+
76
+ class classification_scorer():
77
+ """
78
+ A utility to calculate classification scores.
79
+ The result will contain mcc(matthews corrcoef), acc(accuracy) and support(the number of samples), furthermore:
80
+ if target_label was given(not None), then sensitivity, specificity and coresponding roc-auc score will be added to result.
81
+ if multi_class_extra is True, then one vs rest macro_auc, cross_entropy and cohen_kappa will be added to result.
82
+ """
83
+
84
+ def __init__(self,
85
+ target_label: str = None,
86
+ prefix: str = "",
87
+ multi_class_extra: bool = False):
88
+ """
89
+
90
+ Args:
91
+ target_label (str, optional): the name of target_label. For example, the label in a binary classification task might be {'pos', 'neg'}. Then you can assign 'neg' to target_label, and the result will contain sensitivity, specificity and roc-auc score of label 'neg'. Defaults to None.
92
+ prefix (str, optional): prefix before score names. For example suppose prefix="Train_", then all the scores in result will be like "Train_mcc". Defaults to "".
93
+ multi_class_extra (bool, optional): _description_. Defaults to False.
94
+ """
95
+
96
+ self.prefix = prefix
97
+ self.target_label = target_label
98
+ self.multi_class_extra = multi_class_extra
99
+
100
+ def score(self, y_true: Series,
101
+ y_pred_prob: DataFrame) -> dict[str, float]:
102
+ """
103
+ Scoring y_true and y_pred_prob.
104
+
105
+ Args:
106
+ y_true (Series): The ground True.
107
+ y_pred_prob (DataFrame): The prediction from an estimator. Shape should be (n_sample, n_classes)
108
+
109
+ Returns:
110
+ dict[str, float]: The result stored in a dict, be like {'score_name': score}.
111
+ """
112
+ y_pred = y_pred_prob.idxmax(axis=1)
113
+
114
+ result = {}
115
+ if not self.target_label is None:
116
+ (_, result["sensitivity"], result["f1"],
117
+ _) = metrics.precision_recall_fscore_support(
118
+ y_true=(y_true == self.target_label),
119
+ y_pred=(y_pred == self.target_label),
120
+ average="binary",
121
+ pos_label=True)
122
+
123
+ (_, result["specificity"], _,
124
+ _) = metrics.precision_recall_fscore_support(
125
+ y_true=(y_true == self.target_label),
126
+ y_pred=(y_pred == self.target_label),
127
+ average="binary",
128
+ pos_label=False)
129
+
130
+ # binary
131
+ result["auc"] = metrics.roc_auc_score(
132
+ y_true == self.target_label, y_pred_prob[self.target_label])
133
+
134
+ if self.multi_class_extra:
135
+ result["macro_auc"] = metrics.roc_auc_score(
136
+ y_true,
137
+ y_pred_prob,
138
+ multi_class="ovr",
139
+ labels=y_pred_prob.columns)
140
+ result["macro_f1"] = metrics.f1_score(y_true,
141
+ y_pred,
142
+ average="macro")
143
+ result["cross_entropy"] = metrics.log_loss(y_true, y_pred_prob)
144
+ result["cohen_kappa"] = metrics.cohen_kappa_score(y_true, y_pred)
145
+
146
+ result["mcc"] = metrics.matthews_corrcoef(y_true, y_pred)
147
+ result["accuracy"] = metrics.accuracy_score(y_true, y_pred)
148
+ result["support"] = len(y_true)
149
+
150
+ prefix_result = {}
151
+ for score in result:
152
+ prefix_result[self.prefix + score] = result[score]
153
+
154
+ return prefix_result
155
+
156
+
157
+ class regression_scorer():
158
+ """
159
+ A utility to calculate regression scores. rmse(rooted mean squared error), r2(R squared) and support(the number of samples) are included.
160
+ if y_true and y_pred are all positive, then mape(mean absolute percentage error) will be added.
161
+ """
162
+
163
+ def __init__(self, prefix: str = "", target_label: str = None):
164
+ """
165
+
166
+ Args:
167
+ prefix (str, optional): prefix before score names. For example suppose prefix="Train_", then all the scores in result will be like "Train_mse". Defaults to "".
168
+ target_label (str, optional): A placehold without any facility. Defaults to None.
169
+ """
170
+
171
+ self.prefix = prefix
172
+
173
+ def score(self, y_true: Series, y_pred: Series) -> dict[str, float]:
174
+ """
175
+ calculate the scores
176
+
177
+ Args:
178
+ y_true (Series): Ground true
179
+ y_pred (Series): predicted values
180
+
181
+ Returns:
182
+ dict[str, float]: The result stored in a dict, be like {'score_name': score}.
183
+ """
184
+
185
+ result = {}
186
+ result["rmse"] = metrics.root_mean_squared_error(y_true, y_pred)
187
+ result["r2"] = metrics.r2_score(y_true, y_pred)
188
+ if (y_true > 0).all() and (y_pred > 0).all():
189
+ result["mape"] = metrics.mean_absolute_percentage_error(
190
+ y_true, y_pred)
191
+ result["support"] = len(y_true)
192
+
193
+ prefix_result = {}
194
+ for score in result:
195
+ prefix_result[self.prefix + score] = result[score]
196
+
197
+ return prefix_result
198
+
199
+
200
+ # ToDo: the Integration of .predict and .transform
201
+ class Pine():
202
+ """
203
+ Deep first traversal the given experiment setting.
204
+ the last step of experiment sould be model.
205
+ Please refer to example_Pine.ipynb for usage.
206
+
207
+
208
+ note: experiment step and experiment stage is the same thing.
209
+ """
210
+
211
+ def __init__(self,
212
+ experiment: list[tuple[str, dict[str, object]]],
213
+ target_label: str = None,
214
+ cv_result: bool = False):
215
+ """
216
+ Args:
217
+ experiment (list[tuple[str, dict[str, object]]]): list of experiment steps. step should be in the form: ('step_name', {'method_name': method}). it could be several method in one step and they will fork in deep first traversal. Each method should be either sklearn estimator or transformer.
218
+ target_label (str, optional): the name of target_label. For example, the label in a binary classification task might be {'pos', 'neg'}. Then you can assign 'neg' to target_label, and the result will contain sensitivity, specificity and roc-auc score of label 'neg'. Defaults to None.
219
+ cv_result (bool, optional): Rcording the scores and prediction of cross validation. Defaults to False.
220
+ """
221
+
222
+ self.experiment = experiment
223
+ self.total_stage = len(experiment)
224
+ self.target_label = target_label
225
+ self.cv_result = cv_result
226
+
227
+ self.result = []
228
+
229
+ self.train_pred = []
230
+ self.cv_pred = []
231
+ self.test_pred = []
232
+
233
+ def do_stage(self, train_x: DataFrame, train_y: Series, test_x: DataFrame,
234
+ test_y: Series, stage: int, record: dict) -> None:
235
+ """
236
+ the recursive function to traversal the experiment.
237
+ the socres and path will be stored in self.result amd self.____pred, so there is no return in recursive function.
238
+
239
+ Args:
240
+ train_x (pd.DataFrame): training x
241
+ train_y (pd.Series): training y
242
+ test_x (pd.DataFrame): training x
243
+ test_y (pd.Series): training y
244
+ stage (int): the order of current stage in the experiment setting
245
+ record (dict): record the traversal path in a dict of str
246
+ """
247
+
248
+ # unzip the stage, stage = (stage_name, {operator_name: operator})
249
+ stage_name, operators = self.experiment[stage]
250
+
251
+ # fork to next stage according to the diffirent operator (opt)
252
+ for opt_name in operators:
253
+ record[stage_name] = opt_name
254
+ opt = operators[opt_name]
255
+
256
+ # if not the last stage
257
+ if stage < self.total_stage - 1:
258
+ # transform by operators
259
+ processed_train_x = opt.fit_transform(train_x, train_y)
260
+ if test_x is not None:
261
+ processed_test_x = opt.transform(test_x)
262
+
263
+ # reccursivly call
264
+ self.do_stage(processed_train_x, train_y, processed_test_x,
265
+ test_y, stage + 1, record)
266
+
267
+ # the last layer, it should be models
268
+ else:
269
+ model = opt
270
+ if "predict_proba" in dir(model):
271
+ # is not regression
272
+ f = model.predict_proba
273
+ scorer = classification_scorer
274
+ else:
275
+ # is regression
276
+ f = model.predict
277
+ scorer = regression_scorer
278
+
279
+ # tune/fit the model on training data
280
+ model.fit(train_x, train_y)
281
+
282
+ # compute the training score
283
+ train_pred = f(train_x)
284
+ self.train_pred.append(train_pred)
285
+ train_scores = scorer(prefix="train_",
286
+ target_label=self.target_label).score(
287
+ train_y, train_pred)
288
+
289
+ if test_x is not None:
290
+ # if there is testing data, compute the testing score.
291
+ test_pred = f(test_x)
292
+ self.test_pred.append(test_pred)
293
+ test_scores = scorer(prefix="test_",
294
+ target_label=self.target_label).score(
295
+ test_y, test_pred)
296
+ else:
297
+ test_scores = {}
298
+
299
+ if self.cv_result:
300
+ # compute the cross validation score on training set
301
+ fold_scores = []
302
+ cv_pred = []
303
+ cross_validation = StratifiedKFold(n_splits=5,
304
+ shuffle=True,
305
+ random_state=133)
306
+ for (train_idx, valid_idx) in cross_validation.split(
307
+ train_x, train_y):
308
+
309
+ # fit on training fold
310
+ model.fit(train_x.iloc[train_idx],
311
+ train_y.iloc[train_idx],
312
+ retune=False)
313
+
314
+ # score on testing fold
315
+ fold_pred = f(train_x.iloc[valid_idx])
316
+ cv_pred.append(fold_pred)
317
+ fold_scores.append(
318
+ scorer(prefix="cv_",
319
+ target_label=self.target_label).score(
320
+ train_y.iloc[valid_idx], fold_pred))
321
+ # average the fold scores
322
+ """
323
+ valid_scores = {}
324
+ for metric_name in fold_scores[0].keys():
325
+ valid_scores[metric_name] = np.array([
326
+ fold_scores[cv][metric_name] for cv in range(5)
327
+ ]).mean()
328
+ """
329
+ self.cv_pred.append(concat(cv_pred, axis=0))
330
+ valid_scores = DataFrame(fold_scores).mean().to_dict()
331
+ else:
332
+ valid_scores = {}
333
+
334
+ # concatenate the score dicts
335
+ all_scores = dict(**record, **train_scores, **valid_scores,
336
+ **test_scores)
337
+ self.result.append(all_scores)
338
+
339
+ def do_experiment(self, train_x, train_y, test_x=None, test_y=None):
340
+ """
341
+ the first call of recurssive fuction.
342
+
343
+ Args:
344
+ train_x (pd.DataFrame): training x
345
+ train_y (pd.Series): training y
346
+ test_x (pd.DataFrame): training x
347
+ test_y (pd.Series): training y
348
+
349
+ Returns:
350
+ pd.DataFrame: the result
351
+ """
352
+ # clear the results.
353
+ self.result = []
354
+
355
+ self.do_stage(train_x, train_y, test_x, test_y, 0, {})
356
+ return self.experiment_results()
357
+
358
+ def experiment_results(self) -> DataFrame:
359
+ """
360
+
361
+ Returns:
362
+ DataFrame: The experiment results.
363
+ """
364
+ return DataFrame(self.result)
365
+
366
+ def experiment_predictions(self):
367
+ """
368
+ cv_pred will be empty if cv_result was False in initialization.
369
+
370
+ Returns:
371
+ train_pred, cv_pred, test_pred: the prediction of training set, cross validation and testing set
372
+ """
373
+ return self.train_pred, self.cv_pred, self.test_pred
374
+
375
+ def recall_model(self, id):
376
+ """
377
+ query the last experiment result by id and build the pipeline object.
378
+
379
+ Todo: A proper way to fit the pipeline object.
380
+
381
+ Args:
382
+ id (int): the order of experiment path.
383
+
384
+ Returns:
385
+ sklearn.pipeline.Pipeline: ready to use object.
386
+ """
387
+
388
+ model_spec = self.result[id]
389
+ model_pipeline = []
390
+ for (step_name, methods) in self.experiment:
391
+ using_method = model_spec[step_name]
392
+ model_pipeline.append((step_name, methods[using_method]))
393
+ return Pipeline(model_pipeline)
@@ -0,0 +1,168 @@
1
+ import sklearn.preprocessing as skprpr
2
+ from pandas import DataFrame
3
+
4
+
5
+ class Normalizer():
6
+ """
7
+ A wrapper of sklearn normalizers. This will conserve pandas features.
8
+ method be one of ["StandardScaler", "RobustScaler", "MinMaxScaler", "Normalizer", "PowerTransformer"]
9
+ let x with shape [n, d] where n is the sample size, d is the number of features.
10
+ StandardScaler(x) = (x - x.mean(n)) / x.std(n)
11
+ RobustScaler(x) = (x - x.median(n)) / (x.quartile3(n) - x.quartile1(n))
12
+ MinMaxScaler(x) = (x - x.min(n)) / (x.max(n) - x.min(n))
13
+ Normalizer(x) = x / x.norm(d)
14
+ PowerTransformer(x) = yeo-johnson transform(x)
15
+ """
16
+
17
+ def __init__(self, center=True, scale=True, method="RobustScaler"):
18
+ """
19
+ Args:
20
+ center (bool, optional): Whether to centralize data. Default to True.
21
+ scale (bool, optional): Whether to scaling data after centralized. Default to True.
22
+ method (str, optional): the way to normalize data. Be one of ["StandardScaler", "RobustScaler", "MinMaxScaler", "Normalizer", "PowerTransformer"]
23
+ """
24
+
25
+ kernels = {
26
+ "StandardScaler":
27
+ skprpr.StandardScaler(with_mean=center, with_std=scale),
28
+ "RobustScaler":
29
+ skprpr.RobustScaler(with_centering=center, with_scaling=scale),
30
+ "MinMaxScaler":
31
+ skprpr.MinMaxScaler(),
32
+ "Normalizer":
33
+ skprpr.Normalizer(),
34
+ "PowerTransformer":
35
+ skprpr.PowerTransformer()
36
+ }
37
+ self.kernel = kernels[method]
38
+
39
+ def fit(self, x, y=None):
40
+ """
41
+ Computing and Recording the mean and std of X for prediction.
42
+
43
+ Args:
44
+ x (pandas.DataFrame or a 2D array): The data to normalize.
45
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
46
+
47
+ Returns:
48
+ Normalizer: self after fitting.
49
+ """
50
+
51
+ self.kernel.fit(x, y)
52
+
53
+ return self
54
+
55
+ def transform(self, x):
56
+ """
57
+ Transform input x. Only activates after "fit" was called.
58
+
59
+ Args:
60
+ x (pandas.DataFrame or a 2D array): The data to normalize.
61
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
62
+
63
+ Returns:
64
+ pandas.DataFrame or a 2D array: Normalized x.
65
+ pandas.Series or a 1D array: Same as input y.
66
+ """
67
+ x_normalized = DataFrame(self.kernel.transform(x),
68
+ index=x.index,
69
+ columns=x.columns)
70
+
71
+ return x_normalized
72
+
73
+ def fit_transform(self, x, y=None):
74
+ """
75
+ A functional stack of "fit" and "transform".
76
+
77
+ Args:
78
+ x (pandas.DataFrame or a 2D array): The data to normalize.
79
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
80
+
81
+ Returns:
82
+ pandas.DataFrame or a 2D array: Normalized x.
83
+ """
84
+ self.fit(x, y)
85
+ x_normalized = self.transform(x)
86
+ return x_normalized
87
+
88
+ def inverse_transform(self, x):
89
+ """
90
+ The inverse transform of normalize.
91
+
92
+ Args:
93
+ x (pandas.DataFrame or a 2D array): The data to revert original scale.
94
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
95
+
96
+ Returns:
97
+ pandas.DataFrame or a 2D array: x in original scale.
98
+ """
99
+
100
+ x_original = DataFrame(self.kernel.inverse_transform(x),
101
+ index=x.index,
102
+ columns=x.columns)
103
+
104
+ return x_original
105
+
106
+
107
+ class Pass():
108
+ """
109
+ Do nothing.
110
+ """
111
+
112
+ def __init__(self):
113
+ pass
114
+
115
+ def fit(self, x, y=None):
116
+ """
117
+ Do nothing.
118
+
119
+ Args:
120
+ x (pandas.DataFrame or a 2D array): The data to normalize.
121
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
122
+
123
+ Returns:
124
+ Pass: No, do nothing.
125
+ """
126
+
127
+ return self
128
+
129
+ def transform(self, x):
130
+ """
131
+ Do nothing.
132
+
133
+ Args:
134
+ x (pandas.DataFrame or a 2D array): The data to normalize.
135
+
136
+ Returns:
137
+ pandas.DataFrame or a 2D array: Normalized x.
138
+ """
139
+
140
+ return x
141
+
142
+ def fit_transform(self, x, y=None):
143
+ """
144
+ Do nothing.
145
+
146
+ Args:
147
+ x (pandas.DataFrame or a 2D array): The data to normalize.
148
+ y (pandas.Series or a 1D array): A placeholder only. Normalizer do nothing to y.
149
+
150
+ Returns:
151
+ pandas.DataFrame or a 2D array: x.
152
+ """
153
+ self.fit(x, y)
154
+ x_normalized = self.transform(x)
155
+ return x_normalized
156
+
157
+ def inverse_transform(self, x):
158
+ """
159
+ Do nothing.
160
+
161
+ Args:
162
+ x (pandas.DataFrame or a 2D array): The data to revert original scale.
163
+
164
+ Returns:
165
+ pandas.DataFrame or a 2D array: x.
166
+ """
167
+
168
+ return x