catgen 0.1.0__py3-none-any.whl
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.
- catgen/__init__.py +64 -0
- catgen/datasets/__init__.py +80 -0
- catgen/datasets/biomedical.py +161 -0
- catgen/datasets/boolean_concepts.py +309 -0
- catgen/datasets/geometric.py +246 -0
- catgen/datasets/multiplexer.py +68 -0
- catgen/datasets/snp_generators.py +186 -0
- catgen/datasets/structured.py +150 -0
- catgen/simulation/__init__.py +15 -0
- catgen/simulation/snp.py +563 -0
- catgen-0.1.0.dist-info/METADATA +293 -0
- catgen-0.1.0.dist-info/RECORD +15 -0
- catgen-0.1.0.dist-info/WHEEL +5 -0
- catgen-0.1.0.dist-info/licenses/LICENSE +21 -0
- catgen-0.1.0.dist-info/top_level.txt +1 -0
catgen/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""catgen – Categorical data generator for machine learning benchmarks.
|
|
2
|
+
|
|
3
|
+
Includes SNP genetics simulation, k-multiplexer boolean datasets, DNF
|
|
4
|
+
concepts, MONK benchmarks, epistasis, geometric boundaries, and more.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from catgen.simulation.snp import (
|
|
10
|
+
SimSNPGlm,
|
|
11
|
+
SimSNPCovariateGlm,
|
|
12
|
+
simulate_snp_glm,
|
|
13
|
+
simulate_snp_glm_with_covariates,
|
|
14
|
+
)
|
|
15
|
+
from catgen.datasets import (
|
|
16
|
+
generate_multiplexer_dataset,
|
|
17
|
+
generate_xor_parity_dataset,
|
|
18
|
+
generate_dnf_concept_dataset,
|
|
19
|
+
generate_monk1_dataset,
|
|
20
|
+
generate_monk3_dataset,
|
|
21
|
+
generate_overlapping_rules_dataset,
|
|
22
|
+
generate_modular_sum_dataset,
|
|
23
|
+
generate_epistasis_dataset,
|
|
24
|
+
generate_highdim_lowsample_dataset,
|
|
25
|
+
generate_imbalanced_dataset,
|
|
26
|
+
generate_checkerboard_dataset,
|
|
27
|
+
generate_circle_boundary_dataset,
|
|
28
|
+
generate_diagonal_boundary_dataset,
|
|
29
|
+
generate_spiral_dataset,
|
|
30
|
+
generate_concentric_rings_dataset,
|
|
31
|
+
generate_deep_tree_dataset,
|
|
32
|
+
generate_sequential_threshold_dataset,
|
|
33
|
+
generate_hierarchical_interaction_dataset,
|
|
34
|
+
generate_snp_glm_dataset,
|
|
35
|
+
generate_snp_glm_with_covariates_dataset,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__",
|
|
40
|
+
"SimSNPGlm",
|
|
41
|
+
"SimSNPCovariateGlm",
|
|
42
|
+
"simulate_snp_glm",
|
|
43
|
+
"simulate_snp_glm_with_covariates",
|
|
44
|
+
"generate_multiplexer_dataset",
|
|
45
|
+
"generate_xor_parity_dataset",
|
|
46
|
+
"generate_dnf_concept_dataset",
|
|
47
|
+
"generate_monk1_dataset",
|
|
48
|
+
"generate_monk3_dataset",
|
|
49
|
+
"generate_overlapping_rules_dataset",
|
|
50
|
+
"generate_modular_sum_dataset",
|
|
51
|
+
"generate_epistasis_dataset",
|
|
52
|
+
"generate_highdim_lowsample_dataset",
|
|
53
|
+
"generate_imbalanced_dataset",
|
|
54
|
+
"generate_checkerboard_dataset",
|
|
55
|
+
"generate_circle_boundary_dataset",
|
|
56
|
+
"generate_diagonal_boundary_dataset",
|
|
57
|
+
"generate_spiral_dataset",
|
|
58
|
+
"generate_concentric_rings_dataset",
|
|
59
|
+
"generate_deep_tree_dataset",
|
|
60
|
+
"generate_sequential_threshold_dataset",
|
|
61
|
+
"generate_hierarchical_interaction_dataset",
|
|
62
|
+
"generate_snp_glm_dataset",
|
|
63
|
+
"generate_snp_glm_with_covariates_dataset",
|
|
64
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""catgen.datasets – synthetic benchmark dataset generators.
|
|
2
|
+
|
|
3
|
+
All generators return ``(X, y)`` tuples of NumPy arrays and accept a
|
|
4
|
+
``random_state`` keyword argument for reproducibility.
|
|
5
|
+
|
|
6
|
+
Submodules
|
|
7
|
+
----------
|
|
8
|
+
multiplexer
|
|
9
|
+
k-multiplexer boolean classification tasks.
|
|
10
|
+
boolean_concepts
|
|
11
|
+
XOR/parity, DNF concepts, MONK benchmarks, overlapping rules.
|
|
12
|
+
biomedical
|
|
13
|
+
SNP epistasis (GAMETES-style), high-dimensional low-sample, imbalanced.
|
|
14
|
+
geometric
|
|
15
|
+
Checkerboard, circle/spiral/ring boundaries.
|
|
16
|
+
structured
|
|
17
|
+
Modular sum, deep tree, sequential thresholds, hierarchical interactions.
|
|
18
|
+
snp_generators
|
|
19
|
+
SNP dataset generators with unified (X, y) interface.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from catgen.datasets.multiplexer import generate_multiplexer_dataset
|
|
23
|
+
from catgen.datasets.boolean_concepts import (
|
|
24
|
+
generate_xor_parity_dataset,
|
|
25
|
+
generate_dnf_concept_dataset,
|
|
26
|
+
generate_monk1_dataset,
|
|
27
|
+
generate_monk3_dataset,
|
|
28
|
+
generate_overlapping_rules_dataset,
|
|
29
|
+
generate_modular_sum_dataset,
|
|
30
|
+
)
|
|
31
|
+
from catgen.datasets.biomedical import (
|
|
32
|
+
generate_epistasis_dataset,
|
|
33
|
+
generate_highdim_lowsample_dataset,
|
|
34
|
+
generate_imbalanced_dataset,
|
|
35
|
+
)
|
|
36
|
+
from catgen.datasets.geometric import (
|
|
37
|
+
generate_checkerboard_dataset,
|
|
38
|
+
generate_circle_boundary_dataset,
|
|
39
|
+
generate_diagonal_boundary_dataset,
|
|
40
|
+
generate_spiral_dataset,
|
|
41
|
+
generate_concentric_rings_dataset,
|
|
42
|
+
)
|
|
43
|
+
from catgen.datasets.structured import (
|
|
44
|
+
generate_deep_tree_dataset,
|
|
45
|
+
generate_sequential_threshold_dataset,
|
|
46
|
+
generate_hierarchical_interaction_dataset,
|
|
47
|
+
)
|
|
48
|
+
from catgen.datasets.snp_generators import (
|
|
49
|
+
generate_snp_glm_dataset,
|
|
50
|
+
generate_snp_glm_with_covariates_dataset,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# multiplexer
|
|
55
|
+
"generate_multiplexer_dataset",
|
|
56
|
+
# boolean concepts
|
|
57
|
+
"generate_xor_parity_dataset",
|
|
58
|
+
"generate_dnf_concept_dataset",
|
|
59
|
+
"generate_monk1_dataset",
|
|
60
|
+
"generate_monk3_dataset",
|
|
61
|
+
"generate_overlapping_rules_dataset",
|
|
62
|
+
"generate_modular_sum_dataset",
|
|
63
|
+
# biomedical
|
|
64
|
+
"generate_epistasis_dataset",
|
|
65
|
+
"generate_highdim_lowsample_dataset",
|
|
66
|
+
"generate_imbalanced_dataset",
|
|
67
|
+
# geometric
|
|
68
|
+
"generate_checkerboard_dataset",
|
|
69
|
+
"generate_circle_boundary_dataset",
|
|
70
|
+
"generate_diagonal_boundary_dataset",
|
|
71
|
+
"generate_spiral_dataset",
|
|
72
|
+
"generate_concentric_rings_dataset",
|
|
73
|
+
# structured
|
|
74
|
+
"generate_deep_tree_dataset",
|
|
75
|
+
"generate_sequential_threshold_dataset",
|
|
76
|
+
"generate_hierarchical_interaction_dataset",
|
|
77
|
+
# snp_generators
|
|
78
|
+
"generate_snp_glm_dataset",
|
|
79
|
+
"generate_snp_glm_with_covariates_dataset",
|
|
80
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Biomedical synthetic datasets (epistasis, high-dimensional, imbalanced).
|
|
2
|
+
|
|
3
|
+
These datasets simulate scenarios common in computational biology and
|
|
4
|
+
rare-disease research.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_epistasis_dataset(
|
|
13
|
+
n_samples: int = 1600,
|
|
14
|
+
n_snps: int = 20,
|
|
15
|
+
n_interacting: int = 2,
|
|
16
|
+
heritability: float = 0.4,
|
|
17
|
+
minor_allele_freq: float = 0.3,
|
|
18
|
+
*,
|
|
19
|
+
random_state: int | None = None,
|
|
20
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
21
|
+
"""Generate a synthetic epistasis dataset (SNP-SNP interaction).
|
|
22
|
+
|
|
23
|
+
The class is determined exclusively by an interaction among
|
|
24
|
+
``n_interacting`` SNPs; all other SNPs are noise features.
|
|
25
|
+
The model emulates GAMETES behavior: causal SNPs jointly create an
|
|
26
|
+
XOR-like pattern (no main effect, only interaction).
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
n_samples : int
|
|
31
|
+
Number of instances.
|
|
32
|
+
n_snps : int
|
|
33
|
+
Total number of SNP features (including causal SNPs).
|
|
34
|
+
n_interacting : int
|
|
35
|
+
Number of interacting causal SNPs (2 or 3 recommended).
|
|
36
|
+
heritability : float
|
|
37
|
+
Strength of genetic signal (0 = no signal, 1 = perfectly separable).
|
|
38
|
+
Controls the penetrance table.
|
|
39
|
+
minor_allele_freq : float
|
|
40
|
+
Minor allele frequency for Hardy-Weinberg equilibrium (0 < maf < 0.5).
|
|
41
|
+
random_state : int or None
|
|
42
|
+
Seed for reproducibility.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
X : np.ndarray, shape (n_samples, n_snps), dtype int (values: 0, 1, 2)
|
|
47
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
48
|
+
"""
|
|
49
|
+
rng = np.random.default_rng(random_state)
|
|
50
|
+
if n_interacting > n_snps:
|
|
51
|
+
raise ValueError("n_interacting must not exceed n_snps")
|
|
52
|
+
if not (0 < minor_allele_freq < 0.5):
|
|
53
|
+
raise ValueError("minor_allele_freq must be in (0, 0.5)")
|
|
54
|
+
|
|
55
|
+
p = minor_allele_freq
|
|
56
|
+
hw_probs = [(1 - p) ** 2, 2 * p * (1 - p), p ** 2]
|
|
57
|
+
X = rng.choice(3, size=(n_samples, n_snps), p=hw_probs)
|
|
58
|
+
|
|
59
|
+
causal = X[:, :n_interacting]
|
|
60
|
+
interaction_signal = np.sum(causal, axis=1) % 2
|
|
61
|
+
|
|
62
|
+
pen_high = min(1.0, 0.5 + heritability / 2)
|
|
63
|
+
pen_low = max(0.0, 0.5 - heritability / 2)
|
|
64
|
+
probs = np.where(interaction_signal == 1, pen_high, pen_low)
|
|
65
|
+
y = (rng.random(n_samples) < probs).astype(int)
|
|
66
|
+
|
|
67
|
+
perm = rng.permutation(n_snps)
|
|
68
|
+
X = X[:, perm]
|
|
69
|
+
return X, y
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_highdim_lowsample_dataset(
|
|
73
|
+
n_samples: int = 120,
|
|
74
|
+
n_features: int = 500,
|
|
75
|
+
n_informative: int = 5,
|
|
76
|
+
n_classes: int = 2,
|
|
77
|
+
*,
|
|
78
|
+
random_state: int | None = None,
|
|
79
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
80
|
+
"""Generate a high-dimensional dataset with few samples (p >> n).
|
|
81
|
+
|
|
82
|
+
Typical for genomics scenarios (e.g. microarray, SNP panels): many features,
|
|
83
|
+
few patients, and only a few informative features.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
n_samples : int
|
|
88
|
+
Number of instances (typically small, e.g. 80–200).
|
|
89
|
+
n_features : int
|
|
90
|
+
Total number of features (typically large, e.g. 200–2000).
|
|
91
|
+
n_informative : int
|
|
92
|
+
Number of truly informative features.
|
|
93
|
+
n_classes : int
|
|
94
|
+
Number of classes.
|
|
95
|
+
random_state : int or None
|
|
96
|
+
Seed.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
X : np.ndarray, shape (n_samples, n_features)
|
|
101
|
+
y : np.ndarray, shape (n_samples,), dtype int
|
|
102
|
+
"""
|
|
103
|
+
from sklearn.datasets import make_classification
|
|
104
|
+
|
|
105
|
+
X, y = make_classification(
|
|
106
|
+
n_samples=n_samples,
|
|
107
|
+
n_features=n_features,
|
|
108
|
+
n_informative=n_informative,
|
|
109
|
+
n_redundant=2,
|
|
110
|
+
n_clusters_per_class=1,
|
|
111
|
+
n_classes=n_classes,
|
|
112
|
+
flip_y=0.03,
|
|
113
|
+
class_sep=1.0,
|
|
114
|
+
random_state=random_state,
|
|
115
|
+
)
|
|
116
|
+
return X, y
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def generate_imbalanced_dataset(
|
|
120
|
+
n_samples: int = 1000,
|
|
121
|
+
n_features: int = 10,
|
|
122
|
+
n_informative: int = 5,
|
|
123
|
+
imbalance_ratio: float = 0.1,
|
|
124
|
+
*,
|
|
125
|
+
random_state: int | None = None,
|
|
126
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
127
|
+
"""Generate a dataset with strong class imbalance.
|
|
128
|
+
|
|
129
|
+
Simulates rare diseases or rare events.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
n_samples : int
|
|
134
|
+
Total number of instances.
|
|
135
|
+
n_features : int
|
|
136
|
+
Number of features.
|
|
137
|
+
n_informative : int
|
|
138
|
+
Number of informative features.
|
|
139
|
+
imbalance_ratio : float
|
|
140
|
+
Minority class ratio (e.g. 0.1 = 10%).
|
|
141
|
+
random_state : int or None
|
|
142
|
+
Seed.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
X : np.ndarray, shape (n_samples, n_features)
|
|
147
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
148
|
+
"""
|
|
149
|
+
from sklearn.datasets import make_classification
|
|
150
|
+
|
|
151
|
+
X, y = make_classification(
|
|
152
|
+
n_samples=n_samples,
|
|
153
|
+
n_features=n_features,
|
|
154
|
+
n_informative=n_informative,
|
|
155
|
+
n_redundant=2,
|
|
156
|
+
n_clusters_per_class=1,
|
|
157
|
+
weights=[1 - imbalance_ratio, imbalance_ratio],
|
|
158
|
+
flip_y=0.01,
|
|
159
|
+
random_state=random_state,
|
|
160
|
+
)
|
|
161
|
+
return X, y
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Boolean concept datasets (XOR/parity, DNF, MONK, overlapping rules, modular sum).
|
|
2
|
+
|
|
3
|
+
These datasets expose weaknesses of CART and are well-suited for evaluating
|
|
4
|
+
rule-set learners.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_xor_parity_dataset(
|
|
13
|
+
n_bits: int = 6,
|
|
14
|
+
n_noise_features: int = 14,
|
|
15
|
+
n_samples: int = 2000,
|
|
16
|
+
*,
|
|
17
|
+
random_state: int | None = None,
|
|
18
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
19
|
+
"""Generate an XOR/parity dataset with noise features.
|
|
20
|
+
|
|
21
|
+
The class is the parity (XOR) of the first ``n_bits`` binary features.
|
|
22
|
+
Additionally, ``n_noise_features`` random binary features are appended.
|
|
23
|
+
This dataset is non-linearly separable and requires conjunctive rules.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
n_bits : int
|
|
28
|
+
Number of relevant parity bits.
|
|
29
|
+
n_noise_features : int
|
|
30
|
+
Number of irrelevant noise features.
|
|
31
|
+
n_samples : int
|
|
32
|
+
Number of instances.
|
|
33
|
+
random_state : int or None
|
|
34
|
+
Seed.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
X : np.ndarray, shape (n_samples, n_bits + n_noise_features), dtype int
|
|
39
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
40
|
+
"""
|
|
41
|
+
rng = np.random.default_rng(random_state)
|
|
42
|
+
total_features = n_bits + n_noise_features
|
|
43
|
+
X = rng.integers(0, 2, size=(n_samples, total_features))
|
|
44
|
+
y = np.bitwise_xor.reduce(X[:, :n_bits], axis=1)
|
|
45
|
+
|
|
46
|
+
perm = rng.permutation(total_features)
|
|
47
|
+
X = X[:, perm]
|
|
48
|
+
return X, y
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_dnf_concept_dataset(
|
|
52
|
+
n_disjuncts: int = 3,
|
|
53
|
+
n_conjuncts: int = 2,
|
|
54
|
+
n_noise_features: int = 10,
|
|
55
|
+
n_samples: int = 2000,
|
|
56
|
+
noise_rate: float = 0.0,
|
|
57
|
+
*,
|
|
58
|
+
random_state: int | None = None,
|
|
59
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
60
|
+
"""Generate a dataset with a DNF decision rule.
|
|
61
|
+
|
|
62
|
+
The true concept is a disjunction of ``n_disjuncts`` conjunctions,
|
|
63
|
+
where each conjunction requires ``n_conjuncts`` different binary features.
|
|
64
|
+
CART must duplicate subtrees for this; rule sets can represent each
|
|
65
|
+
disjunct directly as a separate rule.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
n_disjuncts : int
|
|
70
|
+
Number of disjuncts (OR terms).
|
|
71
|
+
n_conjuncts : int
|
|
72
|
+
Number of conjuncts per disjunct (AND terms).
|
|
73
|
+
n_noise_features : int
|
|
74
|
+
Additional irrelevant binary features.
|
|
75
|
+
n_samples : int
|
|
76
|
+
Number of instances.
|
|
77
|
+
noise_rate : float
|
|
78
|
+
Fraction of randomly flipped labels (0 = no noise).
|
|
79
|
+
random_state : int or None
|
|
80
|
+
Seed.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
X : np.ndarray, shape (n_samples, n_disjuncts * n_conjuncts + n_noise_features), dtype int
|
|
85
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
86
|
+
"""
|
|
87
|
+
rng = np.random.default_rng(random_state)
|
|
88
|
+
n_relevant = n_disjuncts * n_conjuncts
|
|
89
|
+
n_features = n_relevant + n_noise_features
|
|
90
|
+
X = rng.integers(0, 2, size=(n_samples, n_features))
|
|
91
|
+
|
|
92
|
+
y = np.zeros(n_samples, dtype=int)
|
|
93
|
+
for d in range(n_disjuncts):
|
|
94
|
+
clause = np.ones(n_samples, dtype=bool)
|
|
95
|
+
for c in range(n_conjuncts):
|
|
96
|
+
clause &= X[:, d * n_conjuncts + c] == 1
|
|
97
|
+
y |= clause.astype(int)
|
|
98
|
+
|
|
99
|
+
if noise_rate > 0:
|
|
100
|
+
flip = rng.random(n_samples) < noise_rate
|
|
101
|
+
y[flip] = 1 - y[flip]
|
|
102
|
+
|
|
103
|
+
perm = rng.permutation(n_features)
|
|
104
|
+
X = X[:, perm]
|
|
105
|
+
return X, y
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def generate_monk1_dataset(
|
|
109
|
+
n_samples: int = 2000,
|
|
110
|
+
*,
|
|
111
|
+
random_state: int | None = None,
|
|
112
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
113
|
+
"""Generate the MONK-1 dataset (disjunction: (a1==a2) OR (a5==1)).
|
|
114
|
+
|
|
115
|
+
MONK-1 is a classic benchmark for rule learners. The decision rule is a
|
|
116
|
+
disjunction that CART can represent only via subtree duplication.
|
|
117
|
+
|
|
118
|
+
Attributes: a1 in {1,2,3}, a2 in {1,2,3}, a3 in {1,2},
|
|
119
|
+
a4 in {1,2,3}, a5 in {1,2,3,4}, a6 in {1,2}
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
n_samples : int
|
|
124
|
+
Number of instances (sampling with replacement if > 432).
|
|
125
|
+
random_state : int or None
|
|
126
|
+
Seed.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
X : np.ndarray, shape (n_samples, 6), dtype float
|
|
131
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
132
|
+
"""
|
|
133
|
+
rng = np.random.default_rng(random_state)
|
|
134
|
+
domains = [3, 3, 2, 3, 4, 2]
|
|
135
|
+
n_total = 1
|
|
136
|
+
for d in domains:
|
|
137
|
+
n_total *= d # 432
|
|
138
|
+
|
|
139
|
+
rows = np.zeros((n_total, 6), dtype=int)
|
|
140
|
+
idx = 0
|
|
141
|
+
for a1 in range(domains[0]):
|
|
142
|
+
for a2 in range(domains[1]):
|
|
143
|
+
for a3 in range(domains[2]):
|
|
144
|
+
for a4 in range(domains[3]):
|
|
145
|
+
for a5 in range(domains[4]):
|
|
146
|
+
for a6 in range(domains[5]):
|
|
147
|
+
rows[idx] = [a1, a2, a3, a4, a5, a6]
|
|
148
|
+
idx += 1
|
|
149
|
+
|
|
150
|
+
y_full = ((rows[:, 0] == rows[:, 1]) | (rows[:, 4] == 1)).astype(int)
|
|
151
|
+
|
|
152
|
+
if n_samples >= n_total:
|
|
153
|
+
X, y = rows, y_full
|
|
154
|
+
else:
|
|
155
|
+
chosen = rng.choice(n_total, size=n_samples, replace=True)
|
|
156
|
+
X, y = rows[chosen], y_full[chosen]
|
|
157
|
+
|
|
158
|
+
return X.astype(float), y
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def generate_monk3_dataset(
|
|
162
|
+
n_samples: int = 2000,
|
|
163
|
+
noise_rate: float = 0.05,
|
|
164
|
+
*,
|
|
165
|
+
random_state: int | None = None,
|
|
166
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
167
|
+
"""Generate the MONK-3 dataset (conjunction + exception + noise).
|
|
168
|
+
|
|
169
|
+
MONK-3 rule: (a5 != 3 AND a4 != 1) OR (a5 == 3 AND a2 != 3),
|
|
170
|
+
plus ``noise_rate`` randomly flipped labels.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
n_samples : int
|
|
175
|
+
Number of instances.
|
|
176
|
+
noise_rate : float
|
|
177
|
+
Fraction of randomly flipped labels (default: 5%).
|
|
178
|
+
random_state : int or None
|
|
179
|
+
Seed.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
X : np.ndarray, shape (n_samples, 6), dtype float
|
|
184
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
185
|
+
"""
|
|
186
|
+
rng = np.random.default_rng(random_state)
|
|
187
|
+
domains = [3, 3, 2, 3, 4, 2]
|
|
188
|
+
n_total = 1
|
|
189
|
+
for d in domains:
|
|
190
|
+
n_total *= d
|
|
191
|
+
|
|
192
|
+
rows = np.zeros((n_total, 6), dtype=int)
|
|
193
|
+
idx = 0
|
|
194
|
+
for a1 in range(domains[0]):
|
|
195
|
+
for a2 in range(domains[1]):
|
|
196
|
+
for a3 in range(domains[2]):
|
|
197
|
+
for a4 in range(domains[3]):
|
|
198
|
+
for a5 in range(domains[4]):
|
|
199
|
+
for a6 in range(domains[5]):
|
|
200
|
+
rows[idx] = [a1, a2, a3, a4, a5, a6]
|
|
201
|
+
idx += 1
|
|
202
|
+
|
|
203
|
+
y_full = (
|
|
204
|
+
((rows[:, 4] != 3) & (rows[:, 3] != 1))
|
|
205
|
+
| ((rows[:, 4] == 3) & (rows[:, 1] != 2))
|
|
206
|
+
).astype(int)
|
|
207
|
+
|
|
208
|
+
if n_samples >= n_total:
|
|
209
|
+
X, y = rows, y_full
|
|
210
|
+
else:
|
|
211
|
+
chosen = rng.choice(n_total, size=n_samples, replace=True)
|
|
212
|
+
X, y = rows[chosen], y_full[chosen]
|
|
213
|
+
|
|
214
|
+
if noise_rate > 0:
|
|
215
|
+
flip = rng.random(len(y)) < noise_rate
|
|
216
|
+
y[flip] = 1 - y[flip]
|
|
217
|
+
|
|
218
|
+
return X.astype(float), y
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def generate_overlapping_rules_dataset(
|
|
222
|
+
n_rules: int = 4,
|
|
223
|
+
n_features_per_rule: int = 2,
|
|
224
|
+
n_noise_features: int = 10,
|
|
225
|
+
n_samples: int = 2000,
|
|
226
|
+
*,
|
|
227
|
+
random_state: int | None = None,
|
|
228
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
229
|
+
"""Generate a dataset with multiple independent, overlapping rules.
|
|
230
|
+
|
|
231
|
+
Each rule defines a positive region using a threshold over
|
|
232
|
+
``n_features_per_rule`` continuous features. The class is 1 if at least
|
|
233
|
+
one rule fires. CART struggles because the rules are not hierarchical.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
n_rules : int
|
|
238
|
+
Number of independent rules.
|
|
239
|
+
n_features_per_rule : int
|
|
240
|
+
Number of features per rule.
|
|
241
|
+
n_noise_features : int
|
|
242
|
+
Additional noise features.
|
|
243
|
+
n_samples : int
|
|
244
|
+
Number of instances.
|
|
245
|
+
random_state : int or None
|
|
246
|
+
Seed.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
X : np.ndarray, shape (n_samples, n_rules * n_features_per_rule + n_noise_features)
|
|
251
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
252
|
+
"""
|
|
253
|
+
rng = np.random.default_rng(random_state)
|
|
254
|
+
n_relevant = n_rules * n_features_per_rule
|
|
255
|
+
n_total_features = n_relevant + n_noise_features
|
|
256
|
+
X = rng.uniform(0, 1, size=(n_samples, n_total_features))
|
|
257
|
+
|
|
258
|
+
y = np.zeros(n_samples, dtype=int)
|
|
259
|
+
for r in range(n_rules):
|
|
260
|
+
start = r * n_features_per_rule
|
|
261
|
+
end = start + n_features_per_rule
|
|
262
|
+
fired = np.all(X[:, start:end] > 0.6, axis=1)
|
|
263
|
+
y[fired] = 1
|
|
264
|
+
|
|
265
|
+
perm = rng.permutation(n_total_features)
|
|
266
|
+
X = X[:, perm]
|
|
267
|
+
return X, y
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def generate_modular_sum_dataset(
|
|
271
|
+
n_relevant: int = 4,
|
|
272
|
+
n_noise_features: int = 8,
|
|
273
|
+
n_samples: int = 2000,
|
|
274
|
+
modulus: int = 3,
|
|
275
|
+
*,
|
|
276
|
+
random_state: int | None = None,
|
|
277
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
278
|
+
"""Generate a dataset where class depends on a modular sum.
|
|
279
|
+
|
|
280
|
+
y = (sum(X[:, :n_relevant]) mod modulus == 0). This pattern creates
|
|
281
|
+
non-axis-parallel decision boundaries that CART can only approximate
|
|
282
|
+
inefficiently.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
n_relevant : int
|
|
287
|
+
Number of relevant features.
|
|
288
|
+
n_noise_features : int
|
|
289
|
+
Additional noise features.
|
|
290
|
+
n_samples : int
|
|
291
|
+
Number of instances.
|
|
292
|
+
modulus : int
|
|
293
|
+
Modulus for the sum function.
|
|
294
|
+
random_state : int or None
|
|
295
|
+
Seed.
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
X : np.ndarray, shape (n_samples, n_relevant + n_noise_features), dtype float
|
|
300
|
+
y : np.ndarray, shape (n_samples,), dtype int (0 or 1)
|
|
301
|
+
"""
|
|
302
|
+
rng = np.random.default_rng(random_state)
|
|
303
|
+
n_total = n_relevant + n_noise_features
|
|
304
|
+
X = rng.integers(0, modulus + 1, size=(n_samples, n_total))
|
|
305
|
+
y = (np.sum(X[:, :n_relevant], axis=1) % modulus == 0).astype(int)
|
|
306
|
+
|
|
307
|
+
perm = rng.permutation(n_total)
|
|
308
|
+
X = X[:, perm]
|
|
309
|
+
return X.astype(float), y
|