bead 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.
Files changed (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. bead-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,354 @@
1
+ """Item selectors for active learning.
2
+
3
+ This module implements sample selection algorithms that use uncertainty
4
+ strategies to intelligently select the most informative items for labeling
5
+ in the active learning loop.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import numpy as np
13
+
14
+ from bead.active_learning.strategies import create_strategy
15
+ from bead.items.item import Item
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ from bead.active_learning.models.base import ActiveLearningModel
21
+ from bead.config.active_learning import UncertaintySamplerConfig
22
+
23
+
24
+ class ItemSelector:
25
+ """Base class for item selection algorithms.
26
+
27
+ Item selectors determine which unlabeled items should be selected
28
+ for annotation in each active learning iteration.
29
+
30
+ Examples
31
+ --------
32
+ >>> selector = ItemSelector()
33
+ >>> # Subclasses implement select() method
34
+ """
35
+
36
+ def select(
37
+ self,
38
+ items: list[Item],
39
+ model: ActiveLearningModel,
40
+ predict_fn: Callable[[ActiveLearningModel, Item], np.ndarray],
41
+ budget: int,
42
+ ) -> list[Item]:
43
+ """Select items for annotation.
44
+
45
+ Parameters
46
+ ----------
47
+ items : list[Item]
48
+ Unlabeled items to select from.
49
+ model : ActiveLearningModel
50
+ Trained model for making predictions.
51
+ predict_fn : Callable[[ActiveLearningModel, Item], np.ndarray]
52
+ Function to get prediction probabilities from model.
53
+ Should return array of shape (n_classes,) with probabilities.
54
+ budget : int
55
+ Number of items to select.
56
+
57
+ Returns
58
+ -------
59
+ list[Item]
60
+ Selected items for annotation.
61
+
62
+ Examples
63
+ --------
64
+ >>> selector = UncertaintySampler() # doctest: +SKIP
65
+ >>> selected = selector.select( # doctest: +SKIP
66
+ ... items, model, predict_fn, budget=10
67
+ ... )
68
+ >>> len(selected) <= 10 # doctest: +SKIP
69
+ True
70
+ """
71
+ raise NotImplementedError("Subclasses must implement select()")
72
+
73
+
74
+ class UncertaintySampler(ItemSelector):
75
+ """Uncertainty-based item selector.
76
+
77
+ Selects items using uncertainty sampling strategies (entropy, margin,
78
+ or least confidence). This is the main item selection algorithm for
79
+ active learning in bead.
80
+
81
+ Parameters
82
+ ----------
83
+ config : UncertaintySamplerConfig | None
84
+ Configuration for the uncertainty sampler.
85
+
86
+ Attributes
87
+ ----------
88
+ config : UncertaintySamplerConfig
89
+ Configuration for the sampler.
90
+ strategy : SamplingStrategy
91
+ The underlying sampling strategy.
92
+
93
+ Examples
94
+ --------
95
+ >>> import numpy as np
96
+ >>> from uuid import uuid4
97
+ >>> from bead.items.item import Item
98
+ >>> from bead.config.active_learning import UncertaintySamplerConfig
99
+ >>> # Create sampler
100
+ >>> config = UncertaintySamplerConfig(method="entropy")
101
+ >>> sampler = UncertaintySampler(config=config)
102
+ >>> # Mock items
103
+ >>> items = [Item(item_template_id=uuid4(), rendered_elements={}) for _ in range(5)]
104
+ >>> # Mock model and predict function
105
+ >>> def predict_fn(model, item):
106
+ ... return np.array([0.5, 0.5]) # Mock probabilities
107
+ >>> # Select items
108
+ >>> selected = sampler.select(items, None, predict_fn, budget=2)
109
+ >>> len(selected)
110
+ 2
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ config: UncertaintySamplerConfig | None = None,
116
+ ) -> None:
117
+ """Initialize uncertainty sampler.
118
+
119
+ Parameters
120
+ ----------
121
+ config : UncertaintySamplerConfig | None
122
+ Configuration for the sampler. If None, uses defaults.
123
+ """
124
+ self.config = config or UncertaintySamplerConfig()
125
+ self.strategy = create_strategy(self.config.method)
126
+
127
+ def select(
128
+ self,
129
+ items: list[Item],
130
+ model: Any,
131
+ predict_fn: Callable[[Any, Item], np.ndarray],
132
+ budget: int,
133
+ ) -> list[Item]:
134
+ """Select items using uncertainty sampling.
135
+
136
+ Parameters
137
+ ----------
138
+ items : list[Item]
139
+ Unlabeled items to select from.
140
+ model : Any
141
+ Trained model for making predictions.
142
+ predict_fn : Callable[[Any, Item], np.ndarray]
143
+ Function to get prediction probabilities from model.
144
+ Should return array of shape (n_classes,) for each item.
145
+ budget : int
146
+ Number of items to select.
147
+
148
+ Returns
149
+ -------
150
+ list[Item]
151
+ Selected items for annotation, ordered by uncertainty (most to least).
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ If items list is empty or budget is invalid.
157
+
158
+ Examples
159
+ --------
160
+ >>> import numpy as np
161
+ >>> from uuid import uuid4
162
+ >>> from bead.items.item import Item
163
+ >>> from bead.config.active_learning import UncertaintySamplerConfig
164
+ >>> config = UncertaintySamplerConfig(method="entropy")
165
+ >>> sampler = UncertaintySampler(config=config)
166
+ >>> items = [
167
+ ... Item(item_template_id=uuid4(), rendered_elements={"text": "item1"}),
168
+ ... Item(item_template_id=uuid4(), rendered_elements={"text": "item2"}),
169
+ ... ]
170
+ >>> def predict_fn(model, item):
171
+ ... # First item is uncertain, second is confident
172
+ ... if "item1" in item.rendered_elements.get("text", ""):
173
+ ... return np.array([0.5, 0.5])
174
+ ... return np.array([0.9, 0.1])
175
+ >>> selected = sampler.select(items, None, predict_fn, budget=1)
176
+ >>> "item1" in selected[0].rendered_elements["text"]
177
+ True
178
+ """
179
+ # Validate inputs
180
+ if not items:
181
+ raise ValueError("Items list cannot be empty")
182
+
183
+ if budget <= 0:
184
+ raise ValueError(f"Budget must be positive, got {budget}")
185
+
186
+ # Handle case where budget >= number of items
187
+ if budget >= len(items):
188
+ return items.copy()
189
+
190
+ # Compute predictions for all items
191
+ probabilities = self._batch_predict(items, model, predict_fn)
192
+
193
+ # Compute uncertainty scores
194
+ scores = self.strategy.compute_scores(probabilities)
195
+
196
+ # Select top k items
197
+ selected_indices = self.strategy.select_top_k(scores, k=budget)
198
+
199
+ # Return selected items (convert numpy array to list of Python ints)
200
+ return [items[i] for i in selected_indices.tolist()]
201
+
202
+ def _batch_predict(
203
+ self,
204
+ items: list[Item],
205
+ model: Any,
206
+ predict_fn: Callable[[Any, Item], np.ndarray],
207
+ ) -> np.ndarray:
208
+ """Compute predictions in batches.
209
+
210
+ Parameters
211
+ ----------
212
+ items : list[Item]
213
+ Items to predict.
214
+ model : Any
215
+ Trained model.
216
+ predict_fn : Callable[[Any, Item], np.ndarray]
217
+ Prediction function.
218
+
219
+ Returns
220
+ -------
221
+ np.ndarray
222
+ Prediction probabilities with shape (n_items, n_classes).
223
+
224
+ Examples
225
+ --------
226
+ >>> import numpy as np
227
+ >>> from uuid import uuid4
228
+ >>> from bead.items.item import Item
229
+ >>> sampler = UncertaintySampler()
230
+ >>> items = [
231
+ ... Item(item_template_id=uuid4(), rendered_elements={})
232
+ ... for _ in range(3)
233
+ ... ]
234
+ >>> def predict_fn(model, item):
235
+ ... return np.array([0.6, 0.4])
236
+ >>> probs = sampler._batch_predict(items, None, predict_fn)
237
+ >>> probs.shape
238
+ (3, 2)
239
+ """
240
+ all_probs = []
241
+
242
+ # Process in batches
243
+ batch_size = self.config.batch_size or 32
244
+ for i in range(0, len(items), batch_size):
245
+ batch_items = items[i : i + batch_size]
246
+
247
+ # Get predictions for batch
248
+ batch_probs = [predict_fn(model, item) for item in batch_items]
249
+
250
+ all_probs.extend(batch_probs)
251
+
252
+ # Stack into array
253
+ return np.array(all_probs)
254
+
255
+
256
+ class RandomSelector(ItemSelector):
257
+ """Random item selector (baseline).
258
+
259
+ Selects items randomly without considering model predictions.
260
+ Useful as a baseline for comparison with uncertainty-based methods.
261
+
262
+ Parameters
263
+ ----------
264
+ seed : int | None
265
+ Random seed for reproducibility.
266
+
267
+ Attributes
268
+ ----------
269
+ rng : np.random.Generator
270
+ Random number generator.
271
+
272
+ Examples
273
+ --------
274
+ >>> from uuid import uuid4
275
+ >>> from bead.items.item import Item
276
+ >>> selector = RandomSelector(seed=42)
277
+ >>> items = [
278
+ ... Item(item_template_id=uuid4(), rendered_elements={})
279
+ ... for _ in range(10)
280
+ ... ]
281
+ >>> selected = selector.select(items, None, None, budget=3)
282
+ >>> len(selected)
283
+ 3
284
+ """
285
+
286
+ def __init__(self, seed: int | None = None) -> None:
287
+ """Initialize random selector.
288
+
289
+ Parameters
290
+ ----------
291
+ seed : int | None
292
+ Random seed for reproducibility.
293
+ """
294
+ self.rng = np.random.default_rng(seed)
295
+
296
+ def select(
297
+ self,
298
+ items: list[Item],
299
+ model: Any,
300
+ predict_fn: Callable[[Any, Item], np.ndarray],
301
+ budget: int,
302
+ ) -> list[Item]:
303
+ """Select items randomly.
304
+
305
+ Parameters
306
+ ----------
307
+ items : list[Item]
308
+ Items to select from.
309
+ model : Any
310
+ Model (unused, kept for interface compatibility).
311
+ predict_fn : Callable[[Any, Item], np.ndarray]
312
+ Prediction function (unused, kept for interface compatibility).
313
+ budget : int
314
+ Number of items to select.
315
+
316
+ Returns
317
+ -------
318
+ list[Item]
319
+ Randomly selected items.
320
+
321
+ Raises
322
+ ------
323
+ ValueError
324
+ If items list is empty or budget is invalid.
325
+
326
+ Examples
327
+ --------
328
+ >>> from uuid import uuid4
329
+ >>> from bead.items.item import Item
330
+ >>> selector = RandomSelector(seed=123)
331
+ >>> items = [
332
+ ... Item(item_template_id=uuid4(), rendered_elements={})
333
+ ... for _ in range(5)
334
+ ... ]
335
+ >>> selected = selector.select(items, None, None, budget=2)
336
+ >>> len(selected)
337
+ 2
338
+ """
339
+ # Validate inputs
340
+ if not items:
341
+ raise ValueError("Items list cannot be empty")
342
+
343
+ if budget <= 0:
344
+ raise ValueError(f"Budget must be positive, got {budget}")
345
+
346
+ # Handle case where budget >= number of items
347
+ if budget >= len(items):
348
+ return items.copy()
349
+
350
+ # Select random indices without replacement
351
+ selected_indices = self.rng.choice(len(items), size=budget, replace=False)
352
+
353
+ # Return selected items (convert numpy array to list of Python ints)
354
+ return [items[i] for i in selected_indices.tolist()]