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.
- dataeval_flow/__init__.py +93 -0
- dataeval_flow/__main__.py +149 -0
- dataeval_flow/_app/__init__.py +5 -0
- dataeval_flow/_app/_model/__init__.py +5 -0
- dataeval_flow/_app/_model/_coerce.py +126 -0
- dataeval_flow/_app/_model/_discover.py +171 -0
- dataeval_flow/_app/_model/_execution.py +108 -0
- dataeval_flow/_app/_model/_introspect.py +280 -0
- dataeval_flow/_app/_model/_item.py +213 -0
- dataeval_flow/_app/_model/_registry.py +255 -0
- dataeval_flow/_app/_model/_state.py +322 -0
- dataeval_flow/_app/_model/_undo.py +61 -0
- dataeval_flow/_app/_panes/__init__.py +35 -0
- dataeval_flow/_app/_panes/_config_pane.py +173 -0
- dataeval_flow/_app/_panes/_result_pane.py +125 -0
- dataeval_flow/_app/_panes/_task_pane.py +91 -0
- dataeval_flow/_app/_panes/_widgets.py +111 -0
- dataeval_flow/_app/_screens/__init__.py +25 -0
- dataeval_flow/_app/_screens/_base.py +242 -0
- dataeval_flow/_app/_screens/_detail.py +333 -0
- dataeval_flow/_app/_screens/_model.py +102 -0
- dataeval_flow/_app/_screens/_params.py +80 -0
- dataeval_flow/_app/_screens/_pathpicker.py +68 -0
- dataeval_flow/_app/_screens/_section.py +621 -0
- dataeval_flow/_app/_screens/_settings.py +183 -0
- dataeval_flow/_app/_viewmodel/__init__.py +15 -0
- dataeval_flow/_app/_viewmodel/_builder_vm.py +272 -0
- dataeval_flow/_app/_viewmodel/_model_vm.py +70 -0
- dataeval_flow/_app/_viewmodel/_rendering.py +189 -0
- dataeval_flow/_app/_viewmodel/_result_vm.py +210 -0
- dataeval_flow/_app/_viewmodel/_section_vm.py +224 -0
- dataeval_flow/_app/app.py +742 -0
- dataeval_flow/_app/cli.py +592 -0
- dataeval_flow/_logging.py +102 -0
- dataeval_flow/cache.py +1355 -0
- dataeval_flow/config/__init__.py +80 -0
- dataeval_flow/config/_loader.py +79 -0
- dataeval_flow/config/_merge.py +92 -0
- dataeval_flow/config/_models.py +115 -0
- dataeval_flow/config/_paths.py +85 -0
- dataeval_flow/config/schemas/__init__.py +112 -0
- dataeval_flow/config/schemas/_dataset.py +111 -0
- dataeval_flow/config/schemas/_extractor.py +119 -0
- dataeval_flow/config/schemas/_metadata.py +28 -0
- dataeval_flow/config/schemas/_preprocessor.py +18 -0
- dataeval_flow/config/schemas/_selection.py +100 -0
- dataeval_flow/config/schemas/_task.py +89 -0
- dataeval_flow/config/schemas/_workflow.py +135 -0
- dataeval_flow/dataset.py +635 -0
- dataeval_flow/embeddings.py +135 -0
- dataeval_flow/metadata.py +48 -0
- dataeval_flow/preprocessing.py +141 -0
- dataeval_flow/py.typed +0 -0
- dataeval_flow/runner.py +118 -0
- dataeval_flow/selection.py +50 -0
- dataeval_flow/workflow/__init__.py +328 -0
- dataeval_flow/workflow/_text_report.py +511 -0
- dataeval_flow/workflow/base.py +69 -0
- dataeval_flow/workflow/orchestrator.py +454 -0
- dataeval_flow/workflows/__init__.py +1 -0
- dataeval_flow/workflows/analysis/__init__.py +38 -0
- dataeval_flow/workflows/analysis/outputs.py +202 -0
- dataeval_flow/workflows/analysis/params.py +114 -0
- dataeval_flow/workflows/analysis/workflow.py +1313 -0
- dataeval_flow/workflows/cleaning/__init__.py +23 -0
- dataeval_flow/workflows/cleaning/outputs.py +200 -0
- dataeval_flow/workflows/cleaning/params.py +160 -0
- dataeval_flow/workflows/cleaning/report.py +304 -0
- dataeval_flow/workflows/cleaning/workflow.py +794 -0
- dataeval_flow/workflows/drift/__init__.py +1 -0
- dataeval_flow/workflows/drift/outputs.py +144 -0
- dataeval_flow/workflows/drift/params.py +332 -0
- dataeval_flow/workflows/drift/report.py +201 -0
- dataeval_flow/workflows/drift/workflow.py +647 -0
- dataeval_flow/workflows/ood/__init__.py +1 -0
- dataeval_flow/workflows/ood/outputs.py +134 -0
- dataeval_flow/workflows/ood/params.py +161 -0
- dataeval_flow/workflows/ood/report.py +311 -0
- dataeval_flow/workflows/ood/workflow.py +728 -0
- dataeval_flow/workflows/prioritization/__init__.py +1 -0
- dataeval_flow/workflows/prioritization/outputs.py +122 -0
- dataeval_flow/workflows/prioritization/params.py +124 -0
- dataeval_flow/workflows/prioritization/report.py +117 -0
- dataeval_flow/workflows/prioritization/workflow.py +587 -0
- dataeval_flow/workflows/splitting/__init__.py +25 -0
- dataeval_flow/workflows/splitting/outputs.py +101 -0
- dataeval_flow/workflows/splitting/params.py +61 -0
- dataeval_flow/workflows/splitting/report.py +485 -0
- dataeval_flow/workflows/splitting/workflow.py +371 -0
- dataeval_flow-0.1.0.dist-info/METADATA +305 -0
- dataeval_flow-0.1.0.dist-info/RECORD +94 -0
- dataeval_flow-0.1.0.dist-info/WHEEL +4 -0
- dataeval_flow-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|