AutoCarver 7.2.1__tar.gz → 7.2.2__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 (71) hide show
  1. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/binary_carver.py +3 -3
  2. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/continuous_carver.py +4 -4
  3. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/multiclass_carver.py +11 -6
  4. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/utils/base_carver.py +11 -10
  5. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/utils/pretty_print.py +3 -3
  6. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/binary/binary_combination_evaluators.py +11 -4
  7. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/binary/binary_target_rates.py +13 -4
  8. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/continuous/continuous_combination_evaluators.py +19 -11
  9. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/continuous/continuous_target_rates.py +19 -6
  10. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/utils/combination_evaluator.py +61 -32
  11. autocarver-7.2.2/AutoCarver/combinations/utils/target_rate.py +52 -0
  12. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/discretizer.py +2 -2
  13. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/qualitatives/categorical_discretizer.py +4 -4
  14. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/qualitatives/chained_discretizer.py +4 -8
  15. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/qualitatives/ordinal_discretizer.py +4 -9
  16. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/qualitatives/qualitative_discretizer.py +3 -7
  17. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/quantitatives/continuous_discretizer.py +1 -3
  18. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/quantitatives/quantitative_discretizer.py +4 -6
  19. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/utils/base_discretizer.py +57 -13
  20. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/utils/type_discretizers.py +4 -4
  21. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/utils/base_selector.py +7 -28
  22. {autocarver-7.2.1 → autocarver-7.2.2}/PKG-INFO +1 -1
  23. {autocarver-7.2.1 → autocarver-7.2.2}/pyproject.toml +2 -8
  24. autocarver-7.2.1/AutoCarver/combinations/utils/target_rate.py +0 -30
  25. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/__init__.py +0 -0
  26. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/__init__.py +0 -0
  27. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/carvers/utils/__init__.py +0 -0
  28. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/__init__.py +0 -0
  29. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/binary/__init__.py +0 -0
  30. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/continuous/__init__.py +0 -0
  31. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/utils/__init__.py +0 -0
  32. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/utils/combinations.py +0 -0
  33. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/combinations/utils/testing.py +0 -0
  34. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/config.py +0 -0
  35. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/__init__.py +0 -0
  36. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/qualitatives/__init__.py +0 -0
  37. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/quantitatives/__init__.py +0 -0
  38. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/utils/__init__.py +0 -0
  39. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/discretizers/utils/multiprocessing.py +0 -0
  40. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/__init__.py +0 -0
  41. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/features.py +0 -0
  42. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/qualitatives/__init__.py +0 -0
  43. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/qualitatives/categorical_feature.py +0 -0
  44. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/qualitatives/ordinal_feature.py +0 -0
  45. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/qualitatives/qualitative_feature.py +0 -0
  46. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/quantitatives/__init__.py +0 -0
  47. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/quantitatives/datetime_feature.py +0 -0
  48. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/quantitatives/numerical_feature.py +0 -0
  49. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/quantitatives/quantitative_feature.py +0 -0
  50. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/utils/__init__.py +0 -0
  51. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/utils/base_feature.py +0 -0
  52. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/utils/grouped_list.py +0 -0
  53. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/features/utils/serialization.py +0 -0
  54. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/__init__.py +0 -0
  55. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/classification_selector.py +0 -0
  56. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/filters/__init__.py +0 -0
  57. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/filters/base_filters.py +0 -0
  58. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/filters/qualitative_filters.py +0 -0
  59. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/filters/quantitative_filters.py +0 -0
  60. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/measures/__init__.py +0 -0
  61. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/measures/base_measures.py +0 -0
  62. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/measures/qualitative_measures.py +0 -0
  63. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/measures/quantitative_measures.py +0 -0
  64. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/regression_selector.py +0 -0
  65. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/utils/__init__.py +0 -0
  66. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/selectors/utils/pretty_print.py +0 -0
  67. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/utils/__init__.py +0 -0
  68. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/utils/attributes.py +0 -0
  69. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/utils/dependencies.py +0 -0
  70. {autocarver-7.2.1 → autocarver-7.2.2}/AutoCarver/utils/extend_docstring.py +0 -0
  71. {autocarver-7.2.1 → autocarver-7.2.2}/README.md +0 -0
@@ -65,7 +65,7 @@ class BinaryCarver(BaseCarver):
65
65
  config=config,
66
66
  )
67
67
 
68
- def _prepare_data(self, samples: Samples) -> Samples:
68
+ def _prepare_samples(self, samples: Samples) -> Samples:
69
69
  """Validates format and content of X and y."""
70
70
  if samples.train.y is None:
71
71
  raise ValueError(f"[{self.__name__}] y must be provided")
@@ -73,9 +73,9 @@ class BinaryCarver(BaseCarver):
73
73
  if not ((0 in y_values) and (1 in y_values)) or len(y_values) != 2:
74
74
  raise ValueError(f"[{self.__name__}] y must be a binary Series of 0 and 1 (int or float, not object)")
75
75
 
76
- return super()._prepare_data(samples)
76
+ return super()._prepare_samples(samples)
77
77
 
78
- def _aggregator(self, X: pd.DataFrame, y: pd.Series) -> dict[str, pd.DataFrame | None]:
78
+ def _aggregator(self, X: pd.DataFrame, y: pd.Series) -> dict[str, pd.Series | pd.DataFrame | None]:
79
79
  """Computes crosstabs for specified features and ensures that the crosstab is ordered
80
80
  according to the known labels"""
81
81
  # checking for empty datasets (dev)
@@ -63,7 +63,7 @@ class ContinuousCarver(BaseCarver):
63
63
  config=config,
64
64
  )
65
65
 
66
- def _prepare_data(self, samples: Samples) -> Samples:
66
+ def _prepare_samples(self, samples: Samples) -> Samples:
67
67
  """Validates format and content of X and y."""
68
68
  if samples.train.y is None:
69
69
  raise ValueError(f"[{self.__name__}] y must be provided")
@@ -74,9 +74,9 @@ class ContinuousCarver(BaseCarver):
74
74
  if len(y_values) <= 2:
75
75
  raise ValueError(f"[{self.__name__}] provided y is binary, consider using BinaryCarver instead.")
76
76
 
77
- return super()._prepare_data(samples)
77
+ return super()._prepare_samples(samples)
78
78
 
79
- def _aggregator(self, X: pd.DataFrame, y: pd.Series) -> dict[str, pd.DataFrame | None]:
79
+ def _aggregator(self, X: pd.DataFrame, y: pd.Series) -> dict[str, pd.Series | pd.DataFrame | None]:
80
80
  """Computes y values for modalities of specified features and ensures the ordering
81
81
  according to the known labels"""
82
82
  # checking for empty datasets
@@ -98,4 +98,4 @@ def get_target_values_by_modality(X: pd.DataFrame, y: pd.Series, feature: BaseFe
98
98
 
99
99
  # reindexing to ensure the right order (labels may be None pre-fit; pandas
100
100
  # treats None as "no reindex" so the original ordering is kept)
101
- return yval.reindex(feature.labels, fill_value=[]) # type: ignore[arg-type]
101
+ return yval.reindex(feature.labels, fill_value=[]) # type: ignore
@@ -53,7 +53,7 @@ class MulticlassCarver(BinaryCarver):
53
53
  if self.config.copy:
54
54
  print("WARNING: can't set copy=True for MulticlassCarver (no inplace DataFrame.assign).")
55
55
 
56
- def _prepare_data(self, samples: Samples) -> Samples:
56
+ def _prepare_samples(self, samples: Samples) -> Samples:
57
57
  """Validates format and content of X and y."""
58
58
  # converting target to str (y is required by Carver.fit)
59
59
  if samples.train.y is None:
@@ -93,11 +93,11 @@ class MulticlassCarver(BinaryCarver):
93
93
  samples = Samples(train=Sample(X, y), dev=Sample(X_dev, y_dev))
94
94
 
95
95
  # preparing datasets and checking for wrong values
96
- samples = self._prepare_data(samples)
96
+ samples = self._prepare_samples(samples)
97
97
 
98
- # getting distinct y classes (_prepare_data raises if y is missing)
99
- assert samples.train.y is not None
100
- y_classes = sorted(samples.train.y.unique().tolist())[1:] # removing one of the classes
98
+ # getting distinct y classes (_prepare_samples raises if y is missing)
99
+ # removing one of the classes
100
+ y_classes = sorted(samples.train.y.unique().tolist())[1:] # type: ignore
101
101
 
102
102
  # adding versionned features
103
103
  self.features.add_feature_versions(y_classes)
@@ -125,7 +125,12 @@ class MulticlassCarver(BinaryCarver):
125
125
  config=replace(self.config, copy=True),
126
126
  )
127
127
 
128
- binary_carver.fit_transform(samples.train.X, train_y_class, X_dev=samples.dev.X, y_dev=dev_y_class)
128
+ binary_carver.fit_transform(
129
+ samples.train.X,
130
+ train_y_class,
131
+ X_dev=samples.dev.X if samples.dev.has_X else None,
132
+ y_dev=dev_y_class,
133
+ )
129
134
 
130
135
  # filtering out dropped features whilst keeping other version tags
131
136
  kept_features = binary_carver.features.versions
@@ -41,13 +41,13 @@ class Samples:
41
41
  dev (Sample): The development sample, containing features (X) and target (y).
42
42
  """
43
43
 
44
- train: Sample = field(default_factory=lambda: Sample(X=None))
45
- dev: Sample = field(default_factory=lambda: Sample(X=None))
44
+ train: Sample = field(default_factory=Sample)
45
+ dev: Sample = field(default_factory=Sample)
46
46
 
47
47
  def fillna(self, features: Features) -> None:
48
48
  """fills up nans in X and X_dev"""
49
49
  self.train.X = features.fillna(self.train.X)
50
- if self.dev.X is not None:
50
+ if self.dev.has_X:
51
51
  self.dev.X = features.fillna(self.dev.X)
52
52
 
53
53
 
@@ -175,14 +175,14 @@ class BaseCarver(BaseDiscretizer, ABC):
175
175
  content["combination_evaluator"] = self.combination_evaluator.to_json()
176
176
  return content
177
177
 
178
- def _prepare_data(self, samples: Samples) -> Samples:
178
+ def _prepare_samples(self, samples: Samples) -> Samples:
179
179
  """Validates format and content of X and y."""
180
180
  if samples.train.y is None:
181
181
  raise ValueError(f"[{self.__name__}] y must be provided, got {samples.train.y}")
182
182
 
183
183
  # Checking for binary target and copying X
184
- samples.train = super()._prepare_data(samples.train)
185
- samples.dev = super()._prepare_data(samples.dev)
184
+ samples.train = super()._prepare_sample(samples.train)
185
+ samples.dev = super()._prepare_sample(samples.dev)
186
186
 
187
187
  # discretizing features at half min_freq so the carver has a finer
188
188
  # granularity to combine when forming optimal groups
@@ -196,7 +196,7 @@ class BaseCarver(BaseDiscretizer, ABC):
196
196
 
197
197
  return samples
198
198
 
199
- def fit( # pylint: disable=W0222
199
+ def fit( # type: ignore
200
200
  self,
201
201
  X: pd.DataFrame,
202
202
  y: pd.Series,
@@ -235,7 +235,7 @@ class BaseCarver(BaseDiscretizer, ABC):
235
235
  samples = Samples(Sample(X, y), Sample(X_dev, y_dev))
236
236
 
237
237
  # preparing datasets and checking for wrong values
238
- samples = self._prepare_data(samples)
238
+ samples = self._prepare_samples(samples)
239
239
 
240
240
  # logging if requested
241
241
  super()._log_if_verbose("---------\n------")
@@ -329,10 +329,11 @@ class BaseCarver(BaseDiscretizer, ABC):
329
329
 
330
330
  # printing carved distribution, for found, suitable combination
331
331
  if best_combination is not None:
332
+ dev_sample = self.combination_evaluator.samples.dev
332
333
  self._print_xagg(
333
334
  feature,
334
335
  xagg=self.combination_evaluator.samples.train.xagg,
335
- xagg_dev=self.combination_evaluator.samples.dev.xagg,
336
+ xagg_dev=dev_sample.xagg if dev_sample.has_xagg else None,
336
337
  message="Carved distribution",
337
338
  )
338
339
 
@@ -482,7 +483,7 @@ def discretize(
482
483
  samples.train.X = discretizer.fit_transform(**samples.train)
483
484
 
484
485
  # applying discretizer on X_dev if provided
485
- if samples.dev.X is not None:
486
+ if samples.dev.has_X:
486
487
  samples.dev.X = discretizer.transform(**samples.dev)
487
488
 
488
489
  return samples
@@ -44,14 +44,14 @@ def prettier_xagg(
44
44
 
45
45
  # adding custom caption/title
46
46
  if caption is not None:
47
- nicer_xagg = nicer_xagg.set_caption(caption)
47
+ nicer_xagg = nicer_xagg.set_caption(caption) # type: ignore
48
48
 
49
49
  # hiding index for dev
50
50
  if hide_index:
51
- nicer_xagg.hide(axis="index")
51
+ nicer_xagg.hide(axis="index") # type: ignore
52
52
 
53
53
  # converting to html
54
- nicer_xagg = nicer_xagg._repr_html_() # pylint: disable=W0212
54
+ nicer_xagg = nicer_xagg._repr_html_() # type: ignore
55
55
 
56
56
  return nicer_xagg
57
57
 
@@ -14,15 +14,19 @@ from AutoCarver.combinations.utils.combination_evaluator import (
14
14
  CombinationEvaluator,
15
15
  )
16
16
  from AutoCarver.combinations.utils.combinations import combination_formatter
17
+ from AutoCarver.combinations.utils.target_rate import TargetRate
17
18
 
18
19
 
19
- class BinaryCombinationEvaluator(CombinationEvaluator, ABC):
20
+ class BinaryCombinationEvaluator(CombinationEvaluator[pd.DataFrame], ABC):
20
21
  """Binary combination evaluator class."""
21
22
 
22
23
  is_y_binary = True
23
24
  _target_rate_classes: list[type[BinaryTargetRate]] = [TargetMean, OddsRatio, Woe]
25
+ # narrow inherited attribute: binary evaluators always carry a BinaryTargetRate
26
+ # (enforced by _init_target_rate).
27
+ target_rate: BinaryTargetRate
24
28
 
25
- def _init_target_rate(self, target_rate: BinaryTargetRate | None) -> BinaryTargetRate:
29
+ def _init_target_rate(self, target_rate: TargetRate[pd.DataFrame] | None) -> BinaryTargetRate:
26
30
  """Initializes target rate."""
27
31
  if target_rate is None:
28
32
  return TargetMean()
@@ -31,8 +35,11 @@ class BinaryCombinationEvaluator(CombinationEvaluator, ABC):
31
35
  return target_rate
32
36
 
33
37
  def _association_measure(
34
- self, xagg: AggregatedSample, n_obs: int | None = None, tol: float = 1e-10
35
- ) -> dict[str, float]:
38
+ self,
39
+ xagg: AggregatedSample | pd.Series | pd.DataFrame,
40
+ n_obs: int | None = None,
41
+ tol: float = 1e-10,
42
+ ) -> dict[str, float | None]:
36
43
  """Computes measures of association between feature and target by crosstab.
37
44
 
38
45
  Used for the raw (one-shot) distribution. The hot per-combination loop
@@ -1,6 +1,7 @@
1
1
  """set of target rates for binary classification"""
2
2
 
3
3
  from abc import ABC
4
+ from typing import overload
4
5
 
5
6
  import numpy as np
6
7
  import pandas as pd
@@ -8,12 +9,16 @@ import pandas as pd
8
9
  from AutoCarver.combinations.utils import TargetRate
9
10
 
10
11
 
11
- class BinaryTargetRate(TargetRate, ABC):
12
+ class BinaryTargetRate(TargetRate[pd.DataFrame], ABC):
12
13
  """Binary target rate class."""
13
14
 
14
15
  __name__ = "binary_target_rate"
15
16
 
16
- def compute(self, xagg: pd.DataFrame) -> pd.DataFrame:
17
+ @overload
18
+ def compute(self, xagg: pd.Series | pd.DataFrame) -> pd.DataFrame: ...
19
+ @overload
20
+ def compute(self, xagg: None) -> None: ...
21
+ def compute(self, xagg: pd.Series | pd.DataFrame | None) -> pd.DataFrame | None:
17
22
  """Computes the target rate.
18
23
 
19
24
  Parameters
@@ -31,8 +36,12 @@ class BinaryTargetRate(TargetRate, ABC):
31
36
  # frequency per modality
32
37
  frequency = xagg.sum(axis=1) / xagg.sum().sum()
33
38
 
34
- # computing target rate
35
- return pd.DataFrame({self.__name__: self._compute(xagg), "frequency": frequency})
39
+ # computing target rate. `_compute` expects pd.DataFrame (Generic
40
+ # XAgg=DataFrame); compute()'s wide signature is for LSP matching,
41
+ # callers always pass a crosstab here.
42
+ return pd.DataFrame(
43
+ {self.__name__: self._compute(xagg), "frequency": frequency} # type: ignore
44
+ )
36
45
  return None
37
46
 
38
47
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from abc import ABC
4
4
  from collections.abc import Iterable, Iterator
5
+ from typing import Any
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
@@ -11,10 +12,11 @@ from tqdm import tqdm
11
12
  from AutoCarver.combinations.continuous.continuous_target_rates import ContinuousTargetRate, TargetMean, TargetMedian
12
13
  from AutoCarver.combinations.utils.combination_evaluator import AggregatedSample, CombinationEvaluator
13
14
  from AutoCarver.combinations.utils.combinations import combination_formatter
15
+ from AutoCarver.combinations.utils.target_rate import TargetRate
14
16
  from AutoCarver.combinations.utils.testing import Keys, is_viable, test_viability
15
17
 
16
18
 
17
- class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
19
+ class ContinuousCombinationEvaluator(CombinationEvaluator[pd.Series], ABC):
18
20
  """Continuous combination evaluator class."""
19
21
 
20
22
  is_y_continuous = True
@@ -23,7 +25,7 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
23
25
  # carvers always carry a ContinuousTargetRate (enforced by _init_target_rate).
24
26
  target_rate: ContinuousTargetRate
25
27
 
26
- def _init_target_rate(self, target_rate: ContinuousTargetRate | None) -> ContinuousTargetRate:
28
+ def _init_target_rate(self, target_rate: TargetRate[pd.Series] | None) -> ContinuousTargetRate:
27
29
  """Initializes target rate."""
28
30
  if target_rate is None:
29
31
  return TargetMean()
@@ -32,7 +34,10 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
32
34
  return target_rate
33
35
 
34
36
  def _association_measure(
35
- self, xagg: AggregatedSample, n_obs: int | None = None, tol: float = 1e-10
37
+ self,
38
+ xagg: AggregatedSample | pd.Series | pd.DataFrame,
39
+ n_obs: int | None = None,
40
+ tol: float = 1e-10,
36
41
  ) -> dict[str, float | None]:
37
42
  """Computes measures of association between feature and quantitative target.
38
43
 
@@ -138,8 +143,8 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
138
143
  :meth:`_association_measure`).
139
144
  """
140
145
  raw_xagg = self.samples.train.xagg
141
- # Pre-rank y once for the whole feature
142
- R_per_mod, n_per_mod, N, tie_corr = _modality_rank_stats(raw_xagg)
146
+ # Pre-rank y once for the whole feature.
147
+ R_per_mod, n_per_mod, N, tie_corr = _modality_rank_stats(raw_xagg) # type: ignore
143
148
 
144
149
  # Map modality label -> position in R_per_mod / n_per_mod
145
150
  mod_to_pos: dict = {m: i for i, m in enumerate(raw_xagg.index)}
@@ -148,14 +153,17 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
148
153
  # Cache per-modality (n, sum_y) for the viability fast path.
149
154
  # Resets each time _compute_associations runs so the nan-pass refreshes
150
155
  # the cache after _apply_best_combination changes samples.train.xagg.
151
- sum_y_per_mod = _modality_sum_y(raw_xagg)
152
- self._train_modality_stats = {
156
+ sum_y_per_mod = _modality_sum_y(raw_xagg) # type: ignore
157
+ # Why: heterogeneous-value dict; annotate `Any` so downstream readers (line 203-204
158
+ # and _get_dev_modality_stats) can narrow to the per-key concrete type without ty
159
+ # unioning across all value types.
160
+ self._train_modality_stats: dict[str, Any] = {
153
161
  "n_per_mod": n_per_mod.astype(float),
154
162
  "sum_y_per_mod": sum_y_per_mod,
155
163
  "mod_to_pos": mod_to_pos,
156
164
  "n_mod": n_mod,
157
165
  }
158
- self._dev_modality_stats: dict | None = None # lazy; aligned to train's mod_to_pos
166
+ self._dev_modality_stats: dict[str, Any] | None = None # lazy; aligned to train's mod_to_pos
159
167
  self._dev_modality_stats_id: int | None = None
160
168
 
161
169
  batch: list[dict] = []
@@ -193,9 +201,9 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
193
201
  computation (the unit tests rely on this; production flows reassign
194
202
  dev only via ``samples.set`` at the start of ``get_best_combination``).
195
203
  """
196
- dev_xagg = self.samples.dev.xagg
197
- if dev_xagg is None:
204
+ if not self.samples.dev.has_xagg:
198
205
  return None
206
+ dev_xagg = self.samples.dev.xagg
199
207
  if self._dev_modality_stats is not None and self._dev_modality_stats_id == id(dev_xagg):
200
208
  return self._dev_modality_stats
201
209
  train_stats = self._train_modality_stats
@@ -261,7 +269,7 @@ class ContinuousCombinationEvaluator(CombinationEvaluator, ABC):
261
269
  """Fast-path viability on dev; falls back to legacy when the active
262
270
  target rate's ``compute_from_stats`` returns ``None``.
263
271
  """
264
- if not test_results[Keys.VIABLE.value] or self.samples.dev.xagg is None:
272
+ if not test_results[Keys.VIABLE.value] or not self.samples.dev.has_xagg:
265
273
  return {**test_results, "dev": {Keys.VIABLE.value: None}}
266
274
 
267
275
  dev_stats = self._get_dev_modality_stats()
@@ -1,6 +1,7 @@
1
1
  """set of target rates for binary classification"""
2
2
 
3
3
  from abc import ABC
4
+ from typing import overload
4
5
 
5
6
  import numpy as np
6
7
  import pandas as pd
@@ -8,12 +9,16 @@ import pandas as pd
8
9
  from AutoCarver.combinations.utils import TargetRate
9
10
 
10
11
 
11
- class ContinuousTargetRate(TargetRate, ABC):
12
+ class ContinuousTargetRate(TargetRate[pd.Series], ABC):
12
13
  """Continuous target rate class."""
13
14
 
14
15
  __name__ = "continuous_target_rate"
15
16
 
16
- def compute(self, xagg: pd.Series) -> pd.DataFrame:
17
+ @overload
18
+ def compute(self, xagg: pd.Series | pd.DataFrame) -> pd.DataFrame: ...
19
+ @overload
20
+ def compute(self, xagg: None) -> None: ...
21
+ def compute(self, xagg: pd.Series | pd.DataFrame | None) -> pd.DataFrame | None:
17
22
  """Computes the target rate.
18
23
 
19
24
  Parameters
@@ -31,8 +36,12 @@ class ContinuousTargetRate(TargetRate, ABC):
31
36
  # frequency per modality
32
37
  frequency = xagg.apply(len) / xagg.apply(len).sum()
33
38
 
34
- # computing target rate
35
- return pd.DataFrame({self.__name__: self._compute(xagg), "frequency": frequency})
39
+ # computing target rate. `_compute` expects pd.Series (Generic
40
+ # XAgg=Series); compute()'s wide signature is for LSP matching,
41
+ # callers always pass a Series-of-y-lists here.
42
+ return pd.DataFrame(
43
+ {self.__name__: self._compute(xagg), "frequency": frequency} # type: ignore
44
+ )
36
45
  return None
37
46
 
38
47
  def compute_from_stats(self, *, stats: dict, index_to_groupby: dict) -> pd.DataFrame | None:
@@ -59,7 +68,10 @@ class TargetMean(ContinuousTargetRate):
59
68
 
60
69
  def _compute(self, xagg: pd.Series | pd.DataFrame) -> pd.Series:
61
70
  """Computes the mean target rate."""
62
- return xagg.apply(np.mean)
71
+ # pandas stubs widen `Series.apply(func)` to `Series | DataFrame`
72
+ # because they can't tell np.mean returns a scalar; the runtime
73
+ # result is always a Series.
74
+ return xagg.apply(np.mean) # type: ignore
63
75
 
64
76
  def compute_from_stats(self, *, stats: dict, index_to_groupby: dict) -> pd.DataFrame | None:
65
77
  """Closed-form ``(target_mean, frequency)`` per group from per-modality stats.
@@ -130,7 +142,8 @@ class TargetMedian(ContinuousTargetRate):
130
142
 
131
143
  def _compute(self, xagg: pd.Series | pd.DataFrame) -> pd.Series:
132
144
  """Computes the mean target rate."""
133
- return xagg.apply(np.median)
145
+ # see TargetMean._compute: same pandas-stub widening on apply().
146
+ return xagg.apply(np.median) # type: ignore
134
147
 
135
148
 
136
149
  # class TargetVariance(ContinuousTargetRate):
@@ -6,6 +6,7 @@ import math
6
6
  from abc import ABC, abstractmethod
7
7
  from collections.abc import Iterable, Iterator
8
8
  from dataclasses import dataclass, field
9
+ from typing import Generic
9
10
 
10
11
  import pandas as pd
11
12
  from tqdm import tqdm
@@ -17,31 +18,46 @@ from AutoCarver.combinations.utils.combinations import (
17
18
  order_apply_combination,
18
19
  xagg_apply_combination,
19
20
  )
20
- from AutoCarver.combinations.utils.target_rate import TargetRate
21
+ from AutoCarver.combinations.utils.target_rate import TargetRate, XAgg
21
22
  from AutoCarver.combinations.utils.testing import Keys, is_viable, test_viability
22
23
  from AutoCarver.features import BaseFeature, GroupedList
23
24
 
24
25
 
25
- @dataclass
26
26
  class AggregatedSample:
27
- """Sample class to store aggregated samples
28
-
29
- Attributes
30
- ----------
31
- xagg : pd.DataFrame
32
- Aggregated sample
33
- raw : pd.DataFrame
34
- Raw aggregated sample
35
- """
27
+ """Sample class to store aggregated samples.
36
28
 
37
- xagg: pd.Series | pd.DataFrame
38
- _raw: pd.Series | pd.DataFrame | None = None
29
+ The public ``xagg`` is typed as mandatory ``pd.Series | pd.DataFrame``. The
30
+ constructor accepts ``None`` so that placeholders (default factories, optional
31
+ dev samples) are expressible, but reading ``.xagg`` on an unset sample raises.
32
+ Use :attr:`has_xagg` to check presence without triggering.
33
+ """
39
34
 
40
- def __post_init__(self):
41
- """Post initialization"""
35
+ def __init__(
36
+ self,
37
+ xagg: pd.Series | pd.DataFrame | None = None,
38
+ _raw: pd.Series | pd.DataFrame | None = None,
39
+ ) -> None:
40
+ self._xagg: pd.Series | pd.DataFrame | None = xagg
41
+ self._raw: pd.Series | pd.DataFrame | None = _raw
42
42
  # setting xtab_dev to xtab if not provided
43
- if self._raw is None and self.xagg is not None:
44
- self._raw = self.xagg.copy()
43
+ if self._raw is None and self._xagg is not None:
44
+ self._raw = self._xagg.copy()
45
+
46
+ @property
47
+ def xagg(self) -> pd.Series | pd.DataFrame:
48
+ """Returns the aggregated sample, or raises if not set."""
49
+ if self._xagg is None:
50
+ raise RuntimeError("[AggregatedSample] xagg is not set")
51
+ return self._xagg
52
+
53
+ @xagg.setter
54
+ def xagg(self, value: pd.Series | pd.DataFrame) -> None:
55
+ self._xagg = value
56
+
57
+ @property
58
+ def has_xagg(self) -> bool:
59
+ """Whether xagg is set."""
60
+ return self._xagg is not None
45
61
 
46
62
  @property
47
63
  def raw(self) -> pd.Series | pd.DataFrame | None:
@@ -88,8 +104,8 @@ class AggregatedSample:
88
104
  class AggregatedSamples:
89
105
  """stores train and dev samples"""
90
106
 
91
- train: AggregatedSample = field(default_factory=lambda: AggregatedSample(None))
92
- dev: AggregatedSample = field(default_factory=lambda: AggregatedSample(None))
107
+ train: AggregatedSample = field(default_factory=AggregatedSample)
108
+ dev: AggregatedSample = field(default_factory=AggregatedSample)
93
109
 
94
110
  def set(self, train: pd.Series | pd.DataFrame | None, dev: pd.Series | pd.DataFrame | None = None) -> None:
95
111
  """Sets the train and dev samples"""
@@ -130,7 +146,7 @@ class AggregatedSamples:
130
146
  self.dev.raw = xagg_apply_combination(self.dev.raw, feature)
131
147
 
132
148
 
133
- class CombinationEvaluator(ABC):
149
+ class CombinationEvaluator(ABC, Generic[XAgg]):
134
150
  """CombinationEvaluator class to evaluate
135
151
  the best combination of modalities for a feature."""
136
152
 
@@ -144,7 +160,7 @@ class CombinationEvaluator(ABC):
144
160
  self,
145
161
  *,
146
162
  verbose: bool = False,
147
- target_rate: TargetRate | None = None,
163
+ target_rate: TargetRate[XAgg] | None = None,
148
164
  ) -> None:
149
165
  """
150
166
  Parameters
@@ -162,10 +178,10 @@ class CombinationEvaluator(ABC):
162
178
  self._feature: BaseFeature | None = None
163
179
  self.samples: AggregatedSamples = AggregatedSamples()
164
180
  self._statistics_cache = None
165
- self.target_rate: TargetRate = self._init_target_rate(target_rate)
181
+ self.target_rate: TargetRate[XAgg] = self._init_target_rate(target_rate)
166
182
 
167
183
  @abstractmethod
168
- def _init_target_rate(self, target_rate: TargetRate | None) -> TargetRate:
184
+ def _init_target_rate(self, target_rate: TargetRate[XAgg] | None) -> TargetRate[XAgg]:
169
185
  """Initializes target rate."""
170
186
  if target_rate is None:
171
187
  raise NotImplementedError("Subclasses must implement _init_target_rate to provide a default target rate")
@@ -209,7 +225,7 @@ class CombinationEvaluator(ABC):
209
225
  combinations. Output is in arrival order; the caller is expected to
210
226
  sort by the configured metric.
211
227
  """
212
- n_obs = self.samples.train.xagg.apply(sum).sum()
228
+ n_obs: int = self.samples.train.xagg.apply(sum).sum() # type: ignore
213
229
  for grouped_xagg in tqdm(grouped_xaggs, desc="Computing associations", disable=not self.verbose):
214
230
  measure = self._association_measure(grouped_xagg["xagg"], n_obs=n_obs)
215
231
  yield {
@@ -218,7 +234,7 @@ class CombinationEvaluator(ABC):
218
234
  **measure,
219
235
  }
220
236
 
221
- def _get_best_association(self, combinations: Iterable[list[str]]) -> dict | None:
237
+ def _get_best_association(self, combinations: Iterable[list[list[str]]]) -> dict | None:
222
238
  """Streams grouping → scoring → viability in one pass.
223
239
 
224
240
  - ``combinations`` is consumed lazily (generator-friendly).
@@ -275,8 +291,13 @@ class CombinationEvaluator(ABC):
275
291
  # applying best_combination to raw xagg and xagg_dev
276
292
  self.samples.apply_combination(self.feature)
277
293
 
278
- # udpating statistics
279
- self.feature.statistics = self.target_rate.compute(self.samples.train.raw)
294
+ # udpating statistics — `apply_combination` always populates
295
+ # `samples.train.raw`; narrow Optional so the overloaded
296
+ # compute() returns DataFrame (not DataFrame | None).
297
+ raw = self.samples.train.raw
298
+ if raw is None:
299
+ raise RuntimeError(f"[{self.__name__}] samples.train.raw is not populated after apply_combination")
300
+ self.feature.statistics = self.target_rate.compute(raw)
280
301
 
281
302
  def _get_best_combination_non_nan(self) -> dict | None:
282
303
  """Computes associations of the tab for each combination of non-nans
@@ -384,7 +405,7 @@ class CombinationEvaluator(ABC):
384
405
  """testing the viability of the combination on xagg_dev"""
385
406
 
386
407
  # case 0: not viable on train or no test sample -> not testing for robustness
387
- if not test_results[Keys.VIABLE.value] or self.samples.dev.xagg is None:
408
+ if not test_results[Keys.VIABLE.value] or not self.samples.dev.has_xagg:
388
409
  return {**test_results, "dev": {Keys.VIABLE.value: None}}
389
410
 
390
411
  # case 1: test sample provided and viable on train -> testing robustness
@@ -440,14 +461,22 @@ class CombinationEvaluator(ABC):
440
461
  return viable_combination
441
462
 
442
463
  @abstractmethod
443
- def _grouper(self, xagg: AggregatedSample, groupby: dict[str, str]) -> pd.Series | pd.DataFrame:
464
+ def _grouper(self, xagg: AggregatedSample, groupby: dict[str, str]) -> XAgg:
444
465
  """Helper to group XAGG's values by groupby (carver specific)"""
445
466
 
446
467
  @abstractmethod
447
468
  def _association_measure(
448
- self, xagg: AggregatedSample, n_obs: int | None = None, tol: float = 1e-10
449
- ) -> dict[str, float]:
450
- """Helper to measure association between X and y (carver specific)"""
469
+ self,
470
+ xagg: AggregatedSample | pd.Series | pd.DataFrame,
471
+ n_obs: int | None = None,
472
+ tol: float = 1e-10,
473
+ ) -> dict[str, float | None]:
474
+ """Helper to measure association between X and y (carver specific).
475
+
476
+ Return value type is widened to ``float | None`` so the continuous
477
+ Kruskal–Wallis path (which returns ``None`` when scipy raises) can
478
+ share the base signature with the binary chi² path.
479
+ """
451
480
 
452
481
  def _historize_remaining_combinations(self, associations: list[dict], n_combination: int) -> None:
453
482
  """historizes the remaining combinations that have not been tested"""
@@ -0,0 +1,52 @@
1
+ """defines a target rate"""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Generic, TypeVar, overload
5
+
6
+ import pandas as pd
7
+
8
+ # parametric type: target rates and evaluators are split by the *kind* of
9
+ # aggregated input — DataFrame crosstabs for binary, Series-of-lists for
10
+ # continuous. A value-constrained TypeVar prevents accidental third instantiations.
11
+ XAgg = TypeVar("XAgg", pd.Series, pd.DataFrame)
12
+
13
+
14
+ class TargetRate(ABC, Generic[XAgg]):
15
+ """Target rate class.
16
+
17
+ Generic over ``XAgg`` for the inner :meth:`_compute` worker so that
18
+ binary (``DataFrame`` crosstabs) and continuous (``Series`` of y-lists)
19
+ subclasses don't violate LSP by narrowing the worker's parameter type.
20
+ The outer :meth:`compute` keeps a wide ``Series | DataFrame | None``
21
+ signature because call sites in :class:`CombinationEvaluator` and
22
+ :class:`BaseCarver` carry that union directly from
23
+ :class:`AggregatedSample.raw` / pretty-printer plumbing.
24
+ """
25
+
26
+ __name__ = "target_rate"
27
+
28
+ @abstractmethod
29
+ def _compute(self, xagg: XAgg) -> pd.Series:
30
+ """Computes the target rate."""
31
+
32
+ # `compute` is overloaded so that callers passing a non-None ``xagg`` get a
33
+ # non-Optional ``pd.DataFrame`` back — required by `_test_viability_*` and
34
+ # the `BaseFeature.statistics` setter, which don't accept ``None``.
35
+ @overload
36
+ def compute(self, xagg: pd.Series | pd.DataFrame) -> pd.DataFrame: ...
37
+ @overload
38
+ def compute(self, xagg: None) -> None: ...
39
+ @abstractmethod
40
+ def compute(self, xagg: pd.Series | pd.DataFrame | None) -> pd.DataFrame | None:
41
+ """Computes the target rate.
42
+
43
+ Parameters
44
+ ----------
45
+ xagg : pd.Series | pd.DataFrame | None
46
+ A crosstab (binary) or Series-of-y-lists (continuous).
47
+
48
+ Returns
49
+ -------
50
+ pd.DataFrame | None
51
+ Target rate frame, or ``None`` if ``xagg`` was ``None``.
52
+ """
@@ -39,9 +39,9 @@ class Discretizer(BaseDiscretizer):
39
39
  super().__init__(features, min_freq=min_freq, config=config)
40
40
 
41
41
  @extend_docstring(BaseDiscretizer.fit)
42
- def fit(self, X: pd.DataFrame, y: pd.Series) -> Self: # pylint: disable=W0222
42
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> Self:
43
43
  # Checking for binary target and copying X
44
- sample = self._prepare_data(Sample(X, y))
44
+ sample = self._prepare_sample(Sample(X, y))
45
45
 
46
46
  # fitting quantitative features if any
47
47
  self._fit_quantitatives(**sample)