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 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