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.
Files changed (93) hide show
  1. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/PKG-INFO +1 -1
  2. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/pyproject.toml +1 -1
  3. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/algorithms/boolean_gp.py +1 -1
  4. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/mutation_executor.py +20 -5
  5. hgp_lib-1.2.4/src/hgp_lib/mutations/utils.py +5 -0
  6. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/binarizer.py +68 -28
  7. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/metrics.py +6 -6
  8. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/PKG-INFO +1 -1
  9. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_binarizer.py +90 -27
  10. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_mutation_executor.py +35 -0
  11. hgp_lib-1.2.2/src/hgp_lib/mutations/utils.py +0 -3
  12. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/LICENSE +0 -0
  13. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/README.md +0 -0
  14. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/setup.cfg +0 -0
  15. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/setup.py +0 -0
  16. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/__init__.py +0 -0
  17. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/algorithms/__init__.py +0 -0
  18. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/__init__.py +0 -0
  19. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/gp_benchmarker.py +0 -0
  20. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/progress.py +0 -0
  21. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/benchmarkers/runner.py +0 -0
  22. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/__init__.py +0 -0
  23. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/benchmarker_config.py +0 -0
  24. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/boolean_gp_config.py +0 -0
  25. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/configs/trainer_config.py +0 -0
  26. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/__init__.py +0 -0
  27. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/crossover_executor.py +0 -0
  28. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/crossover/crossover_factory.py +0 -0
  29. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/__init__.py +0 -0
  30. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/core.py +0 -0
  31. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/history.py +0 -0
  32. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/metrics/results.py +0 -0
  33. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/__init__.py +0 -0
  34. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/base_mutation.py +0 -0
  35. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/literal_mutations.py +0 -0
  36. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/mutation_factory.py +0 -0
  37. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/mutations/operator_mutations.py +0 -0
  38. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/__init__.py +0 -0
  39. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/base_strategy.py +0 -0
  40. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/generator.py +0 -0
  41. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/populations_factory.py +0 -0
  42. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/sampling.py +0 -0
  43. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/populations/strategies.py +0 -0
  44. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/__init__.py +0 -0
  45. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/base.py +0 -0
  46. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/binning.py +0 -0
  47. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/errors.py +0 -0
  48. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/sklearn_binarizer.py +0 -0
  49. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/utils.py +0 -0
  50. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/preprocessing/warnings.py +0 -0
  51. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/__init__.py +0 -0
  52. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/literals.py +0 -0
  53. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/low_memory_operators.py +0 -0
  54. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/operators.py +0 -0
  55. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/rules.py +0 -0
  56. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/rules/utils.py +0 -0
  57. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/__init__.py +0 -0
  58. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/base_selection.py +0 -0
  59. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/roulette_selection.py +0 -0
  60. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/selections/tournament_selection.py +0 -0
  61. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/__init__.py +0 -0
  62. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/boolean_rule_classifier.py +0 -0
  63. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/trainers/gp_trainer.py +0 -0
  64. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/__init__.py +0 -0
  65. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/validation.py +0 -0
  66. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib/utils/warnings.py +0 -0
  67. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/SOURCES.txt +0 -0
  68. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/dependency_links.txt +0 -0
  69. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/requires.txt +0 -0
  70. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/src/hgp_lib.egg-info/top_level.txt +0 -0
  71. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_binarizer_benchmark.py +0 -0
  72. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_boolean_gp.py +0 -0
  73. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_boolean_rule_classifier.py +0 -0
  74. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_configs.py +0 -0
  75. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_crossover.py +0 -0
  76. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_gp_benchmarker.py +0 -0
  77. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_gp_trainer.py +0 -0
  78. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_hierarchical_gp.py +0 -0
  79. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_history.py +0 -0
  80. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_literal_mutations.py +0 -0
  81. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_metrics_core.py +0 -0
  82. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_mutation_factory.py +0 -0
  83. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_operator_mutations.py +0 -0
  84. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_populations.py +0 -0
  85. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_preprocessing_utils.py +0 -0
  86. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_progress.py +0 -0
  87. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_results.py +0 -0
  88. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_rules.py +0 -0
  89. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_rules_utils.py +0 -0
  90. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_sampling.py +0 -0
  91. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_selections.py +0 -0
  92. {hgp_lib-1.2.2 → hgp_lib-1.2.4}/tests/test_utils_metrics.py +0 -0
  93. {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.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hgp-lib"
3
- version = "1.2.2"
3
+ version = "1.2.4"
4
4
  requires-python = ">=3.10"
5
5
  description = "hgp-lib: Hierarchical Genetic Programming Library for Generating Boolean Rules from Tabular Data"
6
6
  readme = "README.md"
@@ -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
- last_try = self.num_tries - 1
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(self.num_tries):
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 self.check_valid is None or self.check_valid(new_rule):
177
+ if no_validator:
172
178
  rule = new_rule
173
179
  break
174
- elif mutation_i != last_mutation or tries != last_try:
175
- new_rule = rule.copy()
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
- Number of decimal places used when formatting numeric bin boundary names.
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.000 2.000 <= val
96
- 0 True True False
97
- 1 False True False
98
- 2 True False 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, precision, numeric_binning)
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
- precision: int,
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(precision, int)
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.500', '2.500 <= x']
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 = pd.cut(values, bins=edges, labels=False, include_lowest=True)
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 = pd.cut(
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(precision=2)
555
+ >>> b = StandardBinarizer()
515
556
  >>> b._format_numeric_bin_name("x", -np.inf, 3.0)
516
- 'x < 3.00'
557
+ 'x < 3.0'
517
558
  >>> b._format_numeric_bin_name("x", 1.0, np.inf)
518
- '1.00 <= x'
519
- >>> b._format_numeric_bin_name("x", 1.0, 3.0)
520
- '1.00 <= x < 3.00'
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:.{precision}f}"
564
+ return f"{column} < {right}"
525
565
  if np.isposinf(right):
526
- return f"{left:.{precision}f} <= {column}"
527
- return f"{left:.{precision}f} <= {column} < {right:.{precision}f}"
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).sum()
42
- fp = (y_pred & ~y_true).sum()
43
- total_true = y_true.sum()
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 = y_pred.sum()
84
- y_true_sum = 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).sum() / (y_pred_sum + y_true_sum))
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.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 test_precision_must_be_int(self):
52
+ def test_round_edges_must_be_bool(self):
52
53
  with self.assertRaises(TypeError):
53
- StandardBinarizer(precision=1.0)
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.500", "2.500 <= x"])
158
- self.assertEqual(result["x < 2.500"].tolist(), [True, True, False, False])
159
- self.assertEqual(result["2.500 <= x"].tolist(), [False, False, True, True])
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(precision=2)
291
- self.assertEqual(b._format_numeric_bin_name("v", -np.inf, 3.0), "v < 3.00")
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(precision=2)
295
- self.assertEqual(b._format_numeric_bin_name("v", 1.0, np.inf), "1.00 <= v")
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(precision=1)
299
- self.assertEqual(b._format_numeric_bin_name("v", 1.0, 3.0), "1.0 <= v < 3.0")
300
-
301
- def test_format_precision_zero(self):
302
- b = StandardBinarizer(precision=0)
303
- self.assertEqual(b._format_numeric_bin_name("x", 2.7, 5.3), "3 <= x < 5")
304
-
305
- def test_format_column_precision_overrides_default(self):
306
- b = StandardBinarizer(precision=5)
307
- b.column_precision["x"] = 1
308
- self.assertEqual(b._format_numeric_bin_name("x", 1.0, 2.0), "1.0 <= x < 2.0")
309
- self.assertEqual(
310
- b._format_numeric_bin_name("y", 1.0, 2.0), "1.00000 <= y < 2.00000"
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
  # ------------------------------------------------------------------ #
@@ -1,3 +0,0 @@
1
- class MutationError(Exception):
2
- # Placeholder class used to signify that a mutation could not be executed because it would violate constraints
3
- pass
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