From f609fb6fc131d934f2b35be064b26daf5e6ae9dc Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 13 Jul 2023 17:06:40 +0900 Subject: [PATCH 01/27] Update hierarchical_shrinkage.py 1. Change estimator_ to estimator. 2. Fix bugs about HSTreeClassifierCV and HSTreeRegressorCV --- imodels/tree/hierarchical_shrinkage.py | 262 ++++++++++--------------- 1 file changed, 100 insertions(+), 162 deletions(-) diff --git a/imodels/tree/hierarchical_shrinkage.py b/imodels/tree/hierarchical_shrinkage.py index ac525438..0ecdb36b 100644 --- a/imodels/tree/hierarchical_shrinkage.py +++ b/imodels/tree/hierarchical_shrinkage.py @@ -1,25 +1,32 @@ -import time from copy import deepcopy from typing import List import numpy as np from sklearn import datasets -from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin -from sklearn.metrics import r2_score, mean_squared_error, log_loss -from sklearn.model_selection import cross_val_score, KFold +from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin, clone +from sklearn.metrics import r2_score +from sklearn.model_selection import cross_val_score from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier, \ export_text -from sklearn.ensemble import GradientBoostingClassifier, RandomForestRegressor +from sklearn.utils import check_X_y +from sklearn.ensemble import GradientBoostingClassifier from imodels.util import checks from imodels.util.arguments import check_fit_arguments from imodels.util.tree import compute_tree_complexity +# leading and traiing undescores +# https://github.com/rasbt/python-machine-learning-book/blob/master/faq/underscore-convention.md +# developer guideline +# https://scikit-learn.org/stable/developers/contributing.html#estimated-attributes -class HSTree: - def __init__(self, estimator_: BaseEstimator = DecisionTreeClassifier(max_leaf_nodes=20), - reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): +# https://scikit-learn.org/stable/developers/contributing.html + + +class HSTree(BaseEstimator): + def __init__(self, estimator=None, + reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): """HSTree (Tree with hierarchical shrinkage applied). Hierarchical shinkage is an extremely fast post-hoc regularization method which works on any decision tree (or tree-based ensemble, such as Random Forest). It does not modify the tree structure, and instead regularizes the tree by shrinking the prediction over each node towards the sample means of its ancestors (using a single regularization parameter). @@ -34,7 +41,7 @@ def __init__(self, estimator_: BaseEstimator = DecisionTreeClassifier(max_leaf_n reg_param: float Higher is more regularization (can be arbitrarily large, should not be < 0) - + shrinkage_scheme: str Experimental: Used to experiment with different forms of shrinkage. options are: (i) node_based shrinks based on number of samples in parent node @@ -43,24 +50,35 @@ def __init__(self, estimator_: BaseEstimator = DecisionTreeClassifier(max_leaf_n """ super().__init__() self.reg_param = reg_param - self.estimator_ = estimator_ + self.estimator = estimator self.shrinkage_scheme_ = shrinkage_scheme_ - if checks.check_is_fitted(self.estimator_): - self._shrink() - def get_params(self, deep=True): - if deep: - return deepcopy({'reg_param': self.reg_param, 'estimator_': self.estimator_, - 'shrinkage_scheme_': self.shrinkage_scheme_}) - return {'reg_param': self.reg_param, 'estimator_': self.estimator_, - 'shrinkage_scheme_': self.shrinkage_scheme_} + + def _validate_estimator(self, default=None): + """Check the base estimator. + + Sets the `estimator_` attributes. + """ + if self.estimator is not None: + self.estimator_ = self.estimator + else: + self.estimator_ = default + def fit(self, X, y, sample_weight=None, *args, **kwargs): - # remove feature_names if it exists (note: only works as keyword-arg) - feature_names = kwargs.pop('feature_names', None) # None returned if not passed - X, y, feature_names = check_fit_arguments(self, X, y, feature_names) - self.estimator_ = self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs) - self._shrink() + + self._validate_estimator() + + if checks.check_is_fitted(self.estimator_): + self._shrink() + + else: + # remove feature_names if it exists (note: only works as keyword-arg) + feature_names = kwargs.pop('feature_names', None) # None returned if not passed + X, y, feature_names = check_fit_arguments(self, X, y, feature_names) + X, y = check_X_y(X,y) + self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs) + self._shrink() # compute complexity if hasattr(self.estimator_, 'tree_'): @@ -73,6 +91,7 @@ def fit(self, X, y, sample_weight=None, *args, **kwargs): assert t.size == 1, 'multiple trees stored under tree_?' t = t[0] self.complexity_ += compute_tree_complexity(t.tree_) + return self def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, cum_sum=0): @@ -86,7 +105,7 @@ def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, c n_samples = tree.weighted_n_node_samples[i] if isinstance(self, RegressorMixin) or isinstance(self.estimator_, GradientBoostingClassifier): val = deepcopy(tree.value[i, :, :]) - else: # If classification, normalize to probability vector + else: # If classification, normalize to probability vector val = tree.value[i, :, :] / n_samples # Step 1: Update cum_sum @@ -100,15 +119,15 @@ def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, c val_new = (val - parent_val) / (1 + reg_param / parent_num) elif self.shrinkage_scheme_ == 'constant': val_new = (val - parent_val) / (1 + reg_param) - else: # leaf_based + else: # leaf_based val_new = 0 cum_sum += val_new # Step 2: Update node values if self.shrinkage_scheme_ == 'node_based' or self.shrinkage_scheme_ == 'constant': tree.value[i, :, :] = cum_sum - else: # leaf_based - if is_leaf: # update node values if leaf_based + else: # leaf_based + if is_leaf: # update node values if leaf_based root_val = tree.value[0, :, :] tree.value[i, :, :] = root_val + (val - root_val) / (1 + reg_param / n_samples) else: @@ -117,11 +136,11 @@ def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, c # Step 3: Recurse if not leaf if not is_leaf: self._shrink_tree(tree, reg_param, left, - parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) + parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) self._shrink_tree(tree, reg_param, right, - parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) + parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) - # edit the non-leaf nodes for later visualization (doesn't effect predictions) + # edit the non-leaf nodes for later visualization (doesn't effect predictions) return tree @@ -150,94 +169,51 @@ def score(self, X, y, *args, **kwargs): else: return NotImplemented - def __str__(self): - s = '> ------------------------------\n' - s += '> Decision Tree with Hierarchical Shrinkage\n' - s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' - s += '> ------------------------------' + '\n' - if hasattr(self, 'feature_names') and self.feature_names is not None: - return s + export_text(self.estimator_, feature_names=self.feature_names, show_weights=True) - else: - return s + export_text(self.estimator_, show_weights=True) - - def __repr__(self): - # s = self.__class__.__name__ - # s += "(" - # s += "estimator_=" - # s += repr(self.estimator_) - # s += ", " - # s += "reg_param=" - # s += str(self.reg_param) - # s += ", " - # s += "shrinkage_scheme_=" - # s += self.shrinkage_scheme_ - # s += ")" - # return s - attr_list = ["estimator_", "reg_param", "shrinkage_scheme_"] - s = self.__class__.__name__ - s += "(" - for attr in attr_list: - s += attr + "=" + repr(getattr(self, attr)) + ", " - s = s[:-2] + ")" - return s - - -class HSTreeRegressor(HSTree, RegressorMixin): - def __init__(self, estimator_: BaseEstimator = DecisionTreeRegressor(max_leaf_nodes=20), - reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): - super().__init__(estimator_=estimator_, - reg_param=reg_param, - shrinkage_scheme_=shrinkage_scheme_, - ) - class HSTreeClassifier(HSTree, ClassifierMixin): - def __init__(self, estimator_: BaseEstimator = DecisionTreeClassifier(max_leaf_nodes=20), - reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): - super().__init__(estimator_=estimator_, - reg_param=reg_param, - shrinkage_scheme_=shrinkage_scheme_, - ) + def __init__(self, estimator=None, + reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): + super().__init__(estimator=estimator, + reg_param=reg_param, + shrinkage_scheme_=shrinkage_scheme_, + ) + def _validate_estimator(self): + """Check the estimator and set the estimator_ attribute.""" + super()._validate_estimator(default=DecisionTreeClassifier(max_leaf_nodes=20)) -def _get_cv_criterion(scorer): - y_true = np.random.binomial(n=1, p=.5, size=100) - - y_pred_good = y_true - y_pred_bad = np.random.uniform(0, 1, 100) - - score_good = scorer(y_true, y_pred_good) - score_bad = scorer(y_true, y_pred_bad) - - if score_good > score_bad: - return np.argmax - elif score_good < score_bad: - return np.argmin +class HSTreeRegressor(HSTree, RegressorMixin): + def __init__(self, estimator=None, + reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): + super().__init__(estimator=estimator, + reg_param=reg_param, + shrinkage_scheme_=shrinkage_scheme_, + ) + def _validate_estimator(self): + """Check the estimator and set the estimator_ attribute.""" + super()._validate_estimator(default=DecisionTreeRegressor(max_leaf_nodes=20)) class HSTreeClassifierCV(HSTreeClassifier): - def __init__(self, estimator_: BaseEstimator = None, - reg_param_list: List[float] = [0, 0.1, 1, 10, 50, 100, 500], - shrinkage_scheme_: str = 'node_based', - max_leaf_nodes: int = 20, - cv: int = 3, scoring=None, *args, **kwargs): + def __init__(self, estimator=None, + reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500], + shrinkage_scheme_: str = 'node_based', + cv: int = 3, scoring=None): """Cross-validation is used to select the best regularization parameter for hierarchical shrinkage. - Params + Params ------ estimator_ Sklearn estimator (already initialized). If no estimator_ is passed, sklearn decision tree is used - max_rules + reg_param_list : list If estimator is None, then max_leaf_nodes is passed to the default decision tree args, kwargs Note: args, kwargs are not used but left so that imodels-experiments can still pass redundant args. """ - if estimator_ is None: - estimator_ = DecisionTreeClassifier(max_leaf_nodes=max_leaf_nodes) - super().__init__(estimator_, reg_param=None) + super().__init__(estimator, reg_param=None) self.reg_param_list = np.array(reg_param_list) self.cv = cv self.scoring = scoring @@ -248,44 +224,26 @@ def __init__(self, estimator_: BaseEstimator = None, # raise Warning('Passed an already fitted estimator,' # 'but shrinking not applied until fit method is called.') + def fit(self, X, y, *args, **kwargs): - self.scores_ = [[] for _ in self.reg_param_list] - scorer = kwargs.get('scoring', log_loss) - kf = KFold(n_splits=self.cv) - for train_index, test_index in kf.split(X): - X_out, y_out = X[test_index, :], y[test_index] - X_in, y_in = X[train_index, :], y[train_index] - base_est = deepcopy(self.estimator_) - base_est.fit(X_in, y_in) - for i, reg_param in enumerate(self.reg_param_list): - est_hs = HSTreeClassifier(base_est, reg_param) - est_hs.fit(X_in, y_in) - self.scores_[i].append(scorer(y_out, est_hs.predict_proba(X_out))) - self.scores_ = [np.mean(s) for s in self.scores_] - cv_criterion = _get_cv_criterion(scorer) - self.reg_param = self.reg_param_list[cv_criterion(self.scores_)] + self.scores_ = [] + for reg_param in self.reg_param_list: + est = HSTreeClassifier(deepcopy(self.estimator), reg_param) + cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring) + self.scores_.append(np.mean(cv_scores)) + self.reg_param = self.reg_param_list[np.argmax(self.scores_)] super().fit(X=X, y=y, *args, **kwargs) - - def __repr__(self): - attr_list = ["estimator_", "reg_param_list", "shrinkage_scheme_", - "cv", "scoring"] - s = self.__class__.__name__ - s += "(" - for attr in attr_list: - s += attr + "=" + repr(getattr(self, attr)) + ", " - s = s[:-2] + ")" - return s + return self class HSTreeRegressorCV(HSTreeRegressor): - def __init__(self, estimator_: BaseEstimator = None, - reg_param_list: List[float] = [0, 0.1, 1, 10, 50, 100, 500], - shrinkage_scheme_: str = 'node_based', - max_leaf_nodes: int = 20, - cv: int = 3, scoring=None, *args, **kwargs): + def __init__(self, estimator=None, + reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500], + shrinkage_scheme_: str = 'node_based', + cv: int = 3, scoring=None): """Cross-validation is used to select the best regularization parameter for hierarchical shrinkage. - Params + Params ------ estimator_ Sklearn estimator (already initialized). @@ -297,9 +255,7 @@ def __init__(self, estimator_: BaseEstimator = None, args, kwargs Note: args, kwargs are not used but left so that imodels-experiments can still pass redundant args. """ - if estimator_ is None: - estimator_ = DecisionTreeRegressor(max_leaf_nodes=max_leaf_nodes) - super().__init__(estimator_, reg_param=None) + super().__init__(estimator, reg_param=None) self.reg_param_list = np.array(reg_param_list) self.cv = cv self.scoring = scoring @@ -311,32 +267,14 @@ def __init__(self, estimator_: BaseEstimator = None, # 'but shrinking not applied until fit method is called.') def fit(self, X, y, *args, **kwargs): - self.scores_ = [[] for _ in self.reg_param_list] - kf = KFold(n_splits=self.cv) - scorer = kwargs.get('scoring', mean_squared_error) - for train_index, test_index in kf.split(X): - X_out, y_out = X[test_index, :], y[test_index] - X_in, y_in = X[train_index, :], y[train_index] - base_est = deepcopy(self.estimator_) - base_est.fit(X_in, y_in) - for i, reg_param in enumerate(self.reg_param_list): - est_hs = HSTreeRegressor(base_est, reg_param) - est_hs.fit(X_in, y_in) - self.scores_[i].append(scorer(est_hs.predict(X_out), y_out)) - self.scores_ = [np.mean(s) for s in self.scores_] - cv_criterion = _get_cv_criterion(scorer) - self.reg_param = self.reg_param_list[cv_criterion(self.scores_)] + self.scores_ = [] + for reg_param in self.reg_param_list: + est = HSTreeRegressor(deepcopy(self.estimator), reg_param) + cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring) + self.scores_.append(np.mean(cv_scores)) + self.reg_param = self.reg_param_list[np.argmax(self.scores_)] super().fit(X=X, y=y, *args, **kwargs) - - def __repr__(self): - attr_list = ["estimator_", "reg_param_list", "shrinkage_scheme_", - "cv", "scoring"] - s = self.__class__.__name__ - s += "(" - for attr in attr_list: - s += attr + "=" + repr(getattr(self, attr)) + ", " - s = s[:-2] + ")" - return s + return self if __name__ == '__main__': @@ -355,7 +293,7 @@ def __repr__(self): # m = HSTree(estimator_=DecisionTreeClassifier(), reg_param=0.1) # m = DecisionTreeClassifier(max_leaf_nodes = 20,random_state=1, max_features=None) - m = DecisionTreeClassifier(random_state=42) + m = DecisionTreeRegressor(random_state=42, max_leaf_nodes=20) # print('best alpha', m.reg_param) m.fit(X_train, y_train) # m.predict_proba(X_train) # just run this @@ -367,9 +305,9 @@ def __repr__(self): # m = HSTree(estimator_=DecisionTreeRegressor(random_state=42, max_features=None), reg_param=10) # m = HSTree(estimator_=DecisionTreeClassifier(random_state=42, max_features=None), reg_param=0) - m = HSTreeRegressorCV(estimator_=DecisionTreeClassifier(random_state=42), - shrinkage_scheme_='node_based', - reg_param_list=[0.1, 1, 2, 5, 10, 25, 50, 100, 500]) + m = HSTreeClassifierCV(estimator_=DecisionTreeRegressor(max_leaf_nodes=10, random_state=1), + shrinkage_scheme_='node_based', + reg_param_list=[0.1, 1, 2, 5, 10, 25, 50, 100, 500]) # m = ShrunkTreeCV(estimator_=DecisionTreeClassifier()) # m = HSTreeClassifier(estimator_ = GradientBoostingClassifier(random_state = 10),reg_param = 5) From e76ea1f064d7f72e294053e66ac7ffd3a36d73d2 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:36:16 +0900 Subject: [PATCH 02/27] Fixed bugs about __str__ and __repr__ before fitted --- imodels/tree/hierarchical_shrinkage.py | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/imodels/tree/hierarchical_shrinkage.py b/imodels/tree/hierarchical_shrinkage.py index 0ecdb36b..a607fce7 100644 --- a/imodels/tree/hierarchical_shrinkage.py +++ b/imodels/tree/hierarchical_shrinkage.py @@ -2,6 +2,7 @@ from typing import List import numpy as np +import sklearn from sklearn import datasets from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin, clone from sklearn.metrics import r2_score @@ -169,6 +170,42 @@ def score(self, X, y, *args, **kwargs): else: return NotImplemented + def __str__(self): + if sklearn.utils.validation.check_is_fitted(self): + s = '> ------------------------------\n' + s += '> Decision Tree with Hierarchical Shrinkage\n' + s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' + s += '> ------------------------------' + '\n' + if hasattr(self, 'feature_names') and self.feature_names is not None: + return s + export_text(self.estimator_, feature_names=self.feature_names, show_weights=True) + else: + return s + export_text(self.estimator_, show_weights=True) + else: + return self.__class__.__name__ + + def __repr__(self): + if sklearn.utils.validation.check_is_fitted(self): + # s = self.__class__.__name__ + # s += "(" + # s += "estimator_=" + # s += repr(self.estimator_) + # s += ", " + # s += "reg_param=" + # s += str(self.reg_param) + # s += ", " + # s += "shrinkage_scheme_=" + # s += self.shrinkage_scheme_ + # s += ")" + # return s + attr_list = ["estimator_", "reg_param", "shrinkage_scheme_"] + s = self.__class__.__name__ + s += "(" + for attr in attr_list: + s += attr + "=" + repr(getattr(self, attr)) + ", " + s = s[:-2] + ")" + return s + else: + return self.__class__.__name__ class HSTreeClassifier(HSTree, ClassifierMixin): def __init__(self, estimator=None, From 04b7b99130de63233b7ba8678073ce94e1dc3f76 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:10:31 +0900 Subject: [PATCH 03/27] Fix bugs about __str__ --- imodels/tree/cart_wrapper.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/imodels/tree/cart_wrapper.py b/imodels/tree/cart_wrapper.py index 7bb9ec93..6167d655 100644 --- a/imodels/tree/cart_wrapper.py +++ b/imodels/tree/cart_wrapper.py @@ -1,6 +1,7 @@ # This is just a simple wrapper around sklearn decisiontree # https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html +import sklearn from sklearn.tree import DecisionTreeClassifier, export_text, DecisionTreeRegressor from imodels.util.arguments import check_fit_arguments @@ -48,15 +49,17 @@ def _set_complexity(self): self.complexity_ = compute_tree_complexity(self.tree_) def __str__(self): - s = '> ------------------------------\n' - s += '> Greedy CART Tree:\n' - s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' - s += '> ------------------------------' + '\n' - if hasattr(self, 'feature_names') and self.feature_names is not None: - return s + export_text(self, feature_names=self.feature_names, show_weights=True) + if sklearn.utils.validation.check_is_fitted(self): + s = '> ------------------------------\n' + s += '> Greedy CART Tree:\n' + s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' + s += '> ------------------------------' + '\n' + if hasattr(self, 'feature_names') and self.feature_names is not None: + return s + export_text(self, feature_names=self.feature_names, show_weights=True) + else: + return s + export_text(self, show_weights=True) else: - return s + export_text(self, show_weights=True) - + return self.__class__.__name__ class GreedyTreeRegressor(DecisionTreeRegressor): """Wrapper around sklearn greedy tree regressor @@ -98,7 +101,10 @@ def _set_complexity(self): self.complexity_ = compute_tree_complexity(self.tree_) def __str__(self): - if hasattr(self, 'feature_names') and self.feature_names is not None: - return 'GreedyTree:\n' + export_text(self, feature_names=self.feature_names, show_weights=True) + if sklearn.utils.validation.check_is_fitted(self): + if hasattr(self, 'feature_names') and self.feature_names is not None: + return 'GreedyTree:\n' + export_text(self, feature_names=self.feature_names, show_weights=True) + else: + return 'GreedyTree:\n' + export_text(self, show_weights=True) else: - return 'GreedyTree:\n' + export_text(self, show_weights=True) \ No newline at end of file + return self.__class__.__name__ From 0c08c065c99bf6221f0a972761cd55d39f1c30b6 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:12:25 +0900 Subject: [PATCH 04/27] Fix bugs about __str__ figs.py --- imodels/tree/figs.py | 211 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 195 insertions(+), 16 deletions(-) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index 7baacb9b..93081add 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -51,12 +51,188 @@ def setattrs(self, **kwargs): setattr(self, k, v) def __str__(self): - if self.is_root: - return f'X_{self.feature} <= {self.threshold:0.3f} (Tree #{self.tree_num} root)' - elif self.left is None and self.right is None: - return f'Val: {self.value[0][0]:0.3f} (leaf)' +from copy import deepcopy +from typing import List + +import numpy as np +import sklearn +from sklearn import datasets +from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin, clone +from sklearn.metrics import r2_score +from sklearn.model_selection import cross_val_score +from sklearn.model_selection import train_test_split +from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier, \ + export_text +from sklearn.utils import check_X_y +from sklearn.ensemble import GradientBoostingClassifier + +from imodels.util import checks +from imodels.util.arguments import check_fit_arguments +from imodels.util.tree import compute_tree_complexity + +# leading and traiing undescores +# https://github.com/rasbt/python-machine-learning-book/blob/master/faq/underscore-convention.md +# developer guideline +# https://scikit-learn.org/stable/developers/contributing.html#estimated-attributes + +# https://scikit-learn.org/stable/developers/contributing.html + + +class HSTree(BaseEstimator): + def __init__(self, estimator=None, + reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): + """HSTree (Tree with hierarchical shrinkage applied). + Hierarchical shinkage is an extremely fast post-hoc regularization method which works on any decision tree (or tree-based ensemble, such as Random Forest). + It does not modify the tree structure, and instead regularizes the tree by shrinking the prediction over each node towards the sample means of its ancestors (using a single regularization parameter). + Experiments over a wide variety of datasets show that hierarchical shrinkage substantially increases the predictive performance of individual decision trees and decision-tree ensembles. + https://arxiv.org/abs/2202.00858 + + Params + ------ + estimator_: sklearn tree or tree ensemble model (e.g. RandomForest or GradientBoosting) + Defaults to CART Classification Tree with 20 max leaf nodes + Note: this estimator will be directly modified + + reg_param: float + Higher is more regularization (can be arbitrarily large, should not be < 0) + + shrinkage_scheme: str + Experimental: Used to experiment with different forms of shrinkage. options are: + (i) node_based shrinks based on number of samples in parent node + (ii) leaf_based only shrinks leaf nodes based on number of leaf samples + (iii) constant shrinks every node by a constant lambda + """ + super().__init__() + self.reg_param = reg_param + self.estimator = estimator + self.shrinkage_scheme_ = shrinkage_scheme_ + + + def _validate_estimator(self, default=None): + """Check the base estimator. + + Sets the `estimator_` attributes. + """ + if self.estimator is not None: + self.estimator_ = self.estimator + else: + self.estimator_ = default + + + def fit(self, X, y, sample_weight=None, *args, **kwargs): + + self._validate_estimator() + + if checks.check_is_fitted(self.estimator_): + self._shrink() + + else: + # remove feature_names if it exists (note: only works as keyword-arg) + feature_names = kwargs.pop('feature_names', None) # None returned if not passed + X, y, feature_names = check_fit_arguments(self, X, y, feature_names) + X, y = check_X_y(X,y) + self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs) + self._shrink() + + # compute complexity + if hasattr(self.estimator_, 'tree_'): + self.complexity_ = compute_tree_complexity(self.estimator_.tree_) + elif hasattr(self.estimator_, 'estimators_'): + self.complexity_ = 0 + for i in range(len(self.estimator_.estimators_)): + t = deepcopy(self.estimator_.estimators_[i]) + if isinstance(t, np.ndarray): + assert t.size == 1, 'multiple trees stored under tree_?' + t = t[0] + self.complexity_ += compute_tree_complexity(t.tree_) + + return self + + def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, cum_sum=0): + """Shrink the tree + """ + if reg_param is None: + reg_param = 1.0 + left = tree.children_left[i] + right = tree.children_right[i] + is_leaf = left == right + n_samples = tree.weighted_n_node_samples[i] + if isinstance(self, RegressorMixin) or isinstance(self.estimator_, GradientBoostingClassifier): + val = deepcopy(tree.value[i, :, :]) + else: # If classification, normalize to probability vector + val = tree.value[i, :, :] / n_samples + + # Step 1: Update cum_sum + # if root + if parent_val is None and parent_num is None: + cum_sum = val + + # if has parent else: - return f'X_{self.feature} <= {self.threshold:0.3f} (split)' + if self.shrinkage_scheme_ == 'node_based': + val_new = (val - parent_val) / (1 + reg_param / parent_num) + elif self.shrinkage_scheme_ == 'constant': + val_new = (val - parent_val) / (1 + reg_param) + else: # leaf_based + val_new = 0 + cum_sum += val_new + + # Step 2: Update node values + if self.shrinkage_scheme_ == 'node_based' or self.shrinkage_scheme_ == 'constant': + tree.value[i, :, :] = cum_sum + else: # leaf_based + if is_leaf: # update node values if leaf_based + root_val = tree.value[0, :, :] + tree.value[i, :, :] = root_val + (val - root_val) / (1 + reg_param / n_samples) + else: + tree.value[i, :, :] = val + + # Step 3: Recurse if not leaf + if not is_leaf: + self._shrink_tree(tree, reg_param, left, + parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) + self._shrink_tree(tree, reg_param, right, + parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) + + # edit the non-leaf nodes for later visualization (doesn't effect predictions) + + return tree + + def _shrink(self): + if hasattr(self.estimator_, 'tree_'): + self._shrink_tree(self.estimator_.tree_, self.reg_param) + elif hasattr(self.estimator_, 'estimators_'): + for t in self.estimator_.estimators_: + if isinstance(t, np.ndarray): + assert t.size == 1, 'multiple trees stored under tree_?' + t = t[0] + self._shrink_tree(t.tree_, self.reg_param) + + def predict(self, X, *args, **kwargs): + return self.estimator_.predict(X, *args, **kwargs) + + def predict_proba(self, X, *args, **kwargs): + if hasattr(self.estimator_, 'predict_proba'): + return self.estimator_.predict_proba(X, *args, **kwargs) + else: + return NotImplemented + + def score(self, X, y, *args, **kwargs): + if hasattr(self.estimator_, 'score'): + return self.estimator_.score(X, y, *args, **kwargs) + else: + return NotImplemented + + def __str__(self): + if sklearn.utils.validation.check_is_fitted(self): + if self.is_root: + return f'X_{self.feature} <= {self.threshold:0.3f} (Tree #{self.tree_num} root)' + elif self.left is None and self.right is None: + return f'Val: {self.value[0][0]:0.3f} (leaf)' + else: + return f'X_{self.feature} <= {self.threshold:0.3f} (split)' + else: + return self.__class__.__name__ def print_root(self, y): try: @@ -411,17 +587,20 @@ def _tree_to_str_with_data(self, X, y, root: Node, prefix=''): self._tree_to_str_with_data(X[~left], y[~left], root.right, pprefix)) def __str__(self): - s = '> ------------------------------\n' - s += '> FIGS-Fast Interpretable Greedy-Tree Sums:\n' - s += '> \tPredictions are made by summing the "Val" reached by traversing each tree.\n' - s += '> \tFor classifiers, a sigmoid function is then applied to the sum.\n' - s += '> ------------------------------\n' - s += '\n\t+\n'.join([self._tree_to_str(t) for t in self.trees_]) - if hasattr(self, 'feature_names_') and self.feature_names_ is not None: - for i in range(len(self.feature_names_))[::-1]: - s = s.replace(f'X_{i}', self.feature_names_[i]) - return s - + if sklearn.utils.validation.check_is_fitted(self): + s = '> ------------------------------\n' + s += '> FIGS-Fast Interpretable Greedy-Tree Sums:\n' + s += '> \tPredictions are made by summing the "Val" reached by traversing each tree.\n' + s += '> \tFor classifiers, a sigmoid function is then applied to the sum.\n' + s += '> ------------------------------\n' + s += '\n\t+\n'.join([self._tree_to_str(t) for t in self.trees_]) + if hasattr(self, 'feature_names_') and self.feature_names_ is not None: + for i in range(len(self.feature_names_))[::-1]: + s = s.replace(f'X_{i}', self.feature_names_[i]) + return s + else: + return self.__class__.__name__ + def print_tree(self, X, y, feature_names=None): s = '------------\n' + \ '\n\t+\n'.join([self._tree_to_str_with_data(X, y, t) From ac1c8d79919ae8ba45f806bc91792b164cc0e52f Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:15:04 +0900 Subject: [PATCH 05/27] Fix bugs about __str__ figs_ensembles.py --- imodels/experimental/figs_ensembles.py | 40 +++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/imodels/experimental/figs_ensembles.py b/imodels/experimental/figs_ensembles.py index ea6fa6c6..1819cb2d 100644 --- a/imodels/experimental/figs_ensembles.py +++ b/imodels/experimental/figs_ensembles.py @@ -72,18 +72,21 @@ def setattrs(self, **kwargs): setattr(self, k, v) def __str__(self): - if self.split_or_linear == 'linear': - if self.is_root: - return f'X_{self.feature} * {self.value:0.3f} (Tree #{self.tree_num} linear root)' + if sklearn.utils.validation.check_is_fitted(self): + if self.split_or_linear == 'linear': + if self.is_root: + return f'X_{self.feature} * {self.value:0.3f} (Tree #{self.tree_num} linear root)' + else: + return f'X_{self.feature} * {self.value:0.3f} (linear)' else: - return f'X_{self.feature} * {self.value:0.3f} (linear)' + if self.is_root: + return f'X_{self.feature} <= {self.threshold:0.3f} (Tree #{self.tree_num} root)' + elif self.left is None and self.right is None: + return f'Val: {self.value[0][0]:0.3f} (leaf)' + else: + return f'X_{self.feature} <= {self.threshold:0.3f} (split)' else: - if self.is_root: - return f'X_{self.feature} <= {self.threshold:0.3f} (Tree #{self.tree_num} root)' - elif self.left is None and self.right is None: - return f'Val: {self.value[0][0]:0.3f} (leaf)' - else: - return f'X_{self.feature} <= {self.threshold:0.3f} (split)' + return self.__class__.__name__ def __repr__(self): return self.__str__() @@ -417,13 +420,16 @@ def _tree_to_str(self, root: Node, prefix=''): pprefix) def __str__(self): - s = '------------\n' + \ - '\n\t+\n'.join([self._tree_to_str(t) for t in self.trees_]) - if hasattr(self, 'feature_names_') and self.feature_names_ is not None: - for i in range(len(self.feature_names_))[::-1]: - s = s.replace(f'X_{i}', self.feature_names_[i]) - return s - + if sklearn.utils.validation.check_is_fitted(self): + s = '------------\n' + \ + '\n\t+\n'.join([self._tree_to_str(t) for t in self.trees_]) + if hasattr(self, 'feature_names_') and self.feature_names_ is not None: + for i in range(len(self.feature_names_))[::-1]: + s = s.replace(f'X_{i}', self.feature_names_[i]) + return s + else: + return self.__class__.__name__ + def predict(self, X): if self.posthoc_ridge and self.weighted_model_: # note, during fitting don't use the weighted moel X_feats = self._extract_tree_predictions(X) From 93e7acf63dcea8a6a7094c711c4da8ab280ee31b Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:16:30 +0900 Subject: [PATCH 06/27] Fix bugs about __str__ corels_wrapper.py --- imodels/rule_list/corels_wrapper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/imodels/rule_list/corels_wrapper.py b/imodels/rule_list/corels_wrapper.py index 39788df6..6945a049 100644 --- a/imodels/rule_list/corels_wrapper.py +++ b/imodels/rule_list/corels_wrapper.py @@ -233,14 +233,17 @@ def _traverse_rule(self, X: np.ndarray, y: np.ndarray, feature_names: List[str], self.str_print = str_print def __str__(self): - if corels_supported: - if self.str_print is not None: - return 'OptimalRuleList:\n\n' + self.str_print + if sklearn.utils.validation.check_is_fitted(self): + if corels_supported: + if self.str_print is not None: + return 'OptimalRuleList:\n\n' + self.str_print + else: + return 'OptimalRuleList:\n\n' + self.rl_.__str__() else: - return 'OptimalRuleList:\n\n' + self.rl_.__str__() + return super().__str__() else: - return super().__str__() - + return self.__class__.__name__ + def _get_complexity(self): return sum([len(corule['antecedents']) for corule in self.rl_.rules]) From af2a4d60432dcbb0511d085c236eb0a69efd9132 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:17:38 +0900 Subject: [PATCH 07/27] Fix bugs about __str__ greedy_rule_list.py --- imodels/rule_list/greedy_rule_list.py | 68 ++++++++++++--------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/imodels/rule_list/greedy_rule_list.py b/imodels/rule_list/greedy_rule_list.py index 0991e2f4..0b6322c0 100644 --- a/imodels/rule_list/greedy_rule_list.py +++ b/imodels/rule_list/greedy_rule_list.py @@ -140,48 +140,42 @@ def predict(self, X): X = check_array(X) return np.argmax(self.predict_proba(X), axis=1) - """ - def __str__(self): - # s = '' - # for rule in self.rules_: - # s += f"mean {rule['val'].round(3)} ({rule['num_pts']} pts)\n" - # if 'col' in rule: - # s += f"if {rule['col']} >= {rule['cutoff']} then {rule['val_right'].round(3)} ({rule['num_pts_right']} pts)\n" - # return s - """ def __str__(self): '''Print out the list in a nice way ''' - s = '> ------------------------------\n> Greedy Rule List\n> ------------------------------\n' - - def red(s): - # return f"\033[91m{s}\033[00m" - return s - - def cyan(s): - # return f"\033[96m{s}\033[00m" + if sklearn.utils.validation.check_is_fitted(self): + s = '> ------------------------------\n> Greedy Rule List\n> ------------------------------\n' + + def red(s): + # return f"\033[91m{s}\033[00m" + return s + + def cyan(s): + # return f"\033[96m{s}\033[00m" + return s + + def rule_name(rule): + if rule['flip']: + return '~' + rule['col'] + return rule['col'] + + # rule = self.rules_[0] + # s += f"{red((100 * rule['val']).round(3))}% IwI ({rule['num_pts']} pts)\n" + for rule in self.rules_: + s += u'\u2193\n' + f"{cyan((100 * rule['val']).round(2))}% risk ({rule['num_pts']} pts)\n" + # s += f"\t{'Else':>45} => {cyan((100 * rule['val']).round(2)):>6}% IwI ({rule['val'] * rule['num_pts']:.0f}/{rule['num_pts']} pts)\n" + if 'col' in rule: + # prefix = f"if {rule['col']} >= {rule['cutoff']}" + prefix = f"if {rule_name(rule)}" + val = f"{100 * rule['val_right'].round(3)}" + s += f"\t{prefix} ==> {red(val)}% risk ({rule['num_pts_right']} pts)\n" + # rule = self.rules_[-1] + # s += f"{red((100 * rule['val']).round(3))}% IwI ({rule['num_pts']} pts)\n" return s - - def rule_name(rule): - if rule['flip']: - return '~' + rule['col'] - return rule['col'] - - # rule = self.rules_[0] - # s += f"{red((100 * rule['val']).round(3))}% IwI ({rule['num_pts']} pts)\n" - for rule in self.rules_: - s += u'\u2193\n' + f"{cyan((100 * rule['val']).round(2))}% risk ({rule['num_pts']} pts)\n" - # s += f"\t{'Else':>45} => {cyan((100 * rule['val']).round(2)):>6}% IwI ({rule['val'] * rule['num_pts']:.0f}/{rule['num_pts']} pts)\n" - if 'col' in rule: - # prefix = f"if {rule['col']} >= {rule['cutoff']}" - prefix = f"if {rule_name(rule)}" - val = f"{100 * rule['val_right'].round(3)}" - s += f"\t{prefix} ==> {red(val)}% risk ({rule['num_pts_right']} pts)\n" - # rule = self.rules_[-1] - # s += f"{red((100 * rule['val']).round(3))}% IwI ({rule['num_pts']} pts)\n" - return s - + else: + return self.__class__.__name__ + ######## HERE ONWARDS CUSTOM SPLITTING (DEPRECATED IN FAVOR OF SKLEARN STUMP) ######## ###################################################################################### def _find_best_split(self, x, y): From c959598beefa34fc4fa8a4daefb4f2591de8f455 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:19:11 +0900 Subject: [PATCH 08/27] Fix bugs about __str__ brs.py --- imodels/rule_set/brs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/imodels/rule_set/brs.py b/imodels/rule_set/brs.py index 933864ca..5dd4aefb 100644 --- a/imodels/rule_set/brs.py +++ b/imodels/rule_set/brs.py @@ -192,8 +192,11 @@ def fit(self, X, y, feature_names: list = None, init=[], verbose=False): return self def __str__(self): - return ' '.join(str(r) for r in self.rules_) - + if sklearn.utils.validation.check_is_fitted(self): + return ' '.join(str(r) for r in self.rules_) + else: + return self.__class__.__name__ + def predict(self, X): check_is_fitted(self) if isinstance(X, np.ndarray): From 2b1ba91fa52b95da151992c6348c5c342371eaf8 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:19:47 +0900 Subject: [PATCH 09/27] Fix bugs about __str__ rule_fit.py --- imodels/rule_set/rule_fit.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/imodels/rule_set/rule_fit.py b/imodels/rule_set/rule_fit.py index dee403e7..be09694f 100644 --- a/imodels/rule_set/rule_fit.py +++ b/imodels/rule_set/rule_fit.py @@ -242,12 +242,15 @@ def visualize(self, decimals=2): return rules[['rule', 'coef']].round(decimals) def __str__(self): - s = '> ------------------------------\n' - s += '> RuleFit:\n' - s += '> \tPredictions are made by summing the coefficients of each rule\n' - s += '> ------------------------------\n' - return s + self.visualize().to_string(index=False) + '\n' - + if sklearn.utils.validation.check_is_fitted(self): + s = '> ------------------------------\n' + s += '> RuleFit:\n' + s += '> \tPredictions are made by summing the coefficients of each rule\n' + s += '> ------------------------------\n' + return s + self.visualize().to_string(index=False) + '\n' + else: + return self.__class__.__name__ + def _extract_rules(self, X, y) -> List[str]: return extract_rulefit(X, y, feature_names=self.feature_placeholders, From db53eb5fac4889b35a3ac0dfe1f71200da9d3259 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:27:36 +0900 Subject: [PATCH 10/27] Update figs.py --- imodels/tree/figs.py | 175 +------------------------------------------ 1 file changed, 1 insertion(+), 174 deletions(-) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index 93081add..37225bff 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -50,179 +50,6 @@ def setattrs(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) - def __str__(self): -from copy import deepcopy -from typing import List - -import numpy as np -import sklearn -from sklearn import datasets -from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin, clone -from sklearn.metrics import r2_score -from sklearn.model_selection import cross_val_score -from sklearn.model_selection import train_test_split -from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier, \ - export_text -from sklearn.utils import check_X_y -from sklearn.ensemble import GradientBoostingClassifier - -from imodels.util import checks -from imodels.util.arguments import check_fit_arguments -from imodels.util.tree import compute_tree_complexity - -# leading and traiing undescores -# https://github.com/rasbt/python-machine-learning-book/blob/master/faq/underscore-convention.md -# developer guideline -# https://scikit-learn.org/stable/developers/contributing.html#estimated-attributes - -# https://scikit-learn.org/stable/developers/contributing.html - - -class HSTree(BaseEstimator): - def __init__(self, estimator=None, - reg_param: float = 1, shrinkage_scheme_: str = 'node_based'): - """HSTree (Tree with hierarchical shrinkage applied). - Hierarchical shinkage is an extremely fast post-hoc regularization method which works on any decision tree (or tree-based ensemble, such as Random Forest). - It does not modify the tree structure, and instead regularizes the tree by shrinking the prediction over each node towards the sample means of its ancestors (using a single regularization parameter). - Experiments over a wide variety of datasets show that hierarchical shrinkage substantially increases the predictive performance of individual decision trees and decision-tree ensembles. - https://arxiv.org/abs/2202.00858 - - Params - ------ - estimator_: sklearn tree or tree ensemble model (e.g. RandomForest or GradientBoosting) - Defaults to CART Classification Tree with 20 max leaf nodes - Note: this estimator will be directly modified - - reg_param: float - Higher is more regularization (can be arbitrarily large, should not be < 0) - - shrinkage_scheme: str - Experimental: Used to experiment with different forms of shrinkage. options are: - (i) node_based shrinks based on number of samples in parent node - (ii) leaf_based only shrinks leaf nodes based on number of leaf samples - (iii) constant shrinks every node by a constant lambda - """ - super().__init__() - self.reg_param = reg_param - self.estimator = estimator - self.shrinkage_scheme_ = shrinkage_scheme_ - - - def _validate_estimator(self, default=None): - """Check the base estimator. - - Sets the `estimator_` attributes. - """ - if self.estimator is not None: - self.estimator_ = self.estimator - else: - self.estimator_ = default - - - def fit(self, X, y, sample_weight=None, *args, **kwargs): - - self._validate_estimator() - - if checks.check_is_fitted(self.estimator_): - self._shrink() - - else: - # remove feature_names if it exists (note: only works as keyword-arg) - feature_names = kwargs.pop('feature_names', None) # None returned if not passed - X, y, feature_names = check_fit_arguments(self, X, y, feature_names) - X, y = check_X_y(X,y) - self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs) - self._shrink() - - # compute complexity - if hasattr(self.estimator_, 'tree_'): - self.complexity_ = compute_tree_complexity(self.estimator_.tree_) - elif hasattr(self.estimator_, 'estimators_'): - self.complexity_ = 0 - for i in range(len(self.estimator_.estimators_)): - t = deepcopy(self.estimator_.estimators_[i]) - if isinstance(t, np.ndarray): - assert t.size == 1, 'multiple trees stored under tree_?' - t = t[0] - self.complexity_ += compute_tree_complexity(t.tree_) - - return self - - def _shrink_tree(self, tree, reg_param, i=0, parent_val=None, parent_num=None, cum_sum=0): - """Shrink the tree - """ - if reg_param is None: - reg_param = 1.0 - left = tree.children_left[i] - right = tree.children_right[i] - is_leaf = left == right - n_samples = tree.weighted_n_node_samples[i] - if isinstance(self, RegressorMixin) or isinstance(self.estimator_, GradientBoostingClassifier): - val = deepcopy(tree.value[i, :, :]) - else: # If classification, normalize to probability vector - val = tree.value[i, :, :] / n_samples - - # Step 1: Update cum_sum - # if root - if parent_val is None and parent_num is None: - cum_sum = val - - # if has parent - else: - if self.shrinkage_scheme_ == 'node_based': - val_new = (val - parent_val) / (1 + reg_param / parent_num) - elif self.shrinkage_scheme_ == 'constant': - val_new = (val - parent_val) / (1 + reg_param) - else: # leaf_based - val_new = 0 - cum_sum += val_new - - # Step 2: Update node values - if self.shrinkage_scheme_ == 'node_based' or self.shrinkage_scheme_ == 'constant': - tree.value[i, :, :] = cum_sum - else: # leaf_based - if is_leaf: # update node values if leaf_based - root_val = tree.value[0, :, :] - tree.value[i, :, :] = root_val + (val - root_val) / (1 + reg_param / n_samples) - else: - tree.value[i, :, :] = val - - # Step 3: Recurse if not leaf - if not is_leaf: - self._shrink_tree(tree, reg_param, left, - parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) - self._shrink_tree(tree, reg_param, right, - parent_val=val, parent_num=n_samples, cum_sum=deepcopy(cum_sum)) - - # edit the non-leaf nodes for later visualization (doesn't effect predictions) - - return tree - - def _shrink(self): - if hasattr(self.estimator_, 'tree_'): - self._shrink_tree(self.estimator_.tree_, self.reg_param) - elif hasattr(self.estimator_, 'estimators_'): - for t in self.estimator_.estimators_: - if isinstance(t, np.ndarray): - assert t.size == 1, 'multiple trees stored under tree_?' - t = t[0] - self._shrink_tree(t.tree_, self.reg_param) - - def predict(self, X, *args, **kwargs): - return self.estimator_.predict(X, *args, **kwargs) - - def predict_proba(self, X, *args, **kwargs): - if hasattr(self.estimator_, 'predict_proba'): - return self.estimator_.predict_proba(X, *args, **kwargs) - else: - return NotImplemented - - def score(self, X, y, *args, **kwargs): - if hasattr(self.estimator_, 'score'): - return self.estimator_.score(X, y, *args, **kwargs) - else: - return NotImplemented - def __str__(self): if sklearn.utils.validation.check_is_fitted(self): if self.is_root: @@ -233,7 +60,7 @@ def __str__(self): return f'X_{self.feature} <= {self.threshold:0.3f} (split)' else: return self.__class__.__name__ - + def print_root(self, y): try: one_count = pd.Series(y).value_counts()[1.0] From 93bb305c30326aa3860eef1fda4f04158ebf14c7 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:28:45 +0900 Subject: [PATCH 11/27] Update figs_ensembles.py --- imodels/experimental/figs_ensembles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/experimental/figs_ensembles.py b/imodels/experimental/figs_ensembles.py index 1819cb2d..5074afff 100644 --- a/imodels/experimental/figs_ensembles.py +++ b/imodels/experimental/figs_ensembles.py @@ -2,6 +2,7 @@ import numpy as np from matplotlib import pyplot as plt +import sklearn from sklearn import datasets from sklearn import tree from sklearn.base import BaseEstimator From 961a09c9e3d99b966e3021ebb6afcace76987c53 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:29:09 +0900 Subject: [PATCH 12/27] Update corels_wrapper.py --- imodels/rule_list/corels_wrapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/rule_list/corels_wrapper.py b/imodels/rule_list/corels_wrapper.py index 6945a049..0c1359db 100644 --- a/imodels/rule_list/corels_wrapper.py +++ b/imodels/rule_list/corels_wrapper.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd +import sklearn from sklearn.preprocessing import KBinsDiscretizer from imodels.rule_list.greedy_rule_list import GreedyRuleListClassifier From 4de8a16ae6a29f37ce815e9831f1e9482b1048b6 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:29:39 +0900 Subject: [PATCH 13/27] Update greedy_rule_list.py --- imodels/rule_list/greedy_rule_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/rule_list/greedy_rule_list.py b/imodels/rule_list/greedy_rule_list.py index 0b6322c0..f32e3be5 100644 --- a/imodels/rule_list/greedy_rule_list.py +++ b/imodels/rule_list/greedy_rule_list.py @@ -8,6 +8,7 @@ from copy import deepcopy import numpy as np +import sklearn from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.utils.multiclass import unique_labels from sklearn.utils.validation import check_array, check_is_fitted From 0783b9202f4be47a8b5c164c3d38fd86ea2447ca Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:30:13 +0900 Subject: [PATCH 14/27] Update brs.py --- imodels/rule_set/brs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/rule_set/brs.py b/imodels/rule_set/brs.py index 5dd4aefb..860ddbee 100644 --- a/imodels/rule_set/brs.py +++ b/imodels/rule_set/brs.py @@ -18,6 +18,7 @@ from numpy.random import random from pandas import read_csv from scipy.sparse import csc_matrix +import sklearn from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.ensemble import RandomForestClassifier from sklearn.utils.multiclass import check_classification_targets From bbe2de77e44654f009923c03ad23775e822144ba Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:30:30 +0900 Subject: [PATCH 15/27] Update rule_fit.py --- imodels/rule_set/rule_fit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/rule_set/rule_fit.py b/imodels/rule_set/rule_fit.py index be09694f..6a7a4dcc 100644 --- a/imodels/rule_set/rule_fit.py +++ b/imodels/rule_set/rule_fit.py @@ -13,6 +13,7 @@ import pandas as pd import scipy from scipy.special import softmax +import sklearn from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin from sklearn.base import TransformerMixin from sklearn.utils.multiclass import unique_labels From f1de89eaeff80382b072d99ea8d572da4d02471f Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:38:37 +0900 Subject: [PATCH 16/27] Update figs.py --- imodels/tree/figs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index 37225bff..72e0f3b3 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd from scipy.special import expit +import sklearn from sklearn import datasets from sklearn import tree from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin From 20ea6e04f7b2fe22e1322ecec866a0e53efb5994 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:42:24 +0900 Subject: [PATCH 17/27] Update figs.py --- imodels/tree/figs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index 72e0f3b3..fd93985e 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -77,7 +77,7 @@ def print_root(self, y): return f'X_{self.feature} <= {self.threshold:0.3f}' + one_proportion def __repr__(self): - return self.__str__() + return self.__str__(self) class FIGS(BaseEstimator): From e4d386340281ca95ff7efb939ae5071914aee5cc Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:51:32 +0900 Subject: [PATCH 18/27] Update figs_ensembles.py --- imodels/experimental/figs_ensembles.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/imodels/experimental/figs_ensembles.py b/imodels/experimental/figs_ensembles.py index 5074afff..634e6048 100644 --- a/imodels/experimental/figs_ensembles.py +++ b/imodels/experimental/figs_ensembles.py @@ -73,7 +73,8 @@ def setattrs(self, **kwargs): setattr(self, k, v) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) if self.split_or_linear == 'linear': if self.is_root: return f'X_{self.feature} * {self.value:0.3f} (Tree #{self.tree_num} linear root)' @@ -86,7 +87,7 @@ def __str__(self): return f'Val: {self.value[0][0]:0.3f} (leaf)' else: return f'X_{self.feature} <= {self.threshold:0.3f} (split)' - else: + except ValueError: return self.__class__.__name__ def __repr__(self): @@ -421,14 +422,15 @@ def _tree_to_str(self, root: Node, prefix=''): pprefix) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '------------\n' + \ '\n\t+\n'.join([self._tree_to_str(t) for t in self.trees_]) if hasattr(self, 'feature_names_') and self.feature_names_ is not None: for i in range(len(self.feature_names_))[::-1]: s = s.replace(f'X_{i}', self.feature_names_[i]) return s - else: + except ValueError: return self.__class__.__name__ def predict(self, X): From fb90b2e250587b0588e60fd91a9bcbc3b7157b6b Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:52:10 +0900 Subject: [PATCH 19/27] Update corels_wrapper.py --- imodels/rule_list/corels_wrapper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imodels/rule_list/corels_wrapper.py b/imodels/rule_list/corels_wrapper.py index 0c1359db..f968e3f5 100644 --- a/imodels/rule_list/corels_wrapper.py +++ b/imodels/rule_list/corels_wrapper.py @@ -234,7 +234,8 @@ def _traverse_rule(self, X: np.ndarray, y: np.ndarray, feature_names: List[str], self.str_print = str_print def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) if corels_supported: if self.str_print is not None: return 'OptimalRuleList:\n\n' + self.str_print @@ -242,7 +243,7 @@ def __str__(self): return 'OptimalRuleList:\n\n' + self.rl_.__str__() else: return super().__str__() - else: + except ValueError: return self.__class__.__name__ def _get_complexity(self): From 73d928311e0df9b4ab23ac7d3a8dbf9e02d02958 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:53:11 +0900 Subject: [PATCH 20/27] Update greedy_rule_list.py --- imodels/rule_list/greedy_rule_list.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imodels/rule_list/greedy_rule_list.py b/imodels/rule_list/greedy_rule_list.py index f32e3be5..962f9998 100644 --- a/imodels/rule_list/greedy_rule_list.py +++ b/imodels/rule_list/greedy_rule_list.py @@ -145,7 +145,8 @@ def predict(self, X): def __str__(self): '''Print out the list in a nice way ''' - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '> ------------------------------\n> Greedy Rule List\n> ------------------------------\n' def red(s): @@ -174,7 +175,7 @@ def rule_name(rule): # rule = self.rules_[-1] # s += f"{red((100 * rule['val']).round(3))}% IwI ({rule['num_pts']} pts)\n" return s - else: + except ValueError: return self.__class__.__name__ ######## HERE ONWARDS CUSTOM SPLITTING (DEPRECATED IN FAVOR OF SKLEARN STUMP) ######## From 0c1ceb1654ac8a0562915b4734dc2402a5f13227 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:54:12 +0900 Subject: [PATCH 21/27] Update brs.py --- imodels/rule_set/brs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imodels/rule_set/brs.py b/imodels/rule_set/brs.py index 860ddbee..a65c2abb 100644 --- a/imodels/rule_set/brs.py +++ b/imodels/rule_set/brs.py @@ -193,9 +193,10 @@ def fit(self, X, y, feature_names: list = None, init=[], verbose=False): return self def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) return ' '.join(str(r) for r in self.rules_) - else: + except ValueError: return self.__class__.__name__ def predict(self, X): From fd38c2786bb3459b3538bea30d024308bf8799df Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:55:03 +0900 Subject: [PATCH 22/27] Update rule_fit.py --- imodels/rule_set/rule_fit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imodels/rule_set/rule_fit.py b/imodels/rule_set/rule_fit.py index 6a7a4dcc..a6d05d2f 100644 --- a/imodels/rule_set/rule_fit.py +++ b/imodels/rule_set/rule_fit.py @@ -243,13 +243,14 @@ def visualize(self, decimals=2): return rules[['rule', 'coef']].round(decimals) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '> ------------------------------\n' s += '> RuleFit:\n' s += '> \tPredictions are made by summing the coefficients of each rule\n' s += '> ------------------------------\n' return s + self.visualize().to_string(index=False) + '\n' - else: + except ValueError: return self.__class__.__name__ def _extract_rules(self, X, y) -> List[str]: From 8c75d6a4914cb428d5f72db29e240a14209cc657 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:55:52 +0900 Subject: [PATCH 23/27] Update cart_wrapper.py --- imodels/tree/cart_wrapper.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/imodels/tree/cart_wrapper.py b/imodels/tree/cart_wrapper.py index 6167d655..2f6f7021 100644 --- a/imodels/tree/cart_wrapper.py +++ b/imodels/tree/cart_wrapper.py @@ -49,7 +49,8 @@ def _set_complexity(self): self.complexity_ = compute_tree_complexity(self.tree_) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '> ------------------------------\n' s += '> Greedy CART Tree:\n' s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' @@ -58,7 +59,7 @@ def __str__(self): return s + export_text(self, feature_names=self.feature_names, show_weights=True) else: return s + export_text(self, show_weights=True) - else: + except ValueError: return self.__class__.__name__ class GreedyTreeRegressor(DecisionTreeRegressor): @@ -101,10 +102,11 @@ def _set_complexity(self): self.complexity_ = compute_tree_complexity(self.tree_) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) if hasattr(self, 'feature_names') and self.feature_names is not None: return 'GreedyTree:\n' + export_text(self, feature_names=self.feature_names, show_weights=True) else: return 'GreedyTree:\n' + export_text(self, show_weights=True) - else: + except ValueError: return self.__class__.__name__ From 5626c2332480478768e17f9491c0abe24904cda7 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:56:35 +0900 Subject: [PATCH 24/27] Update figs.py --- imodels/tree/figs.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index fd93985e..5464e337 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -52,14 +52,15 @@ def setattrs(self, **kwargs): setattr(self, k, v) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) if self.is_root: return f'X_{self.feature} <= {self.threshold:0.3f} (Tree #{self.tree_num} root)' elif self.left is None and self.right is None: return f'Val: {self.value[0][0]:0.3f} (leaf)' else: return f'X_{self.feature} <= {self.threshold:0.3f} (split)' - else: + except ValueError: return self.__class__.__name__ def print_root(self, y): @@ -415,7 +416,8 @@ def _tree_to_str_with_data(self, X, y, root: Node, prefix=''): self._tree_to_str_with_data(X[~left], y[~left], root.right, pprefix)) def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '> ------------------------------\n' s += '> FIGS-Fast Interpretable Greedy-Tree Sums:\n' s += '> \tPredictions are made by summing the "Val" reached by traversing each tree.\n' @@ -426,7 +428,7 @@ def __str__(self): for i in range(len(self.feature_names_))[::-1]: s = s.replace(f'X_{i}', self.feature_names_[i]) return s - else: + except ValueError: return self.__class__.__name__ def print_tree(self, X, y, feature_names=None): From b71550d6083673bdb01cc91db2c837711c78ed95 Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:57:18 +0900 Subject: [PATCH 25/27] Update hierarchical_shrinkage.py --- imodels/tree/hierarchical_shrinkage.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/imodels/tree/hierarchical_shrinkage.py b/imodels/tree/hierarchical_shrinkage.py index a607fce7..a91a062d 100644 --- a/imodels/tree/hierarchical_shrinkage.py +++ b/imodels/tree/hierarchical_shrinkage.py @@ -171,7 +171,8 @@ def score(self, X, y, *args, **kwargs): return NotImplemented def __str__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) s = '> ------------------------------\n' s += '> Decision Tree with Hierarchical Shrinkage\n' s += '> \tPrediction is made by looking at the value in the appropriate leaf of the tree\n' @@ -180,11 +181,12 @@ def __str__(self): return s + export_text(self.estimator_, feature_names=self.feature_names, show_weights=True) else: return s + export_text(self.estimator_, show_weights=True) - else: + except: return self.__class__.__name__ def __repr__(self): - if sklearn.utils.validation.check_is_fitted(self): + try: + sklearn.utils.validation.check_is_fitted(self) # s = self.__class__.__name__ # s += "(" # s += "estimator_=" @@ -204,7 +206,7 @@ def __repr__(self): s += attr + "=" + repr(getattr(self, attr)) + ", " s = s[:-2] + ")" return s - else: + except ValueError: return self.__class__.__name__ class HSTreeClassifier(HSTree, ClassifierMixin): From e049632489506def9d5e22f1e8d202bbe6acc1ab Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:13:50 +0900 Subject: [PATCH 26/27] Update hierarchical_shrinkage.py --- imodels/tree/hierarchical_shrinkage.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/imodels/tree/hierarchical_shrinkage.py b/imodels/tree/hierarchical_shrinkage.py index a91a062d..bfe99c33 100644 --- a/imodels/tree/hierarchical_shrinkage.py +++ b/imodels/tree/hierarchical_shrinkage.py @@ -206,7 +206,7 @@ def __repr__(self): s += attr + "=" + repr(getattr(self, attr)) + ", " s = s[:-2] + ")" return s - except ValueError: + except : return self.__class__.__name__ class HSTreeClassifier(HSTree, ClassifierMixin): @@ -330,7 +330,7 @@ def fit(self, X, y, *args, **kwargs): print('X.shape', X.shape) print('ys', np.unique(y_train)) - # m = HSTree(estimator_=DecisionTreeClassifier(), reg_param=0.1) + # m = HSTree(estimator=DecisionTreeClassifier(), reg_param=0.1) # m = DecisionTreeClassifier(max_leaf_nodes = 20,random_state=1, max_features=None) m = DecisionTreeRegressor(random_state=42, max_leaf_nodes=20) # print('best alpha', m.reg_param) @@ -342,14 +342,15 @@ def fit(self, X, y, *args, **kwargs): # x = DecisionTreeRegressor(random_state = 42, ccp_alpha = 0.3) # x.fit(X_train,y_train) - # m = HSTree(estimator_=DecisionTreeRegressor(random_state=42, max_features=None), reg_param=10) - # m = HSTree(estimator_=DecisionTreeClassifier(random_state=42, max_features=None), reg_param=0) - m = HSTreeClassifierCV(estimator_=DecisionTreeRegressor(max_leaf_nodes=10, random_state=1), + # m = HSTree(estimator=DecisionTreeRegressor(random_state=42, max_features=None), reg_param=10) + # m = HSTree(estimator=DecisionTreeClassifier(random_state=42, max_features=None), reg_param=0) + m = HSTreeClassifierCV(estimator=DecisionTreeRegressor(max_leaf_nodes=10, random_state=1), shrinkage_scheme_='node_based', reg_param_list=[0.1, 1, 2, 5, 10, 25, 50, 100, 500]) - # m = ShrunkTreeCV(estimator_=DecisionTreeClassifier()) + print(m) + # m = ShrunkTreeCV(estimator=DecisionTreeClassifier()) - # m = HSTreeClassifier(estimator_ = GradientBoostingClassifier(random_state = 10),reg_param = 5) + # m = HSTreeClassifier(estimator = GradientBoostingClassifier(random_state = 10),reg_param = 5) m.fit(X_train, y_train) print('best alpha', m.reg_param) # m.predict_proba(X_train) # just run this From ecf3044d0db580667377022b1be6ff860299baff Mon Sep 17 00:00:00 2001 From: jak_tkvs <32125829+jckkvs@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:38:52 +0900 Subject: [PATCH 27/27] Update figs.py --- imodels/tree/figs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/imodels/tree/figs.py b/imodels/tree/figs.py index 5464e337..2c2e5104 100644 --- a/imodels/tree/figs.py +++ b/imodels/tree/figs.py @@ -77,8 +77,6 @@ def print_root(self, y): else: return f'X_{self.feature} <= {self.threshold:0.3f}' + one_proportion - def __repr__(self): - return self.__str__(self) class FIGS(BaseEstimator):