tabdat-synth 0.1.0__tar.gz → 0.2.0__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.
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/PKG-INFO +3 -3
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/README.md +2 -2
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/pyproject.toml +1 -1
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/__init__.py +2 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/validation.py +61 -1
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/domain/contracts.py +11 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/interfaces/bundles.py +35 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/interfaces/io.py +35 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/version.py +1 -1
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/workflows/pipelines.py +43 -1
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth.egg-info/PKG-INFO +3 -3
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/LICENSE +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/setup.cfg +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/__init__.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/evaluation.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/probability.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/sampling.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/core/statistics.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/domain/__init__.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/interfaces/__init__.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/interfaces/models.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/interfaces/reports.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/workflows/__init__.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth/workflows/evaluation.py +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth.egg-info/SOURCES.txt +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth.egg-info/dependency_links.txt +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth.egg-info/requires.txt +0 -0
- {tabdat_synth-0.1.0 → tabdat_synth-0.2.0}/src/tabdat_synth.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabdat-synth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Spec-driven tabular data synthesis with reusable fitted bundles.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/SaehwanPark/tabdat-synth
|
|
@@ -54,7 +54,7 @@ Real tabular data often carries privacy, governance, licensing, or access constr
|
|
|
54
54
|
|
|
55
55
|
- Spec-driven generation from YAML configuration.
|
|
56
56
|
- Directed synthesis workflows with stable topological execution.
|
|
57
|
-
- Empirical, truncated-normal, categorical-model, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
57
|
+
- Empirical, truncated-normal, categorical-model, bin-map, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
58
58
|
- Built-in categorical backends for LightGBM and logistic regression.
|
|
59
59
|
- Reusable fitted bundles for generating later without the original source file.
|
|
60
60
|
- Evaluation reports for marginal quality and row-level disclosure-risk heuristics.
|
|
@@ -70,7 +70,7 @@ A synthesis run has five stages:
|
|
|
70
70
|
4. Sample synthetic rows from the fitted artifacts.
|
|
71
71
|
5. Evaluate the synthetic table against source data when appropriate.
|
|
72
72
|
|
|
73
|
-
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order.
|
|
73
|
+
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order by default, or exactly the columns named in an optional `feature_columns` tuple. Bin-map steps convert a generated numeric column into a categorical column through fixed breaks and labels, with no fitting and no random draw.
|
|
74
74
|
|
|
75
75
|
## Installation
|
|
76
76
|
|
|
@@ -24,7 +24,7 @@ Real tabular data often carries privacy, governance, licensing, or access constr
|
|
|
24
24
|
|
|
25
25
|
- Spec-driven generation from YAML configuration.
|
|
26
26
|
- Directed synthesis workflows with stable topological execution.
|
|
27
|
-
- Empirical, truncated-normal, categorical-model, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
27
|
+
- Empirical, truncated-normal, categorical-model, bin-map, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
28
28
|
- Built-in categorical backends for LightGBM and logistic regression.
|
|
29
29
|
- Reusable fitted bundles for generating later without the original source file.
|
|
30
30
|
- Evaluation reports for marginal quality and row-level disclosure-risk heuristics.
|
|
@@ -40,7 +40,7 @@ A synthesis run has five stages:
|
|
|
40
40
|
4. Sample synthetic rows from the fitted artifacts.
|
|
41
41
|
5. Evaluate the synthetic table against source data when appropriate.
|
|
42
42
|
|
|
43
|
-
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order.
|
|
43
|
+
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order by default, or exactly the columns named in an optional `feature_columns` tuple. Bin-map steps convert a generated numeric column into a categorical column through fixed breaks and labels, with no fitting and no random draw.
|
|
44
44
|
|
|
45
45
|
## Installation
|
|
46
46
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from tabdat_synth.version import __version__
|
|
4
4
|
from tabdat_synth.domain.contracts import (
|
|
5
5
|
BernoulliResponseSpec,
|
|
6
|
+
BinMapSpec,
|
|
6
7
|
CategoricalModelStepArtifact,
|
|
7
8
|
ColumnSpec,
|
|
8
9
|
CoefficientSumSpec,
|
|
@@ -40,6 +41,7 @@ from tabdat_synth.workflows.pipelines import (
|
|
|
40
41
|
__all__ = [
|
|
41
42
|
"__version__",
|
|
42
43
|
"BernoulliResponseSpec",
|
|
44
|
+
"BinMapSpec",
|
|
43
45
|
"CategoricalModelStepArtifact",
|
|
44
46
|
"ColumnSpec",
|
|
45
47
|
"CoefficientSumSpec",
|
|
@@ -8,6 +8,7 @@ import polars as pl
|
|
|
8
8
|
|
|
9
9
|
from tabdat_synth.domain.contracts import (
|
|
10
10
|
BernoulliResponseSpec,
|
|
11
|
+
BinMapSpec,
|
|
11
12
|
CoefficientSumSpec,
|
|
12
13
|
ColumnSpec,
|
|
13
14
|
DatasetSchema,
|
|
@@ -24,6 +25,7 @@ ALLOWED_MISSING_VALUE_POLICIES = {
|
|
|
24
25
|
_SOURCE_STEP_KINDS = {"empirical", "truncated_normal", "categorical_model"}
|
|
25
26
|
_FEATURE_STEP_KINDS = {"categorical_model", "coefficient_sum"}
|
|
26
27
|
_INPUT_STEP_KINDS = {"sigmoid_probability", "bernoulli_response"}
|
|
28
|
+
_BIN_MAP_STEP_KINDS = {"bin_map"}
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def validate_generation_spec(spec: GenerationSpec) -> None:
|
|
@@ -61,6 +63,7 @@ def validate_generation_spec(spec: GenerationSpec) -> None:
|
|
|
61
63
|
if dependency == step.output_column:
|
|
62
64
|
raise ValueError(f"step '{step.name}' cannot depend on itself")
|
|
63
65
|
|
|
66
|
+
_validate_feature_columns(spec)
|
|
64
67
|
_validate_terminal_bernoulli_steps(spec)
|
|
65
68
|
topologically_sort_steps(spec)
|
|
66
69
|
|
|
@@ -163,12 +166,17 @@ def _validate_step_shape(
|
|
|
163
166
|
elif step.source_column is not None:
|
|
164
167
|
raise ValueError(f"{step.kind} step '{step.name}' cannot declare source_column")
|
|
165
168
|
|
|
166
|
-
if step.kind in _INPUT_STEP_KINDS:
|
|
169
|
+
if step.kind in _INPUT_STEP_KINDS or step.kind in _BIN_MAP_STEP_KINDS:
|
|
167
170
|
if step.input_column is None:
|
|
168
171
|
raise ValueError(f"{step.kind} step '{step.name}' requires input_column")
|
|
169
172
|
elif step.input_column is not None:
|
|
170
173
|
raise ValueError(f"{step.kind} step '{step.name}' cannot declare input_column")
|
|
171
174
|
|
|
175
|
+
if step.kind == "bin_map":
|
|
176
|
+
_validate_bin_map_step(step, output_spec)
|
|
177
|
+
elif step.bin_map is not None:
|
|
178
|
+
raise ValueError(f"{step.kind} step '{step.name}' cannot declare bin_map")
|
|
179
|
+
|
|
172
180
|
if step.kind == "categorical_model":
|
|
173
181
|
if not step.parent_columns:
|
|
174
182
|
raise ValueError(
|
|
@@ -183,6 +191,11 @@ def _validate_step_shape(
|
|
|
183
191
|
elif step.parent_columns:
|
|
184
192
|
raise ValueError(f"{step.kind} step '{step.name}' cannot declare parent_columns")
|
|
185
193
|
|
|
194
|
+
if step.feature_columns and step.kind != "categorical_model":
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"{step.kind} step '{step.name}' cannot declare feature_columns"
|
|
197
|
+
)
|
|
198
|
+
|
|
186
199
|
if step.kind == "truncated_normal" and output_spec.data_type == "categorical":
|
|
187
200
|
raise ValueError(
|
|
188
201
|
f"truncated_normal step '{step.name}' cannot target a categorical column"
|
|
@@ -253,6 +266,53 @@ def _validate_response_spec(step_name: str, response: BernoulliResponseSpec) ->
|
|
|
253
266
|
)
|
|
254
267
|
|
|
255
268
|
|
|
269
|
+
def _validate_feature_columns(spec: GenerationSpec) -> None:
|
|
270
|
+
for step in spec.steps:
|
|
271
|
+
if not step.feature_columns:
|
|
272
|
+
continue
|
|
273
|
+
if len(set(step.feature_columns)) != len(step.feature_columns):
|
|
274
|
+
raise ValueError(f"step '{step.name}' has duplicate feature_columns")
|
|
275
|
+
ancestors = resolve_incoming_ancestor_outputs(spec, step)
|
|
276
|
+
unknown = [column for column in step.feature_columns if column not in ancestors]
|
|
277
|
+
if unknown:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"step '{step.name}' feature_columns must already be ancestor outputs; "
|
|
280
|
+
f"unknown feature columns: {unknown}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _validate_bin_map_step(step: GenerationStep, output_spec: ColumnSpec) -> None:
|
|
285
|
+
if step.source_column is not None:
|
|
286
|
+
raise ValueError(f"bin_map step '{step.name}' cannot declare source_column")
|
|
287
|
+
if step.parent_columns:
|
|
288
|
+
raise ValueError(f"bin_map step '{step.name}' cannot declare parent_columns")
|
|
289
|
+
if step.response is not None:
|
|
290
|
+
raise ValueError(f"bin_map step '{step.name}' cannot declare response")
|
|
291
|
+
if step.coefficient_sum is not None:
|
|
292
|
+
raise ValueError(f"bin_map step '{step.name}' cannot declare coefficient_sum")
|
|
293
|
+
if step.feature_columns:
|
|
294
|
+
raise ValueError(f"bin_map step '{step.name}' cannot declare feature_columns")
|
|
295
|
+
if output_spec.data_type != "categorical":
|
|
296
|
+
raise ValueError(f"bin_map step '{step.name}' requires categorical output")
|
|
297
|
+
if step.bin_map is None:
|
|
298
|
+
raise ValueError(f"bin_map step '{step.name}' requires bin_map")
|
|
299
|
+
_validate_bin_map_spec(step.name, step.bin_map)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _validate_bin_map_spec(step_name: str, bin_map: BinMapSpec) -> None:
|
|
303
|
+
breaks = bin_map.breaks
|
|
304
|
+
labels = bin_map.labels
|
|
305
|
+
|
|
306
|
+
if not breaks:
|
|
307
|
+
raise ValueError(f"bin_map step '{step_name}' requires at least one break")
|
|
308
|
+
if len(labels) != len(breaks) + 1:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"bin_map step '{step_name}' requires len(labels) == len(breaks) + 1"
|
|
311
|
+
)
|
|
312
|
+
if any(right <= left for left, right in zip(breaks, breaks[1:])):
|
|
313
|
+
raise ValueError(f"bin_map step '{step_name}' requires strictly increasing breaks")
|
|
314
|
+
|
|
315
|
+
|
|
256
316
|
def _validate_forbidden_specs(
|
|
257
317
|
step: GenerationStep,
|
|
258
318
|
*,
|
|
@@ -16,6 +16,7 @@ GenerationStepKind = Literal[
|
|
|
16
16
|
"coefficient_sum",
|
|
17
17
|
"sigmoid_probability",
|
|
18
18
|
"bernoulli_response",
|
|
19
|
+
"bin_map",
|
|
19
20
|
]
|
|
20
21
|
BackendParamValue: TypeAlias = str | int | float | bool
|
|
21
22
|
BackendScalarValue: TypeAlias = str | int | float
|
|
@@ -65,6 +66,14 @@ class BernoulliResponseSpec:
|
|
|
65
66
|
slope: float = 1.0
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class BinMapSpec:
|
|
71
|
+
"""Typed deterministic binning configuration for one numeric input column."""
|
|
72
|
+
|
|
73
|
+
breaks: tuple[float, ...] = field(default_factory=tuple)
|
|
74
|
+
labels: tuple[str, ...] = field(default_factory=tuple)
|
|
75
|
+
|
|
76
|
+
|
|
68
77
|
@dataclass(frozen=True)
|
|
69
78
|
class ColumnSpec:
|
|
70
79
|
"""Declarative schema metadata for one source or generated column."""
|
|
@@ -103,6 +112,8 @@ class GenerationStep:
|
|
|
103
112
|
mean_shift: float = 0.0
|
|
104
113
|
coefficient_sum: CoefficientSumSpec | None = None
|
|
105
114
|
response: BernoulliResponseSpec | None = None
|
|
115
|
+
feature_columns: tuple[str, ...] = field(default_factory=tuple)
|
|
116
|
+
bin_map: BinMapSpec | None = None
|
|
106
117
|
|
|
107
118
|
|
|
108
119
|
@dataclass(frozen=True)
|
|
@@ -13,6 +13,7 @@ import yaml # type: ignore[import-untyped]
|
|
|
13
13
|
|
|
14
14
|
from tabdat_synth.domain.contracts import (
|
|
15
15
|
BernoulliResponseSpec,
|
|
16
|
+
BinMapSpec,
|
|
16
17
|
CategoricalModelStepArtifact,
|
|
17
18
|
CoefficientSumSpec,
|
|
18
19
|
CoefficientTerm,
|
|
@@ -330,6 +331,10 @@ def _load_generation_step(raw_step: object) -> GenerationStep:
|
|
|
330
331
|
mean_shift=float(raw_step.get("mean_shift", 0.0)),
|
|
331
332
|
coefficient_sum=_load_coefficient_sum(raw_step.get("coefficient_sum")),
|
|
332
333
|
response=_load_response(raw_step.get("response")),
|
|
334
|
+
feature_columns=tuple(
|
|
335
|
+
str(column) for column in raw_step.get("feature_columns", [])
|
|
336
|
+
),
|
|
337
|
+
bin_map=_load_bin_map(raw_step.get("bin_map")),
|
|
333
338
|
)
|
|
334
339
|
|
|
335
340
|
|
|
@@ -351,6 +356,10 @@ def _generation_step_to_dict(step: GenerationStep) -> dict[str, object]:
|
|
|
351
356
|
payload["coefficient_sum"] = _coefficient_sum_to_dict(step.coefficient_sum)
|
|
352
357
|
if step.response is not None:
|
|
353
358
|
payload["response"] = _response_to_dict(step.response)
|
|
359
|
+
if step.feature_columns:
|
|
360
|
+
payload["feature_columns"] = list(step.feature_columns)
|
|
361
|
+
if step.bin_map is not None:
|
|
362
|
+
payload["bin_map"] = _bin_map_to_dict(step.bin_map)
|
|
354
363
|
return payload
|
|
355
364
|
|
|
356
365
|
|
|
@@ -421,5 +430,31 @@ def _response_to_dict(response: BernoulliResponseSpec) -> dict[str, object]:
|
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
|
|
433
|
+
def _load_bin_map(raw_bin_map: object) -> BinMapSpec | None:
|
|
434
|
+
if raw_bin_map is None:
|
|
435
|
+
return None
|
|
436
|
+
if not isinstance(raw_bin_map, dict):
|
|
437
|
+
raise ValueError("bundle step bin_map must be a mapping")
|
|
438
|
+
|
|
439
|
+
raw_breaks = raw_bin_map.get("breaks", [])
|
|
440
|
+
raw_labels = raw_bin_map.get("labels", [])
|
|
441
|
+
if not isinstance(raw_breaks, list):
|
|
442
|
+
raise ValueError("bundle step bin_map.breaks must be a list")
|
|
443
|
+
if not isinstance(raw_labels, list):
|
|
444
|
+
raise ValueError("bundle step bin_map.labels must be a list")
|
|
445
|
+
|
|
446
|
+
return BinMapSpec(
|
|
447
|
+
breaks=tuple(float(value) for value in raw_breaks),
|
|
448
|
+
labels=tuple(str(value) for value in raw_labels),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _bin_map_to_dict(bin_map: BinMapSpec) -> dict[str, object]:
|
|
453
|
+
return {
|
|
454
|
+
"breaks": list(bin_map.breaks),
|
|
455
|
+
"labels": list(bin_map.labels),
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
424
459
|
def _safe_token(value: str) -> str:
|
|
425
460
|
return re.sub(r"[^A-Za-z0-9_.-]+", "_", value)
|
|
@@ -12,6 +12,7 @@ import yaml # type: ignore[import-untyped]
|
|
|
12
12
|
from tabdat_synth.core.sampling import MISSING_CATEGORY_LABEL
|
|
13
13
|
from tabdat_synth.domain.contracts import (
|
|
14
14
|
BernoulliResponseSpec,
|
|
15
|
+
BinMapSpec,
|
|
15
16
|
CoefficientSumSpec,
|
|
16
17
|
CoefficientTerm,
|
|
17
18
|
ColumnSpec,
|
|
@@ -294,6 +295,10 @@ def _load_generation_steps(raw_steps: object) -> tuple[GenerationStep, ...]:
|
|
|
294
295
|
mean_shift=float(cast(Any, raw_step.get("mean_shift", 0.0))),
|
|
295
296
|
coefficient_sum=_load_coefficient_sum(raw_step.get("coefficient_sum")),
|
|
296
297
|
response=_load_response(raw_step.get("response")),
|
|
298
|
+
feature_columns=tuple(
|
|
299
|
+
str(column) for column in raw_step.get("feature_columns", [])
|
|
300
|
+
),
|
|
301
|
+
bin_map=_load_bin_map(raw_step.get("bin_map")),
|
|
297
302
|
)
|
|
298
303
|
)
|
|
299
304
|
|
|
@@ -369,6 +374,25 @@ def _load_response(raw_response: object) -> BernoulliResponseSpec | None:
|
|
|
369
374
|
)
|
|
370
375
|
|
|
371
376
|
|
|
377
|
+
def _load_bin_map(raw_bin_map: object) -> BinMapSpec | None:
|
|
378
|
+
if raw_bin_map is None:
|
|
379
|
+
return None
|
|
380
|
+
if not isinstance(raw_bin_map, dict):
|
|
381
|
+
raise ValueError("bin_map must be a mapping when provided")
|
|
382
|
+
|
|
383
|
+
raw_breaks = raw_bin_map.get("breaks", [])
|
|
384
|
+
raw_labels = raw_bin_map.get("labels", [])
|
|
385
|
+
if not isinstance(raw_breaks, list):
|
|
386
|
+
raise ValueError("bin_map.breaks must be a list when provided")
|
|
387
|
+
if not isinstance(raw_labels, list):
|
|
388
|
+
raise ValueError("bin_map.labels must be a list when provided")
|
|
389
|
+
|
|
390
|
+
return BinMapSpec(
|
|
391
|
+
breaks=tuple(float(cast(Any, value)) for value in raw_breaks),
|
|
392
|
+
labels=tuple(str(value) for value in raw_labels),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
372
396
|
def _column_spec_to_dict(column: ColumnSpec) -> dict[str, object]:
|
|
373
397
|
payload: dict[str, object] = {
|
|
374
398
|
"name": column.name,
|
|
@@ -406,6 +430,10 @@ def _generation_step_to_dict(step: GenerationStep) -> dict[str, object]:
|
|
|
406
430
|
payload["coefficient_sum"] = _coefficient_sum_to_dict(step.coefficient_sum)
|
|
407
431
|
if step.response is not None:
|
|
408
432
|
payload["response"] = _response_to_dict(step.response)
|
|
433
|
+
if step.feature_columns:
|
|
434
|
+
payload["feature_columns"] = list(step.feature_columns)
|
|
435
|
+
if step.bin_map is not None:
|
|
436
|
+
payload["bin_map"] = _bin_map_to_dict(step.bin_map)
|
|
409
437
|
return payload
|
|
410
438
|
|
|
411
439
|
|
|
@@ -436,3 +464,10 @@ def _response_to_dict(response: BernoulliResponseSpec) -> dict[str, object]:
|
|
|
436
464
|
"intercept": response.intercept,
|
|
437
465
|
"slope": response.slope,
|
|
438
466
|
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _bin_map_to_dict(bin_map: BinMapSpec) -> dict[str, object]:
|
|
470
|
+
return {
|
|
471
|
+
"breaks": list(bin_map.breaks),
|
|
472
|
+
"labels": list(bin_map.labels),
|
|
473
|
+
}
|
|
@@ -186,7 +186,7 @@ def _fit_generation_step(
|
|
|
186
186
|
if step.kind == "categorical_model":
|
|
187
187
|
assert step.source_column is not None
|
|
188
188
|
source_series = prepared_source_df[step.source_column]
|
|
189
|
-
input_columns =
|
|
189
|
+
input_columns = _resolve_categorical_input_columns(spec, step)
|
|
190
190
|
X_source = source_generated_df.select(list(input_columns))
|
|
191
191
|
model = fit_categorical_model(
|
|
192
192
|
X_source,
|
|
@@ -220,6 +220,14 @@ def _fit_generation_step(
|
|
|
220
220
|
column_spec,
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
+
if step.kind == "bin_map":
|
|
224
|
+
binned_values = _calculate_bin_map_series(step, source_generated_df)
|
|
225
|
+
return ConfiguredStepArtifact(step=step), _cast_output_series(
|
|
226
|
+
step.output_column,
|
|
227
|
+
binned_values,
|
|
228
|
+
column_spec,
|
|
229
|
+
)
|
|
230
|
+
|
|
223
231
|
if step.kind == "sigmoid_probability":
|
|
224
232
|
probability_values = _calculate_sigmoid_probability_series(
|
|
225
233
|
step,
|
|
@@ -289,6 +297,10 @@ def _execute_fitted_step(
|
|
|
289
297
|
values = _calculate_coefficient_sum_series(step, synth_df, n_samples)
|
|
290
298
|
return _cast_output_series(step.output_column, values, column_spec)
|
|
291
299
|
|
|
300
|
+
if step.kind == "bin_map":
|
|
301
|
+
values = _calculate_bin_map_series(step, synth_df)
|
|
302
|
+
return _cast_output_series(step.output_column, values, column_spec)
|
|
303
|
+
|
|
292
304
|
if step.kind == "sigmoid_probability":
|
|
293
305
|
values = _calculate_sigmoid_probability_series(step, synth_df)
|
|
294
306
|
return _cast_output_series(step.output_column, values, column_spec)
|
|
@@ -329,6 +341,36 @@ def _calculate_sigmoid_probability_series(
|
|
|
329
341
|
return sigmoid(_input_values(step, frame))
|
|
330
342
|
|
|
331
343
|
|
|
344
|
+
def _calculate_bin_map_series(
|
|
345
|
+
step: GenerationStep,
|
|
346
|
+
frame: pl.DataFrame,
|
|
347
|
+
) -> np.ndarray:
|
|
348
|
+
assert step.bin_map is not None
|
|
349
|
+
assert step.input_column is not None
|
|
350
|
+
breaks = list(step.bin_map.breaks)
|
|
351
|
+
labels = list(step.bin_map.labels)
|
|
352
|
+
binned = (
|
|
353
|
+
frame.select(
|
|
354
|
+
pl.col(step.input_column)
|
|
355
|
+
.cut(breaks=breaks, labels=labels)
|
|
356
|
+
.cast(pl.String)
|
|
357
|
+
.alias(step.output_column)
|
|
358
|
+
)
|
|
359
|
+
.to_series()
|
|
360
|
+
.to_numpy()
|
|
361
|
+
)
|
|
362
|
+
return binned
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _resolve_categorical_input_columns(
|
|
366
|
+
spec: GenerationSpec,
|
|
367
|
+
step: GenerationStep,
|
|
368
|
+
) -> tuple[str, ...]:
|
|
369
|
+
if step.feature_columns:
|
|
370
|
+
return step.feature_columns
|
|
371
|
+
return resolve_incoming_ancestor_outputs(spec, step)
|
|
372
|
+
|
|
373
|
+
|
|
332
374
|
def _input_values(step: GenerationStep, frame: pl.DataFrame) -> np.ndarray:
|
|
333
375
|
assert step.input_column is not None
|
|
334
376
|
return frame[step.input_column].cast(pl.Float64).to_numpy()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabdat-synth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Spec-driven tabular data synthesis with reusable fitted bundles.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/SaehwanPark/tabdat-synth
|
|
@@ -54,7 +54,7 @@ Real tabular data often carries privacy, governance, licensing, or access constr
|
|
|
54
54
|
|
|
55
55
|
- Spec-driven generation from YAML configuration.
|
|
56
56
|
- Directed synthesis workflows with stable topological execution.
|
|
57
|
-
- Empirical, truncated-normal, categorical-model, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
57
|
+
- Empirical, truncated-normal, categorical-model, bin-map, coefficient-sum, sigmoid-probability, and Bernoulli-response steps.
|
|
58
58
|
- Built-in categorical backends for LightGBM and logistic regression.
|
|
59
59
|
- Reusable fitted bundles for generating later without the original source file.
|
|
60
60
|
- Evaluation reports for marginal quality and row-level disclosure-risk heuristics.
|
|
@@ -70,7 +70,7 @@ A synthesis run has five stages:
|
|
|
70
70
|
4. Sample synthetic rows from the fitted artifacts.
|
|
71
71
|
5. Evaluate the synthetic table against source data when appropriate.
|
|
72
72
|
|
|
73
|
-
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order.
|
|
73
|
+
The DAG controls which generated columns are available to later steps. For categorical-model steps, declared parents define graph edges, and the fitted model receives the full incoming ancestor context in stable order by default, or exactly the columns named in an optional `feature_columns` tuple. Bin-map steps convert a generated numeric column into a categorical column through fixed breaks and labels, with no fitting and no random draw.
|
|
74
74
|
|
|
75
75
|
## Installation
|
|
76
76
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|