dataeval-flow 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 (94) hide show
  1. dataeval_flow/__init__.py +93 -0
  2. dataeval_flow/__main__.py +149 -0
  3. dataeval_flow/_app/__init__.py +5 -0
  4. dataeval_flow/_app/_model/__init__.py +5 -0
  5. dataeval_flow/_app/_model/_coerce.py +126 -0
  6. dataeval_flow/_app/_model/_discover.py +171 -0
  7. dataeval_flow/_app/_model/_execution.py +108 -0
  8. dataeval_flow/_app/_model/_introspect.py +280 -0
  9. dataeval_flow/_app/_model/_item.py +213 -0
  10. dataeval_flow/_app/_model/_registry.py +255 -0
  11. dataeval_flow/_app/_model/_state.py +322 -0
  12. dataeval_flow/_app/_model/_undo.py +61 -0
  13. dataeval_flow/_app/_panes/__init__.py +35 -0
  14. dataeval_flow/_app/_panes/_config_pane.py +173 -0
  15. dataeval_flow/_app/_panes/_result_pane.py +125 -0
  16. dataeval_flow/_app/_panes/_task_pane.py +91 -0
  17. dataeval_flow/_app/_panes/_widgets.py +111 -0
  18. dataeval_flow/_app/_screens/__init__.py +25 -0
  19. dataeval_flow/_app/_screens/_base.py +242 -0
  20. dataeval_flow/_app/_screens/_detail.py +333 -0
  21. dataeval_flow/_app/_screens/_model.py +102 -0
  22. dataeval_flow/_app/_screens/_params.py +80 -0
  23. dataeval_flow/_app/_screens/_pathpicker.py +68 -0
  24. dataeval_flow/_app/_screens/_section.py +621 -0
  25. dataeval_flow/_app/_screens/_settings.py +183 -0
  26. dataeval_flow/_app/_viewmodel/__init__.py +15 -0
  27. dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
  28. dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
  29. dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
  30. dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
  31. dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
  32. dataeval_flow/_app/app.py +742 -0
  33. dataeval_flow/_app/cli.py +592 -0
  34. dataeval_flow/_logging.py +102 -0
  35. dataeval_flow/cache.py +1355 -0
  36. dataeval_flow/config/__init__.py +80 -0
  37. dataeval_flow/config/_loader.py +79 -0
  38. dataeval_flow/config/_merge.py +92 -0
  39. dataeval_flow/config/_models.py +115 -0
  40. dataeval_flow/config/_paths.py +85 -0
  41. dataeval_flow/config/schemas/__init__.py +112 -0
  42. dataeval_flow/config/schemas/_dataset.py +111 -0
  43. dataeval_flow/config/schemas/_extractor.py +119 -0
  44. dataeval_flow/config/schemas/_metadata.py +28 -0
  45. dataeval_flow/config/schemas/_preprocessor.py +18 -0
  46. dataeval_flow/config/schemas/_selection.py +100 -0
  47. dataeval_flow/config/schemas/_task.py +89 -0
  48. dataeval_flow/config/schemas/_workflow.py +135 -0
  49. dataeval_flow/dataset.py +635 -0
  50. dataeval_flow/embeddings.py +135 -0
  51. dataeval_flow/metadata.py +48 -0
  52. dataeval_flow/preprocessing.py +141 -0
  53. dataeval_flow/py.typed +0 -0
  54. dataeval_flow/runner.py +118 -0
  55. dataeval_flow/selection.py +50 -0
  56. dataeval_flow/workflow/__init__.py +328 -0
  57. dataeval_flow/workflow/_text_report.py +511 -0
  58. dataeval_flow/workflow/base.py +69 -0
  59. dataeval_flow/workflow/orchestrator.py +454 -0
  60. dataeval_flow/workflows/__init__.py +1 -0
  61. dataeval_flow/workflows/analysis/__init__.py +38 -0
  62. dataeval_flow/workflows/analysis/outputs.py +202 -0
  63. dataeval_flow/workflows/analysis/params.py +114 -0
  64. dataeval_flow/workflows/analysis/workflow.py +1313 -0
  65. dataeval_flow/workflows/cleaning/__init__.py +23 -0
  66. dataeval_flow/workflows/cleaning/outputs.py +200 -0
  67. dataeval_flow/workflows/cleaning/params.py +160 -0
  68. dataeval_flow/workflows/cleaning/report.py +304 -0
  69. dataeval_flow/workflows/cleaning/workflow.py +794 -0
  70. dataeval_flow/workflows/drift/__init__.py +1 -0
  71. dataeval_flow/workflows/drift/outputs.py +144 -0
  72. dataeval_flow/workflows/drift/params.py +332 -0
  73. dataeval_flow/workflows/drift/report.py +201 -0
  74. dataeval_flow/workflows/drift/workflow.py +647 -0
  75. dataeval_flow/workflows/ood/__init__.py +1 -0
  76. dataeval_flow/workflows/ood/outputs.py +134 -0
  77. dataeval_flow/workflows/ood/params.py +161 -0
  78. dataeval_flow/workflows/ood/report.py +311 -0
  79. dataeval_flow/workflows/ood/workflow.py +728 -0
  80. dataeval_flow/workflows/prioritization/__init__.py +1 -0
  81. dataeval_flow/workflows/prioritization/outputs.py +122 -0
  82. dataeval_flow/workflows/prioritization/params.py +124 -0
  83. dataeval_flow/workflows/prioritization/report.py +117 -0
  84. dataeval_flow/workflows/prioritization/workflow.py +587 -0
  85. dataeval_flow/workflows/splitting/__init__.py +25 -0
  86. dataeval_flow/workflows/splitting/outputs.py +101 -0
  87. dataeval_flow/workflows/splitting/params.py +61 -0
  88. dataeval_flow/workflows/splitting/report.py +485 -0
  89. dataeval_flow/workflows/splitting/workflow.py +371 -0
  90. dataeval_flow-0.1.0.dist-info/METADATA +305 -0
  91. dataeval_flow-0.1.0.dist-info/RECORD +94 -0
  92. dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
  93. dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
  94. dataeval_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,587 @@
1
+ """Data prioritization workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import logging
7
+ import time as _time
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ from dataeval.flags import ImageStats
13
+ from dataeval.protocols import AnnotatedDataset
14
+ from dataeval.quality import Duplicates, Outliers
15
+ from dataeval.scope import Prioritize
16
+ from numpy.typing import NDArray
17
+ from pydantic import BaseModel
18
+
19
+ from dataeval_flow.cache import (
20
+ active_cache,
21
+ get_or_compute_embeddings,
22
+ get_or_compute_stats,
23
+ selection_repr,
24
+ )
25
+ from dataeval_flow.embeddings import build_extractor
26
+ from dataeval_flow.workflow import DatasetContext, WorkflowContext, WorkflowProtocol, WorkflowResult
27
+ from dataeval_flow.workflows.prioritization.outputs import (
28
+ CleaningSummaryDict,
29
+ DataPrioritizationMetadata,
30
+ DataPrioritizationOutputs,
31
+ DataPrioritizationRawOutputs,
32
+ DataPrioritizationReport,
33
+ PerDatasetPrioritizationDict,
34
+ )
35
+ from dataeval_flow.workflows.prioritization.params import (
36
+ CleaningConfig,
37
+ DataPrioritizationParameters,
38
+ )
39
+ from dataeval_flow.workflows.prioritization.report import build_findings
40
+
41
+ logger: logging.Logger = logging.getLogger(__name__)
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Embedding extraction helper (mirrors OOD pattern)
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ def _get_embeddings_for_context(
50
+ dc: DatasetContext,
51
+ dataset: AnnotatedDataset[Any],
52
+ ) -> NDArray[np.float32]:
53
+ """Extract embeddings for a dataset context, using cache if available."""
54
+ if dc.extractor is None:
55
+ raise ValueError(
56
+ "Data prioritization requires a model/extractor to compute embeddings. "
57
+ "Configure an extractor in the task config."
58
+ )
59
+ sel_key = selection_repr(dataset)
60
+ with contextlib.ExitStack() as stack:
61
+ if dc.cache is not None:
62
+ stack.enter_context(active_cache(dc.cache, sel_key))
63
+ return get_or_compute_embeddings(
64
+ dataset,
65
+ dc.extractor,
66
+ dc.transforms,
67
+ dc.batch_size,
68
+ )
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Flag resolution for cleaning
73
+ # ---------------------------------------------------------------------------
74
+
75
+ _OUTLIER_FLAG_MAP: dict[str, ImageStats] = {
76
+ "dimension": ImageStats.DIMENSION,
77
+ "pixel": ImageStats.PIXEL,
78
+ "visual": ImageStats.VISUAL,
79
+ }
80
+
81
+ _HASH_FLAG_MAP: dict[str, ImageStats] = {
82
+ "hash_basic": ImageStats.HASH_DUPLICATES_BASIC,
83
+ "hash_d4": ImageStats.HASH_DUPLICATES_D4,
84
+ }
85
+
86
+
87
+ def _resolve_cleaning_flags(
88
+ cleaning: CleaningConfig,
89
+ ) -> tuple[ImageStats, ImageStats]:
90
+ """Resolve outlier and hash flags from cleaning config."""
91
+ outlier_flags = ImageStats.NONE
92
+ for flag_name in cleaning.outlier_flags:
93
+ outlier_flags |= _OUTLIER_FLAG_MAP[flag_name]
94
+
95
+ hash_flags = ImageStats.NONE
96
+ if cleaning.duplicate_flags is not None:
97
+ for flag_name in cleaning.duplicate_flags:
98
+ hash_flags |= _HASH_FLAG_MAP[flag_name]
99
+ else:
100
+ hash_flags = ImageStats.HASH_DUPLICATES_BASIC
101
+
102
+ return outlier_flags, hash_flags
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Cleaning step
107
+ # ---------------------------------------------------------------------------
108
+
109
+
110
+ def _run_outlier_detection_per_source(
111
+ cleaning: CleaningConfig,
112
+ outlier_flags: ImageStats,
113
+ hash_flags: ImageStats,
114
+ dc: DatasetContext,
115
+ dataset: AnnotatedDataset[Any],
116
+ ) -> set[int]:
117
+ """Run stats-based outlier detection on a single dataset.
118
+
119
+ Returns the set of flagged item indices.
120
+ """
121
+ sel_key = selection_repr(dataset)
122
+ with contextlib.ExitStack() as stack:
123
+ if dc.cache is not None:
124
+ stack.enter_context(active_cache(dc.cache, sel_key))
125
+ calc_result = get_or_compute_stats(
126
+ desired_flags=outlier_flags | hash_flags,
127
+ dataset=dataset,
128
+ )
129
+
130
+ outliers_eval = Outliers(
131
+ flags=outlier_flags,
132
+ outlier_threshold=(cleaning.outlier_method, cleaning.outlier_threshold),
133
+ )
134
+ outlier_output = outliers_eval.from_stats(calc_result) # type: ignore[arg-type]
135
+ return set(outlier_output.outliers.keys())
136
+
137
+
138
+ def _run_duplicate_detection_cross_dataset(
139
+ cleaning: CleaningConfig,
140
+ hash_flags: ImageStats,
141
+ all_datasets: list[tuple[str, DatasetContext, AnnotatedDataset[Any]]],
142
+ ) -> dict[int, set[int]]:
143
+ """Run hash-based duplicate detection across all datasets.
144
+
145
+ Returns a mapping of dataset_index -> set of flagged item indices.
146
+ Cross-dataset duplicates that include items from the reference (dataset 0)
147
+ only flag the non-reference side.
148
+ """
149
+ datasets = [ds for _, _, ds in all_datasets]
150
+ if len(datasets) < 2:
151
+ return {}
152
+
153
+ dup_kwargs: dict[str, object] = {"merge_near_duplicates": cleaning.duplicate_merge_near}
154
+ if cleaning.duplicate_flags is not None:
155
+ dup_kwargs["flags"] = hash_flags
156
+ duplicates_eval = Duplicates(**dup_kwargs) # type: ignore[arg-type]
157
+ dup_result = duplicates_eval.evaluate(*datasets)
158
+
159
+ flagged: dict[int, set[int]] = {i: set() for i in range(len(datasets))}
160
+ near = {} if cleaning.duplicate_exact_only else dup_result.near
161
+ _collect_flagged_from_groups(dup_result.exact, near, flagged)
162
+ return flagged
163
+
164
+
165
+ def _collect_flagged_from_groups(
166
+ exact_groups: Mapping[int, Sequence[Sequence[int]]],
167
+ near_groups: Mapping[int, Sequence[tuple[Sequence[int], Sequence[str]]]],
168
+ flagged: dict[int, set[int]],
169
+ ) -> None:
170
+ """Populate *flagged* from exact and near duplicate groups."""
171
+ for ds_idx, groups in exact_groups.items():
172
+ for group in groups:
173
+ flagged[ds_idx].update(group[1:])
174
+
175
+ for ds_idx, groups in near_groups.items():
176
+ for indices, _methods in groups:
177
+ flagged[ds_idx].update(indices[1:])
178
+
179
+
180
+ def _run_cleaning(
181
+ cleaning: CleaningConfig,
182
+ ref_dc: DatasetContext,
183
+ ref_dataset: AnnotatedDataset[Any],
184
+ add_datasets: list[tuple[str, DatasetContext, AnnotatedDataset[Any]]],
185
+ ) -> tuple[
186
+ dict[str, set[int]], # per-source flagged indices
187
+ CleaningSummaryDict,
188
+ ]:
189
+ """Run the optional cleaning step across all datasets.
190
+
191
+ Returns per-source flagged indices and a summary.
192
+ """
193
+ logger.info("[3/?] Running pre-prioritization cleaning…")
194
+ t0 = _time.monotonic()
195
+
196
+ outlier_flags, hash_flags = _resolve_cleaning_flags(cleaning)
197
+
198
+ # --- Per-dataset outlier detection ---
199
+ all_sources = [("__reference__", ref_dc, ref_dataset)] + list(add_datasets)
200
+ flagged_outliers: dict[str, set[int]] = {}
201
+ total_outliers = 0
202
+ for name, dc, ds in all_sources:
203
+ flagged = _run_outlier_detection_per_source(cleaning, outlier_flags, hash_flags, dc, ds)
204
+ flagged_outliers[name] = flagged
205
+ total_outliers += len(flagged)
206
+ logger.info(" Outliers in %s: %d", name, len(flagged))
207
+
208
+ # --- Cross-dataset duplicate detection ---
209
+ dup_flagged = _run_duplicate_detection_cross_dataset(cleaning, hash_flags, all_sources)
210
+ flagged_duplicates: dict[str, set[int]] = {}
211
+ total_duplicates = 0
212
+ for i, (name, _, _) in enumerate(all_sources):
213
+ ds_flagged = dup_flagged.get(i, set())
214
+ flagged_duplicates[name] = ds_flagged
215
+ total_duplicates += len(ds_flagged)
216
+ if ds_flagged:
217
+ logger.info(" Duplicates in %s: %d", name, len(ds_flagged))
218
+
219
+ # --- Combine flagged sets ---
220
+ combined_flagged: dict[str, set[int]] = {}
221
+ for name, _, _ in all_sources:
222
+ combined_flagged[name] = flagged_outliers.get(name, set()) | flagged_duplicates.get(name, set())
223
+
224
+ total_combined = sum(len(ds) for _, _, ds in all_sources)
225
+ total_removed = sum(len(s) for s in combined_flagged.values())
226
+
227
+ summary = CleaningSummaryDict(
228
+ total_combined=total_combined,
229
+ outliers_flagged=total_outliers,
230
+ duplicates_flagged=total_duplicates,
231
+ total_removed=total_removed,
232
+ )
233
+
234
+ logger.info(
235
+ "[3/?] Cleaning complete in %.1fs: removed %d/%d items",
236
+ _time.monotonic() - t0,
237
+ total_removed,
238
+ total_combined,
239
+ )
240
+
241
+ return combined_flagged, summary
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Index remapping
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def _build_clean_mapping(
250
+ total: int,
251
+ flagged: set[int],
252
+ ) -> tuple[NDArray[np.intp], list[int]]:
253
+ """Build a boolean mask and clean-to-original index mapping.
254
+
255
+ Returns
256
+ -------
257
+ mask : NDArray[np.bool_]
258
+ Boolean mask where True = clean (not flagged).
259
+ clean_to_original : list[int]
260
+ Maps clean-space index to original-space index.
261
+ """
262
+ mask = np.ones(total, dtype=bool)
263
+ for idx in flagged:
264
+ if 0 <= idx < total:
265
+ mask[idx] = False
266
+ clean_to_original = [i for i in range(total) if mask[i]]
267
+ return mask, clean_to_original
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Workflow class
272
+ # ---------------------------------------------------------------------------
273
+
274
+
275
+ class DataPrioritizationWorkflow(WorkflowProtocol[DataPrioritizationMetadata, DataPrioritizationOutputs]):
276
+ """Data prioritization workflow using DataEval Prioritize."""
277
+
278
+ @property
279
+ def name(self) -> str:
280
+ """Workflow identifier used in configs and task routing."""
281
+ return "data-prioritization"
282
+
283
+ @property
284
+ def description(self) -> str:
285
+ """Human-readable description of the workflow."""
286
+ return "Prioritize unlabeled data for labeling based on a reference dataset and optional cleaning"
287
+
288
+ @property
289
+ def params_schema(self) -> type[DataPrioritizationParameters]:
290
+ """Pydantic model for workflow parameters."""
291
+ return DataPrioritizationParameters
292
+
293
+ @property
294
+ def output_schema(self) -> type[DataPrioritizationOutputs]:
295
+ """Pydantic model for workflow output."""
296
+ return DataPrioritizationOutputs
297
+
298
+ def execute(
299
+ self,
300
+ context: WorkflowContext,
301
+ params: BaseModel | None = None,
302
+ ) -> WorkflowResult[DataPrioritizationMetadata, DataPrioritizationOutputs]:
303
+ """Run the data-prioritization workflow."""
304
+ if not isinstance(context, WorkflowContext):
305
+ return WorkflowResult(
306
+ name=self.name,
307
+ success=False,
308
+ data=self._empty_outputs(),
309
+ errors=[f"Expected WorkflowContext, got {type(context).__name__}"],
310
+ metadata=DataPrioritizationMetadata(),
311
+ )
312
+
313
+ if params is None:
314
+ return WorkflowResult(
315
+ name=self.name,
316
+ success=False,
317
+ data=self._empty_outputs(),
318
+ errors=["DataPrioritizationParameters required"],
319
+ metadata=DataPrioritizationMetadata(),
320
+ )
321
+
322
+ if not isinstance(params, DataPrioritizationParameters):
323
+ return WorkflowResult(
324
+ name=self.name,
325
+ success=False,
326
+ data=self._empty_outputs(),
327
+ errors=[f"Expected DataPrioritizationParameters, got {type(params).__name__}"],
328
+ metadata=DataPrioritizationMetadata(),
329
+ )
330
+
331
+ try:
332
+ return self._run(context, params)
333
+ except Exception as e:
334
+ logger.exception("Workflow '%s' failed", self.name)
335
+ return WorkflowResult(
336
+ name=self.name,
337
+ success=False,
338
+ data=self._empty_outputs(),
339
+ errors=[f"Workflow execution failed: {e}"],
340
+ metadata=DataPrioritizationMetadata(),
341
+ )
342
+
343
+ def _run(
344
+ self,
345
+ context: WorkflowContext,
346
+ params: DataPrioritizationParameters,
347
+ ) -> WorkflowResult[DataPrioritizationMetadata, DataPrioritizationOutputs]:
348
+ # --- 1. Validate: need 2+ datasets and an extractor ---
349
+ dc_items = list(context.dataset_contexts.items())
350
+ if len(dc_items) < 2:
351
+ return WorkflowResult(
352
+ name=self.name,
353
+ success=False,
354
+ data=self._empty_outputs(),
355
+ errors=[
356
+ f"Data prioritization requires at least 2 datasets (reference + data to prioritize), "
357
+ f"got {len(dc_items)}: {[n for n, _ in dc_items]}"
358
+ ],
359
+ metadata=DataPrioritizationMetadata(),
360
+ )
361
+
362
+ # --- 2. Prepare datasets ---
363
+ ref_dc, ref_dataset, add_datasets = self._prepare_datasets(dc_items)
364
+
365
+ # --- 3. Extract embeddings ---
366
+ ref_embeddings, add_embeddings = self._extract_all_embeddings(ref_dc, ref_dataset, add_datasets)
367
+
368
+ # --- 4. Optional cleaning ---
369
+ cleaning_summary: CleaningSummaryDict | None = None
370
+ per_source_flagged: dict[str, set[int]] = {}
371
+ total_removed = 0
372
+
373
+ if params.cleaning is not None:
374
+ per_source_flagged, cleaning_summary = _run_cleaning(params.cleaning, ref_dc, ref_dataset, add_datasets)
375
+ total_removed = cleaning_summary["total_removed"]
376
+
377
+ # --- 5. Build clean embeddings ---
378
+ ref_size = len(ref_dataset)
379
+ ref_flagged = per_source_flagged.get("__reference__", set())
380
+ ref_mask, ref_clean_to_orig = _build_clean_mapping(ref_size, ref_flagged)
381
+ clean_ref_embeddings = ref_embeddings[ref_mask]
382
+
383
+ add_clean_info: dict[str, tuple[NDArray[np.float32], list[int], int]] = {}
384
+ for name, _dc, ds in add_datasets:
385
+ ds_size = len(ds)
386
+ ds_flagged = per_source_flagged.get(name, set())
387
+ ds_mask, ds_clean_to_orig = _build_clean_mapping(ds_size, ds_flagged)
388
+ clean_emb = add_embeddings[name][ds_mask]
389
+ add_clean_info[name] = (clean_emb, ds_clean_to_orig, ds_size)
390
+
391
+ # --- 6. Prioritization ---
392
+ prioritization_results = self._run_prioritization(params, ref_dc, clean_ref_embeddings, add_clean_info)
393
+
394
+ # --- 7. Build outputs ---
395
+ return self._build_workflow_result(
396
+ params,
397
+ ref_size,
398
+ cleaning_summary,
399
+ total_removed,
400
+ prioritization_results,
401
+ ref_clean_to_orig,
402
+ add_clean_info,
403
+ )
404
+
405
+ def _prepare_datasets(
406
+ self,
407
+ dc_items: list[tuple[str, DatasetContext]],
408
+ ) -> tuple[DatasetContext, AnnotatedDataset[Any], list[tuple[str, DatasetContext, AnnotatedDataset[Any]]]]:
409
+ """Identify reference vs additional datasets and apply selections."""
410
+ from dataeval_flow.selection import build_selection
411
+
412
+ ref_name, ref_dc = dc_items[0]
413
+ add_contexts = dc_items[1:]
414
+
415
+ logger.info(
416
+ "[1/?] Preparing datasets: reference=%s, additional=%s",
417
+ ref_name,
418
+ [n for n, _ in add_contexts],
419
+ )
420
+
421
+ ref_dataset: AnnotatedDataset[Any] = ref_dc.dataset
422
+ if ref_dc.selection_steps:
423
+ ref_dataset = build_selection(ref_dataset, ref_dc.selection_steps) # type: ignore[arg-type]
424
+
425
+ add_datasets: list[tuple[str, DatasetContext, AnnotatedDataset[Any]]] = []
426
+ for a_name, a_dc in add_contexts:
427
+ a_ds = a_dc.dataset
428
+ if a_dc.selection_steps:
429
+ a_ds = build_selection(a_ds, a_dc.selection_steps) # type: ignore[arg-type]
430
+ add_datasets.append((a_name, a_dc, a_ds))
431
+
432
+ return ref_dc, ref_dataset, add_datasets
433
+
434
+ def _extract_all_embeddings(
435
+ self,
436
+ ref_dc: DatasetContext,
437
+ ref_dataset: AnnotatedDataset[Any],
438
+ add_datasets: list[tuple[str, DatasetContext, AnnotatedDataset[Any]]],
439
+ ) -> tuple[NDArray[np.float32], dict[str, NDArray[np.float32]]]:
440
+ """Extract embeddings for reference and additional datasets."""
441
+ logger.info("[2/?] Extracting embeddings…")
442
+ t0 = _time.monotonic()
443
+
444
+ ref_embeddings = _get_embeddings_for_context(ref_dc, ref_dataset)
445
+ logger.info(" Reference embeddings: %s", ref_embeddings.shape)
446
+
447
+ add_embeddings: dict[str, NDArray[np.float32]] = {}
448
+ for a_name, a_dc, a_ds in add_datasets:
449
+ emb = _get_embeddings_for_context(a_dc, a_ds)
450
+ add_embeddings[a_name] = emb
451
+ logger.info(" Additional embeddings (%s): %s", a_name, emb.shape)
452
+
453
+ logger.info("[2/?] Embeddings ready in %.1fs", _time.monotonic() - t0)
454
+ return ref_embeddings, add_embeddings
455
+
456
+ def _run_prioritization(
457
+ self,
458
+ params: DataPrioritizationParameters,
459
+ ref_dc: DatasetContext,
460
+ clean_ref_embeddings: NDArray[np.float32],
461
+ add_clean_info: dict[str, tuple[NDArray[np.float32], list[int], int]],
462
+ ) -> dict[str, tuple[list[int], list[float] | None]]:
463
+ """Run prioritization for each additional dataset.
464
+
465
+ Returns a mapping of source_name -> (original_indices, scores).
466
+ """
467
+ logger.info(
468
+ "[4/?] Running prioritization (method=%s, order=%s, policy=%s)…",
469
+ params.method,
470
+ params.order,
471
+ params.policy,
472
+ )
473
+ t0 = _time.monotonic()
474
+
475
+ # Build extractor for Prioritize constructor
476
+ extractor = build_extractor(ref_dc.extractor, ref_dc.transforms) # type: ignore[arg-type]
477
+
478
+ results: dict[str, tuple[list[int], list[float] | None]] = {}
479
+
480
+ for name, (clean_emb, clean_to_orig, _orig_size) in add_clean_info.items():
481
+ if len(clean_emb) == 0:
482
+ logger.warning(" %s: no items after cleaning, skipping", name)
483
+ results[name] = ([], None)
484
+ continue
485
+
486
+ prioritizer = Prioritize(
487
+ extractor=extractor,
488
+ method=params.method,
489
+ k=params.k,
490
+ c=params.c,
491
+ n_init=params.n_init,
492
+ max_cluster_size=params.max_cluster_size,
493
+ order=params.order,
494
+ policy=params.policy,
495
+ num_bins=params.num_bins,
496
+ reference=clean_ref_embeddings,
497
+ )
498
+ p_result = prioritizer.evaluate(clean_emb)
499
+
500
+ # Map clean-space indices back to original-space indices
501
+ original_indices = [clean_to_orig[int(i)] for i in p_result.indices]
502
+ scores: list[float] | None = None
503
+ if p_result.scores is not None:
504
+ scores = [float(s) for s in p_result.scores]
505
+
506
+ results[name] = (original_indices, scores)
507
+ logger.info(" %s: %d items prioritized", name, len(original_indices))
508
+
509
+ logger.info("[4/?] Prioritization complete in %.1fs", _time.monotonic() - t0)
510
+ return results
511
+
512
+ def _build_workflow_result(
513
+ self,
514
+ params: DataPrioritizationParameters,
515
+ ref_size: int,
516
+ cleaning_summary: CleaningSummaryDict | None,
517
+ total_removed: int,
518
+ prioritization_results: dict[str, tuple[list[int], list[float] | None]],
519
+ ref_clean_to_orig: list[int],
520
+ add_clean_info: dict[str, tuple[NDArray[np.float32], list[int], int]],
521
+ ) -> WorkflowResult[DataPrioritizationMetadata, DataPrioritizationOutputs]:
522
+ """Build the final workflow result."""
523
+ total_prioritized = sum(len(indices) for indices, _ in prioritization_results.values())
524
+
525
+ prioritizations: list[PerDatasetPrioritizationDict] = []
526
+ for name, (indices, scores) in prioritization_results.items():
527
+ _, clean_to_orig, orig_size = add_clean_info[name]
528
+ prioritizations.append(
529
+ PerDatasetPrioritizationDict(
530
+ source_name=name,
531
+ original_size=orig_size,
532
+ cleaned_size=len(clean_to_orig),
533
+ prioritized_indices=indices,
534
+ scores=scores,
535
+ )
536
+ )
537
+
538
+ raw = DataPrioritizationRawOutputs(
539
+ dataset_size=ref_size + sum(info[2] for info in add_clean_info.values()),
540
+ reference_size=ref_size,
541
+ method=params.method,
542
+ order=params.order,
543
+ policy=params.policy,
544
+ cleaning_summary=cleaning_summary,
545
+ prioritizations=prioritizations,
546
+ )
547
+
548
+ findings = build_findings(raw, params)
549
+
550
+ summary = f"Prioritization complete. {total_prioritized} items ranked via {params.method}."
551
+
552
+ report = DataPrioritizationReport(summary=summary, findings=findings)
553
+
554
+ # Build metadata
555
+ per_source_clean: dict[str, list[int]] = {}
556
+ per_source_prioritized: dict[str, list[int]] = {}
557
+
558
+ if params.mode == "preparatory":
559
+ per_source_clean["__reference__"] = ref_clean_to_orig
560
+ for name, (_, clean_to_orig, _) in add_clean_info.items():
561
+ per_source_clean[name] = clean_to_orig
562
+ for name, (indices, _) in prioritization_results.items():
563
+ per_source_prioritized[name] = indices
564
+
565
+ metadata = DataPrioritizationMetadata(
566
+ mode=params.mode,
567
+ method=params.method,
568
+ order=params.order,
569
+ policy=params.policy,
570
+ cleaning_enabled=params.cleaning is not None,
571
+ items_removed_by_cleaning=total_removed,
572
+ per_source_clean_indices=per_source_clean,
573
+ per_source_prioritized_indices=per_source_prioritized,
574
+ )
575
+
576
+ return WorkflowResult(
577
+ name=self.name,
578
+ success=True,
579
+ data=DataPrioritizationOutputs(raw=raw, report=report),
580
+ metadata=metadata,
581
+ )
582
+
583
+ def _empty_outputs(self) -> DataPrioritizationOutputs:
584
+ return DataPrioritizationOutputs(
585
+ raw=DataPrioritizationRawOutputs(dataset_size=0),
586
+ report=DataPrioritizationReport(summary="Workflow failed", findings=[]),
587
+ )
@@ -0,0 +1,25 @@
1
+ """Dataset splitting workflow."""
2
+
3
+ __all__ = [
4
+ "DataSplittingMetadata",
5
+ "DataSplittingOutputs",
6
+ "DataSplittingParameters",
7
+ "DataSplittingRawOutputs",
8
+ "DataSplittingReport",
9
+ "DataSplittingResult",
10
+ "DataSplittingWorkflow",
11
+ "SplitInfo",
12
+ "is_splitting_result",
13
+ ]
14
+
15
+ from dataeval_flow.workflows.splitting.outputs import (
16
+ DataSplittingMetadata,
17
+ DataSplittingOutputs,
18
+ DataSplittingRawOutputs,
19
+ DataSplittingReport,
20
+ DataSplittingResult,
21
+ SplitInfo,
22
+ is_splitting_result,
23
+ )
24
+ from dataeval_flow.workflows.splitting.params import DataSplittingParameters
25
+ from dataeval_flow.workflows.splitting.workflow import DataSplittingWorkflow