Skip to content

Commit 4b4ecfd

Browse files
committed
feat: migrated components from decisiontree.py to a sub-package. also migrated the tests accordingly
1 parent 28f50c6 commit 4b4ecfd

33 files changed

Lines changed: 2305 additions & 2305 deletions

src/random_tree_models/decisiontree.py

Lines changed: 0 additions & 829 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .estimators import (
2+
DecisionTreeClassifier as DecisionTreeClassifier,
3+
)
4+
from .estimators import (
5+
DecisionTreeRegressor as DecisionTreeRegressor,
6+
)
7+
from .estimators import (
8+
DecisionTreeTemplate as DecisionTreeTemplate,
9+
)
10+
from .predict import find_leaf_node as find_leaf_node
11+
from .train import grow_tree as grow_tree
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import typing as T
2+
3+
import numpy as np
4+
import sklearn.base as base
5+
from sklearn.utils.multiclass import check_classification_targets, type_of_target
6+
from sklearn.utils.validation import check_is_fitted, validate_data # type: ignore
7+
8+
import random_tree_models.params
9+
from random_tree_models.decisiontree.node import Node
10+
from random_tree_models.decisiontree.predict import predict_with_tree
11+
from random_tree_models.decisiontree.train import grow_tree
12+
from random_tree_models.params import MetricNames
13+
14+
15+
class DecisionTreeTemplate(base.BaseEstimator):
16+
"""Template for DecisionTree classes
17+
18+
Based on: https://scikit-learn.org/stable/developers/develop.html#rolling-your-own-estimator
19+
"""
20+
21+
max_depth: int
22+
measure_name: random_tree_models.params.MetricNames
23+
min_improvement: float
24+
lam: float
25+
frac_subsamples: float
26+
frac_features: float
27+
random_state: int
28+
threshold_method: random_tree_models.params.ThresholdSelectionMethod
29+
threshold_quantile: float
30+
n_thresholds: int
31+
column_method: random_tree_models.params.ColumnSelectionMethod
32+
n_columns_to_try: int | None
33+
ensure_all_finite: bool
34+
tree_: Node
35+
36+
def __init__(
37+
self,
38+
measure_name: random_tree_models.params.MetricNames,
39+
max_depth: int = 2,
40+
min_improvement: float = 0.0,
41+
lam: float = 0.0,
42+
frac_subsamples: float = 1.0,
43+
frac_features: float = 1.0,
44+
threshold_method: random_tree_models.params.ThresholdSelectionMethod = random_tree_models.params.ThresholdSelectionMethod.bruteforce,
45+
threshold_quantile: float = 0.1,
46+
n_thresholds: int = 100,
47+
column_method: random_tree_models.params.ColumnSelectionMethod = random_tree_models.params.ColumnSelectionMethod.ascending,
48+
n_columns_to_try: int | None = None,
49+
random_state: int = 42,
50+
ensure_all_finite: bool = True,
51+
) -> None:
52+
self.max_depth = max_depth
53+
self.measure_name = measure_name
54+
self.min_improvement = min_improvement
55+
self.lam = lam
56+
self.frac_subsamples = frac_subsamples
57+
self.frac_features = frac_features
58+
self.random_state = random_state
59+
self.threshold_method = threshold_method
60+
self.threshold_quantile = threshold_quantile
61+
self.n_thresholds = n_thresholds
62+
self.column_method = column_method
63+
self.n_columns_to_try = n_columns_to_try
64+
self.ensure_all_finite = ensure_all_finite
65+
66+
def _organize_growth_parameters(self):
67+
self.growth_params_ = random_tree_models.params.TreeGrowthParameters(
68+
max_depth=self.max_depth,
69+
min_improvement=self.min_improvement,
70+
lam=-abs(self.lam),
71+
frac_subsamples=float(self.frac_subsamples),
72+
frac_features=float(self.frac_features),
73+
random_state=int(self.random_state),
74+
threshold_params=random_tree_models.params.ThresholdSelectionParameters(
75+
method=self.threshold_method,
76+
quantile=self.threshold_quantile,
77+
n_thresholds=self.n_thresholds,
78+
random_state=int(self.random_state),
79+
),
80+
column_params=random_tree_models.params.ColumnSelectionParameters(
81+
method=self.column_method,
82+
n_trials=self.n_columns_to_try,
83+
),
84+
)
85+
86+
def _select_samples_and_features(
87+
self, X: np.ndarray, y: np.ndarray
88+
) -> T.Tuple[np.ndarray, np.ndarray, np.ndarray]:
89+
"Sub-samples rows and columns from X and y"
90+
if not hasattr(self, "growth_params_"):
91+
raise ValueError(f"Try calling `fit` first.")
92+
93+
ix = np.arange(len(X))
94+
rng = np.random.RandomState(self.growth_params_.random_state)
95+
if self.growth_params_.frac_subsamples < 1.0:
96+
n_samples = int(self.growth_params_.frac_subsamples * len(X))
97+
ix_samples = rng.choice(ix, size=n_samples, replace=False)
98+
else:
99+
ix_samples = ix
100+
101+
if self.frac_features < 1.0:
102+
n_columns = int(X.shape[1] * self.frac_features)
103+
ix_features = rng.choice(
104+
np.arange(X.shape[1]),
105+
size=n_columns,
106+
replace=False,
107+
)
108+
else:
109+
ix_features = np.arange(X.shape[1])
110+
111+
_X = X[ix_samples, :]
112+
_X = _X[:, ix_features]
113+
114+
_y = y[ix_samples]
115+
return _X, _y, ix_features
116+
117+
def _select_features(self, X: np.ndarray, ix_features: np.ndarray) -> np.ndarray:
118+
return X[:, ix_features]
119+
120+
def fit(
121+
self,
122+
X: np.ndarray,
123+
y: np.ndarray,
124+
) -> "DecisionTreeTemplate":
125+
raise NotImplementedError()
126+
127+
def predict(self, X: np.ndarray) -> np.ndarray:
128+
raise NotImplementedError()
129+
130+
131+
class DecisionTreeRegressor(base.RegressorMixin, DecisionTreeTemplate):
132+
"""DecisionTreeRegressor
133+
134+
Based on: https://scikit-learn.org/stable/developers/develop.html#rolling-your-own-estimator
135+
"""
136+
137+
def __init__(
138+
self,
139+
measure_name: MetricNames = MetricNames.variance,
140+
max_depth: int = 2,
141+
min_improvement: float = 0.0,
142+
lam: float = 0.0,
143+
frac_subsamples: float = 1.0,
144+
frac_features: float = 1.0,
145+
threshold_method: random_tree_models.params.ThresholdSelectionMethod = random_tree_models.params.ThresholdSelectionMethod.bruteforce,
146+
threshold_quantile: float = 0.1,
147+
n_thresholds: int = 100,
148+
column_method: random_tree_models.params.ColumnSelectionMethod = random_tree_models.params.ColumnSelectionMethod.ascending,
149+
n_columns_to_try: int | None = None,
150+
random_state: int = 42,
151+
ensure_all_finite: bool = True,
152+
) -> None:
153+
super().__init__(
154+
measure_name=measure_name,
155+
max_depth=max_depth,
156+
min_improvement=min_improvement,
157+
lam=lam,
158+
frac_subsamples=frac_subsamples,
159+
frac_features=frac_features,
160+
threshold_method=threshold_method,
161+
threshold_quantile=threshold_quantile,
162+
n_thresholds=n_thresholds,
163+
column_method=column_method,
164+
n_columns_to_try=n_columns_to_try,
165+
random_state=random_state,
166+
ensure_all_finite=ensure_all_finite,
167+
)
168+
169+
def fit(
170+
self,
171+
X: np.ndarray,
172+
y: np.ndarray,
173+
**kwargs,
174+
) -> "DecisionTreeRegressor":
175+
self._organize_growth_parameters()
176+
177+
X, y = validate_data(self, X, y, ensure_all_finite=False)
178+
179+
_X, _y, self.ix_features_ = self._select_samples_and_features(X, y)
180+
181+
self.tree_ = grow_tree(
182+
_X,
183+
_y,
184+
measure_name=self.measure_name,
185+
growth_params=self.growth_params_,
186+
random_state=self.random_state,
187+
**kwargs,
188+
)
189+
190+
return self
191+
192+
def predict(self, X: np.ndarray) -> np.ndarray:
193+
check_is_fitted(self, ("tree_", "growth_params_"))
194+
195+
X = validate_data(self, X, reset=False, ensure_all_finite=False)
196+
197+
_X = self._select_features(X, self.ix_features_)
198+
199+
y = predict_with_tree(self.tree_, _X)
200+
201+
return y
202+
203+
204+
class DecisionTreeClassifier(base.ClassifierMixin, DecisionTreeTemplate):
205+
"""DecisionTreeClassifier
206+
207+
Based on: https://scikit-learn.org/stable/developers/develop.html#rolling-your-own-estimator
208+
"""
209+
210+
def __init__(
211+
self,
212+
measure_name: MetricNames = MetricNames.gini,
213+
max_depth: int = 2,
214+
min_improvement: float = 0.0,
215+
lam: float = 0.0,
216+
frac_subsamples: float = 1.0,
217+
frac_features: float = 1.0,
218+
threshold_method: random_tree_models.params.ThresholdSelectionMethod = random_tree_models.params.ThresholdSelectionMethod.bruteforce,
219+
threshold_quantile: float = 0.1,
220+
n_thresholds: int = 100,
221+
column_method: random_tree_models.params.ColumnSelectionMethod = random_tree_models.params.ColumnSelectionMethod.ascending,
222+
n_columns_to_try: int | None = None,
223+
random_state: int = 42,
224+
ensure_all_finite: bool = True,
225+
) -> None:
226+
super().__init__(
227+
measure_name=measure_name,
228+
max_depth=max_depth,
229+
min_improvement=min_improvement,
230+
lam=lam,
231+
frac_subsamples=frac_subsamples,
232+
frac_features=frac_features,
233+
threshold_method=threshold_method,
234+
threshold_quantile=threshold_quantile,
235+
n_thresholds=n_thresholds,
236+
column_method=column_method,
237+
n_columns_to_try=n_columns_to_try,
238+
random_state=random_state,
239+
)
240+
self.ensure_all_finite = ensure_all_finite
241+
242+
def _more_tags(self) -> T.Dict[str, bool]:
243+
"""Describes to scikit-learn parametrize_with_checks the scope of this class
244+
245+
Reference: https://scikit-learn.org/stable/developers/develop.html#estimator-tags
246+
"""
247+
return {"binary_only": True}
248+
249+
def __sklearn_tags__(self):
250+
# https://scikit-learn.org/stable/developers/develop.html
251+
tags = super().__sklearn_tags__() # type: ignore
252+
tags.classifier_tags.multi_class = False
253+
return tags
254+
255+
def fit(
256+
self,
257+
X: np.ndarray,
258+
y: np.ndarray,
259+
) -> "DecisionTreeClassifier":
260+
X, y = validate_data(self, X, y, ensure_all_finite=False)
261+
262+
check_classification_targets(y)
263+
264+
y_type = type_of_target(y, input_name="y", raise_unknown=True) # type: ignore
265+
if y_type != "binary":
266+
raise ValueError(
267+
"Only binary classification is supported. The type of the target "
268+
f"is {y_type}."
269+
)
270+
271+
if len(np.unique(y)) == 1:
272+
raise ValueError("Cannot train with only one class present")
273+
274+
self._organize_growth_parameters()
275+
276+
self.classes_, y = np.unique(y, return_inverse=True)
277+
278+
_X, _y, self.ix_features_ = self._select_samples_and_features(X, y)
279+
280+
self.tree_ = grow_tree(
281+
_X,
282+
_y,
283+
measure_name=self.measure_name,
284+
growth_params=self.growth_params_,
285+
random_state=self.random_state,
286+
)
287+
288+
return self
289+
290+
def predict_proba(self, X: np.ndarray) -> np.ndarray:
291+
check_is_fitted(self, ("tree_", "classes_", "growth_params_"))
292+
X = validate_data(self, X, reset=False, ensure_all_finite=False)
293+
294+
_X = self._select_features(X, self.ix_features_)
295+
296+
proba = predict_with_tree(self.tree_, _X)
297+
proba = np.array([1 - proba, proba]).T
298+
return proba
299+
300+
def predict(self, X: np.ndarray) -> np.ndarray:
301+
proba = self.predict_proba(X)
302+
ix = np.argmax(proba, axis=1)
303+
y = self.classes_[ix]
304+
305+
return y
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import uuid
2+
3+
from pydantic import StrictInt, StrictStr
4+
from pydantic.dataclasses import dataclass
5+
6+
from random_tree_models.decisiontree.split_objects import SplitScore
7+
8+
9+
@dataclass
10+
class Node:
11+
"""Decision node in a decision tree"""
12+
13+
# Stuff for making a decision
14+
array_column: StrictInt | None = None # index of the column to use
15+
threshold: float | None = None # threshold for decision
16+
prediction: float | None = None # value to use for predictions
17+
default_is_left: bool | None = None # default direction is x is nan
18+
19+
# decendants
20+
right: "Node | None" = None # right decendany of type Node
21+
left: "Node | None" = None # left decendant of type Node
22+
23+
# misc info
24+
measure: SplitScore | None = None
25+
26+
n_obs: StrictInt | None = None # number of observations in node
27+
reason: StrictStr | None = None # place for some comment
28+
29+
depth: StrictInt | None = None # depth of the node
30+
31+
def __post_init__(self):
32+
# unique identifier of the node
33+
self.node_id = uuid.uuid4()
34+
35+
@property
36+
def is_leaf(self) -> bool:
37+
return self.left is None and self.right is None

0 commit comments

Comments
 (0)