mlsampler 0.3.0__tar.gz → 0.3.3__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.
- mlsampler-0.3.3/PKG-INFO +36 -0
- mlsampler-0.3.3/README.md +23 -0
- {mlsampler-0.3.0 → mlsampler-0.3.3}/pyproject.toml +1 -1
- mlsampler-0.3.3/src/mlsampler/__init__.py +12 -0
- mlsampler-0.3.3/src/mlsampler/base.py +160 -0
- {mlsampler-0.3.0/src → mlsampler-0.3.3/src/mlsampler}/constraints/__init__.py +2 -2
- {mlsampler-0.3.0/src → mlsampler-0.3.3/src/mlsampler}/constraints/constraints.py +36 -29
- {mlsampler-0.3.0/src → mlsampler-0.3.3/src/mlsampler}/engine/__init__.py +3 -0
- mlsampler-0.3.3/src/mlsampler/engine/hypergrid.py +136 -0
- {mlsampler-0.3.0/src → mlsampler-0.3.3/src/mlsampler}/engine/random.py +123 -73
- mlsampler-0.3.3/src/mlsampler/errors.py +23 -0
- mlsampler-0.3.3/src/mlsampler/terminals.py +27 -0
- mlsampler-0.3.3/src/mlsampler/types.py +7 -0
- mlsampler-0.3.3/src/mlsampler/validate.py +48 -0
- mlsampler-0.3.3/src/mlsampler.egg-info/PKG-INFO +36 -0
- mlsampler-0.3.3/src/mlsampler.egg-info/SOURCES.txt +20 -0
- mlsampler-0.3.3/src/mlsampler.egg-info/top_level.txt +1 -0
- mlsampler-0.3.0/PKG-INFO +0 -21
- mlsampler-0.3.0/README.md +0 -8
- mlsampler-0.3.0/src/mlsampler.egg-info/PKG-INFO +0 -21
- mlsampler-0.3.0/src/mlsampler.egg-info/SOURCES.txt +0 -13
- mlsampler-0.3.0/src/mlsampler.egg-info/top_level.txt +0 -2
- {mlsampler-0.3.0 → mlsampler-0.3.3}/LICENSE +0 -0
- {mlsampler-0.3.0 → mlsampler-0.3.3}/setup.cfg +0 -0
- {mlsampler-0.3.0/src → mlsampler-0.3.3/src/mlsampler}/constraints/base.py +0 -0
- {mlsampler-0.3.0 → mlsampler-0.3.3}/src/mlsampler.egg-info/dependency_links.txt +0 -0
- {mlsampler-0.3.0 → mlsampler-0.3.3}/src/mlsampler.egg-info/requires.txt +0 -0
mlsampler-0.3.3/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlsampler
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Flexible constrained random sampler for reverse analysis in machine learning
|
|
5
|
+
Author: Jugai O
|
|
6
|
+
License: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: joblib
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# RandomSampler
|
|
15
|
+
|
|
16
|
+
mlsampler is a flexible, constraint-aware sampling library designed for reverse analysis and data exploration in machine learning workflows.
|
|
17
|
+
|
|
18
|
+
It allows you to generate synthetic samples that satisfy complex constraints across continuous, integer, binary, and categorical features.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
- Constraint-based sampling
|
|
22
|
+
- Support for continuous / categorical features
|
|
23
|
+
- Parallel generation
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
https://jugais.github.io/randsampler/
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install mlsampler
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use Cases
|
|
35
|
+
- Reverse analysis (finding inputs that satisfy target conditions)
|
|
36
|
+
- Candidate generation for optimization
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# RandomSampler
|
|
2
|
+
|
|
3
|
+
mlsampler is a flexible, constraint-aware sampling library designed for reverse analysis and data exploration in machine learning workflows.
|
|
4
|
+
|
|
5
|
+
It allows you to generate synthetic samples that satisfy complex constraints across continuous, integer, binary, and categorical features.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
- Constraint-based sampling
|
|
9
|
+
- Support for continuous / categorical features
|
|
10
|
+
- Parallel generation
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
https://jugais.github.io/randsampler/
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install mlsampler
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use Cases
|
|
22
|
+
- Reverse analysis (finding inputs that satisfy target conditions)
|
|
23
|
+
- Candidate generation for optimization
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .base import BaseSampler, SamplerConfig, FeatureMeta
|
|
2
|
+
from .engine.random import RandomSampler
|
|
3
|
+
from .engine.hypergrid import HyperGridSampler
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"BaseSampler",
|
|
7
|
+
"SamplerConfig",
|
|
8
|
+
"FeatureMeta",
|
|
9
|
+
"RandomSampler",
|
|
10
|
+
"HyperGridSampler",
|
|
11
|
+
]
|
|
12
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from .constraints import Constraints
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class FeatureMeta:
|
|
9
|
+
index: Optional[int] = None
|
|
10
|
+
low: Optional[float] = None
|
|
11
|
+
high: Optional[float] = None
|
|
12
|
+
dtype: str = "float" # "float", "int", "binary", "categorical"
|
|
13
|
+
categories:Optional[list] = None
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class DtypeMeta:
|
|
17
|
+
float: str = "float"
|
|
18
|
+
integer: str = "int"
|
|
19
|
+
bin:str = "binary"
|
|
20
|
+
cat:str = "categorical"
|
|
21
|
+
const:str = "constant"
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SamplerConfig:
|
|
25
|
+
features: list[FeatureMeta]
|
|
26
|
+
batch_size: int = 1000
|
|
27
|
+
random_state: Optional[int] = None
|
|
28
|
+
n_jobs: int = -1
|
|
29
|
+
max_retries: int = 1000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BaseSampler(ABC):
|
|
33
|
+
"""
|
|
34
|
+
Base class for constraint-based samplers.
|
|
35
|
+
|
|
36
|
+
This class provides common functionality for sampling based on feature metadata
|
|
37
|
+
and registered constraints. It is designed to be extended by specific sampler implementations.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
config : SamplerConfig
|
|
42
|
+
Configuration object containing feature metadata and sampling settings.
|
|
43
|
+
"""
|
|
44
|
+
constraints: list[Constraints]
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: SamplerConfig) -> None:
|
|
47
|
+
self.config = config
|
|
48
|
+
self._constraints = []
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def setup(
|
|
52
|
+
cls,
|
|
53
|
+
X: np.ndarray,
|
|
54
|
+
random_state: Optional[int] = None,
|
|
55
|
+
n_jobs: int = -1,
|
|
56
|
+
max_retries: int = 1000,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Create a RandomSampler from provided training data.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
X : np.ndarray
|
|
64
|
+
Input dataset used to infer feature ranges and types.
|
|
65
|
+
batch_size : int, default=1000
|
|
66
|
+
Number of samples generated per batch.
|
|
67
|
+
random_state : int or None, default=None
|
|
68
|
+
Seed for reproducible sampling.
|
|
69
|
+
n_jobs : int, default=-1
|
|
70
|
+
Number of parallel jobs.
|
|
71
|
+
max_retries : int, default=1000
|
|
72
|
+
Maximum retries for satisfying constraints.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
RandomSampler
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
features = []
|
|
80
|
+
|
|
81
|
+
for col in range(X.shape[1]):
|
|
82
|
+
col_data = X[:, col]
|
|
83
|
+
|
|
84
|
+
if any(val is None or (
|
|
85
|
+
isinstance(val, float) and np.isnan(val)
|
|
86
|
+
) for val in col_data):
|
|
87
|
+
raise ValueError(f"Column {col} contains missing values (NaN/None).")
|
|
88
|
+
|
|
89
|
+
low, high = None, None
|
|
90
|
+
categories = None
|
|
91
|
+
dtype = None
|
|
92
|
+
unique_vals = np.unique(col_data)
|
|
93
|
+
try:
|
|
94
|
+
numeric_data = col_data.astype(float)
|
|
95
|
+
low = numeric_data.min()
|
|
96
|
+
high = numeric_data.max()
|
|
97
|
+
|
|
98
|
+
if np.isclose(low, high, equal_nan=True):
|
|
99
|
+
dtype = DtypeMeta.const
|
|
100
|
+
elif set(unique_vals).issubset({0, 1}):
|
|
101
|
+
dtype = DtypeMeta.bin
|
|
102
|
+
elif np.all(numeric_data.astype(int) == numeric_data):
|
|
103
|
+
dtype = DtypeMeta.integer
|
|
104
|
+
else:
|
|
105
|
+
dtype = DtypeMeta.float
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
if len(unique_vals) == 1:
|
|
108
|
+
dtype = DtypeMeta.const
|
|
109
|
+
else:
|
|
110
|
+
dtype = DtypeMeta.cat
|
|
111
|
+
categories=np.unique(col_data).tolist()
|
|
112
|
+
|
|
113
|
+
features.append(
|
|
114
|
+
FeatureMeta(
|
|
115
|
+
index=col,
|
|
116
|
+
low=low,
|
|
117
|
+
high=high,
|
|
118
|
+
dtype=dtype,
|
|
119
|
+
categories=categories
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
config = SamplerConfig(
|
|
124
|
+
features = features,
|
|
125
|
+
random_state = random_state,
|
|
126
|
+
n_jobs = n_jobs,
|
|
127
|
+
max_retries = max_retries,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return cls(config)
|
|
131
|
+
|
|
132
|
+
def set_constraints(self, constraint_fn: str, **kwargs) -> None:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def apply_constraints(self, row: np.ndarray) -> Optional[np.ndarray]:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def sample(self, n_samples: int) -> np.ndarray:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def n_features(self) -> int:
|
|
144
|
+
return len(self.config.features)
|
|
145
|
+
|
|
146
|
+
def __len__(self):
|
|
147
|
+
return len(self.constraints)
|
|
148
|
+
|
|
149
|
+
def __getitem__(self, key):
|
|
150
|
+
return self.constraints[key]
|
|
151
|
+
|
|
152
|
+
def __repr__(self):
|
|
153
|
+
return f"<{self.__class__.__name__} with {len(self)} constraints>"
|
|
154
|
+
|
|
155
|
+
def __iter__(self):
|
|
156
|
+
return iter(self._constraints)
|
|
157
|
+
|
|
158
|
+
def __bool__(self):
|
|
159
|
+
return bool(self.constraints)
|
|
160
|
+
|
|
@@ -6,7 +6,7 @@ from .constraints import (
|
|
|
6
6
|
RangeConstraint,
|
|
7
7
|
CategoriesConstraint,
|
|
8
8
|
StepConstraint,
|
|
9
|
-
|
|
9
|
+
SumStepConstraint,
|
|
10
10
|
FunctionConstraint
|
|
11
11
|
)
|
|
12
12
|
from .base import Constraints
|
|
@@ -20,6 +20,6 @@ __all__ = [
|
|
|
20
20
|
"RangeConstraint",
|
|
21
21
|
"CategoriesConstraint",
|
|
22
22
|
"StepConstraint",
|
|
23
|
-
"
|
|
23
|
+
"SumStepConstraint",
|
|
24
24
|
"FunctionConstraint"
|
|
25
25
|
]
|
|
@@ -4,7 +4,7 @@ from .base import Constraints, SelectConstraint
|
|
|
4
4
|
from .. import validate as v
|
|
5
5
|
from ..types import Numeric, ArrayLike, Bool, ConstraintFn
|
|
6
6
|
from typing import Optional, Callable
|
|
7
|
-
from ..errors import ConstraintViolationError, ConstraintError
|
|
7
|
+
from ..errors import ConstraintViolationError, ConstraintError, ConstraintTypeError
|
|
8
8
|
|
|
9
9
|
class MultihotConstraint(SelectConstraint):
|
|
10
10
|
def __init__(
|
|
@@ -29,7 +29,7 @@ class MultihotConstraint(SelectConstraint):
|
|
|
29
29
|
) -> np.ndarray:
|
|
30
30
|
row[selected] = 1
|
|
31
31
|
return row
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
class RandomSelectConstraint(SelectConstraint):
|
|
34
34
|
def __init__(
|
|
35
35
|
self,
|
|
@@ -53,7 +53,8 @@ class RandomSelectConstraint(SelectConstraint):
|
|
|
53
53
|
class SumConstraint(SelectConstraint):
|
|
54
54
|
def __init__(self,
|
|
55
55
|
cols: list[int],
|
|
56
|
-
sum_value: Numeric = 1,
|
|
56
|
+
sum_value: Numeric = 1,
|
|
57
|
+
method: str = 'dirichlet',
|
|
57
58
|
alpha: Optional[np.ndarray] = None,
|
|
58
59
|
rng: Optional[np.random.Generator] = None,
|
|
59
60
|
**kwargs
|
|
@@ -62,6 +63,7 @@ class SumConstraint(SelectConstraint):
|
|
|
62
63
|
|
|
63
64
|
super().__init__(cols, **kwargs)
|
|
64
65
|
self.sum_value = sum_value
|
|
66
|
+
self.method = method
|
|
65
67
|
self.alpha = alpha
|
|
66
68
|
self.rng = self._rng(rng)
|
|
67
69
|
|
|
@@ -80,14 +82,20 @@ class SumConstraint(SelectConstraint):
|
|
|
80
82
|
Returns:
|
|
81
83
|
np.ndarray: The modified row
|
|
82
84
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
if self.method == 'dirichlet':
|
|
87
|
+
if self.alpha is None:
|
|
88
|
+
alpha = np.ones(len(selected))
|
|
89
|
+
else:
|
|
90
|
+
alpha = self.alpha
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
weights = self.rng.dirichlet(alpha)
|
|
93
|
+
row[selected] = weights * self.sum_value
|
|
94
|
+
elif self.method == 'simple':
|
|
95
|
+
row[selected] = row / np.sum(row) * self.sum_value
|
|
96
|
+
else:
|
|
97
|
+
raise ConstraintTypeError(f'Method: {self.method} not implemented')
|
|
89
98
|
|
|
90
|
-
row[selected] = weights * self.sum_value
|
|
91
99
|
return row
|
|
92
100
|
|
|
93
101
|
class SumIntConstraint(SumConstraint):
|
|
@@ -103,7 +111,13 @@ class SumIntConstraint(SumConstraint):
|
|
|
103
111
|
return row
|
|
104
112
|
|
|
105
113
|
cuts = np.sort(self.rng.choice((self.sum_value + k - 1), k - 1, replace=False))
|
|
106
|
-
parts = np.diff(
|
|
114
|
+
parts = np.diff(
|
|
115
|
+
np.concatenate((
|
|
116
|
+
np.array([-1], dtype=cuts.dtype),
|
|
117
|
+
cuts,
|
|
118
|
+
np.array([self.sum_value + k - 1], dtype=cuts.dtype)
|
|
119
|
+
))
|
|
120
|
+
) - 1
|
|
107
121
|
|
|
108
122
|
row[selected] = parts
|
|
109
123
|
return row
|
|
@@ -134,11 +148,11 @@ class CategoriesConstraint(Constraints):
|
|
|
134
148
|
elif self.strength == "soft":
|
|
135
149
|
mask = mask
|
|
136
150
|
else:
|
|
137
|
-
raise ConstraintError(f"{self.strength} was not supported")
|
|
151
|
+
raise ConstraintError(f"{self.strength} was not supported.")
|
|
138
152
|
|
|
139
153
|
valid_patterns = self.values[mask]
|
|
140
154
|
if len(valid_patterns) == 0:
|
|
141
|
-
raise ConstraintViolationError("No patterns found in categories")
|
|
155
|
+
raise ConstraintViolationError("No patterns found in categories.")
|
|
142
156
|
|
|
143
157
|
idx = rng.integers(len(valid_patterns))
|
|
144
158
|
selected_pattern = valid_patterns[idx]
|
|
@@ -159,7 +173,7 @@ class RangeConstraint(Constraints):
|
|
|
159
173
|
return row
|
|
160
174
|
|
|
161
175
|
class StepConstraint(Constraints):
|
|
162
|
-
def __init__(self, col:int,
|
|
176
|
+
def __init__(self, col:int, step: float, low: float, high: float, **kwargs):
|
|
163
177
|
super().__init__(cols=[col], **kwargs)
|
|
164
178
|
self.values = np.arange(low, high + step, step)
|
|
165
179
|
self.col = col
|
|
@@ -179,7 +193,7 @@ class StepConstraint(Constraints):
|
|
|
179
193
|
row[self.col] = rng.choice(self.values)
|
|
180
194
|
return row
|
|
181
195
|
|
|
182
|
-
class
|
|
196
|
+
class SumStepConstraint(StepConstraint):
|
|
183
197
|
def __init__(
|
|
184
198
|
self,
|
|
185
199
|
cols: list[int],
|
|
@@ -210,10 +224,7 @@ class StepSumConstraint(StepConstraint):
|
|
|
210
224
|
def _constrain(self, row: np.ndarray, rng: Optional[np.random.Generator] = None) -> np.ndarray:
|
|
211
225
|
rng = self._rng(rng)
|
|
212
226
|
|
|
213
|
-
# 1. Initialize all target columns with their respective minimum (low) values
|
|
214
227
|
current_values = self.lows.copy().astype(float)
|
|
215
|
-
|
|
216
|
-
# 2. Calculate the difference to reach the target sum_value
|
|
217
228
|
current_sum = np.sum(current_values)
|
|
218
229
|
residual = self.sum_value - current_sum
|
|
219
230
|
|
|
@@ -224,7 +235,7 @@ class StepSumConstraint(StepConstraint):
|
|
|
224
235
|
if not np.isclose(residual % self.step, 0) and not np.isclose(residual % self.step, self.step):
|
|
225
236
|
raise ConstraintViolationError(f"Residual ({residual}) is not a multiple of step ({self.step}).")
|
|
226
237
|
|
|
227
|
-
#
|
|
238
|
+
# Randomly distribute the residual in 'step' increments
|
|
228
239
|
num_steps = int(round(residual / self.step))
|
|
229
240
|
|
|
230
241
|
for _ in range(num_steps):
|
|
@@ -237,18 +248,16 @@ class StepSumConstraint(StepConstraint):
|
|
|
237
248
|
if not eligible_indices:
|
|
238
249
|
raise ConstraintViolationError("Target sum_value is unreachable within defined highs.")
|
|
239
250
|
|
|
240
|
-
# Pick a random column and increment it
|
|
241
251
|
target_idx = rng.choice(eligible_indices)
|
|
242
252
|
current_values[target_idx] += self.step
|
|
243
253
|
|
|
244
|
-
# 4. Final assignment to the row
|
|
245
254
|
row[self.cols] = current_values
|
|
246
255
|
return row
|
|
247
256
|
|
|
248
257
|
|
|
249
258
|
class FunctionConstraint(Constraints):
|
|
250
|
-
def __init__(self, fn: ConstraintFn):
|
|
251
|
-
super().__init__(cols=
|
|
259
|
+
def __init__(self, fn: ConstraintFn, cols:list[int]):
|
|
260
|
+
super().__init__(cols=cols)
|
|
252
261
|
self.fn = fn
|
|
253
262
|
|
|
254
263
|
def _constrain(
|
|
@@ -256,15 +265,13 @@ class FunctionConstraint(Constraints):
|
|
|
256
265
|
row: np.ndarray,
|
|
257
266
|
rng: Optional[np.random.Generator] = None
|
|
258
267
|
) -> Optional[np.ndarray]:
|
|
268
|
+
|
|
259
269
|
result = self.fn(row)
|
|
260
270
|
|
|
261
271
|
if isinstance(result, Bool):
|
|
262
|
-
if
|
|
263
|
-
return None
|
|
264
|
-
else:
|
|
265
|
-
return row
|
|
272
|
+
return row if result else None
|
|
266
273
|
elif isinstance(result, np.ndarray):
|
|
267
|
-
|
|
274
|
+
row[self.cols] = result
|
|
275
|
+
return row
|
|
268
276
|
else:
|
|
269
|
-
|
|
270
|
-
raise ConstraintTypeError("Constraint function must return either a boolean or a numpy array")
|
|
277
|
+
raise ConstraintTypeError("Constraint function must return either a boolean or a numpy array")
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from .random import RandomSampler
|
|
2
|
+
from .hypergrid import HyperGridSampler
|
|
2
3
|
from ..base import BaseSampler
|
|
3
4
|
|
|
4
5
|
__annotations__ = {
|
|
5
6
|
"RandomSampler": BaseSampler,
|
|
7
|
+
"HyperGridSampler": BaseSampler
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
__all__ = [
|
|
9
11
|
"RandomSampler",
|
|
12
|
+
"HyperGridSampler"
|
|
10
13
|
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from scipy.stats import qmc
|
|
4
|
+
from ..base import BaseSampler, DtypeMeta as dm
|
|
5
|
+
from ..errors import ConstraintViolationError
|
|
6
|
+
from ..terminals import spinning
|
|
7
|
+
|
|
8
|
+
class HyperGridSampler(BaseSampler):
|
|
9
|
+
"""
|
|
10
|
+
Hybrid grid and LHS-based constraint sampler.
|
|
11
|
+
|
|
12
|
+
This sampler generates samples based on feature metadata inferred
|
|
13
|
+
from training data. Continuous features are sampled using Latin
|
|
14
|
+
Hypercube Sampling (LHS), while discrete features (e.g., integer,
|
|
15
|
+
binary, categorical) are sampled using grid random selection
|
|
16
|
+
over their respective value spaces.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
config : SamplerConfig
|
|
21
|
+
Configuration object containing feature metadata and sampling settings.
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
Sampling strategy depends on feature data types:
|
|
26
|
+
|
|
27
|
+
- Float features:
|
|
28
|
+
Sampled using Latin Hypercube Sampling (LHS), then scaled to
|
|
29
|
+
their respective [low, high] ranges.
|
|
30
|
+
|
|
31
|
+
- Integer features:
|
|
32
|
+
Sampled uniformly from the inclusive range [low, high].
|
|
33
|
+
|
|
34
|
+
- Binary features:
|
|
35
|
+
Sampled uniformly from {0, 1}.
|
|
36
|
+
|
|
37
|
+
- Categorical features:
|
|
38
|
+
Sampled uniformly from the provided category list.
|
|
39
|
+
|
|
40
|
+
- Constant features:
|
|
41
|
+
Always return the same fixed value.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config):
|
|
45
|
+
super().__init__(config)
|
|
46
|
+
self.rng = np.random.default_rng(config.random_state)
|
|
47
|
+
|
|
48
|
+
def _lhs(self, n_samples: int, float_features: list):
|
|
49
|
+
"""LHS sampling for float columns"""
|
|
50
|
+
if not float_features:
|
|
51
|
+
return np.empty((n_samples, 0))
|
|
52
|
+
|
|
53
|
+
dim = len(float_features)
|
|
54
|
+
sampler = qmc.LatinHypercube(d=dim, seed=self.config.random_state)
|
|
55
|
+
sample = sampler.random(n=n_samples)
|
|
56
|
+
|
|
57
|
+
lows = np.array([f.low for f in float_features])
|
|
58
|
+
highs = np.array([f.high for f in float_features])
|
|
59
|
+
|
|
60
|
+
scaled = qmc.scale(sample, lows, highs)
|
|
61
|
+
return scaled
|
|
62
|
+
|
|
63
|
+
def _discrete(self, n_samples: int, features: list):
|
|
64
|
+
"""Grid random sampling for discrete columns"""
|
|
65
|
+
cols = []
|
|
66
|
+
|
|
67
|
+
for f in features:
|
|
68
|
+
if f.dtype == dm.bin:
|
|
69
|
+
vals = np.array([0, 1])
|
|
70
|
+
elif f.dtype == dm.integer:
|
|
71
|
+
vals = np.arange(int(f.low), int(f.high) + 1)
|
|
72
|
+
elif f.dtype == dm.cat:
|
|
73
|
+
vals = np.array(f.categories)
|
|
74
|
+
elif f.dtype == dm.const:
|
|
75
|
+
vals = np.array([f.low])
|
|
76
|
+
else:
|
|
77
|
+
raise ConstraintViolationError(f"Unsupported dtype: {f.dtype}")
|
|
78
|
+
|
|
79
|
+
sampled = self.rng.choice(vals, size=n_samples)
|
|
80
|
+
cols.append(sampled)
|
|
81
|
+
|
|
82
|
+
return np.column_stack(cols) if cols else np.empty((n_samples, 0))
|
|
83
|
+
|
|
84
|
+
def _sample(self, n_samples: int) -> np.ndarray:
|
|
85
|
+
"""
|
|
86
|
+
Generate samples based on feature data types.
|
|
87
|
+
|
|
88
|
+
Continuous features are sampled using Latin Hypercube Sampling (LHS),
|
|
89
|
+
while discrete features (e.g., "int", "binary", "categorical", "constant") are
|
|
90
|
+
sampled using uniform random selection over their respective value spaces.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
n_samples : int
|
|
95
|
+
Total number of samples to generate.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
np.ndarray
|
|
100
|
+
Array of shape (n_samples, n_features) containing the generated samples.
|
|
101
|
+
The column order matches the input feature configuration.
|
|
102
|
+
|
|
103
|
+
Notes
|
|
104
|
+
-----
|
|
105
|
+
- Float features are scaled to their respective [low, high] ranges.
|
|
106
|
+
- Integer features are sampled from the inclusive range [low, high].
|
|
107
|
+
- Categorical features are sampled from the provided category list.
|
|
108
|
+
- Constant features return the same value for all samples.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
float_feats = [f for f in self.config.features if f.dtype == dm.float]
|
|
112
|
+
discrete_feats = [f for f in self.config.features if f.dtype != dm.float]
|
|
113
|
+
|
|
114
|
+
float_part = self._lhs(n_samples, float_feats)
|
|
115
|
+
discrete_part = self._discrete(n_samples, discrete_feats)
|
|
116
|
+
|
|
117
|
+
result = np.zeros((n_samples, self.n_features), dtype=object)
|
|
118
|
+
|
|
119
|
+
float_idx = 0
|
|
120
|
+
disc_idx = 0
|
|
121
|
+
|
|
122
|
+
for i, f in enumerate(self.config.features):
|
|
123
|
+
if f.dtype == dm.float:
|
|
124
|
+
result[:, i] = float_part[:, float_idx]
|
|
125
|
+
float_idx += 1
|
|
126
|
+
else:
|
|
127
|
+
result[:, i] = discrete_part[:, disc_idx]
|
|
128
|
+
disc_idx += 1
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def sample(self, n_samples: int) -> np.ndarray:
|
|
133
|
+
with spinning():
|
|
134
|
+
samples = self._sample(n_samples)
|
|
135
|
+
return samples
|
|
136
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
|
|
2
|
+
from ..terminals import spinning
|
|
3
3
|
from ..base import BaseSampler, SamplerConfig, DtypeMeta as dm
|
|
4
4
|
from ..constraints import *
|
|
5
5
|
from ..errors import (
|
|
@@ -8,7 +8,8 @@ from ..errors import (
|
|
|
8
8
|
DuplicateColumnWarning
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
from typing import Optional, Callable
|
|
11
|
+
from typing import Optional, Callable, Any
|
|
12
|
+
from types import MappingProxyType
|
|
12
13
|
from collections import defaultdict
|
|
13
14
|
from joblib import Parallel, delayed
|
|
14
15
|
|
|
@@ -19,7 +20,7 @@ class RandomSampler(BaseSampler):
|
|
|
19
20
|
Random constraint-based sampler.
|
|
20
21
|
|
|
21
22
|
This sampler generates samples based on feature metadata
|
|
22
|
-
inferred from training data. Users can register constraints
|
|
23
|
+
inferred from training data. Users can register constraints
|
|
23
24
|
that are applied during sample generation.
|
|
24
25
|
|
|
25
26
|
Parameters
|
|
@@ -31,8 +32,15 @@ class RandomSampler(BaseSampler):
|
|
|
31
32
|
-----
|
|
32
33
|
Two types of constraints are supported:
|
|
33
34
|
|
|
34
|
-
- Validation constraints:
|
|
35
|
-
|
|
35
|
+
- Validation constraints:
|
|
36
|
+
Return a boolean. If False, the sample is rejected.
|
|
37
|
+
|
|
38
|
+
- Constructive constraints:
|
|
39
|
+
Return a modified numpy array.
|
|
40
|
+
|
|
41
|
+
Internally, constraints are unified to return either:
|
|
42
|
+
- np.ndarray (valid sample)
|
|
43
|
+
- None (invalid sample)
|
|
36
44
|
|
|
37
45
|
Sampling is performed with retry logic up to `max_retries`.
|
|
38
46
|
Parallel generation is supported via joblib.
|
|
@@ -44,14 +52,14 @@ class RandomSampler(BaseSampler):
|
|
|
44
52
|
super().__init__(config)
|
|
45
53
|
self.rng = np.random.default_rng(config.random_state)
|
|
46
54
|
self.features = config.features
|
|
47
|
-
self.batch_size = config.batch_size
|
|
48
55
|
self.n_jobs = config.n_jobs
|
|
49
56
|
self.seed = config.random_state
|
|
50
57
|
self.max_retries = config.max_retries
|
|
51
58
|
|
|
52
59
|
self.dim = len(self.features)
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
60
|
+
self._constraints:list[Constraints] = []
|
|
61
|
+
self._funcs: list[FunctionConstraint] = []
|
|
62
|
+
self._registry = MappingProxyType({
|
|
55
63
|
"sum": SumConstraint,
|
|
56
64
|
"sumint": SumIntConstraint,
|
|
57
65
|
"multihot": MultihotConstraint,
|
|
@@ -59,17 +67,23 @@ class RandomSampler(BaseSampler):
|
|
|
59
67
|
"range": RangeConstraint,
|
|
60
68
|
"categories": CategoriesConstraint,
|
|
61
69
|
"step": StepConstraint,
|
|
62
|
-
"stepsum":
|
|
63
|
-
|
|
70
|
+
"stepsum": SumStepConstraint,
|
|
71
|
+
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def constraints(self):
|
|
76
|
+
"""Get a registered constraint class"""
|
|
77
|
+
return tuple(self._constraints.copy())
|
|
64
78
|
|
|
65
79
|
def reset_constraints(self):
|
|
66
80
|
"""Clear all registered constraints."""
|
|
67
|
-
self.
|
|
68
|
-
|
|
81
|
+
self._constraints = []
|
|
82
|
+
|
|
69
83
|
def set_constraints(
|
|
70
84
|
self,
|
|
71
|
-
constraint_fn: str | Callable
|
|
72
|
-
|
|
85
|
+
constraint_fn: str | Callable[[np.ndarray], bool],
|
|
86
|
+
reset=False,
|
|
73
87
|
**kwargs
|
|
74
88
|
):
|
|
75
89
|
"""
|
|
@@ -78,14 +92,16 @@ class RandomSampler(BaseSampler):
|
|
|
78
92
|
----------
|
|
79
93
|
constraint_fn : str or callable, default="sum"
|
|
80
94
|
Type of constraint. Supported types:
|
|
81
|
-
- callable: user-defined function that takes a row and returns a boolean.
|
|
95
|
+
- callable: user-defined function that takes a row and returns a boolean or a new row. `cols` must be provided in kwargs.
|
|
82
96
|
- "sum": constraint based on the sum of selected columns. `sum_value` and `cols` must be provided in kwargs.
|
|
83
97
|
- "sumint": similar to "sum" but ensures the sum is an integer. `sum_value` and `cols` must be provided in kwargs.
|
|
84
98
|
- "multihot": constraint ensuring a specified number of columns in a set are active.`n_hot` and `cols` must be provided in kwargs.
|
|
85
99
|
- "random": constraint selecting a random subset of columns. `cols`, `min_used`, and `max_used` can be provided in kwargs.
|
|
86
|
-
- "range": constraint setting values within a specified range. `cols`, `
|
|
87
|
-
- "categories": constraint selecting from a list of categorical values. `
|
|
88
|
-
|
|
100
|
+
- "range": constraint setting values within a specified range. `cols`, `low`, and `high` must be provided in kwargs.
|
|
101
|
+
- "categories": constraint selecting from a list of categorical values. `cols` and `values` must be provided in kwargs.
|
|
102
|
+
- "step": constraint selecting values that are multiples of a step. `cols` and `step`, `low`, and `high` must be provided in kwargs.
|
|
103
|
+
- "stepsum": constraint ensuring the sum of values is a multiple of a step.
|
|
104
|
+
reset : bool, default=True
|
|
89
105
|
If True, clears existing constraints before adding the new one.
|
|
90
106
|
**kwargs
|
|
91
107
|
Additional parameters specific to the constraint type.
|
|
@@ -97,38 +113,40 @@ class RandomSampler(BaseSampler):
|
|
|
97
113
|
>>> # Custom function constraint
|
|
98
114
|
>>> sampler.set_constraints(lambda x: (0 < x[0] < 1) and (0 < x[1] < 1))
|
|
99
115
|
>>> # Sum constraint
|
|
100
|
-
>>> sampler.set_constraints("sum", sum_value=1, cols=[2, 3, 4], max_used=
|
|
116
|
+
>>> sampler.set_constraints("sum", sum_value=1, cols=[2, 3, 4], max_used=2)
|
|
101
117
|
"""
|
|
102
118
|
|
|
103
|
-
if
|
|
104
|
-
self.
|
|
105
|
-
|
|
119
|
+
if reset:
|
|
120
|
+
self._constraints = []
|
|
121
|
+
self._funcs = []
|
|
122
|
+
|
|
106
123
|
if callable(constraint_fn):
|
|
107
|
-
self.
|
|
124
|
+
self._constraints.append(FunctionConstraint(fn=constraint_fn, **kwargs))
|
|
125
|
+
self._funcs.append(FunctionConstraint(fn=constraint_fn, **kwargs))
|
|
108
126
|
elif isinstance(constraint_fn, str) and constraint_fn in self._registry:
|
|
109
|
-
self.
|
|
127
|
+
self._constraints.append(self._registry[constraint_fn](**kwargs))
|
|
110
128
|
else:
|
|
111
129
|
raise ConstraintTypeError(
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
f"Unsupported constraint type: {constraint_fn}"
|
|
131
|
+
)
|
|
114
132
|
|
|
115
133
|
def _base_sample(self, rng: np.random.Generator):
|
|
116
|
-
|
|
117
|
-
x = np.empty(len(self.config.features), dtype=object)
|
|
118
|
-
|
|
134
|
+
x = np.empty(self.dim, dtype=object)
|
|
119
135
|
for i, f in enumerate(self.features):
|
|
120
|
-
if f.dtype == dm.
|
|
136
|
+
if f.dtype == dm.const:
|
|
137
|
+
x[i] = f.low
|
|
138
|
+
elif f.dtype == dm.bin:
|
|
121
139
|
x[i] = rng.integers(0, 2)
|
|
122
|
-
elif f.dtype == dm.integer:
|
|
140
|
+
elif f.dtype == dm.integer and f.low is not None and f.high is not None:
|
|
123
141
|
x[i] = rng.integers(int(f.low), int(f.high) + 1)
|
|
124
|
-
elif f.dtype == dm.
|
|
142
|
+
elif f.dtype == dm.float and f.low is not None and f.high is not None:
|
|
125
143
|
x[i] = rng.uniform(f.low, f.high)
|
|
126
144
|
else:
|
|
127
145
|
x[i] = 0
|
|
128
146
|
|
|
129
147
|
return x
|
|
130
148
|
|
|
131
|
-
def
|
|
149
|
+
def _fill_categoricals(self, x, rng: np.random.Generator):
|
|
132
150
|
for i, f in enumerate(self.features):
|
|
133
151
|
if f.dtype == dm.cat:
|
|
134
152
|
if f.categories:
|
|
@@ -139,34 +157,71 @@ class RandomSampler(BaseSampler):
|
|
|
139
157
|
|
|
140
158
|
def _apply_constraints(self, row: np.ndarray) -> Optional[np.ndarray]:
|
|
141
159
|
"""
|
|
142
|
-
Apply registered constraints
|
|
160
|
+
Apply all registered constraints sequentially.
|
|
143
161
|
|
|
144
162
|
Parameters
|
|
145
163
|
----------
|
|
146
|
-
row : np.ndarray
|
|
147
|
-
The generated sample to validate/modify.
|
|
164
|
+
row : np.ndarray Input sample.
|
|
148
165
|
|
|
149
166
|
Returns
|
|
150
167
|
-------
|
|
151
168
|
np.ndarray or None
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
- np.ndarray: if all constraints succeed
|
|
170
|
+
- None: if any constraint fails
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
Each constraint must return either:
|
|
175
|
+
- np.ndarray (possibly modified)
|
|
176
|
+
- None (indicating failure)
|
|
155
177
|
"""
|
|
156
178
|
for constraint in self:
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
if isinstance(constraint, FunctionConstraint):
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
result = constraint(row)
|
|
183
|
+
if result is None:
|
|
159
184
|
return None # Constraint violation
|
|
185
|
+
row = result
|
|
160
186
|
return row
|
|
161
187
|
|
|
188
|
+
def _apply_funcs(self, row: np.ndarray) -> Optional[np.ndarray]:
|
|
189
|
+
for constraint in self._funcs:
|
|
190
|
+
result = constraint(row)
|
|
191
|
+
if result is None:
|
|
192
|
+
return None
|
|
193
|
+
return row
|
|
194
|
+
|
|
162
195
|
def _detect_conflicts(self):
|
|
163
196
|
col_usage = defaultdict(list)
|
|
197
|
+
constraints_by_col = defaultdict(list)
|
|
164
198
|
|
|
165
|
-
for i, c in enumerate(self.
|
|
199
|
+
for i, c in enumerate(self._constraints):
|
|
166
200
|
for col in getattr(c, "cols", []):
|
|
167
201
|
col_usage[col].append(i)
|
|
202
|
+
constraints_by_col[col].append(c)
|
|
168
203
|
|
|
169
204
|
for col, ids in col_usage.items():
|
|
205
|
+
constraints = constraints_by_col[col]
|
|
206
|
+
types = {getattr(c, "constraint_fn", None) for c in constraints}
|
|
207
|
+
|
|
208
|
+
meta = self.config.features[col]
|
|
209
|
+
|
|
210
|
+
if meta.dtype == dm.cat:
|
|
211
|
+
invalid = types & {"sum", "sumint", "range"}
|
|
212
|
+
|
|
213
|
+
if invalid:
|
|
214
|
+
raise ConstraintViolationError(
|
|
215
|
+
f"{invalid} cannot be applied to categorical column: {col}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if meta.dtype == dm.const:
|
|
219
|
+
if len(ids) > 1:
|
|
220
|
+
warnings.warn(
|
|
221
|
+
f"Const column {col} has multiple constraints {ids} (types={types})",
|
|
222
|
+
DuplicateColumnWarning
|
|
223
|
+
)
|
|
224
|
+
|
|
170
225
|
if len(ids) > 1:
|
|
171
226
|
warnings.warn(
|
|
172
227
|
f"Column {col} used in multiple constraints {ids}",
|
|
@@ -176,22 +231,35 @@ class RandomSampler(BaseSampler):
|
|
|
176
231
|
def _generate_one(self, seed_offset: int = 0):
|
|
177
232
|
base_seed = self.seed if self.seed is not None else None
|
|
178
233
|
rng = np.random.default_rng(
|
|
179
|
-
|
|
234
|
+
(base_seed + seed_offset) if base_seed else None
|
|
180
235
|
)
|
|
181
236
|
|
|
182
237
|
for _ in range(self.max_retries):
|
|
183
238
|
x = self._base_sample(rng)
|
|
184
|
-
x = self.
|
|
185
|
-
x = self._apply_constraints(x)
|
|
239
|
+
x = self._fill_categoricals(x, rng)
|
|
240
|
+
x = self._apply_constraints(x)
|
|
241
|
+
if x is None:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
x = self._apply_funcs(x)
|
|
186
245
|
if x is not None:
|
|
187
246
|
return x
|
|
188
|
-
|
|
247
|
+
|
|
189
248
|
raise ConstraintViolationError("Max retries exceeded.")
|
|
190
|
-
|
|
191
|
-
def _generate_batch(self, batch_size):
|
|
192
|
-
return np.array([self._generate_one() for _ in range(batch_size)])
|
|
193
249
|
|
|
194
|
-
def
|
|
250
|
+
def _sample(self, n_samples: int) -> np.ndarray:
|
|
251
|
+
if self.n_jobs == 1:
|
|
252
|
+
samples = [
|
|
253
|
+
self._generate_one(i) for i in range(n_samples)
|
|
254
|
+
]
|
|
255
|
+
else:
|
|
256
|
+
samples = Parallel(n_jobs=self.n_jobs)(
|
|
257
|
+
delayed(self._generate_one)(i)
|
|
258
|
+
for i in range(n_samples)
|
|
259
|
+
)
|
|
260
|
+
return np.array(samples)
|
|
261
|
+
|
|
262
|
+
def sample(self, n_samples: int) -> np.ndarray:
|
|
195
263
|
"""
|
|
196
264
|
Generate samples satisfying all registered constraints.
|
|
197
265
|
|
|
@@ -206,31 +274,13 @@ class RandomSampler(BaseSampler):
|
|
|
206
274
|
Array of shape (n_samples, n_features).
|
|
207
275
|
|
|
208
276
|
"""
|
|
209
|
-
|
|
277
|
+
|
|
210
278
|
self._detect_conflicts()
|
|
211
279
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
while remaining > 0:
|
|
218
|
-
current_batch = min(self.batch_size, remaining)
|
|
219
|
-
|
|
220
|
-
if self.n_jobs == 1:
|
|
221
|
-
batch = self._generate_batch(current_batch)
|
|
222
|
-
else:
|
|
223
|
-
results = Parallel(n_jobs=self.n_jobs)(
|
|
224
|
-
delayed(self._generate_one)()
|
|
225
|
-
for _ in range(current_batch)
|
|
226
|
-
)
|
|
227
|
-
batch = np.array(results)
|
|
228
|
-
|
|
229
|
-
batches.append(batch)
|
|
230
|
-
|
|
231
|
-
remaining -= current_batch
|
|
280
|
+
with spinning():
|
|
281
|
+
samples = self._sample(n_samples)
|
|
282
|
+
|
|
283
|
+
return samples
|
|
232
284
|
|
|
233
|
-
print(f"{n_samples - remaining}/{n_samples}", flush=True)
|
|
234
285
|
|
|
235
|
-
return np.vstack(batches)
|
|
236
286
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class ConstraintError(ValueError):
|
|
2
|
+
"""Custom exception for constraint violations"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class ConstraintTypeError(ConstraintError):
|
|
6
|
+
"""Raised when an unsupported constraint type is specified."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class ConstraintViolationError(ConstraintError):
|
|
10
|
+
"""Raised when a constraint is violated during data generation"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class ConstraintValidationError(ConstraintError):
|
|
14
|
+
"""Raised when a constraint definition is invalid."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class ConstraintWarning(UserWarning):
|
|
18
|
+
"""Base warning for constraint-related issues."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class DuplicateColumnWarning(ConstraintWarning):
|
|
22
|
+
"""Column is used in multiple constraints."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
def spinner(stop_event):
|
|
7
|
+
symbols = ["|", "/", "-", "\\"]
|
|
8
|
+
i = 0
|
|
9
|
+
while not stop_event.is_set():
|
|
10
|
+
sys.stdout.write(f"\rsampling... {symbols[i % len(symbols)]}")
|
|
11
|
+
sys.stdout.flush()
|
|
12
|
+
time.sleep(0.1)
|
|
13
|
+
i += 1
|
|
14
|
+
|
|
15
|
+
sys.stdout.write("\rsampling completed \n")
|
|
16
|
+
sys.stdout.flush()
|
|
17
|
+
|
|
18
|
+
@contextmanager
|
|
19
|
+
def spinning(msg="sampling..."):
|
|
20
|
+
stop_event = threading.Event()
|
|
21
|
+
t = threading.Thread(target=spinner, args=(stop_event,))
|
|
22
|
+
t.start()
|
|
23
|
+
try:
|
|
24
|
+
yield
|
|
25
|
+
finally:
|
|
26
|
+
stop_event.set()
|
|
27
|
+
t.join()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import TypeAlias, Union, Callable
|
|
3
|
+
|
|
4
|
+
Numeric: TypeAlias = Union[int, float, np.integer, np.floating]
|
|
5
|
+
ArrayLike: TypeAlias = Union[np.ndarray, list, tuple]
|
|
6
|
+
Bool: TypeAlias = Union[bool, np.bool_]
|
|
7
|
+
ConstraintFn: TypeAlias = Callable[[np.ndarray], Union[Bool, np.ndarray]]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .errors import *
|
|
4
|
+
from .types import Numeric, ArrayLike
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
def validate_cols(cols):
|
|
8
|
+
if not isinstance(cols, ArrayLike):
|
|
9
|
+
raise ConstraintValidationError("cols must be an array-like of column indices")
|
|
10
|
+
|
|
11
|
+
if len(set(cols)) != len(cols):
|
|
12
|
+
raise ConstraintValidationError("Column indices must be unique")
|
|
13
|
+
|
|
14
|
+
for c in cols:
|
|
15
|
+
if not isinstance(c, (int, np.integer)) or c < 0:
|
|
16
|
+
raise ConstraintValidationError("Column indices must be non-negative integers")
|
|
17
|
+
|
|
18
|
+
def validate_usage(min_used: int, max_used: Optional[int]):
|
|
19
|
+
if min_used < 0:
|
|
20
|
+
raise ConstraintValidationError("min_used must be non-negative")
|
|
21
|
+
|
|
22
|
+
if max_used is None:
|
|
23
|
+
raise ConstraintValidationError("max_used must be specified")
|
|
24
|
+
|
|
25
|
+
if max_used is not None and max_used < 0:
|
|
26
|
+
raise ConstraintValidationError("max_used must be non-negative")
|
|
27
|
+
|
|
28
|
+
if min_used > max_used:
|
|
29
|
+
raise ConstraintValidationError("min_used must be <= max_used")
|
|
30
|
+
|
|
31
|
+
def validate_values(value: Numeric):
|
|
32
|
+
if not isinstance(value, Numeric):
|
|
33
|
+
raise ConstraintValidationError("Value must be a numeric")
|
|
34
|
+
|
|
35
|
+
if value < 0:
|
|
36
|
+
warnings.warn("Value should be non-negative", ConstraintWarning)
|
|
37
|
+
|
|
38
|
+
def validate_range(low: Numeric, high: Numeric):
|
|
39
|
+
if not isinstance(low, Numeric):
|
|
40
|
+
raise ConstraintTypeError("low must be a numeric")
|
|
41
|
+
|
|
42
|
+
if not isinstance(high, Numeric):
|
|
43
|
+
raise ConstraintTypeError("high must be a numeric")
|
|
44
|
+
|
|
45
|
+
if low > high:
|
|
46
|
+
raise ConstraintValidationError("low must be <= high")
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlsampler
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Flexible constrained random sampler for reverse analysis in machine learning
|
|
5
|
+
Author: Jugai O
|
|
6
|
+
License: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: joblib
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# RandomSampler
|
|
15
|
+
|
|
16
|
+
mlsampler is a flexible, constraint-aware sampling library designed for reverse analysis and data exploration in machine learning workflows.
|
|
17
|
+
|
|
18
|
+
It allows you to generate synthetic samples that satisfy complex constraints across continuous, integer, binary, and categorical features.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
- Constraint-based sampling
|
|
22
|
+
- Support for continuous / categorical features
|
|
23
|
+
- Parallel generation
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
https://jugais.github.io/randsampler/
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install mlsampler
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use Cases
|
|
35
|
+
- Reverse analysis (finding inputs that satisfy target conditions)
|
|
36
|
+
- Candidate generation for optimization
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/mlsampler/__init__.py
|
|
5
|
+
src/mlsampler/base.py
|
|
6
|
+
src/mlsampler/errors.py
|
|
7
|
+
src/mlsampler/terminals.py
|
|
8
|
+
src/mlsampler/types.py
|
|
9
|
+
src/mlsampler/validate.py
|
|
10
|
+
src/mlsampler.egg-info/PKG-INFO
|
|
11
|
+
src/mlsampler.egg-info/SOURCES.txt
|
|
12
|
+
src/mlsampler.egg-info/dependency_links.txt
|
|
13
|
+
src/mlsampler.egg-info/requires.txt
|
|
14
|
+
src/mlsampler.egg-info/top_level.txt
|
|
15
|
+
src/mlsampler/constraints/__init__.py
|
|
16
|
+
src/mlsampler/constraints/base.py
|
|
17
|
+
src/mlsampler/constraints/constraints.py
|
|
18
|
+
src/mlsampler/engine/__init__.py
|
|
19
|
+
src/mlsampler/engine/hypergrid.py
|
|
20
|
+
src/mlsampler/engine/random.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mlsampler
|
mlsampler-0.3.0/PKG-INFO
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mlsampler
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Flexible constrained random sampler for reverse analysis in machine learning
|
|
5
|
-
Author: Jugai O
|
|
6
|
-
License: LICENSE
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
11
|
-
Requires-Dist: joblib
|
|
12
|
-
Dynamic: license-file
|
|
13
|
-
|
|
14
|
-
# RandomSampler
|
|
15
|
-
|
|
16
|
-
Flexible constrained random sampler for reverse analysis in machine learning.
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install mlsampler
|
mlsampler-0.3.0/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mlsampler
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Flexible constrained random sampler for reverse analysis in machine learning
|
|
5
|
-
Author: Jugai O
|
|
6
|
-
License: LICENSE
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
11
|
-
Requires-Dist: joblib
|
|
12
|
-
Dynamic: license-file
|
|
13
|
-
|
|
14
|
-
# RandomSampler
|
|
15
|
-
|
|
16
|
-
Flexible constrained random sampler for reverse analysis in machine learning.
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install mlsampler
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
src/constraints/__init__.py
|
|
5
|
-
src/constraints/base.py
|
|
6
|
-
src/constraints/constraints.py
|
|
7
|
-
src/engine/__init__.py
|
|
8
|
-
src/engine/random.py
|
|
9
|
-
src/mlsampler.egg-info/PKG-INFO
|
|
10
|
-
src/mlsampler.egg-info/SOURCES.txt
|
|
11
|
-
src/mlsampler.egg-info/dependency_links.txt
|
|
12
|
-
src/mlsampler.egg-info/requires.txt
|
|
13
|
-
src/mlsampler.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|