hgp-lib 1.2.2__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/PKG-INFO +1 -1
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/pyproject.toml +1 -1
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/algorithms/boolean_gp.py +1 -1
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/mutation_executor.py +20 -5
- hgp_lib-1.2.4/src/hgp_lib/mutations/utils.py +5 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/binarizer.py +68 -28
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/metrics.py +6 -6
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/PKG-INFO +1 -1
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_binarizer.py +90 -27
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_mutation_executor.py +35 -0
- hgp_lib-1.2.2/src/hgp_lib/mutations/utils.py +0 -3
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/LICENSE +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/README.md +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/setup.cfg +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/setup.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/algorithms/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/gp_benchmarker.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/progress.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/runner.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/benchmarker_config.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/boolean_gp_config.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/trainer_config.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/crossover_executor.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/crossover_factory.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/core.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/history.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/results.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/base_mutation.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/literal_mutations.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/mutation_factory.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/operator_mutations.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/base_strategy.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/generator.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/populations_factory.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/sampling.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/strategies.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/base.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/binning.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/errors.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/sklearn_binarizer.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/utils.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/warnings.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/literals.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/low_memory_operators.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/operators.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/rules.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/utils.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/base_selection.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/roulette_selection.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/tournament_selection.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/boolean_rule_classifier.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/gp_trainer.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/__init__.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/validation.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/warnings.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/SOURCES.txt +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/dependency_links.txt +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/requires.txt +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/top_level.txt +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_binarizer_benchmark.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_boolean_gp.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_boolean_rule_classifier.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_configs.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_crossover.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_gp_benchmarker.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_gp_trainer.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_hierarchical_gp.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_history.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_literal_mutations.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_metrics_core.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_mutation_factory.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_operator_mutations.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_populations.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_preprocessing_utils.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_progress.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_results.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_rules.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_rules_utils.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_sampling.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_selections.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_utils_metrics.py +0 -0
- {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_utils_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hgp-lib
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: hgp-lib: Hierarchical Genetic Programming Library for Generating Boolean Rules from Tabular Data
|
|
5
5
|
Author-email: Ramona-Georgiana Albert <ramona-georgiana.albert@info.uaic.ro>, George Stoica <george.stoica@info.uaic.ro>
|
|
6
6
|
Maintainer-email: George Stoica <george.stoica@info.uaic.ro>, Ramona-Georgiana Albert <ramona-georgiana.albert@info.uaic.ro>
|
|
@@ -75,7 +75,7 @@ class BooleanGP:
|
|
|
75
75
|
self.score_fn = score_fn
|
|
76
76
|
self.train_cm = train_cm
|
|
77
77
|
self.complexity_penalty = config.complexity_penalty
|
|
78
|
-
self.train_data = train_data
|
|
78
|
+
self.train_data = np.asfortranarray(train_data)
|
|
79
79
|
self.train_labels = train_labels
|
|
80
80
|
|
|
81
81
|
self.current_depth = current_depth
|
|
@@ -149,13 +149,17 @@ class MutationExecutor:
|
|
|
149
149
|
>>> str(rule)
|
|
150
150
|
'0'
|
|
151
151
|
"""
|
|
152
|
+
|
|
152
153
|
new_rule = rule.copy()
|
|
153
154
|
|
|
154
155
|
last_mutation = n_mutations - 1
|
|
155
|
-
|
|
156
|
+
num_tries = self.num_tries
|
|
157
|
+
last_try = num_tries - 1
|
|
158
|
+
check_valid = self.check_valid
|
|
159
|
+
no_validator = check_valid is None
|
|
156
160
|
|
|
157
161
|
for mutation_i in range(n_mutations):
|
|
158
|
-
for tries in range(
|
|
162
|
+
for tries in range(num_tries):
|
|
159
163
|
selected = select_crossover_point(new_rule, operator_p=self.operator_p)
|
|
160
164
|
# selected = random.choice(new_rule.flatten())
|
|
161
165
|
|
|
@@ -166,11 +170,22 @@ class MutationExecutor:
|
|
|
166
170
|
else self.operator_mutations
|
|
167
171
|
).apply(selected)
|
|
168
172
|
except MutationError:
|
|
173
|
+
# TODO: Document the fact that MutationError triggers when mutations would leave the rules in
|
|
174
|
+
# illegal states. But a MutationError does not mean the rule has reached an illegal state
|
|
169
175
|
continue
|
|
170
176
|
|
|
171
|
-
if
|
|
177
|
+
if no_validator:
|
|
172
178
|
rule = new_rule
|
|
173
179
|
break
|
|
174
|
-
elif
|
|
175
|
-
|
|
180
|
+
elif check_valid(new_rule):
|
|
181
|
+
if mutation_i == last_mutation:
|
|
182
|
+
return new_rule # no need to copy anymore
|
|
183
|
+
else:
|
|
184
|
+
rule = new_rule.copy()
|
|
185
|
+
# We copy so that future mutations that do not pass checks would not affect the rule
|
|
186
|
+
break
|
|
187
|
+
else:
|
|
188
|
+
# Validation failed => we need to copy the rule
|
|
189
|
+
if mutation_i != last_mutation or tries != last_try:
|
|
190
|
+
new_rule = rule.copy()
|
|
176
191
|
return rule
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
class MutationError(Exception):
|
|
2
|
+
# Placeholder class used to signify that a mutation could not be executed because it would violate constraints
|
|
3
|
+
# TODO: Write in documentation that when creating mutations, mutations should not leave a rule in an invalid state.
|
|
4
|
+
# If they do, they should revert the changes and just raise MutationError
|
|
5
|
+
pass
|
|
@@ -35,6 +35,44 @@ class ColumnKind(str, Enum):
|
|
|
35
35
|
NUMERIC = "numeric"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def round_edges(edges: np.ndarray, values: np.ndarray) -> np.ndarray:
|
|
39
|
+
"""
|
|
40
|
+
Round each finite edge to the fewest decimals, at least 2, that keep every value in
|
|
41
|
+
the same bin. The edges are returned unchanged if they would no longer increase.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
edges (np.ndarray): Sorted bin edges, starting with ``-inf`` and ending with ``inf``.
|
|
45
|
+
values (np.ndarray): The values the edges were fitted on, without missing values.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
np.ndarray: The rounded edges.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> import numpy as np
|
|
52
|
+
>>> from hgp_lib.preprocessing.binarizer import round_edges
|
|
53
|
+
>>> values = np.array([0.1602, 0.1604, 0.4, 0.6])
|
|
54
|
+
>>> round_edges(np.array([-np.inf, 0.16029999405145645, 0.51, np.inf]), values).tolist()
|
|
55
|
+
[-inf, 0.1603, 0.51, inf]
|
|
56
|
+
"""
|
|
57
|
+
values = np.sort(values)
|
|
58
|
+
rounded = edges.copy()
|
|
59
|
+
for i in range(1, len(edges) - 1):
|
|
60
|
+
edge = float(edges[i])
|
|
61
|
+
below = np.searchsorted(values, edge) # Values below the edge.
|
|
62
|
+
low = values[below - 1] if below > 0 else -np.inf
|
|
63
|
+
high = values[below] if below < len(values) else np.inf
|
|
64
|
+
decimals = 2
|
|
65
|
+
while not low < round(edge, decimals) <= high:
|
|
66
|
+
decimals += 1
|
|
67
|
+
rounded[i] = round(edge, decimals) + 0.0 # + 0.0 turns -0.0 into 0.0.
|
|
68
|
+
return rounded if np.all(np.diff(rounded) > 0) else edges
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cut(values: np.ndarray, edges: np.ndarray) -> np.ndarray:
|
|
72
|
+
"""The index of the bin ``edges[i] <= value < edges[i + 1]`` of each value."""
|
|
73
|
+
return pd.cut(values, bins=edges, labels=False, include_lowest=True, right=False)
|
|
74
|
+
|
|
75
|
+
|
|
38
76
|
class StandardBinarizer(Binarizer):
|
|
39
77
|
"""
|
|
40
78
|
Converts a mixed-type DataFrame into a purely boolean DataFrame.
|
|
@@ -53,7 +91,7 @@ class StandardBinarizer(Binarizer):
|
|
|
53
91
|
generalization.
|
|
54
92
|
- Numeric columns are split into bins by a :class:`BinningStrategy`. When ``y`` is
|
|
55
93
|
provided and no strategy is set, :class:`SupervisedTreeBinning` is used, otherwise
|
|
56
|
-
:class:`QuantileBinning`.
|
|
94
|
+
:class:`QuantileBinning`. Bins are closed on the left: ``left <= x < right``.
|
|
57
95
|
- A column that contains missing values also gets a boolean ``<col>_is_NA``
|
|
58
96
|
indicator column.
|
|
59
97
|
|
|
@@ -69,8 +107,7 @@ class StandardBinarizer(Binarizer):
|
|
|
69
107
|
Per-column override for the number of bins. Keys are column names, values are
|
|
70
108
|
the desired bin count (each >= 2). Default: `None`.
|
|
71
109
|
precision (int):
|
|
72
|
-
|
|
73
|
-
Must be >= 0. Default: `3`.
|
|
110
|
+
Deprecated and unused. Default: `3`.
|
|
74
111
|
numeric_binning (BinningStrategy | None):
|
|
75
112
|
Strategy for computing numeric bin edges. When `None`, the binarizer uses
|
|
76
113
|
:class:`SupervisedTreeBinning` if labels are provided to ``fit_transform``
|
|
@@ -79,6 +116,10 @@ class StandardBinarizer(Binarizer):
|
|
|
79
116
|
Whether to show progress bar. Default: `True`.
|
|
80
117
|
leave_progress_bar (bool):
|
|
81
118
|
Whether to leave progress bar. Default: `False`.
|
|
119
|
+
round_edges (bool):
|
|
120
|
+
Whether to round each numeric edge to the fewest decimals, at least 2, that
|
|
121
|
+
keep every training value in the same bin (see :func:`round_edges`).
|
|
122
|
+
Default: `True`.
|
|
82
123
|
|
|
83
124
|
|
|
84
125
|
Examples:
|
|
@@ -92,10 +133,10 @@ class StandardBinarizer(Binarizer):
|
|
|
92
133
|
>>> result["flag"].tolist()
|
|
93
134
|
[True, False, True]
|
|
94
135
|
>>> result
|
|
95
|
-
flag val < 2.
|
|
96
|
-
0 True
|
|
97
|
-
1 False
|
|
98
|
-
2 True
|
|
136
|
+
flag val < 2.0 2.0 <= val
|
|
137
|
+
0 True True False
|
|
138
|
+
1 False False True
|
|
139
|
+
2 True False True
|
|
99
140
|
"""
|
|
100
141
|
|
|
101
142
|
def __init__(
|
|
@@ -106,13 +147,15 @@ class StandardBinarizer(Binarizer):
|
|
|
106
147
|
numeric_binning: BinningStrategy | None = None,
|
|
107
148
|
progress_bar: bool = True,
|
|
108
149
|
leave_progress_bar: bool = False,
|
|
150
|
+
round_edges: bool = True,
|
|
109
151
|
):
|
|
110
|
-
self._validate_params(num_bins, column_strategy,
|
|
152
|
+
self._validate_params(num_bins, column_strategy, round_edges, numeric_binning)
|
|
111
153
|
self.num_bins = num_bins
|
|
112
154
|
self.column_strategy = column_strategy or {}
|
|
113
|
-
self.precision = precision
|
|
155
|
+
self.precision = precision # Deprecated: unused.
|
|
156
|
+
self.column_precision: dict[str, int] = {} # Deprecated: unused.
|
|
157
|
+
self.round_edges = round_edges
|
|
114
158
|
self.numeric_binning = numeric_binning
|
|
115
|
-
self.column_precision: dict[str, int] = {}
|
|
116
159
|
self.progress_bar = progress_bar
|
|
117
160
|
self.leave_progress_bar = leave_progress_bar
|
|
118
161
|
|
|
@@ -130,7 +173,7 @@ class StandardBinarizer(Binarizer):
|
|
|
130
173
|
self,
|
|
131
174
|
num_bins: int,
|
|
132
175
|
column_strategy: dict[str, int] | None,
|
|
133
|
-
|
|
176
|
+
round_edges: bool,
|
|
134
177
|
numeric_binning: BinningStrategy | None,
|
|
135
178
|
) -> None:
|
|
136
179
|
check_isinstance(num_bins, int)
|
|
@@ -146,9 +189,7 @@ class StandardBinarizer(Binarizer):
|
|
|
146
189
|
f"Number of bins for column {col} must be an integer >= 2, is {bins}"
|
|
147
190
|
)
|
|
148
191
|
|
|
149
|
-
check_isinstance(
|
|
150
|
-
if precision < 0:
|
|
151
|
-
raise ValueError(f"precision must be an integer >= 0, is {precision}")
|
|
192
|
+
check_isinstance(round_edges, bool)
|
|
152
193
|
|
|
153
194
|
if numeric_binning is not None:
|
|
154
195
|
check_isinstance(numeric_binning, BinningStrategy)
|
|
@@ -240,7 +281,7 @@ class StandardBinarizer(Binarizer):
|
|
|
240
281
|
>>> b = StandardBinarizer(num_bins=2)
|
|
241
282
|
>>> _ = b.fit_transform(pd.DataFrame({"x": [1.0, 2.0, 3.0, 4.0]}))
|
|
242
283
|
>>> b.get_feature_names_out()
|
|
243
|
-
['x < 2.
|
|
284
|
+
['x < 2.5', '2.5 <= x']
|
|
244
285
|
"""
|
|
245
286
|
if not self._is_fitted:
|
|
246
287
|
raise NotFittedError(
|
|
@@ -379,9 +420,11 @@ class StandardBinarizer(Binarizer):
|
|
|
379
420
|
|
|
380
421
|
strategy = self._resolve_numeric_binning(y)
|
|
381
422
|
edges = strategy.compute_edges(fit_values, fit_y, n_bins)
|
|
423
|
+
if self.round_edges:
|
|
424
|
+
edges = round_edges(edges, fit_values)
|
|
382
425
|
self._numerical_bins[column] = edges
|
|
383
426
|
|
|
384
|
-
binned =
|
|
427
|
+
binned = _cut(values, edges)
|
|
385
428
|
return [
|
|
386
429
|
(
|
|
387
430
|
self._format_numeric_bin_name(column, edges[i], edges[i + 1]),
|
|
@@ -417,9 +460,7 @@ class StandardBinarizer(Binarizer):
|
|
|
417
460
|
|
|
418
461
|
def _transform_numeric(self, column: str, series: pd.Series) -> list[np.ndarray]:
|
|
419
462
|
edges = self._numerical_bins[column]
|
|
420
|
-
binned =
|
|
421
|
-
series.to_numpy(), bins=edges, labels=False, include_lowest=True
|
|
422
|
-
)
|
|
463
|
+
binned = _cut(series.to_numpy(), edges)
|
|
423
464
|
return [binned == i for i in range(len(edges) - 1)]
|
|
424
465
|
|
|
425
466
|
def _resolve_numeric_binning(self, y: np.ndarray | None) -> BinningStrategy:
|
|
@@ -511,20 +552,19 @@ class StandardBinarizer(Binarizer):
|
|
|
511
552
|
Examples:
|
|
512
553
|
>>> import numpy as np
|
|
513
554
|
>>> from hgp_lib.preprocessing import StandardBinarizer
|
|
514
|
-
>>> b = StandardBinarizer(
|
|
555
|
+
>>> b = StandardBinarizer()
|
|
515
556
|
>>> b._format_numeric_bin_name("x", -np.inf, 3.0)
|
|
516
|
-
'x < 3.
|
|
557
|
+
'x < 3.0'
|
|
517
558
|
>>> b._format_numeric_bin_name("x", 1.0, np.inf)
|
|
518
|
-
'1.
|
|
519
|
-
>>> b._format_numeric_bin_name("x",
|
|
520
|
-
'
|
|
559
|
+
'1.0 <= x'
|
|
560
|
+
>>> b._format_numeric_bin_name("x", 0.1597, 0.16029999405145645)
|
|
561
|
+
'0.1597 <= x < 0.16029999405145645'
|
|
521
562
|
"""
|
|
522
|
-
precision = self.column_precision.get(column, self.precision)
|
|
523
563
|
if np.isneginf(left):
|
|
524
|
-
return f"{column} < {right
|
|
564
|
+
return f"{column} < {right}"
|
|
525
565
|
if np.isposinf(right):
|
|
526
|
-
return f"{left
|
|
527
|
-
return f"{left
|
|
566
|
+
return f"{left} <= {column}"
|
|
567
|
+
return f"{left} <= {column} < {right}"
|
|
528
568
|
|
|
529
569
|
|
|
530
570
|
if __name__ == "__main__":
|
|
@@ -38,9 +38,9 @@ def confusion_matrix(
|
|
|
38
38
|
(1, 1, 1, 1)
|
|
39
39
|
"""
|
|
40
40
|
if sample_weight is None:
|
|
41
|
-
tp = (y_pred & y_true)
|
|
42
|
-
fp = (y_pred & ~y_true)
|
|
43
|
-
total_true =
|
|
41
|
+
tp = np.count_nonzero(y_pred & y_true)
|
|
42
|
+
fp = np.count_nonzero(y_pred & ~y_true)
|
|
43
|
+
total_true = np.count_nonzero(y_true)
|
|
44
44
|
fn = total_true - tp
|
|
45
45
|
tn = len(y_pred) - total_true - fp
|
|
46
46
|
else:
|
|
@@ -80,11 +80,11 @@ def fast_f1_score(
|
|
|
80
80
|
0.5
|
|
81
81
|
"""
|
|
82
82
|
if sample_weight is None:
|
|
83
|
-
y_pred_sum =
|
|
84
|
-
y_true_sum =
|
|
83
|
+
y_pred_sum = np.count_nonzero(y_pred)
|
|
84
|
+
y_true_sum = np.count_nonzero(y_true)
|
|
85
85
|
if y_pred_sum == 0 or y_true_sum == 0:
|
|
86
86
|
return 1.0 if y_pred_sum == 0 and y_true_sum == 0 else 0.0
|
|
87
|
-
return float(2 * (y_pred & y_true)
|
|
87
|
+
return float(2 * np.count_nonzero(y_pred & y_true) / (y_pred_sum + y_true_sum))
|
|
88
88
|
|
|
89
89
|
y_pred_sum = np.dot(y_pred, sample_weight)
|
|
90
90
|
y_true_sum = np.dot(y_true, sample_weight)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hgp-lib
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: hgp-lib: Hierarchical Genetic Programming Library for Generating Boolean Rules from Tabular Data
|
|
5
5
|
Author-email: Ramona-Georgiana Albert <ramona-georgiana.albert@info.uaic.ro>, George Stoica <george.stoica@info.uaic.ro>
|
|
6
6
|
Maintainer-email: George Stoica <george.stoica@info.uaic.ro>, Ramona-Georgiana Albert <ramona-georgiana.albert@info.uaic.ro>
|
|
@@ -5,6 +5,7 @@ import pandas as pd
|
|
|
5
5
|
from sklearn.exceptions import NotFittedError
|
|
6
6
|
|
|
7
7
|
from hgp_lib.preprocessing import SchemaMismatchError, StandardBinarizer
|
|
8
|
+
from hgp_lib.preprocessing.binarizer import round_edges
|
|
8
9
|
from hgp_lib.preprocessing.binning import (
|
|
9
10
|
BinningStrategy,
|
|
10
11
|
QuantileBinning,
|
|
@@ -48,14 +49,10 @@ class TestStandardBinarizer(unittest.TestCase):
|
|
|
48
49
|
with self.assertRaises(ValueError):
|
|
49
50
|
StandardBinarizer(column_strategy={"a": 1})
|
|
50
51
|
|
|
51
|
-
def
|
|
52
|
+
def test_round_edges_must_be_bool(self):
|
|
52
53
|
with self.assertRaises(TypeError):
|
|
53
|
-
StandardBinarizer(
|
|
54
|
-
|
|
55
|
-
def test_precision_minimum(self):
|
|
56
|
-
with self.assertRaises(ValueError):
|
|
57
|
-
StandardBinarizer(precision=-1)
|
|
58
|
-
StandardBinarizer(precision=0)
|
|
54
|
+
StandardBinarizer(round_edges=1)
|
|
55
|
+
StandardBinarizer(round_edges=True)
|
|
59
56
|
|
|
60
57
|
def test_numeric_binning_must_be_strategy(self):
|
|
61
58
|
with self.assertRaises(TypeError):
|
|
@@ -154,9 +151,9 @@ class TestStandardBinarizer(unittest.TestCase):
|
|
|
154
151
|
def test_numeric_exact_columns_and_values(self):
|
|
155
152
|
df = pd.DataFrame({"x": [1.0, 2.0, 3.0, 4.0]})
|
|
156
153
|
result = StandardBinarizer(num_bins=2).fit_transform(df)
|
|
157
|
-
self.assertEqual(list(result.columns), ["x < 2.
|
|
158
|
-
self.assertEqual(result["x < 2.
|
|
159
|
-
self.assertEqual(result["2.
|
|
154
|
+
self.assertEqual(list(result.columns), ["x < 2.5", "2.5 <= x"])
|
|
155
|
+
self.assertEqual(result["x < 2.5"].tolist(), [True, True, False, False])
|
|
156
|
+
self.assertEqual(result["2.5 <= x"].tolist(), [False, False, True, True])
|
|
160
157
|
|
|
161
158
|
def test_numeric_column_strategy_override(self):
|
|
162
159
|
df = pd.DataFrame({"a": range(20), "b": range(20)})
|
|
@@ -287,28 +284,94 @@ class TestStandardBinarizer(unittest.TestCase):
|
|
|
287
284
|
# _format_numeric_bin_name
|
|
288
285
|
# ------------------------------------------------------------------ #
|
|
289
286
|
def test_format_left_inf(self):
|
|
290
|
-
b = StandardBinarizer(
|
|
291
|
-
self.assertEqual(b._format_numeric_bin_name("v", -np.inf, 3.0), "v < 3.
|
|
287
|
+
b = StandardBinarizer()
|
|
288
|
+
self.assertEqual(b._format_numeric_bin_name("v", -np.inf, 3.0), "v < 3.0")
|
|
292
289
|
|
|
293
290
|
def test_format_right_inf(self):
|
|
294
|
-
b = StandardBinarizer(
|
|
295
|
-
self.assertEqual(b._format_numeric_bin_name("v", 1.0, np.inf), "1.
|
|
291
|
+
b = StandardBinarizer()
|
|
292
|
+
self.assertEqual(b._format_numeric_bin_name("v", 1.0, np.inf), "1.0 <= v")
|
|
296
293
|
|
|
297
294
|
def test_format_both_finite(self):
|
|
298
|
-
b = StandardBinarizer(
|
|
299
|
-
self.assertEqual(b._format_numeric_bin_name("v", 1.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
295
|
+
b = StandardBinarizer()
|
|
296
|
+
self.assertEqual(b._format_numeric_bin_name("v", 1.5, 3.0), "1.5 <= v < 3.0")
|
|
297
|
+
|
|
298
|
+
# ------------------------------------------------------------------ #
|
|
299
|
+
# round_edges
|
|
300
|
+
# ------------------------------------------------------------------ #
|
|
301
|
+
def test_value_on_an_edge_falls_in_the_right_bin(self):
|
|
302
|
+
class EdgeAtTwo(BinningStrategy):
|
|
303
|
+
def compute_edges(self, values, y, n_bins):
|
|
304
|
+
return np.array([-np.inf, 2.0, np.inf])
|
|
305
|
+
|
|
306
|
+
b = StandardBinarizer(numeric_binning=EdgeAtTwo())
|
|
307
|
+
result = b.fit_transform(pd.DataFrame({"x": [1.0, 2.0, 3.0]}))
|
|
308
|
+
self.assertEqual(result["2.0 <= x"].tolist(), [False, True, True])
|
|
309
|
+
test = b.transform(pd.DataFrame({"x": [2.0, np.nextafter(2.0, 0)]}))
|
|
310
|
+
self.assertEqual(test["2.0 <= x"].tolist(), [True, False])
|
|
311
|
+
|
|
312
|
+
def test_round_edges_false_keeps_the_strategy_edges(self):
|
|
313
|
+
X = pd.DataFrame({"x": [0.1597, 0.1602, 0.1604, 0.1611]})
|
|
314
|
+
y = np.array([0, 0, 1, 1])
|
|
315
|
+
b = StandardBinarizer(num_bins=2, round_edges=False)
|
|
316
|
+
b.fit_transform(X, y)
|
|
317
|
+
expected = SupervisedTreeBinning().compute_edges(X["x"].to_numpy(), y, 2)
|
|
318
|
+
np.testing.assert_array_equal(b._numerical_bins["x"], expected)
|
|
319
|
+
self.assertEqual(b.get_feature_names_out()[0], "x < 0.16030000150203705")
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _rounded(values, *edges):
|
|
323
|
+
edges = np.array([-np.inf, *edges, np.inf])
|
|
324
|
+
return round_edges(edges, np.array(values))[1:-1].tolist()
|
|
325
|
+
|
|
326
|
+
def test_round_edges_uses_fewest_decimals(self):
|
|
327
|
+
self.assertEqual(self._rounded([0.1602, 0.1604], 0.16029999405145645), [0.1603])
|
|
328
|
+
|
|
329
|
+
def test_round_edges_keeps_at_least_two_decimals(self):
|
|
330
|
+
self.assertEqual(self._rounded([0.4, 0.6], 0.51), [0.51])
|
|
331
|
+
self.assertEqual(self._rounded([0.1, 0.2], 0.1234), [0.12])
|
|
332
|
+
|
|
333
|
+
def test_round_edges_adds_decimals_when_rounding_moves_a_value(self):
|
|
334
|
+
# 1.00 would move 1.0 to the right bin, and 2.00 would move 1.996 to the left.
|
|
335
|
+
self.assertEqual(self._rounded([1.0, 1.01], 1.004), [1.004])
|
|
336
|
+
self.assertEqual(self._rounded([1.0, 1.996], 1.9958), [1.996])
|
|
337
|
+
|
|
338
|
+
def test_round_edges_keeps_a_value_on_the_edge_in_the_right_bin(self):
|
|
339
|
+
self.assertEqual(self._rounded([1.0, 2.123456, 3.0], 2.123456), [2.12])
|
|
340
|
+
|
|
341
|
+
def test_round_edges_outside_the_data(self):
|
|
342
|
+
self.assertEqual(self._rounded([0.0, 1.0], 5.1234), [5.12])
|
|
343
|
+
self.assertEqual(self._rounded([10.0, 11.0], -3.456), [-3.46])
|
|
344
|
+
|
|
345
|
+
def test_round_edges_unchanged_when_edges_collide(self):
|
|
346
|
+
# No value lies between the two edges, so both would round to 1.5.
|
|
347
|
+
self.assertEqual(self._rounded([0.0, 1.0, 2.0], 1.5, 1.501), [1.5, 1.501])
|
|
348
|
+
|
|
349
|
+
def test_round_edges_never_moves_training_values(self):
|
|
350
|
+
from sklearn.datasets import load_breast_cancer
|
|
351
|
+
|
|
352
|
+
rng = np.random.default_rng(0)
|
|
353
|
+
random = pd.DataFrame(
|
|
354
|
+
{
|
|
355
|
+
"normal": rng.normal(size=400),
|
|
356
|
+
"ties": rng.integers(0, 6, size=400).astype(float),
|
|
357
|
+
"tiny": rng.normal(scale=1e-6, size=400),
|
|
358
|
+
"large": rng.normal(loc=1e6, scale=1e3, size=400),
|
|
359
|
+
}
|
|
311
360
|
)
|
|
361
|
+
random.loc[::17, "normal"] = np.nan
|
|
362
|
+
cancer, cancer_y = load_breast_cancer(return_X_y=True, as_frame=True)
|
|
363
|
+
datasets = [
|
|
364
|
+
(random, (rng.random(400) < 0.4).astype(int)), # Tree binning.
|
|
365
|
+
(random, None), # Quantile binning.
|
|
366
|
+
(cancer, cancer_y.to_numpy()),
|
|
367
|
+
]
|
|
368
|
+
for X, y in datasets:
|
|
369
|
+
full = StandardBinarizer(round_edges=False).fit_transform(X, y)
|
|
370
|
+
b = StandardBinarizer()
|
|
371
|
+
rounded = b.fit_transform(X, y)
|
|
372
|
+
np.testing.assert_array_equal(rounded.to_numpy(), full.to_numpy())
|
|
373
|
+
np.testing.assert_array_equal(b.transform(X).to_numpy(), full.to_numpy())
|
|
374
|
+
self.assertEqual(len(set(rounded.columns)), len(rounded.columns))
|
|
312
375
|
|
|
313
376
|
# ------------------------------------------------------------------ #
|
|
314
377
|
# Mixed types
|
|
@@ -169,6 +169,41 @@ class TestMutationExecutor(unittest.TestCase):
|
|
|
169
169
|
executor.apply(rules)
|
|
170
170
|
self.assertEqual(rules[0].value, 1)
|
|
171
171
|
|
|
172
|
+
def test_apply_keeps_first_mutation_when_second_is_invalid(self):
|
|
173
|
+
increment = _IncrementLiteral()
|
|
174
|
+
negate = NegateMutation()
|
|
175
|
+
|
|
176
|
+
def valid_incremented_non_negated_rule(rule: Rule):
|
|
177
|
+
return rule.value == 1 and not rule.negated
|
|
178
|
+
|
|
179
|
+
executor = MutationExecutor(
|
|
180
|
+
literal_mutations=[increment, negate],
|
|
181
|
+
operator_mutations=[_ToggleOperator()],
|
|
182
|
+
mutation_p=1.0,
|
|
183
|
+
check_valid=valid_incremented_non_negated_rule,
|
|
184
|
+
num_tries=1,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
rules = [Literal(value=0)]
|
|
188
|
+
|
|
189
|
+
def apply_twice(self, rules: list[Rule]):
|
|
190
|
+
rules[0] = self._mutate(rules[0], 2)
|
|
191
|
+
|
|
192
|
+
with (
|
|
193
|
+
patch.object(MutationExecutor, "apply", new=apply_twice),
|
|
194
|
+
patch(
|
|
195
|
+
"hgp_lib.mutations.mutation_executor.random.choice",
|
|
196
|
+
side_effect=[increment, negate],
|
|
197
|
+
),
|
|
198
|
+
):
|
|
199
|
+
executor.apply(rules)
|
|
200
|
+
|
|
201
|
+
# The increment is valid and is therefore preserved.
|
|
202
|
+
self.assertEqual(rules[0].value, 1)
|
|
203
|
+
|
|
204
|
+
# The subsequent negation is invalid and is therefore discarded.
|
|
205
|
+
self.assertFalse(rules[0].negated)
|
|
206
|
+
|
|
172
207
|
# ------------------------------------------------------------------ #
|
|
173
208
|
# _mutate — direct tests
|
|
174
209
|
# ------------------------------------------------------------------ #
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|