cross_validate_with_fine_tuning.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Thu Oct 29 13:58:23 2020
  5. @author: tanya
  6. @description:
  7. * Input:
  8. - pipeline/hyperparameter space
  9. - data_train
  10. - cv
  11. - cv_folds
  12. * For each pipeline:
  13. -> Split data_train into folds according to cv
  14. -> For each fold:
  15. => get data_train_fold, data_test_fold, cv_fold
  16. => split data_train_fold into subfolds according to cv_fold
  17. => For each subfold:
  18. ==> get data_train_subfold, data_test_subfold
  19. ==> train pipeline on data_train_subfold
  20. ==> find best_threshold_subfold on data_test_subfold
  21. => Find averaged_threshold_fold averaged over best_threshold_subfold
  22. => train pipeline on data_train_fold
  23. => find score_fold on data_test_fold with proba_threshold_fold
  24. => find best_threshold_fold on data_test_fold
  25. -> find score averaged over score_fold
  26. -> find averaged_threshold averaged over best_threshold_fold
  27. * choose (pipeline/hyperparameters, threshold) in the space with best score
  28. """
  29. import sys
  30. import pandas as pd
  31. import numpy as np
  32. from itertools import zip_longest
  33. if sys.version_info >= (3, 8):
  34. from typing import Callable, Dict, Iterable, Union
  35. else:
  36. from typing_extensions import Callable, Dict, Iterable, Union
  37. from copy import deepcopy
  38. from sklearn.model_selection import StratifiedKFold
  39. from cdplib.log import Log
  40. from cdplib.ml_validation.CVComposer import CVComposer
  41. # TODO: write with yield !!!!
  42. def get_optimal_proba_threshold(score_func: Callable,
  43. y_true: Union[pd.Series, np.ndarray],
  44. proba: Union[pd.Series, np.ndarray],
  45. threshold_set: Union[Iterable, None] = None):
  46. """
  47. """
  48. scores = {}
  49. if threshold_set is None:
  50. threshold_set = np.arange(0, 1, 0.1)
  51. for threshold in threshold_set:
  52. y_pred = (proba >= threshold).astype(int)
  53. scores[threshold] = score_func(y_true, y_pred)
  54. return max(scores, key=scores.get)
  55. def cross_validate_with_optimal_threshold(
  56. score_func_threshold: Callable,
  57. estimator: object,
  58. X: Union[pd.DataFrame, np.ndarray],
  59. y: Union[pd.Series, np.ndarray, None] = None,
  60. scoring: Union[Callable, Dict] = None,
  61. cv: Union[Iterable, int, None] = None,
  62. X_val: Union[pd.DataFrame, np.ndarray, None] = None,
  63. y_val: Union[pd.Series, np.ndarray, None] = None,
  64. X_val_threshold: Union[pd.DataFrame, np.ndarray, None] = None,
  65. y_val_threshold: Union[pd.Series, np.ndarray, None] = None,
  66. cv_threshold: Union[Iterable, int, None] = None,
  67. threshold_set: Union[Iterable, None] = None,
  68. scores: Dict = None)-> Dict:
  69. """
  70. """
  71. logger = Log("cross_validate_with_optimal_threshold:")
  72. X_train = deepcopy(X)
  73. y_train = deepcopy(y)
  74. X_val = deepcopy(X_val)
  75. y_val = deepcopy(y_val)
  76. X_val_threshold = deepcopy(X_val_threshold)
  77. y_val_threshold = deepcopy(y_val_threshold)
  78. scores = scores or {"test_threshold": [],
  79. "test_score_threshold": [],
  80. "train_score_threshold": []}
  81. scoring = scoring or {}
  82. for metric_name, metric in scoring.items():
  83. if "test_" + metric_name not in scores:
  84. scores["test_" + metric_name] = []
  85. scores["train_" + metric_name] = []
  86. if cv is None:
  87. # test score is calculated on X_vals
  88. assert((X_val is not None) and (y_val is not None)),\
  89. "Validation set must be set"
  90. if cv_threshold is None:
  91. refit = (X_val_threshold is not None)
  92. # if a validation set for proba threshold tuning is not given,
  93. # we use the validation set on which we calculate the test score
  94. # (this might lead to overfitting)
  95. X_val_threshold = X_val_threshold if refit else deepcopy(X_val)
  96. y_val_threshold = y_val_threshold if refit else deepcopy(y_val)
  97. cv_threshold, X_train, y_train =\
  98. CVComposer().dummy_cv_and_concatenated_data_set(
  99. X_train=X_train,
  100. X_test=X_val_threshold,
  101. y_train=y_train,
  102. y_test=y_val_threshold)
  103. else:
  104. # if cv_threshold is given, we find the optimal threshold
  105. # on each fold and output the average value for the threshold
  106. if (X_val_threshold is not None):
  107. logger.log_and_throw_warning((
  108. "X_val_threshold is set "
  109. "but cv_threshold will be used"))
  110. if isinstance(cv_threshold, int):
  111. cv_threshold = StratifiedKFold(n_splits=cv_threshold)\
  112. .split(X=X_train, y=y_train)
  113. refit = True
  114. thresholds = []
  115. for train_inds, val_inds in cv_threshold:
  116. X_train_fold, X_val_fold, y_train_fold, y_val_fold =\
  117. CVComposer().cv_slice_dataset(
  118. X=X_train,
  119. y=y_train,
  120. train_inds=train_inds,
  121. test_inds=val_inds)
  122. estimator.fit(X_train_fold, y_train_fold)
  123. proba_val = estimator.predict_proba(X_val_fold)[:, 1]
  124. threshold = get_optimal_proba_threshold(
  125. score_func=score_func_threshold,
  126. y_true=y_val_fold,
  127. proba=proba_val)
  128. thresholds.append(threshold)
  129. scores["test_threshold"].append(np.mean(thresholds))
  130. if refit:
  131. estimator.fit(X_train, y_train)
  132. proba_val = estimator.predict_proba(X_val)[:, 1]
  133. proba_train = estimator.predict_proba(X_train)[:, 1]
  134. pred_train = (proba_train >= threshold)
  135. pred_val = (proba_val >= threshold)
  136. train_score = score_func_threshold(y_train, pred_train)
  137. test_score = score_func_threshold(y_val, pred_val)
  138. for metric_name, metric in scoring.items():
  139. scores["train_" + metric_name].append(metric(y_train, pred_train))
  140. scores["test_" + metric_name].append(metric(y_val, pred_val))
  141. scores["train_score_threshold"].append(train_score)
  142. scores["test_score_threshold"].append(test_score)
  143. return scores
  144. else:
  145. if isinstance(cv, int):
  146. cv = StratifiedKFold(n_splits=cv).split(X=X_train, y=y_train)
  147. cv_threshold = cv_threshold or []
  148. for (train_inds, val_inds), cv_fold in zip_longest(cv, cv_threshold):
  149. X_train_fold, X_val_fold, y_train_fold, y_val_fold =\
  150. CVComposer().cv_slice_dataset(
  151. X=X_train,
  152. y=y_train,
  153. train_inds=train_inds,
  154. test_inds=val_inds)
  155. scores = cross_validate_with_optimal_threshold(
  156. estimator=estimator,
  157. score_func_threshold=score_func_threshold,
  158. X=X_train_fold,
  159. y=y_train_fold,
  160. X_val=X_val_fold,
  161. y_val=y_val_fold,
  162. cv_threshold=cv_fold,
  163. scoring=scoring,
  164. threshold_set=threshold_set,
  165. scores=scores)
  166. return scores
  167. if __name__ == "__main__":
  168. from sklearn.metrics import accuracy_score, precision_score
  169. from sklearn.datasets import load_breast_cancer
  170. from xgboost import XGBRFClassifier
  171. from sklearn.model_selection import train_test_split
  172. data_loader = load_breast_cancer()
  173. X = data_loader["data"]
  174. y = data_loader["target"]
  175. X_train, X_val, y_train, y_val = train_test_split(X, y)
  176. estimator = XGBRFClassifier(use_label_encoder=False,
  177. eval_metric="logloss")
  178. score_func = accuracy_score
  179. scoring = {"precision": precision_score}
  180. averaged_scores = []
  181. averaged_thresholds = []
  182. print("\nTesting cv=None, cv_threshold=None, X_val_threshold=None\n")
  183. scores = cross_validate_with_optimal_threshold(
  184. score_func_threshold=accuracy_score,
  185. estimator=estimator,
  186. X=X_train,
  187. y=y_train,
  188. scoring=scoring,
  189. cv=None,
  190. X_val=X_val,
  191. y_val=y_val,
  192. X_val_threshold=None,
  193. y_val_threshold=None,
  194. cv_threshold=None)
  195. print("\nScores:", scores)
  196. averaged_scores.append(np.mean(scores["test_score_threshold"]))
  197. averaged_thresholds.append(np.mean(scores["test_threshold"]))
  198. print("\n ########################################################## \n")
  199. X_train, X_val_threshold, y_train, y_val_threshold =\
  200. train_test_split(X_train, y_train)
  201. print("\nTesting cv=None, cv_threshold=None, X_val_threshold\n")
  202. scores = cross_validate_with_optimal_threshold(
  203. score_func_threshold=accuracy_score,
  204. estimator=estimator,
  205. X=X_train,
  206. y=y_train,
  207. scoring=scoring,
  208. cv=None,
  209. X_val=X_val,
  210. y_val=y_val,
  211. X_val_threshold=X_val_threshold,
  212. y_val_threshold=y_val_threshold,
  213. cv_threshold=None)
  214. print("\nScores:", scores)
  215. averaged_scores.append(np.mean(scores["test_score_threshold"]))
  216. averaged_thresholds.append(np.mean(scores["test_threshold"]))
  217. print("\n ########################################################## \n")
  218. print("\nTesting cv=None, cv_threshold=3 \n")
  219. scores = cross_validate_with_optimal_threshold(
  220. score_func_threshold=accuracy_score,
  221. estimator=estimator,
  222. X=X_train,
  223. y=y_train,
  224. scoring=scoring,
  225. cv=None,
  226. X_val=X_val,
  227. y_val=y_val,
  228. X_val_threshold=X_val_threshold,
  229. y_val_threshold=y_val_threshold,
  230. cv_threshold=3)
  231. print("\nScores:", scores)
  232. averaged_scores.append(np.mean(scores["test_score_threshold"]))
  233. averaged_thresholds.append(np.mean(scores["test_threshold"]))
  234. print("\n ########################################################## \n")
  235. print("\nTesting cv=3, cv_threshold=None \n")
  236. scores = cross_validate_with_optimal_threshold(
  237. score_func_threshold=accuracy_score,
  238. estimator=estimator,
  239. X=X_train,
  240. y=y_train,
  241. scoring=scoring,
  242. cv=3,
  243. X_val=None,
  244. y_val=None,
  245. X_val_threshold=None,
  246. y_val_threshold=None,
  247. cv_threshold=None)
  248. print("\nScores:", scores)
  249. print("\n ########################################################## \n")
  250. print("\nTesting cv=3, cv_threshold=[3, 3, 3] \n")
  251. scores = cross_validate_with_optimal_threshold(
  252. score_func_threshold=accuracy_score,
  253. estimator=estimator,
  254. X=X_train,
  255. y=y_train,
  256. scoring=scoring,
  257. cv=3,
  258. X_val=X_val,
  259. y_val=y_val,
  260. X_val_threshold=X_val_threshold,
  261. y_val_threshold=y_val_threshold,
  262. cv_threshold=[3, 3, 3])
  263. print("\nScores:", scores)
  264. averaged_scores.append(np.mean(scores["test_score_threshold"]))
  265. averaged_thresholds.append(np.mean(scores["test_threshold"]))
  266. print("\n ########################################################## \n")
  267. # TODO: check overwriting X_train,
  268. # additional metrics append instead of overwrite
  269. # check the length of cv_threshold
  270. # test custom cv, cv_threshold
  271. print("\n Averaged test score:", averaged_scores)
  272. print("\n Averaged threshold:", averaged_thresholds)