Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions smarttree/_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def check__params(
cat_features=None,
rank_features=None,
hierarchy=None,
na_mode=None,
num_na_mode=None,
cat_na_mode=None,
cat_na_filler=None,
Expand Down Expand Up @@ -73,6 +74,9 @@ def check__params(
if hierarchy is not None:
_check__hierarchy(hierarchy)

if na_mode is not None:
_check_na_mode(na_mode)

if num_na_mode is not None:
_check__num_na_mode(num_na_mode)

Expand Down Expand Up @@ -272,6 +276,14 @@ def _check__hierarchy(hierarchy):
)


def _check_na_mode(na_mode):
if na_mode not in ("include_all", "include_best"):
raise ValueError(
"`num_na_mode` must be Literal['include_all', 'include_best']."
f" The current value of `na_mode` is {na_mode!r}."
)


def _check__num_na_mode(num_na_mode):
if num_na_mode not in ("min", "max", "include_all", "include_best"):
raise ValueError(
Expand Down
84 changes: 48 additions & 36 deletions smarttree/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ._types import (
CatNaModeType,
ClassificationCriterionType,
CommonNaModeType,
NaModeType,
NumNaModeType,
VerboseType,
Expand All @@ -43,10 +44,11 @@ def __init__(
cat_features: list[str] | str | None = None,
rank_features: dict[str, list] | None = None,
hierarchy: dict[str, str | list[str]] | None = None,
num_na_mode: NumNaModeType = "min",
cat_na_mode: CatNaModeType = "as_category",
na_mode: CommonNaModeType = "include_best",
num_na_mode: NumNaModeType | None = None,
cat_na_mode: CatNaModeType | None = None,
cat_na_filler: str = "missing_value",
feature_na_mode: dict[str, NaModeType | None] | None = None,
feature_na_mode: dict[str, NaModeType] | None = None,
verbose: VerboseType = "WARNING",
) -> None:

Expand All @@ -62,6 +64,7 @@ def __init__(
cat_features=cat_features,
rank_features=rank_features,
hierarchy=hierarchy,
na_mode=na_mode,
num_na_mode=num_na_mode,
cat_na_mode=cat_na_mode,
cat_na_filler=cat_na_filler,
Expand All @@ -75,7 +78,7 @@ def __init__(
self.__max_leaf_nodes = max_leaf_nodes
self.__min_impurity_decrease = min_impurity_decrease
self.__max_childs = max_childs
self.__hierarchy = dict() if hierarchy is None else hierarchy
self.__hierarchy = hierarchy or dict()

if num_features is None:
self.__num_features = []
Expand All @@ -91,28 +94,15 @@ def __init__(
else:
self.__cat_features = cat_features

self.__rank_features = dict() if rank_features is None else rank_features
self.__rank_features = rank_features or dict()

self._all_features: list[str] = []

self.__na_mode = na_mode
self.__num_na_mode = num_na_mode
self.__cat_na_mode = cat_na_mode
self.__cat_na_filler = cat_na_filler

self.__feature_na_mode: dict[str, NaModeType | None]
if feature_na_mode is None:
self.__feature_na_mode = dict()
else:
self.__feature_na_mode = feature_na_mode
for num_feature in self.num_features:
if num_feature not in self.__feature_na_mode:
self.__feature_na_mode[num_feature] = self.num_na_mode
for cat_feature in self.cat_features:
if cat_feature not in self.__feature_na_mode:
self.__feature_na_mode[cat_feature] = self.cat_na_mode
for rank_feature in self.rank_features:
if rank_feature not in self.__feature_na_mode:
self.__feature_na_mode[rank_feature] = None
self.__feature_na_mode: dict[str, NaModeType] = feature_na_mode or dict()

self.logger = logging.getLogger()
self.logger.setLevel(verbose)
Expand Down Expand Up @@ -172,19 +162,23 @@ def hierarchy(self) -> dict[str, str | list[str]]:
return self.__hierarchy

@property
def num_na_mode(self) -> NumNaModeType:
def na_mode(self) -> CommonNaModeType:
return self.__na_mode

@property
def num_na_mode(self) -> NumNaModeType | None:
return self.__num_na_mode

@property
def cat_na_mode(self) -> CatNaModeType:
def cat_na_mode(self) -> CatNaModeType | None:
return self.__cat_na_mode

@property
def cat_na_filler(self) -> str:
return self.__cat_na_filler

@property
def feature_na_mode(self) -> dict[str, NaModeType | None]:
def feature_na_mode(self) -> dict[str, NaModeType]:
return self.__feature_na_mode

@property
Expand Down Expand Up @@ -244,6 +238,7 @@ def get_params(
"cat_features": self.cat_features,
"rank_features": self.rank_features,
"hierarchy": self.hierarchy,
"na_mode": self.na_mode,
"num_na_mode": self.num_na_mode,
"cat_na_mode": self.cat_na_mode,
"cat_na_filler": self.cat_na_filler,
Expand Down Expand Up @@ -362,7 +357,17 @@ class SmartDecisionTreeClassifier(BaseSmartDecisionTree):
If provided, the algorithm will respect these dependencies when
selecting features for splits.

num_na_mode: {"min", "max", "include_all", "include_best"}, default="min"
na_mode: {"include_all", "include_best"}, default="include_best"
The mode of handling missing values in a feature.

- If "include_all", then while training samples with missing values
are included into all child nodes. While predicting decision is
weighted mean of all decisions in child nodes.
- If "include_best", then while training and prediction samples with
missing values are included into the best child node according to
information gain.

num_na_mode: {"min", "max", "include_all", "include_best"}, default=None
The mode of handling missing values in a numerical feature.

- If "min", then missing values are filled with minimum value of
Expand All @@ -376,7 +381,7 @@ class SmartDecisionTreeClassifier(BaseSmartDecisionTree):
missing values are included into the best child node according to
information gain.

cat_na_mode: {"as_category", "include_all", "include_best"}, default="as_category"
cat_na_mode: {"as_category", "include_all", "include_best"}, default=None
The mode of handling missing values in a categorical feature.

- If "as_category", then while training and predicting missing values
Expand Down Expand Up @@ -416,10 +421,11 @@ def __init__(
cat_features: list[str] | str | None = None,
rank_features: dict[str, list] | None = None,
hierarchy: dict[str, str | list[str]] | None = None,
num_na_mode: NumNaModeType = "min",
cat_na_mode: CatNaModeType = "as_category",
na_mode: CommonNaModeType = "include_best",
num_na_mode: NumNaModeType | None = None,
cat_na_mode: CatNaModeType | None = None,
cat_na_filler: str = "missing_value",
feature_na_mode: dict[str, NaModeType | None] | None = None,
feature_na_mode: dict[str, NaModeType] | None = None,
verbose: VerboseType = "WARNING",
) -> None:

Expand All @@ -435,6 +441,7 @@ def __init__(
cat_features=cat_features,
rank_features=rank_features,
hierarchy=hierarchy,
na_mode=na_mode,
num_na_mode=num_na_mode,
cat_na_mode=cat_na_mode,
cat_na_filler=cat_na_filler,
Expand Down Expand Up @@ -474,9 +481,11 @@ def __repr__(self) -> str:
repr_.append(f"rank_features={self.rank_features}")
if self.hierarchy:
repr_.append(f"hierarchy={self.hierarchy}")
if self.num_na_mode != "min":
if self.na_mode != "include_best":
repr_.append(f"na_mode={self.na_mode!r}")
if self.num_na_mode:
repr_.append(f"num_na_mode={self.num_na_mode!r}")
if self.cat_na_mode != "as_category":
if self.cat_na_mode:
repr_.append(f"cat_na_mode={self.cat_na_mode!r}")
if self.cat_na_filler != "missing_value":
repr_.append(f"cat_na_filler={self.cat_na_filler!r}")
Expand Down Expand Up @@ -534,24 +543,27 @@ def fit(self, X: pd.DataFrame, y: pd.Series) -> Self:
)
if unknown_num_features:
self.num_features.extend(unknown_num_features)
for num_feature in unknown_num_features:
if num_feature not in self.feature_na_mode:
self.feature_na_mode[num_feature] = self.num_na_mode
self.logger.info(
f"[{self.__class__.__name__}] [Info] {unknown_num_features} are"
" added to `num_features`."
)
if unknown_cat_features:
self.cat_features.extend(unknown_cat_features)
for cat_feature in unknown_cat_features:
if cat_feature not in self.feature_na_mode:
self.feature_na_mode[cat_feature] = self.cat_na_mode
self.logger.info(
f"[{self.__class__.__name__}] [Info] {unknown_cat_features} are"
" added to `cat_features`."
)

self._all_features = X.columns.to_list()

temp_feature_na_mode = self.feature_na_mode.copy()
self.feature_na_mode.update({f: self.na_mode for f in self._all_features})
if self.num_na_mode is not None:
self.feature_na_mode.update({f: self.num_na_mode for f in self.num_features})
if self.cat_na_mode is not None:
self.feature_na_mode.update({f: self.cat_na_mode for f in self.cat_features})
self.feature_na_mode.update(temp_feature_na_mode)

self.__classes = np.sort(y.unique())

for feature, na_mode in self.feature_na_mode.items():
Expand Down
8 changes: 4 additions & 4 deletions smarttree/_column_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(
criterion: ClassificationCriterionType,
min_samples_split: int,
min_samples_leaf: int,
feature_na_mode: dict[str, NaModeType | None],
feature_na_mode: dict[str, NaModeType],
) -> None:

self.dataset = dataset
Expand Down Expand Up @@ -173,7 +173,7 @@ def __init__(
criterion: ClassificationCriterionType,
min_samples_split: int,
min_samples_leaf: int,
feature_na_mode: dict[str, NaModeType | None],
feature_na_mode: dict[str, NaModeType],
) -> None:

super().__init__(
Expand Down Expand Up @@ -272,7 +272,7 @@ def __init__(
min_samples_leaf: int,
max_leaf_nodes: int | float,
max_childs: int | float,
feature_na_mode: dict[str, NaModeType | None],
feature_na_mode: dict[str, NaModeType],
) -> None:

super().__init__(
Expand Down Expand Up @@ -397,7 +397,7 @@ def __init__(
min_samples_split: int,
min_samples_leaf: int,
rank_features: dict[str, list],
feature_na_mode: dict[str, NaModeType | None],
feature_na_mode: dict[str, NaModeType],
) -> None:

super().__init__(
Expand Down
2 changes: 1 addition & 1 deletion smarttree/_node_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(
num_features: list[str],
cat_features: list[str],
rank_features: dict[str, list],
feature_na_mode: dict[str, NaModeType | None],
feature_na_mode: dict[str, NaModeType],
) -> None:

self.max_depth = max_depth
Expand Down
4 changes: 4 additions & 0 deletions smarttree/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@


ClassificationCriterionType = Literal["gini", "entropy", "log_loss"]

CommonNaModeType = Literal["include_all", "include_best"]
NumNaModeType = Literal["min", "max", "include_all", "include_best"]
CatNaModeType = Literal["as_category", "include_all", "include_best"]
NaModeType = Literal["min", "max", "as_category", "include_all", "include_best"]

VerboseType = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] | int

SplitType = Literal["numerical", "categorical", "rank"]
9 changes: 6 additions & 3 deletions tests/decision_tree/base/test__get_set_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
"cat_features": [],
"rank_features": {},
"hierarchy": {},
"num_na_mode": "min",
"cat_na_mode": "as_category",
"na_mode": "include_best",
"num_na_mode": None,
"cat_na_mode": None,
"cat_na_filler": "missing_value",
"feature_na_mode": {},
}
Expand All @@ -45,6 +46,7 @@ def test__get_params(concrete_smart_tree):
({"cat_features": ["cat_feature"]}, does_not_raise()),
({"rank_features": {"rank_feature": ["1", "2", "3"]}}, does_not_raise()),
({"hierarchy": {"num_feature": "rank_feature"}}, does_not_raise()),
({"na_mode": "include_all"}, does_not_raise()),
({"num_na_mode": "max"}, does_not_raise()),
({"cat_na_mode": "as_category"}, does_not_raise()),
({"cat_na_filler": "NA"}, does_not_raise()),
Expand All @@ -58,7 +60,7 @@ def test__get_params(concrete_smart_tree):
" Valid parameters are: criterion, max_depth, min_samples_split,"
" min_samples_leaf, max_leaf_nodes, min_impurity_decrease,"
" max_childs, num_features, cat_features, rank_features, hierarchy,"
" num_na_mode, cat_na_mode, cat_na_filler, feature_na_mode."
" na_mode, num_na_mode, cat_na_mode, cat_na_filler, feature_na_mode."
),
),
),
Expand All @@ -76,6 +78,7 @@ def test__get_params(concrete_smart_tree):
"cat_features",
"rank_features",
"hierarchy",
"na_mode",
"num_na_mode",
"cat_na_mode",
"cat_na_filler",
Expand Down
24 changes: 21 additions & 3 deletions tests/decision_tree/classifier/test__repr_tree.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import pytest

from smarttree import SmartDecisionTreeClassifier
from smarttree._types import CatNaModeType, ClassificationCriterionType, NumNaModeType
from smarttree._types import (
CatNaModeType,
ClassificationCriterionType,
CommonNaModeType,
NumNaModeType,
)


CLASS_NAME = SmartDecisionTreeClassifier.__name__
Expand Down Expand Up @@ -167,10 +172,23 @@ def test_repr_tree__hierarchy(hierarchy, expected):
assert repr(tree_classifier) == expected


@pytest.mark.parametrize(
("na_mode", "expected"),
[
("include_best", f"{CLASS_NAME}()"),
("include_all", f"{CLASS_NAME}(na_mode='include_all')"),
],
)
def test_repr_tree__na_mode(na_mode, expected):
na_mode: CommonNaModeType
tree_classifier = SmartDecisionTreeClassifier(na_mode=na_mode)
assert repr(tree_classifier) == expected


@pytest.mark.parametrize(
("num_na_mode", "expected"),
[
("min", f"{CLASS_NAME}()"),
(None, f"{CLASS_NAME}()"),
("max", f"{CLASS_NAME}(num_na_mode='max')"),
],
ids=["default value", "not default value"],
Expand All @@ -184,7 +202,7 @@ def test_repr_tree__num_na_mode(num_na_mode, expected):
@pytest.mark.parametrize(
("cat_na_mode", "expected"),
[
("as_category", f"{CLASS_NAME}()"),
(None, f"{CLASS_NAME}()"),
("include_all", f"{CLASS_NAME}(cat_na_mode='include_all')"),
],
ids=["default value", "not default value"],
Expand Down
Loading