subsetmatrix 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.
File without changes
@@ -0,0 +1,74 @@
1
+ import numpy as np
2
+ from math import comb
3
+ from subsetmatrix.engine import generateMatrix
4
+ from subsetmatrix.selecting_subsets import normalize_k_values
5
+ from typing import Any
6
+ from subsetmatrix.payload_args_validation import validate_args, validate_kwargs
7
+
8
+ class ObservationSet:
9
+ """This class instantiates objects with a set of points and keeps a
10
+ matrix with all possible combinations for subsets, the main dataset and
11
+ performs methods on them for several usecases.
12
+ ### properties:
13
+ - X is a list of labels for the observations. They might come as small strings
14
+ or numbers, dates or whatever else picks your fancy, or not come at all, case
15
+ in which we will generate a list of integers as index to the observations;
16
+ - Y is a list of observations. For scientific ends, this one should be some
17
+ type of numeric value, but since this tool is here to answer diversefied needs,
18
+ no use limiting its reach through that, so it might also be a list of anything.
19
+ """
20
+ def __init__(self, *args, **kwargs) -> None:
21
+ # grabbing the first significant argument
22
+ args_dict = validate_args(*args) if args else {}
23
+ kwargs_dict = validate_kwargs(
24
+ **{k:v for k, v in kwargs.items() if k != "indexing_as_one"}
25
+ ) if kwargs else {}
26
+
27
+ if not args_dict and not kwargs_dict:
28
+ raise ValueError("No data was detected while initiating this object.")
29
+
30
+ self.Y = args_dict.get("Y")
31
+ if self.Y is None:
32
+ self.Y = kwargs_dict.get("Y")
33
+
34
+ if self.Y is None:
35
+ raise ValueError("No values for Y were detected while initiating this object.")
36
+
37
+ self.n = len(self.Y)
38
+
39
+ self.X = args_dict.get("X")
40
+ if self.X is None:
41
+ self.X = kwargs_dict.get("X")
42
+
43
+ if self.X is None:
44
+ self.indexing_as_one = bool(kwargs.get("indexing_as_one", True))
45
+ start = 1 if self.indexing_as_one else 0
46
+ self.X = list(range(start, self.n + start))
47
+
48
+ if len(self.X) != len(self.Y):
49
+ raise ValueError(f"The length of the X set and of the Y set are of different sizes: X: {len(self.X)} and Y: {len(self.Y)}")
50
+
51
+ def get_subsets(self, k:int|list[int]) -> list[list[list[Any]]]:
52
+ k_values = normalize_k_values(k, self.n)
53
+ temp_matrix = generateMatrix(self.n, k_values)
54
+
55
+ # one bulk nonzero pass over the whole matrix instead of one
56
+ # np.flatnonzero call per row: rows are grouped by k in k_values
57
+ # order and every row within a k-group has exactly k ones, so the
58
+ # flat column-index stream can be reshaped per group without
59
+ # re-touching numpy per row.
60
+ _, col_idx = np.nonzero(temp_matrix)
61
+
62
+ res = []
63
+ pos = 0
64
+ for k_val in k_values:
65
+ count = comb(self.n, k_val)
66
+ # .tolist() converts the whole block in one bulk C call; iterating
67
+ # a numpy array element-by-element boxes each entry into a numpy
68
+ # scalar, which is far costlier than plain-int indexing below.
69
+ block = col_idx[pos:pos + count * k_val].reshape(count, k_val).tolist()
70
+ pos += count * k_val
71
+ for combo in block:
72
+ res.append([[self.X[j], self.Y[j]] for j in combo]) # type: ignore
73
+ return res
74
+
subsetmatrix/engine.py ADDED
@@ -0,0 +1,52 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from math import comb
4
+ from typing import Iterator
5
+
6
+ def iter_k_masks(n: int, k: int) -> Iterator[int]:
7
+ """Generator yielding subsets with the same ammount
8
+ of points (k) to the limit of observations on the set."""
9
+ if not 0 < k <= n:
10
+ raise ValueError("K must satisfy 0 < k <= n")
11
+ limit = (1 << n) # assuming n = 3, we would have 1 -> 2 -> 4 -> 8, thus the limit is 8
12
+ mask = (1 << k) - 1 # assuming k = 2, first k would be 1 -> 2 -> 4, 4 - 1 = 3
13
+
14
+ while mask < limit:
15
+ yield mask
16
+
17
+ # Gosper's hack: next integer with the same number of set bits, implying subsets of the same size
18
+ c = mask & -mask
19
+ r = mask + c
20
+ mask = (((r ^ mask) >> 2) // c) | r
21
+
22
+ def cardinality(mask: int) -> int:
23
+ """Useful if you're getting a mask outside of context and needs to know where it belongs."""
24
+ return mask.bit_count()
25
+
26
+ def generateMatrix(n:int, K:list=[]) -> npt.NDArray[np.uint32]:
27
+ """The matrix won't follow the cardinal order of growth, but the k-sized groups."""
28
+ if not K:
29
+ K = list(range(2, n + 1)) # skip singletons (k=1); include the full set (k=n)
30
+ if n < 3:
31
+ raise ValueError("The number of observations should be > 2.")
32
+ if n > 20:
33
+ raise ValueError("Dense uint32 generation is temporarily capped at n <=20")
34
+
35
+ total_rows = sum(comb(n, k) for k in K) # sized to whatever K actually requests, not the full 2**n - 2 span
36
+ matr = np.empty((total_rows, n), dtype=np.uint32)
37
+ bits = np.arange(n, dtype=np.uint32)
38
+ row_start = 0
39
+ for k in K:
40
+ # mask generation stays a plain-Python loop (cheap); the mask -> row
41
+ # expansion is done ONCE per k-group via broadcasting instead of once
42
+ # per row, since numpy's per-call dispatch overhead otherwise dwarfs
43
+ # the actual work on rows this small.
44
+ count = comb(n, k)
45
+ masks = np.fromiter(iter_k_masks(n, k), dtype=np.uint32, count=count)
46
+ matr[row_start:row_start + count] = (masks[:, None] >> bits[None, :]) & 1
47
+ row_start += count
48
+ return matr
49
+
50
+
51
+ # if __name__ == "__main__":
52
+ # generateMatrix(3)
@@ -0,0 +1,139 @@
1
+ def validate_args(*args) -> dict:
2
+ """Validates args and delivers them organized in a preconceived dict."""
3
+ res = {}
4
+ all_args = [arg for arg in args]
5
+ if not all_args:
6
+ return res
7
+
8
+ if isinstance(all_args[0], dict):
9
+ points = all_args[0]
10
+ y_temp = points.get("Y")
11
+ x_temp = points.get("X")
12
+ if isinstance(y_temp, dict) or isinstance(x_temp, dict):
13
+ raise ValueError("Y and X should be lists or tuples, like so: 'Y': [1, 2, 3, 4, 5].")
14
+ if isinstance(y_temp, list|tuple) and len(y_temp) > 2:
15
+ # for now we keep iterables out of the story. Later on it makes sense to consider
16
+ pairs = [p for p in y_temp if isinstance(p, list|tuple) and len(p) == 2]
17
+ if len(pairs) > 2:
18
+ res["Y"] = []
19
+ res["X"] = []
20
+ for i in pairs:
21
+ if isinstance(i[0], int|float|complex|str) and isinstance(i[1], int|float|complex|str):
22
+ res["X"].append(i[0])
23
+ res["Y"].append(i[1])
24
+ else:
25
+ res["Y"] = [x for x in y_temp if isinstance(x, int|float|complex|str)]
26
+ elif isinstance(y_temp, dict) and len(y_temp) > 2:
27
+ res["Y"] = []
28
+ res["X"] = []
29
+ for k, v in y_temp.items():
30
+ if isinstance(k, int|float|complex|str) and isinstance(v, int|float|complex|str):
31
+ res["X"].append(k)
32
+ res["Y"].append(v)
33
+ if isinstance(x_temp, list|tuple):
34
+ res["X"] = [x for x in x_temp if isinstance(x, int|float|complex|str)]
35
+
36
+ elif isinstance(all_args[0], list|tuple) and len(all_args[0]) > 2:
37
+ y_temp = all_args[0]
38
+ # I could seed directly into res["Y"] and res["X"], but checking if this fits sounds better
39
+ pairs = [p for p in y_temp if isinstance(p, list|tuple) and len(p) == 2]
40
+ if len(pairs) > 2:
41
+ res["Y"] = []
42
+ res["X"] = []
43
+ for i in pairs:
44
+ if isinstance(i[0], int|float|complex|str) and isinstance(i[1], int|float|complex|str):
45
+ res["X"].append(i[0])
46
+ res["Y"].append(i[1])
47
+ elif not pairs:
48
+ res["Y"] = [y for y in y_temp if isinstance(y, int|float|complex|str)]
49
+
50
+ else:
51
+ # by now we stablished args[0] wasn't a list|tuple, so it should
52
+ # make no sense to treat args as pairs, therefore I'll just grab
53
+ # whatever is args and not an iterable
54
+ res["Y"] = [y for y in all_args if isinstance(y, int|float|complex|str)]
55
+ res["residual_args"] = [r for r in all_args[1:] if not isinstance(r, int|float|complex|str)]
56
+
57
+ if not res.get("Y"):
58
+ return {"residual_args": all_args}
59
+
60
+ if len(res["Y"]) < 3:
61
+ raise ValueError("There should be more than two 'Y' entries.")
62
+ if len(all_args) > 1 and not res.get("residual_args"):
63
+ res["residual_args"] = all_args[1:]
64
+
65
+ # I shouldn't worry about building X from list(range(len(res["Y"]))) now, only after calling both methods on object creation
66
+ return res
67
+
68
+ def validate_kwargs(**kwargs) -> dict:
69
+ """Validates kwargs and delivers them organized in a preconceived dict."""
70
+ res = {}
71
+ all_kwargs = dict(kwargs)
72
+ if not all_kwargs:
73
+ return res
74
+
75
+ if "Y" in all_kwargs:
76
+ y_temp = all_kwargs.get("Y") # it makes no sense to validate it to points since the
77
+ # argument came as "Y" inside a kwarg, so I won't.
78
+
79
+ if not isinstance(y_temp, list|tuple):
80
+ raise ValueError("If you're trying to send Y values via kwarg, the value should be a list, like so: 'Y': [1, 2, 3, 4, 5].")
81
+
82
+ if len(y_temp) > 2:
83
+ res["Y"] = [y for y in y_temp if isinstance(y, int|float|complex|str)]
84
+
85
+ res["residual_kwargs"] = {k:v for k, v in all_kwargs.items() if k != "Y" and k != "X"}
86
+
87
+ if "X" in all_kwargs:
88
+ x_temp = all_kwargs.get("X")
89
+ if isinstance(x_temp, list|tuple) and len(x_temp) > 2:
90
+ res["X"] = [x for x in x_temp if isinstance(x, int|float|complex|str)]
91
+ if "residual_kwargs" not in res:
92
+ res["residual_kwargs"] = {k: v for k, v in all_kwargs.items() if k != "Y" and k != "X"}
93
+
94
+ if not "Y" in res and not "X" in res:
95
+ if "points" in all_kwargs:
96
+ points = all_kwargs.get("points")
97
+ if not points:
98
+ raise ValueError("Points should have values into it.")
99
+
100
+ if "Y" in points:
101
+ y_temp = points.get("Y")
102
+ y_temp_temp = [y for y in y_temp if isinstance(y, int|float|complex|str)]
103
+ if not y_temp_temp or len(y_temp_temp) < 3:
104
+ raise ValueError("There should be more than two 'Y' entries.")
105
+ res["Y"] = y_temp_temp
106
+
107
+ if "Y" in res and len(res["Y"]) > 2 and "X" in points:
108
+ x_temp = points.get("X")
109
+ x_temp_temp = [x for x in x_temp if isinstance(x, int|float|complex|str)]
110
+ if not x_temp_temp or len(x_temp_temp) != len(res["Y"]):
111
+ raise ValueError("There should be the same amount of values in X than there are in Y.")
112
+ res["X"] = x_temp_temp
113
+
114
+ res["residual_kwargs"] = {k:v for k, v in all_kwargs.items() if k != "X" and k != "Y"}
115
+
116
+ if not "points" in all_kwargs and not "Y" in all_kwargs:
117
+ res["Y"] = []
118
+ res["X"] = []
119
+ for k, v in all_kwargs.items():
120
+ if isinstance(k, int|float|complex|str) and isinstance(v, int|float|complex|str):
121
+ res["X"].append(k)
122
+ res["Y"].append(v)
123
+ res["residual_kwargs"] = {k:v for k, v in all_kwargs.items() if not isinstance(k, int|float|complex|str) or not isinstance(v, int|float|complex|str)}
124
+
125
+ if not "Y" in res:
126
+ raise ValueError("No valid entry for Y values was detected in your keyword arguments.")
127
+
128
+ if len(res["Y"]) < 3:
129
+ raise ValueError("There should be more than two 'Y' entries.")
130
+
131
+ if not "X" in res and "X" in all_kwargs:
132
+ x_temp = all_kwargs.get("X")
133
+ if not isinstance(x_temp, list|tuple):
134
+ raise ValueError("X should be a list or a tuple.")
135
+ x_temp_temp = [x for x in x_temp if isinstance(x, int|float|complex|str)]
136
+ if len(x_temp_temp) != res["Y"]:
137
+ raise ValueError("There should be an equal ammount of itens in X and Y.")
138
+
139
+ return res
@@ -0,0 +1,70 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from math import comb
4
+ from operator import index
5
+
6
+ def total_window_size(k_values:list[int], n:int) -> int:
7
+ """Calculates the window size for a list of k values. Mainly for array building purposes."""
8
+ if not k_values:
9
+ raise ValueError("k_values cannot be empty.")
10
+ return sum(comb(n, k) for k in k_values)
11
+
12
+ def validate_k(k: int, n: int) -> int:
13
+ """Validates k, and normalizes it in cases like np.int64(3), against an n-observation matrix."""
14
+ if isinstance(k, bool):
15
+ raise TypeError(f"k must be an integer, not bool. Received {k}.")
16
+
17
+ try:
18
+ k = index(k)
19
+ except TypeError as exc:
20
+ raise TypeError(f"k must be an integer. Received {k}.") from exc
21
+
22
+ if not 0 < k <= n:
23
+ raise ValueError(f"k must satisfy 0 < k <= n. Received k={k}, n={n}.")
24
+
25
+ return k
26
+
27
+ def normalize_k_values(k: int | list[int], n: int) -> list[int]:
28
+ """Normalize k input into a sorted list of valid integer k values."""
29
+ if isinstance(k, list):
30
+ if not k:
31
+ raise ValueError("k cannot be an empty list.")
32
+
33
+ k_values = [validate_k(x, n) for x in k]
34
+ return sorted(set(k_values))
35
+
36
+ return [validate_k(k, n)]
37
+
38
+ def extract_k_window(
39
+ matrix: npt.NDArray[np.uint32],
40
+ k: int | list[int],
41
+ ) -> npt.NDArray[np.uint32]:
42
+ """
43
+ Extract one or more k-sized subset groups from a generated subset matrix.
44
+
45
+ If k is a list, groups are returned in ascending k order.
46
+
47
+ Offsets are computed from n and k alone, assuming `matrix` was built
48
+ with the canonical full k=1..n-1 sweep (e.g. engine.generateMatrix(n,
49
+ list(range(1, n)))). engine.generateMatrix's bare-call default no
50
+ longer produces that layout (it skips k=1 and includes k=n), so
51
+ passing a bare generateMatrix(n) result here silently extracts the
52
+ wrong rows instead of raising.
53
+ """
54
+ if matrix.ndim != 2:
55
+ raise ValueError("matrix must be a 2D array filled with integers.")
56
+
57
+ n = matrix.shape[1]
58
+ k_values = normalize_k_values(k, n)
59
+ window_size = total_window_size(k_values, n)
60
+ base = np.empty((window_size, n), dtype=matrix.dtype)
61
+ current_row = 0
62
+ for i in k_values:
63
+ start = sum(comb(n, j) for j in range(1, i))
64
+ size = comb(n, i)
65
+ end = start + size
66
+
67
+ base[current_row: current_row + size] = matrix[start:end]
68
+ current_row += size
69
+
70
+ return base
@@ -0,0 +1,671 @@
1
+ Metadata-Version: 2.4
2
+ Name: subsetmatrix
3
+ Version: 0.1.0
4
+ Summary: Generate, select, and materialize subset membership matrices from observation sets.
5
+ Author: Lucas Dornelles Cherobim
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/EngDornelles/subsetMatrix
8
+ Project-URL: Repository, https://github.com/EngDornelles/subsetMatrix
9
+ Project-URL: Issues, https://github.com/EngDornelles/subsetMatrix/issues
10
+ Keywords: subsets,combinations,matrix,numpy,combinatorics,dataset,mask
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.26
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # subsetMatrix
31
+
32
+ `subsetMatrix` is a small Python library for generating, selecting, and materializing subsets from an observation set.
33
+
34
+ It starts with a simple idea:
35
+
36
+ > Given `n` observations, generate a binary matrix where each row represents one subset.
37
+
38
+ Each column represents one observation.
39
+ Each row represents one subset.
40
+ A value of `1` means the observation belongs to that subset.
41
+ A value of `0` means it does not.
42
+
43
+ For `n = 3`, the generated matrix is:
44
+
45
+ ```text
46
+ [[1 0 0]
47
+ [0 1 0]
48
+ [0 0 1]
49
+ [1 1 0]
50
+ [1 0 1]
51
+ [0 1 1]]
52
+ ```
53
+
54
+ The empty subset `[0 0 0]` and the full subset `[1 1 1]` are excluded by default.
55
+
56
+ ---
57
+
58
+ ## Why this exists
59
+
60
+ Many workflows need to explore combinations of observations, points, features, candidates, or records.
61
+
62
+ `subsetMatrix` provides a deterministic substrate for that kind of work.
63
+
64
+ It can be useful for:
65
+
66
+ * combinatorial analysis;
67
+ * subset generation;
68
+ * fixed-size subset selection;
69
+ * dataset slicing;
70
+ * candidate generation;
71
+ * research prototypes;
72
+ * model experimentation;
73
+ * matrix-based workflows;
74
+ * observation-subset analysis.
75
+
76
+ The library intentionally keeps interpretation out of the core.
77
+
78
+ It does not decide what a subset means.
79
+ It only helps generate, select, and materialize subsets.
80
+
81
+ ---
82
+
83
+ ## Core behavior
84
+
85
+ For `n` observations, there are:
86
+
87
+ ```text
88
+ 2^n
89
+ ```
90
+
91
+ possible subsets.
92
+
93
+ `subsetMatrix` excludes the empty and full subsets, so the generated matrix has:
94
+
95
+ ```text
96
+ 2^n - 2
97
+ ```
98
+
99
+ rows.
100
+
101
+ The matrix shape is:
102
+
103
+ ```text
104
+ (2^n - 2, n)
105
+ ```
106
+
107
+ Examples:
108
+
109
+ ```text
110
+ n = 3 → 6 rows
111
+ n = 4 → 14 rows
112
+ n = 20 → 1,048,574 rows
113
+ ```
114
+
115
+ Rows are grouped by subset size `k`.
116
+
117
+ For `n = 4`, rows are ordered as:
118
+
119
+ ```text
120
+ k = 1 → subsets with one active observation
121
+ k = 2 → subsets with two active observations
122
+ k = 3 → subsets with three active observations
123
+ ```
124
+
125
+ The groups `k = 0` and `k = n` are skipped.
126
+
127
+ ---
128
+
129
+ ## Installation
130
+
131
+ Clone the repository:
132
+
133
+ ```bash
134
+ git clone https://github.com/EngDornelles/subsetMatrix.git
135
+ cd subsetMatrix
136
+ ```
137
+
138
+ Create a virtual environment.
139
+
140
+ On Windows PowerShell:
141
+
142
+ ```powershell
143
+ py -m venv .venv
144
+ .\.venv\Scripts\activate
145
+ ```
146
+
147
+ Install the package in editable mode:
148
+
149
+ ```powershell
150
+ py -m pip install -e .
151
+ ```
152
+
153
+ Install test dependencies:
154
+
155
+ ```powershell
156
+ py -m pip install pytest
157
+ ```
158
+
159
+ Run tests:
160
+
161
+ ```powershell
162
+ py -m pytest -v
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Quick start
168
+
169
+ ### Generate a subset matrix
170
+
171
+ ```python
172
+ from subsetmatrix.engine import generateMatrix
173
+
174
+ matrix = generateMatrix(3)
175
+
176
+ print(matrix)
177
+ ```
178
+
179
+ By default (no `K` given), rows are grouped by k, skipping singletons
180
+ (k=1) and including the full set (k=n):
181
+
182
+ Output:
183
+
184
+ ```text
185
+ [[1 1 0]
186
+ [1 0 1]
187
+ [0 1 1]
188
+ [1 1 1]]
189
+ ```
190
+
191
+ To include singletons or restrict to specific subset sizes, pass `K`
192
+ explicitly:
193
+
194
+ ```python
195
+ matrix = generateMatrix(3, [1, 2, 3])
196
+ ```
197
+
198
+ ---
199
+
200
+ ## User-facing dataset workflow
201
+
202
+ The easiest way to use the library is through `ObservationSet`.
203
+
204
+ ```python
205
+ from subsetmatrix.dataset_payload import ObservationSet
206
+
207
+ obs = ObservationSet(
208
+ {
209
+ "Y": [10, 20, 30, 40],
210
+ "X": ["A", "B", "C", "D"],
211
+ }
212
+ )
213
+
214
+ subsets = obs.get_subsets(2)
215
+
216
+ print(subsets)
217
+ ```
218
+
219
+ Output:
220
+
221
+ ```python
222
+ [
223
+ [["A", 10], ["B", 20]],
224
+ [["A", 10], ["C", 30]],
225
+ [["B", 20], ["C", 30]],
226
+ [["A", 10], ["D", 40]],
227
+ [["B", 20], ["D", 40]],
228
+ [["C", 30], ["D", 40]],
229
+ ]
230
+ ```
231
+
232
+ `X` contains labels.
233
+ `Y` contains observations.
234
+
235
+ If `X` is not provided, labels are generated automatically.
236
+
237
+ ```python
238
+ obs = ObservationSet(
239
+ {
240
+ "Y": [10, 20, 30, 40],
241
+ }
242
+ )
243
+
244
+ print(obs.X)
245
+ ```
246
+
247
+ Output:
248
+
249
+ ```python
250
+ [1, 2, 3, 4]
251
+ ```
252
+
253
+ By default, generated labels are one-based.
254
+
255
+ To use zero-based labels:
256
+
257
+ ```python
258
+ obs = ObservationSet(
259
+ {
260
+ "Y": [10, 20, 30, 40],
261
+ },
262
+ indexing_as_one=False,
263
+ )
264
+
265
+ print(obs.X)
266
+ ```
267
+
268
+ Output:
269
+
270
+ ```python
271
+ [0, 1, 2, 3]
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Selecting subset windows by `k`
277
+
278
+ You can extract only the rows for a specific subset size.
279
+
280
+ `extract_k_window` computes row offsets assuming `matrix` was built
281
+ with the full k=1..n-1 sweep, so pass that explicit `K` to
282
+ `generateMatrix` — its own default (no `K`) skips k=1 and includes
283
+ k=n, which no longer matches those offsets. If you just want specific
284
+ k-sized subsets, prefer calling `generateMatrix(n, K)` directly (see
285
+ above) instead of going through `extract_k_window`.
286
+
287
+ ```python
288
+ from subsetmatrix.engine import generateMatrix
289
+ from subsetmatrix.selecting_subsets import extract_k_window
290
+
291
+ matrix = generateMatrix(4, list(range(1, 4)))
292
+
293
+ k2_matrix = extract_k_window(matrix, 2)
294
+
295
+ print(k2_matrix)
296
+ ```
297
+
298
+ Output:
299
+
300
+ ```text
301
+ [[1 1 0 0]
302
+ [1 0 1 0]
303
+ [0 1 1 0]
304
+ [1 0 0 1]
305
+ [0 1 0 1]
306
+ [0 0 1 1]]
307
+ ```
308
+
309
+ You can also extract multiple `k` groups:
310
+
311
+ ```python
312
+ selected = extract_k_window(matrix, [1, 3])
313
+ ```
314
+
315
+ The list is normalized, sorted, and deduplicated.
316
+
317
+ So this:
318
+
319
+ ```python
320
+ extract_k_window(matrix, [3, 1, 1])
321
+ ```
322
+
323
+ behaves like:
324
+
325
+ ```python
326
+ extract_k_window(matrix, [1, 3])
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Fixed-size mask generation
332
+
333
+ `subsetMatrix` uses integer masks internally to generate subset rows.
334
+
335
+ You can generate masks directly for a fixed subset size `k`:
336
+
337
+ ```python
338
+ from subsetmatrix.engine import iter_k_masks
339
+
340
+ for mask in iter_k_masks(n=4, k=2):
341
+ print(mask)
342
+ ```
343
+
344
+ Output:
345
+
346
+ ```text
347
+ 3
348
+ 5
349
+ 6
350
+ 9
351
+ 10
352
+ 12
353
+ ```
354
+
355
+ Those masks correspond to:
356
+
357
+ ```text
358
+ 0011
359
+ 0101
360
+ 0110
361
+ 1001
362
+ 1010
363
+ 1100
364
+ ```
365
+
366
+ Each mask has exactly two active bits.
367
+
368
+ ---
369
+
370
+ ## Cardinality
371
+
372
+ You can check how many active observations a mask contains:
373
+
374
+ ```python
375
+ from subsetmatrix.engine import cardinality
376
+
377
+ print(cardinality(5))
378
+ ```
379
+
380
+ Output:
381
+
382
+ ```text
383
+ 2
384
+ ```
385
+
386
+ Because:
387
+
388
+ ```text
389
+ 5 = 0101
390
+ ```
391
+
392
+ has two active bits.
393
+
394
+ ---
395
+
396
+ ## Current API
397
+
398
+ ### `generateMatrix(n: int, K: list[int] = [])`
399
+
400
+ Generates the subset membership matrix for the requested subset sizes,
401
+ grouped by k. If `K` is omitted, defaults to `range(2, n + 1)` —
402
+ singletons (k=1) are skipped and the full set (k=n) is included.
403
+
404
+ ```python
405
+ from subsetmatrix.engine import generateMatrix
406
+
407
+ matrix = generateMatrix(4)
408
+ ```
409
+
410
+ For `n = 4`, the shape is:
411
+
412
+ ```text
413
+ (11, 4)
414
+ ```
415
+
416
+ Pass `K` explicitly to select specific subset sizes:
417
+
418
+ ```python
419
+ matrix = generateMatrix(4, [2]) # only pairs
420
+ matrix = generateMatrix(4, [1, 2, 3]) # the pre-1.0 default: full k=1..n-1 sweep
421
+ ```
422
+
423
+ ---
424
+
425
+ ### `iter_k_masks(n: int, k: int)`
426
+
427
+ Yields integer masks with exactly `k` active observations.
428
+
429
+ ```python
430
+ from subsetmatrix.engine import iter_k_masks
431
+
432
+ masks = list(iter_k_masks(4, 2))
433
+ ```
434
+
435
+ ---
436
+
437
+ ### `cardinality(mask: int)`
438
+
439
+ Returns how many active bits exist in a mask.
440
+
441
+ ```python
442
+ from subsetmatrix.engine import cardinality
443
+
444
+ cardinality(12)
445
+ ```
446
+
447
+ ---
448
+
449
+ ### `extract_k_window(matrix, k)`
450
+
451
+ Extracts rows for one or more subset sizes.
452
+
453
+ ```python
454
+ from subsetmatrix.selecting_subsets import extract_k_window
455
+
456
+ k2 = extract_k_window(matrix, 2)
457
+ mixed = extract_k_window(matrix, [1, 3])
458
+ ```
459
+
460
+ ---
461
+
462
+ ### `ObservationSet(points).get_subsets(k)`
463
+
464
+ Materializes actual dataset subsets.
465
+
466
+ ```python
467
+ from subsetmatrix.dataset_payload import ObservationSet
468
+
469
+ obs = ObservationSet(
470
+ {
471
+ "Y": [10, 20, 30, 40],
472
+ "X": ["A", "B", "C", "D"],
473
+ }
474
+ )
475
+
476
+ obs.get_subsets(2)
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Repository structure
482
+
483
+ ```text
484
+ subsetMatrix/
485
+ ├── LICENSE
486
+ ├── README.md
487
+ ├── pyproject.toml
488
+ ├── src/
489
+ │ └── subsetmatrix/
490
+ │ ├── __init__.py
491
+ │ ├── engine.py
492
+ │ ├── selecting_subsets.py
493
+ │ └── dataset_payload.py
494
+ └── tests/
495
+ ├── test_engine.py
496
+ ├── test_selecting_subsets.py
497
+ └── test_dataset_payload.py
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Design notes
503
+
504
+ ### Matrix generation
505
+
506
+ The generated matrix is a binary membership matrix.
507
+
508
+ Each row is a subset.
509
+ Each column is an observation.
510
+
511
+ Example:
512
+
513
+ ```text
514
+ [1 0 1 0]
515
+ ```
516
+
517
+ means:
518
+
519
+ ```text
520
+ include observation 0
521
+ exclude observation 1
522
+ include observation 2
523
+ exclude observation 3
524
+ ```
525
+
526
+ ---
527
+
528
+ ### Cardinality grouping
529
+
530
+ Rows are grouped by subset size `k`.
531
+
532
+ This makes it possible to extract all subsets of a specific size without scanning the whole matrix.
533
+
534
+ For example, if you only need subsets with `k = 3`, you can extract only that window.
535
+
536
+ ---
537
+
538
+ ### Empty and full subsets
539
+
540
+ The empty subset and full subset are excluded.
541
+
542
+ They are usually not useful for workflows where subsets are being compared, sampled, scored, or transformed.
543
+
544
+ Excluded rows:
545
+
546
+ ```text
547
+ [0 0 0 ... 0]
548
+ [1 1 1 ... 1]
549
+ ```
550
+
551
+ ---
552
+
553
+ ### Dense matrix warning
554
+
555
+ The full dense matrix grows quickly.
556
+
557
+ ```text
558
+ n = 20 → 1,048,574 rows
559
+ n = 26 → 67,108,862 rows
560
+ ```
561
+
562
+ Future versions may add:
563
+
564
+ * mask-only output;
565
+ * chunked generation;
566
+ * memory estimation;
567
+ * packed storage;
568
+ * optional export formats;
569
+ * lazy payload materialization.
570
+
571
+ The current version prioritizes clarity and deterministic behavior.
572
+
573
+ ---
574
+
575
+ ## Testing
576
+
577
+ Run:
578
+
579
+ ```powershell
580
+ py -m pytest -v
581
+ ```
582
+
583
+ Current test coverage validates:
584
+
585
+ * matrix shape;
586
+ * exact output for `n = 3`;
587
+ * cardinality grouping;
588
+ * exclusion of empty and full rows;
589
+ * invalid `n`;
590
+ * k-window extraction;
591
+ * sorted and deduplicated `k` lists;
592
+ * rejection of invalid `k`;
593
+ * NumPy integer support;
594
+ * dataset payload materialization;
595
+ * default generated labels;
596
+ * custom labels;
597
+ * invalid input handling.
598
+
599
+ Example current test result:
600
+
601
+ ```text
602
+ 18 passed
603
+ ```
604
+
605
+ ---
606
+
607
+ ## Development status
608
+
609
+ `subsetMatrix` is in early development.
610
+
611
+ Current stable layers:
612
+
613
+ ```text
614
+ engine.py
615
+ → generate subset matrix
616
+
617
+ selecting_subsets.py
618
+ → extract k-window slices
619
+
620
+ dataset_payload.py
621
+ → materialize dataset subsets
622
+ ```
623
+
624
+ Planned improvements may include:
625
+
626
+ * snake_case aliases;
627
+ * chunked matrix generation;
628
+ * mask-first public workflows;
629
+ * memory estimation helpers;
630
+ * optional pandas helpers;
631
+ * optional export utilities;
632
+ * expanded documentation;
633
+ * performance benchmarks.
634
+
635
+ ---
636
+
637
+ ## Naming
638
+
639
+ The GitHub repository is named:
640
+
641
+ ```text
642
+ subsetMatrix
643
+ ```
644
+
645
+ The Python package is imported as:
646
+
647
+ ```python
648
+ import subsetmatrix
649
+ ```
650
+
651
+ This follows Python package naming conventions while preserving the repository’s public name.
652
+
653
+ ---
654
+
655
+ ## License
656
+
657
+ This project is licensed under the MIT License.
658
+
659
+ See:
660
+
661
+ ```text
662
+ LICENSE
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Author
668
+
669
+ Created by Lucas Dornelles Cherobim.
670
+
671
+ GitHub: [EngDornelles](https://github.com/EngDornelles)
@@ -0,0 +1,10 @@
1
+ subsetmatrix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ subsetmatrix/dataset_payload.py,sha256=oTMZf-0-bmHrSQl5iYmhRJlLbWSWI00XqDc_IWroUok,3299
3
+ subsetmatrix/engine.py,sha256=6j3P13R0gqWUoPVMgoBOcY05913vc0xyHy18p53-Ygg,2155
4
+ subsetmatrix/payload_args_validation.py,sha256=PEnVcHsMfYx4sBHZGkey4eh4CoLpzfLPSzw1Lm0rMzI,6489
5
+ subsetmatrix/selecting_subsets.py,sha256=PGbx9_IdT4-C7zAOMlAJHSXp_rukHeos_r9adTZF_sU,2421
6
+ subsetmatrix-0.1.0.dist-info/licenses/LICENSE,sha256=OBAWiIKPH-1aydk7x5_lVYVqJh8dFJZp_iH4uv9F4f8,1081
7
+ subsetmatrix-0.1.0.dist-info/METADATA,sha256=wOchTwwvvl-bT-TE1KZISAX6ZNGJc5wnBb3VKL5BsGg,11261
8
+ subsetmatrix-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ subsetmatrix-0.1.0.dist-info/top_level.txt,sha256=Q-tNlDpdWaJvJ64Of70xUTDDqDyVk9BwXVyqfCv7ByA,13
10
+ subsetmatrix-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Dornelles Cherobim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ subsetmatrix