datapruning 1.0.10__tar.gz → 2.1.0__tar.gz

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 (70) hide show
  1. {datapruning-1.0.10/datapruning.egg-info → datapruning-2.1.0}/PKG-INFO +4 -1
  2. datapruning-2.1.0/datapruning/algorithms/__init__.py +7 -0
  3. datapruning-2.1.0/datapruning/algorithms/el2n_scorer.py +67 -0
  4. datapruning-2.1.0/datapruning/algorithms/fusion_pipeline.py +69 -0
  5. datapruning-2.1.0/datapruning/algorithms/kcenter_select.py +58 -0
  6. datapruning-2.1.0/datapruning/algorithms/margin_scorer.py +78 -0
  7. datapruning-2.1.0/datapruning/algorithms/sliding_window.py +87 -0
  8. datapruning-2.1.0/datapruning/engine/strategy_display.py +118 -0
  9. datapruning-2.1.0/datapruning/multimodal/__init__.py +1 -0
  10. datapruning-2.1.0/datapruning/multimodal/clip_encoder.py +179 -0
  11. datapruning-2.1.0/datapruning/research_pruning/__init__.py +82 -0
  12. datapruning-2.1.0/datapruning/research_pruning/benchmark/__init__.py +0 -0
  13. datapruning-2.1.0/datapruning/research_pruning/benchmark/compare.py +131 -0
  14. datapruning-2.1.0/datapruning/research_pruning/benchmark/evaluation.py +96 -0
  15. datapruning-2.1.0/datapruning/research_pruning/benchmark/synthetic.py +74 -0
  16. datapruning-2.1.0/datapruning/research_pruning/difficulty.py +68 -0
  17. datapruning-2.1.0/datapruning/research_pruning/embeddings/__init__.py +0 -0
  18. datapruning-2.1.0/datapruning/research_pruning/embeddings/torch_utils.py +174 -0
  19. datapruning-2.1.0/datapruning/research_pruning/fusion.py +102 -0
  20. datapruning-2.1.0/datapruning/research_pruning/noise.py +118 -0
  21. datapruning-2.1.0/datapruning/research_pruning/normalize.py +67 -0
  22. datapruning-2.1.0/datapruning/research_pruning/pipeline.py +245 -0
  23. datapruning-2.1.0/datapruning/research_pruning/redundancy.py +122 -0
  24. datapruning-2.1.0/datapruning/research_pruning/scores/__init__.py +0 -0
  25. datapruning-2.1.0/datapruning/research_pruning/scores/aum.py +71 -0
  26. datapruning-2.1.0/datapruning/research_pruning/scores/el2n.py +67 -0
  27. datapruning-2.1.0/datapruning/research_pruning/scores/forgetting.py +64 -0
  28. datapruning-2.1.0/datapruning/research_pruning/scores/grand.py +98 -0
  29. datapruning-2.1.0/datapruning/research_pruning/scores/margin.py +51 -0
  30. datapruning-2.1.0/datapruning/research_pruning/selection.py +127 -0
  31. datapruning-2.1.0/datapruning/research_pruning/selectors/__init__.py +0 -0
  32. datapruning-2.1.0/datapruning/research_pruning/selectors/facility_location.py +88 -0
  33. datapruning-2.1.0/datapruning/research_pruning/selectors/kcenter.py +102 -0
  34. datapruning-2.1.0/datapruning/research_pruning/selectors/out_of_fold.py +120 -0
  35. datapruning-2.1.0/datapruning/research_pruning/strategy.py +130 -0
  36. datapruning-2.1.0/datapruning/sdk.py +289 -0
  37. {datapruning-1.0.10 → datapruning-2.1.0/datapruning.egg-info}/PKG-INFO +4 -1
  38. datapruning-2.1.0/datapruning.egg-info/SOURCES.txt +59 -0
  39. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning.egg-info/requires.txt +4 -0
  40. {datapruning-1.0.10 → datapruning-2.1.0}/pyproject.toml +7 -1
  41. datapruning-1.0.10/datapruning/algorithms/__init__.py +0 -7
  42. datapruning-1.0.10/datapruning/algorithms/cluster_centroids.py +0 -71
  43. datapruning-1.0.10/datapruning/algorithms/density_pruner.py +0 -61
  44. datapruning-1.0.10/datapruning/algorithms/duplicate_aware.py +0 -50
  45. datapruning-1.0.10/datapruning/algorithms/instance_hardness.py +0 -74
  46. datapruning-1.0.10/datapruning/algorithms/random_undersampler.py +0 -57
  47. datapruning-1.0.10/datapruning/engine/strategy_display.py +0 -111
  48. datapruning-1.0.10/datapruning/sdk.py +0 -80
  49. datapruning-1.0.10/datapruning.egg-info/SOURCES.txt +0 -32
  50. {datapruning-1.0.10 → datapruning-2.1.0}/LICENSE +0 -0
  51. {datapruning-1.0.10 → datapruning-2.1.0}/MANIFEST.in +0 -0
  52. {datapruning-1.0.10 → datapruning-2.1.0}/README.md +0 -0
  53. {datapruning-1.0.10 → datapruning-2.1.0}/build_src/_core.c +0 -0
  54. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/__init__.py +0 -0
  55. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/algorithms/base.py +0 -0
  56. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/algorithms/registry.py +0 -0
  57. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/__init__.py +0 -0
  58. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/benchmark.py +0 -0
  59. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/explainability.py +0 -0
  60. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/intelligence.py +0 -0
  61. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/optimizer.py +0 -0
  62. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/scanner.py +0 -0
  63. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/engine/strategy_selector.py +0 -0
  64. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/pipeline.py +0 -0
  65. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/reports/__init__.py +0 -0
  66. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning/reports/exporter.py +0 -0
  67. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning.egg-info/dependency_links.txt +0 -0
  68. {datapruning-1.0.10 → datapruning-2.1.0}/datapruning.egg-info/top_level.txt +0 -0
  69. {datapruning-1.0.10 → datapruning-2.1.0}/setup.cfg +0 -0
  70. {datapruning-1.0.10 → datapruning-2.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 1.0.10
3
+ Version: 2.1.0
4
4
  Summary: Intelligent data pruning for ML datasets
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -21,6 +21,9 @@ Requires-Dist: numpy>=1.24.0
21
21
  Requires-Dist: pandas>=2.0.0
22
22
  Requires-Dist: scikit-learn>=1.3.0
23
23
  Requires-Dist: torch>=2.0.0
24
+ Provides-Extra: clip
25
+ Requires-Dist: transformers>=4.30.0; extra == "clip"
26
+ Requires-Dist: pillow>=9.0.0; extra == "clip"
24
27
  Dynamic: license-file
25
28
 
26
29
  # DataPruning
@@ -0,0 +1,7 @@
1
+ from datapruning.algorithms import margin_scorer
2
+ from datapruning.algorithms import el2n_scorer
3
+ from datapruning.algorithms import kcenter_select
4
+ from datapruning.algorithms import sliding_window
5
+ from datapruning.algorithms import fusion_pipeline
6
+
7
+ from datapruning.algorithms.registry import registry
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.algorithms.base import BaseOptimizer
5
+ from datapruning.algorithms.registry import register
6
+
7
+
8
+ @register
9
+ class EL2NScorerOptimizer(BaseOptimizer):
10
+ key = "el2n"
11
+ display_name = "Difficulty Scoring"
12
+
13
+ def __init__(self):
14
+ self._scores = None
15
+
16
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "EL2NScorerOptimizer":
17
+ X_arr = X.values.astype(np.float64)
18
+ y_arr = y.values
19
+ feat_norms = np.linalg.norm(X_arr, axis=1)
20
+ feat_norms_normed = (feat_norms - feat_norms.min()) / max(feat_norms.max() - feat_norms.min(), 1e-8)
21
+ classes = np.unique(y_arr)
22
+ centroid_scores = np.zeros(len(y_arr), dtype=np.float64)
23
+ for cls in classes:
24
+ cls_mask = y_arr == cls
25
+ centroid = X_arr[cls_mask].mean(axis=0)
26
+ dists = np.linalg.norm(X_arr - centroid, axis=1)
27
+ dists_normed = (dists - dists.min()) / max(dists.max() - dists.min(), 1e-8)
28
+ centroid_scores = np.maximum(centroid_scores, dists_normed)
29
+ combined = 0.5 * feat_norms_normed + 0.5 * centroid_scores
30
+ self._scores = pd.Series(combined, index=y.index)
31
+ return self
32
+
33
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
34
+ if self._scores is None:
35
+ self.fit(X, y)
36
+ minority_class = y.value_counts().idxmin()
37
+ minority_idx = y[y == minority_class].index
38
+ majority_idx = y[y != minority_class].index
39
+ n_total_target = int(len(y) * keep_ratio)
40
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
41
+ majority_scores = self._scores.loc[majority_idx].sort_values(ascending=False)
42
+ kept_majority_idx = majority_scores.index[:n_majority_target]
43
+ return minority_idx.append(kept_majority_idx)
44
+
45
+ def why_selected(self) -> str:
46
+ return ("Difficulty Scoring uses feature-space distance analysis to identify "
47
+ "samples that are far from their class centroid. These informative "
48
+ "boundary samples help the model learn better decision regions. "
49
+ "Works as a reliable fallback for any dataset shape.")
50
+
51
+ def limitations(self) -> list[str]:
52
+ return [
53
+ "Uses feature norms as proxy — less precise than real model logits.",
54
+ "May select outliers on very noisy datasets.",
55
+ ]
56
+
57
+ @staticmethod
58
+ def fit_score(analysis: dict) -> float:
59
+ scan = analysis.get("scan", analysis)
60
+ task_type = scan.get("task_type", "unknown")
61
+ n_rows = scan.get("rows", 0)
62
+
63
+ if task_type == "regression":
64
+ return 0.0
65
+ if n_rows < 200:
66
+ return 0.1
67
+ return 0.5
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.research_pruning.pipeline import ResearchFusionEngine
5
+ from datapruning.algorithms.base import BaseOptimizer
6
+ from datapruning.algorithms.registry import register
7
+
8
+
9
+ @register
10
+ class FusionPipelineOptimizer(BaseOptimizer):
11
+ key = "fusion"
12
+ display_name = "Full Pipeline"
13
+
14
+ def __init__(self):
15
+ self._selected_indices = None
16
+
17
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "FusionPipelineOptimizer":
18
+ self._selected_indices = None
19
+ return self
20
+
21
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
22
+ X_arr = X.values.astype(np.float64)
23
+ y_arr = y.values.astype(np.int64)
24
+ engine = ResearchFusionEngine(config={"keep_ratio": keep_ratio})
25
+ pruned_X, pruned_y, report = engine.run(
26
+ X_arr, y_arr,
27
+ keep_ratio=keep_ratio,
28
+ auto_strategy=True,
29
+ )
30
+ selected = report.get("selected_indices", [])
31
+ self._selected_indices = X.index[selected]
32
+ return self._selected_indices
33
+
34
+ def why_selected(self) -> str:
35
+ return ("Full Pipeline runs the complete ResearchFusionEngine: "
36
+ "multi-signal scoring (EL2N + Margin), normalization, "
37
+ "adaptive fusion, noise detection, difficulty classification, "
38
+ "redundancy removal, and diversity-aware selection. "
39
+ "Maximum quality for complex datasets.")
40
+
41
+ def limitations(self) -> list[str]:
42
+ return [
43
+ "Slower than individual methods — runs multiple scoring steps.",
44
+ "May be overkill for simple, clean datasets.",
45
+ ]
46
+
47
+ @staticmethod
48
+ def fit_score(analysis: dict) -> float:
49
+ scan = analysis.get("scan", analysis)
50
+ task_type = scan.get("task_type", "unknown")
51
+ n_rows = scan.get("rows", 0)
52
+ n_cols = scan.get("columns", 0)
53
+ duplicate_pct = scan.get("duplicate_pct", 0)
54
+ imbalance = scan.get("imbalance_ratio", 1.0)
55
+
56
+ if task_type == "regression":
57
+ return 0.0
58
+ if n_rows < 1000:
59
+ return 0.2
60
+ score = 0.5
61
+ if n_rows > 10000:
62
+ score += 0.15
63
+ if n_cols > 30:
64
+ score += 0.1
65
+ if duplicate_pct > 3:
66
+ score += 0.1
67
+ if imbalance > 3:
68
+ score += 0.1
69
+ return min(score, 0.9)
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.preprocessing import StandardScaler
5
+ from datapruning.research_pruning.selectors.kcenter import kcenter_greedy
6
+ from datapruning.algorithms.base import BaseOptimizer
7
+ from datapruning.algorithms.registry import register
8
+
9
+
10
+ @register
11
+ class KCenterSelectOptimizer(BaseOptimizer):
12
+ key = "kcenter"
13
+ display_name = "Diversity Selection"
14
+
15
+ def __init__(self):
16
+ self._selected_indices = None
17
+
18
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "KCenterSelectOptimizer":
19
+ self._selected_indices = None
20
+ return self
21
+
22
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
23
+ X_arr = X.values.astype(np.float64)
24
+ n = len(X_arr)
25
+ budget = max(1, int(n * keep_ratio))
26
+ scaler = StandardScaler()
27
+ X_scaled = scaler.fit_transform(X_arr)
28
+ selected = kcenter_greedy(X_scaled, budget=min(budget, n), random_seed=42)
29
+ self._selected_indices = X.index[selected]
30
+ return self._selected_indices
31
+
32
+ def why_selected(self) -> str:
33
+ return ("Diversity Selection uses K-Center Greedy to pick the most "
34
+ "representative and diverse subset. It ensures broad coverage "
35
+ "of the feature space, avoiding redundancy. Ideal for "
36
+ "high-dimensional data or when you need maximum data variety.")
37
+
38
+ def limitations(self) -> list[str]:
39
+ return [
40
+ "Does not consider class labels directly — relies on feature diversity.",
41
+ "May underperform on highly imbalanced datasets without minority preservation.",
42
+ "Slower than scoring-based methods on very large datasets.",
43
+ ]
44
+
45
+ @staticmethod
46
+ def fit_score(analysis: dict) -> float:
47
+ scan = analysis.get("scan", analysis)
48
+ task_type = scan.get("task_type", "unknown")
49
+ n_rows = scan.get("rows", 0)
50
+ n_cols = scan.get("columns", 0)
51
+
52
+ if task_type == "regression":
53
+ return 0.2
54
+ if n_cols > n_rows:
55
+ return 0.8
56
+ if n_cols > 50:
57
+ return 0.65
58
+ return 0.4
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.linear_model import LogisticRegression
5
+ from datapruning.algorithms.base import BaseOptimizer
6
+ from datapruning.algorithms.registry import register
7
+
8
+
9
+ @register
10
+ class MarginScorerOptimizer(BaseOptimizer):
11
+ key = "margin"
12
+ display_name = "Margin Optimization"
13
+
14
+ def __init__(self):
15
+ self._scores = None
16
+
17
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "MarginScorerOptimizer":
18
+ X_arr, y_arr = X.values.astype(np.float64), y.values
19
+ classes = np.unique(y_arr)
20
+ if len(classes) < 2:
21
+ self._scores = pd.Series(np.zeros(len(y_arr)), index=y.index)
22
+ return self
23
+ try:
24
+ lr = LogisticRegression(max_iter=1000, random_state=42)
25
+ lr.fit(X_arr, y_arr)
26
+ probs = lr.predict_proba(X_arr)
27
+ class_to_col = {c: i for i, c in enumerate(lr.classes_)}
28
+ true_probs = np.array([probs[i, class_to_col[y_arr[i]]] for i in range(len(y_arr))])
29
+ self._scores = pd.Series(true_probs, index=y.index)
30
+ except Exception:
31
+ feat_norms = np.linalg.norm(X_arr, axis=1)
32
+ self._scores = pd.Series(feat_norms, index=y.index)
33
+ return self
34
+
35
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
36
+ if self._scores is None:
37
+ self.fit(X, y)
38
+ classes = y.unique()
39
+ if len(classes) < 2:
40
+ n_target = int(len(y) * keep_ratio)
41
+ return y.nlargest(n_target).index
42
+ minority_class = y.value_counts().idxmin()
43
+ minority_idx = y[y == minority_class].index
44
+ majority_idx = y[y != minority_class].index
45
+ n_total_target = int(len(y) * keep_ratio)
46
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
47
+ majority_scores = self._scores.loc[majority_idx].sort_values(ascending=False)
48
+ kept_majority_idx = majority_scores.index[:n_majority_target]
49
+ return minority_idx.append(kept_majority_idx)
50
+
51
+ def why_selected(self) -> str:
52
+ return ("Margin Optimization uses logistic regression to measure how confidently "
53
+ "each sample is classified. It keeps the most informative majority-class "
54
+ "samples (high margin = clear signal) and preserves 100% of the minority class. "
55
+ "Best for general tabular data with clear class boundaries.")
56
+
57
+ def limitations(self) -> list[str]:
58
+ return [
59
+ "Requires fitting a quick logistic regression model.",
60
+ "May underperform on very high-dimensional data without feature selection.",
61
+ ]
62
+
63
+ @staticmethod
64
+ def fit_score(analysis: dict) -> float:
65
+ scan = analysis.get("scan", analysis)
66
+ task_type = scan.get("task_type", "unknown")
67
+ n_rows = scan.get("rows", 0)
68
+ n_cols = scan.get("columns", 0)
69
+
70
+ if task_type == "regression":
71
+ return 0.0
72
+ if n_rows < 500:
73
+ return 0.1
74
+ if n_rows > 200_000:
75
+ return 0.6
76
+ if n_cols > 100:
77
+ return 0.7
78
+ return 0.85
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.linear_model import LogisticRegression
5
+ from datapruning.research_pruning.selection import sliding_window_select
6
+ from datapruning.algorithms.base import BaseOptimizer
7
+ from datapruning.algorithms.registry import register
8
+
9
+
10
+ @register
11
+ class SlidingWindowOptimizer(BaseOptimizer):
12
+ key = "sliding_window"
13
+ display_name = "Noise Filtering"
14
+
15
+ def __init__(self):
16
+ self._scores = None
17
+
18
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "SlidingWindowOptimizer":
19
+ X_arr, y_arr = X.values.astype(np.float64), y.values
20
+ classes = np.unique(y_arr)
21
+ if len(classes) < 2:
22
+ self._scores = np.zeros(len(y_arr))
23
+ return self
24
+ try:
25
+ lr = LogisticRegression(max_iter=1000, random_state=42)
26
+ lr.fit(X_arr, y_arr)
27
+ probs = lr.predict_proba(X_arr)
28
+ class_to_col = {c: i for i, c in enumerate(lr.classes_)}
29
+ true_probs = np.array([probs[i, class_to_col[y_arr[i]]] for i in range(len(y_arr))])
30
+ self._scores = true_probs
31
+ except Exception:
32
+ self._scores = np.linalg.norm(X_arr, axis=1)
33
+ return self
34
+
35
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
36
+ if self._scores is None:
37
+ self.fit(X, y)
38
+ y_arr = y.values
39
+ classes = np.unique(y_arr)
40
+ low_pct = max(0, (1 - keep_ratio) * 100 / 2)
41
+ high_pct = min(100, 100 - low_pct)
42
+ if len(classes) < 2:
43
+ n_target = int(len(y) * keep_ratio)
44
+ return y.nlargest(n_target).index
45
+ selected = sliding_window_select(
46
+ self._scores,
47
+ low_percentile=low_pct,
48
+ high_percentile=high_pct,
49
+ labels=y_arr,
50
+ preserve_minority=True,
51
+ )
52
+ minority_class = y.value_counts().idxmin()
53
+ minority_idx = y[y == minority_class].index
54
+ selected_idx = X.index[selected]
55
+ all_idx = selected_idx.union(minority_idx).unique()
56
+ n_target = int(len(y) * keep_ratio)
57
+ if len(all_idx) > n_target:
58
+ scores_at_idx = pd.Series(self._scores, index=X.index).loc[all_idx]
59
+ all_idx = scores_at_idx.sort_values(ascending=False).index[:n_target]
60
+ return all_idx
61
+
62
+ def why_selected(self) -> str:
63
+ return ("Noise Filtering drops both the easiest (redundant) and hardest "
64
+ "(potentially mislabeled) samples, keeping the informative middle. "
65
+ "This is the sliding window method from the EL2N/GraNd paper. "
66
+ "Best for datasets with suspected label noise.")
67
+
68
+ def limitations(self) -> list[str]:
69
+ return [
70
+ "Requires fitting a logistic regression for scoring.",
71
+ "May discard genuinely hard but correct samples on complex datasets.",
72
+ ]
73
+
74
+ @staticmethod
75
+ def fit_score(analysis: dict) -> float:
76
+ scan = analysis.get("scan", analysis)
77
+ task_type = scan.get("task_type", "unknown")
78
+ n_rows = scan.get("rows", 0)
79
+ duplicate_pct = scan.get("duplicate_pct", 0)
80
+
81
+ if task_type == "regression":
82
+ return 0.0
83
+ if n_rows < 500:
84
+ return 0.1
85
+ if duplicate_pct > 5:
86
+ return 0.75
87
+ return 0.6
@@ -0,0 +1,118 @@
1
+ STRATEGY_DISPLAY = {
2
+ "margin": {
3
+ "display_name": "Margin Optimization",
4
+ "icon": "\U0001F3AF",
5
+ "description": "Uses logistic regression to measure classification confidence. Keeps the most informative samples near decision boundaries while preserving all minority-class examples.",
6
+ "best_for": "General tabular data, binary and multi-class classification",
7
+ "advantages": [
8
+ "Best overall accuracy on benchmarks (+12% avg improvement)",
9
+ "Fast execution — single logistic regression fit",
10
+ "Preserves 100% of minority class",
11
+ ],
12
+ "tradeoffs": [
13
+ "Requires fitting a quick model",
14
+ "May underperform on extremely high-dimensional data",
15
+ ],
16
+ "recommended_models": ["XGBoost", "LightGBM", "Random Forest", "CatBoost"],
17
+ },
18
+ "el2n": {
19
+ "display_name": "Difficulty Scoring",
20
+ "icon": "\U0001F4CA",
21
+ "description": "Analyzes feature-space distances to identify informative boundary samples. Works as a reliable fallback for any dataset shape.",
22
+ "best_for": "Fallback method, any dataset type, quick analysis",
23
+ "advantages": [
24
+ "Works on any dataset without model training",
25
+ "Reliable baseline performance",
26
+ "Very fast execution",
27
+ ],
28
+ "tradeoffs": [
29
+ "Less precise than model-based scoring",
30
+ "May select outliers on very noisy datasets",
31
+ ],
32
+ "recommended_models": ["Any model — general purpose"],
33
+ },
34
+ "kcenter": {
35
+ "display_name": "Diversity Selection",
36
+ "icon": "\U0001F308",
37
+ "description": "Picks the most representative and diverse subset using K-Center Greedy. Ensures broad coverage of the feature space, avoiding redundancy.",
38
+ "best_for": "High-dimensional data, datasets with redundancy, when variety matters",
39
+ "advantages": [
40
+ "Maximum feature space coverage",
41
+ "Eliminates redundant samples",
42
+ "Great for high-dimensional data",
43
+ ],
44
+ "tradeoffs": [
45
+ "Does not consider class labels directly",
46
+ "Slower on very large datasets",
47
+ ],
48
+ "recommended_models": ["LightGBM", "Random Forest", "Any ensemble"],
49
+ },
50
+ "sliding_window": {
51
+ "display_name": "Noise Filtering",
52
+ "icon": "\U0001F9FB",
53
+ "description": "Drops both the easiest (redundant) and hardest (potentially mislabeled) samples, keeping the informative middle. Based on the EL2N/GraNd research paper.",
54
+ "best_for": "Datasets with suspected label noise, corrupted data",
55
+ "advantages": [
56
+ "Removes both redundant and noisy samples",
57
+ "Research-backed method from top ML papers",
58
+ "Good for cleaning real-world messy data",
59
+ ],
60
+ "tradeoffs": [
61
+ "Requires fitting a logistic regression",
62
+ "May discard genuinely hard but correct samples",
63
+ ],
64
+ "recommended_models": ["XGBoost", "LightGBM", "Any robust model"],
65
+ },
66
+ "fusion": {
67
+ "display_name": "Full Pipeline",
68
+ "icon": "\u2728",
69
+ "description": "Runs the complete ResearchFusionEngine: multi-signal scoring, normalization, adaptive fusion, noise detection, difficulty classification, redundancy removal, and diversity-aware selection. Maximum quality.",
70
+ "best_for": "Complex datasets, when quality matters more than speed",
71
+ "advantages": [
72
+ "Most comprehensive pruning pipeline",
73
+ "Combines multiple research-backed signals",
74
+ "Handles noise, redundancy, and diversity",
75
+ ],
76
+ "tradeoffs": [
77
+ "Slower than individual methods",
78
+ "May be overkill for simple clean datasets",
79
+ ],
80
+ "recommended_models": ["Any — maximizes data quality"],
81
+ },
82
+ }
83
+
84
+
85
+ def get_strategy_display(key: str) -> dict:
86
+ return dict(STRATEGY_DISPLAY.get(key, {
87
+ "display_name": key.replace("_", " ").title(),
88
+ "icon": "\U0001F527",
89
+ "description": "",
90
+ "best_for": "",
91
+ "advantages": [],
92
+ "tradeoffs": [],
93
+ "recommended_models": [],
94
+ }))
95
+
96
+
97
+ def enrich_algorithm_info(algo: dict) -> dict:
98
+ display = get_strategy_display(algo.get("key", ""))
99
+ return {**algo, **display}
100
+
101
+
102
+ def enrich_recommendation(rec: dict) -> dict:
103
+ key = rec.get("algorithm_key", "")
104
+ display = get_strategy_display(key)
105
+ enriched = {
106
+ "friendly_name": display["display_name"],
107
+ "icon": display["icon"],
108
+ "description": display["description"],
109
+ "best_for": display["best_for"],
110
+ "advantages": display["advantages"],
111
+ "tradeoffs": display["tradeoffs"],
112
+ "recommended_models": display["recommended_models"],
113
+ }
114
+ return {**rec, **enriched}
115
+
116
+
117
+ def enrich_alternatives(alternatives: list) -> list:
118
+ return [enrich_algorithm_info(alt) for alt in alternatives]
@@ -0,0 +1 @@
1
+ """Multimodal pruning support — encode images and text with CLIP, then prune."""