datapruning 2.1.3__tar.gz → 2.1.5__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 (62) hide show
  1. {datapruning-2.1.3/datapruning.egg-info → datapruning-2.1.5}/PKG-INFO +6 -4
  2. {datapruning-2.1.3 → datapruning-2.1.5}/README.md +5 -3
  3. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/base.py +84 -3
  4. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/el2n_scorer.py +22 -11
  5. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/margin_scorer.py +14 -14
  6. datapruning-2.1.5/datapruning/algorithms/oof_utils.py +75 -0
  7. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/sliding_window.py +9 -11
  8. {datapruning-2.1.3 → datapruning-2.1.5/datapruning.egg-info}/PKG-INFO +6 -4
  9. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/SOURCES.txt +1 -0
  10. {datapruning-2.1.3 → datapruning-2.1.5}/pyproject.toml +1 -1
  11. {datapruning-2.1.3 → datapruning-2.1.5}/LICENSE +0 -0
  12. {datapruning-2.1.3 → datapruning-2.1.5}/MANIFEST.in +0 -0
  13. {datapruning-2.1.3 → datapruning-2.1.5}/build_src/_core.c +0 -0
  14. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/__init__.py +0 -0
  15. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/__init__.py +0 -0
  16. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/fusion_pipeline.py +0 -0
  17. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/kcenter_select.py +0 -0
  18. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/registry.py +0 -0
  19. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/__init__.py +0 -0
  20. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/benchmark.py +0 -0
  21. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/explainability.py +0 -0
  22. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/intelligence.py +0 -0
  23. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/optimizer.py +0 -0
  24. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/scanner.py +0 -0
  25. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/strategy_display.py +0 -0
  26. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/strategy_selector.py +0 -0
  27. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/multimodal/__init__.py +0 -0
  28. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/multimodal/clip_encoder.py +0 -0
  29. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/pipeline.py +0 -0
  30. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/reports/__init__.py +0 -0
  31. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/reports/exporter.py +0 -0
  32. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/__init__.py +0 -0
  33. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/__init__.py +0 -0
  34. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/compare.py +0 -0
  35. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
  36. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
  37. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/difficulty.py +0 -0
  38. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/embeddings/__init__.py +0 -0
  39. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
  40. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/fusion.py +0 -0
  41. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/noise.py +0 -0
  42. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/normalize.py +0 -0
  43. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/pipeline.py +0 -0
  44. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/redundancy.py +0 -0
  45. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/__init__.py +0 -0
  46. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/aum.py +0 -0
  47. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/el2n.py +0 -0
  48. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/forgetting.py +0 -0
  49. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/grand.py +0 -0
  50. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/margin.py +0 -0
  51. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selection.py +0 -0
  52. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/__init__.py +0 -0
  53. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/facility_location.py +0 -0
  54. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/kcenter.py +0 -0
  55. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
  56. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/strategy.py +0 -0
  57. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/sdk.py +0 -0
  58. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/dependency_links.txt +0 -0
  59. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/requires.txt +0 -0
  60. {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/top_level.txt +0 -0
  61. {datapruning-2.1.3 → datapruning-2.1.5}/setup.cfg +0 -0
  62. {datapruning-2.1.3 → datapruning-2.1.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.1.3
3
+ Version: 2.1.5
4
4
  Summary: Intelligent dataset optimization for cleaner, smaller training data
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -104,12 +104,14 @@ DataPruning selects the best algorithm automatically based on dataset characteri
104
104
 
105
105
  | Method | What it does | Best for |
106
106
  |--------|-------------|----------|
107
- | **Margin Optimization** | Analyzes classification confidence, keeps boundary samples | General tabular data |
108
- | **Importance Scoring** | Measures sample difficulty via feature distances | Quick baseline, any dataset |
107
+ | **Margin Optimization** | Analyzes classification confidence gap via cross-validation, keeps boundary samples | General tabular data |
108
+ | **Importance Scoring** | Measures sample difficulty via model prediction confidence across CV folds | Quick baseline, any dataset |
109
109
  | **Diversity Selection** | Picks the most representative subset (K-Center) | High-dimensional data |
110
- | **Noise Filtering** | Drops redundant and potentially mislabeled samples | Noisy/messy data |
110
+ | **Noise Filtering** | Drops redundant and potentially mislabeled samples via CV-based difficulty | Noisy/messy data |
111
111
  | **Full Pipeline** | Combines all signals for maximum quality | Complex datasets |
112
112
 
113
+ All scoring methods use **out-of-fold (OOF) model logits** — each sample is scored by a model that never saw it during training, producing unbiased difficulty estimates. The optimizer also includes a **"must beat random" gate** that validates the selected strategy actually improves over random subsampling on an internal holdout; if not, it safely falls back to random selection.
114
+
113
115
  ## Limits
114
116
 
115
117
  - Minimum: 1,000 rows (tabular) or 10 images
@@ -74,12 +74,14 @@ DataPruning selects the best algorithm automatically based on dataset characteri
74
74
 
75
75
  | Method | What it does | Best for |
76
76
  |--------|-------------|----------|
77
- | **Margin Optimization** | Analyzes classification confidence, keeps boundary samples | General tabular data |
78
- | **Importance Scoring** | Measures sample difficulty via feature distances | Quick baseline, any dataset |
77
+ | **Margin Optimization** | Analyzes classification confidence gap via cross-validation, keeps boundary samples | General tabular data |
78
+ | **Importance Scoring** | Measures sample difficulty via model prediction confidence across CV folds | Quick baseline, any dataset |
79
79
  | **Diversity Selection** | Picks the most representative subset (K-Center) | High-dimensional data |
80
- | **Noise Filtering** | Drops redundant and potentially mislabeled samples | Noisy/messy data |
80
+ | **Noise Filtering** | Drops redundant and potentially mislabeled samples via CV-based difficulty | Noisy/messy data |
81
81
  | **Full Pipeline** | Combines all signals for maximum quality | Complex datasets |
82
82
 
83
+ All scoring methods use **out-of-fold (OOF) model logits** — each sample is scored by a model that never saw it during training, producing unbiased difficulty estimates. The optimizer also includes a **"must beat random" gate** that validates the selected strategy actually improves over random subsampling on an internal holdout; if not, it safely falls back to random selection.
84
+
83
85
  ## Limits
84
86
 
85
87
  - Minimum: 1,000 rows (tabular) or 10 images
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  from abc import ABC, abstractmethod
3
3
  from dataclasses import dataclass, field
4
4
  from typing import Optional
5
+ import numpy as np
5
6
  import pandas as pd
6
7
 
7
8
 
@@ -15,6 +16,8 @@ class OptimizationResult:
15
16
  minority_count_before: Optional[int]
16
17
  minority_count_after: Optional[int]
17
18
  runtime_seconds: float
19
+ beneficial: bool = True
20
+ strategy_method: str = ""
18
21
 
19
22
 
20
23
  @dataclass
@@ -27,6 +30,7 @@ class Explanation:
27
30
  estimated_training_gain_pct: float
28
31
  memory_saved_pct: float
29
32
  limitations: list[str] = field(default_factory=list)
33
+ beneficial: bool = True
30
34
 
31
35
 
32
36
  class BaseOptimizer(ABC):
@@ -45,18 +49,83 @@ class BaseOptimizer(ABC):
45
49
  keep_idx = self._select_rows(X, y, keep_ratio)
46
50
  return X.loc[keep_idx], y.loc[keep_idx]
47
51
 
52
+ def _beat_random_gate(
53
+ self,
54
+ X: pd.DataFrame,
55
+ y: pd.Series,
56
+ keep_ratio: float,
57
+ n_random: int = 5,
58
+ n_splits: int = 3,
59
+ seed: int = 0,
60
+ ) -> bool:
61
+ """Check whether this strategy beats random on internal holdouts.
62
+
63
+ Runs n_splits stratified train/val splits, each with n_random
64
+ random baseline subsets. The strategy is beneficial only if its
65
+ mean accuracy exceeds random mean by at least half the random
66
+ standard deviation — removing noisy single-split false positives.
67
+ """
68
+ from sklearn.model_selection import StratifiedKFold
69
+ from sklearn.linear_model import LogisticRegression
70
+
71
+ if len(y) < 50:
72
+ return True
73
+ y_vals = y.values if hasattr(y, "values") else np.asarray(y)
74
+ n_unique = len(np.unique(y_vals))
75
+ if n_unique < 2 or n_unique >= len(y_vals) * 0.8:
76
+ return True
77
+
78
+ strat_accs_all = []
79
+ random_accs_all = []
80
+
81
+ try:
82
+ skf = StratifiedKFold(
83
+ n_splits=n_splits, shuffle=True, random_state=seed
84
+ )
85
+ for tr_idx, va_idx in skf.split(np.arange(len(y)), y.values):
86
+ X_tr, y_tr = X.iloc[tr_idx], y.iloc[tr_idx]
87
+ X_va, y_va = X.iloc[va_idx], y.iloc[va_idx]
88
+ n_keep = max(1, int(len(tr_idx) * keep_ratio))
89
+
90
+ self.fit(X_tr, y_tr)
91
+ s_keep = self._select_rows(X_tr, y_tr, keep_ratio)
92
+ m = LogisticRegression(max_iter=300, C=0.1, random_state=seed)
93
+ m.fit(X_tr.loc[s_keep], y_tr.loc[s_keep])
94
+ strat_accs_all.append(m.score(X_va, y_va))
95
+
96
+ rng = np.random.RandomState(seed + len(strat_accs_all))
97
+ for _ in range(n_random):
98
+ r_idx = rng.choice(len(tr_idx), n_keep, replace=False)
99
+ r_keep = X_tr.index[r_idx]
100
+ m2 = LogisticRegression(max_iter=300, C=0.1, random_state=seed)
101
+ m2.fit(X_tr.loc[r_keep], y_tr.loc[r_keep])
102
+ random_accs_all.append(m2.score(X_va, y_va))
103
+
104
+ mean_strat = np.mean(strat_accs_all)
105
+ mean_random = np.mean(random_accs_all)
106
+ std_random = np.std(random_accs_all)
107
+ return mean_strat > mean_random + std_random * 0.5
108
+ except Exception:
109
+ return True
110
+
111
+ def _random_select(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float, seed: int = 42) -> pd.Index:
112
+ n_target = max(1, int(len(y) * keep_ratio))
113
+ rng = np.random.RandomState(seed)
114
+ indices = rng.choice(len(y), n_target, replace=False)
115
+ return X.index[indices]
116
+
48
117
  def optimize(self, df: pd.DataFrame, target_col: str, keep_ratio: float = 0.5) -> OptimizationResult:
49
118
  import time
50
- import numpy as np
51
- from sklearn.preprocessing import LabelEncoder
52
119
 
53
120
  X, y = df.drop(columns=[target_col]), df[target_col].copy()
54
121
 
55
122
  for col in X.columns:
56
123
  if not np.issubdtype(X[col].dtype, np.number):
124
+ from sklearn.preprocessing import LabelEncoder
57
125
  X[col] = LabelEncoder().fit_transform(X[col].astype(str))
58
126
 
59
127
  if not np.issubdtype(y.dtype, np.number):
128
+ from sklearn.preprocessing import LabelEncoder
60
129
  y = pd.Series(LabelEncoder().fit_transform(y.astype(str)), index=y.index)
61
130
 
62
131
  X = X.fillna(0).astype(np.float64)
@@ -65,6 +134,15 @@ class BaseOptimizer(ABC):
65
134
  self.fit(X, y)
66
135
  X_res, y_res = self.transform(X, y, keep_ratio)
67
136
  runtime = time.time() - t0
137
+
138
+ beneficial = self._beat_random_gate(X, y, keep_ratio)
139
+ if not beneficial:
140
+ keep_idx = self._random_select(X, y, keep_ratio)
141
+ X_res, y_res = X.loc[keep_idx], y.loc[keep_idx]
142
+ strategy_method = "random (fallback)"
143
+ else:
144
+ strategy_method = self.display_name
145
+
68
146
  minority_after = int(y_res.value_counts().min()) if y_res.nunique() > 1 else None
69
147
  optimized_df = X_res.copy()
70
148
  optimized_df[target_col] = y_res.values
@@ -77,6 +155,8 @@ class BaseOptimizer(ABC):
77
155
  minority_count_before=minority_before,
78
156
  minority_count_after=minority_after,
79
157
  runtime_seconds=runtime,
158
+ beneficial=beneficial,
159
+ strategy_method=strategy_method,
80
160
  )
81
161
 
82
162
  def explain(self, result: OptimizationResult) -> Explanation:
@@ -88,7 +168,7 @@ class BaseOptimizer(ABC):
88
168
  (result.minority_count_after or 0) / result.minority_count_before * 100, 1
89
169
  )
90
170
  return Explanation(
91
- algorithm=self.display_name,
171
+ algorithm=result.strategy_method or self.display_name,
92
172
  why_selected=self.why_selected(),
93
173
  rows_removed=rows_removed,
94
174
  reduction_pct=reduction_pct,
@@ -96,6 +176,7 @@ class BaseOptimizer(ABC):
96
176
  estimated_training_gain_pct=reduction_pct,
97
177
  memory_saved_pct=reduction_pct,
98
178
  limitations=self.limitations(),
179
+ beneficial=result.beneficial,
99
180
  )
100
181
 
101
182
  @staticmethod
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import numpy as np
3
3
  import pandas as pd
4
+ from datapruning.algorithms.oof_utils import compute_oof_logits, logits_to_el2n
4
5
  from datapruning.algorithms.base import BaseOptimizer
5
6
  from datapruning.algorithms.registry import register
6
7
 
@@ -15,15 +16,24 @@ class EL2NScorerOptimizer(BaseOptimizer):
15
16
 
16
17
  def fit(self, X: pd.DataFrame, y: pd.Series) -> "EL2NScorerOptimizer":
17
18
  X_arr = X.values.astype(np.float64)
18
- y_arr = y.values
19
+ y_arr = y.values.astype(int)
19
20
  classes = np.unique(y_arr)
20
- scores = np.zeros(len(y_arr), dtype=np.float64)
21
- for cls in classes:
22
- cls_mask = y_arr == cls
23
- centroid = X_arr[cls_mask].mean(axis=0)
24
- dists = np.linalg.norm(X_arr - centroid, axis=1)
25
- scores = np.maximum(scores, dists)
26
- self._scores = pd.Series(scores, index=y.index)
21
+ if len(classes) < 2:
22
+ self._scores = pd.Series(np.zeros(len(y_arr)), index=y.index)
23
+ return self
24
+ try:
25
+ logits = compute_oof_logits(X_arr, y_arr)
26
+ el2n = logits_to_el2n(logits, y_arr)
27
+ self._scores = pd.Series(el2n, index=y.index)
28
+ except Exception:
29
+ centroids = np.zeros((len(classes), X_arr.shape[1]))
30
+ for i, c in enumerate(classes):
31
+ centroids[i] = X_arr[y_arr == c].mean(axis=0)
32
+ scores = np.zeros(len(y_arr), dtype=np.float64)
33
+ for i, c in enumerate(classes):
34
+ dists = np.linalg.norm(X_arr - centroids[i], axis=1)
35
+ scores = np.maximum(scores, dists)
36
+ self._scores = pd.Series(scores, index=y.index)
27
37
  return self
28
38
 
29
39
  def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
@@ -46,12 +56,13 @@ class EL2NScorerOptimizer(BaseOptimizer):
46
56
 
47
57
  def why_selected(self) -> str:
48
58
  return ("Measures how difficult each sample is for the model to classify by analyzing "
49
- "feature-space distances. Keeps the most informative boundary samples that "
50
- "help the model learn better decision regions. Works reliably on any dataset.")
59
+ "model prediction confidence across cross-validation folds. Keeps the most "
60
+ "informative boundary samples that help the model learn better decision regions. "
61
+ "Works reliably on any dataset.")
51
62
 
52
63
  def limitations(self) -> list[str]:
53
64
  return [
54
- "Uses distance-based approximation slightly less precise than model-based methods.",
65
+ "Requires a quick cross-validation step during analysis.",
55
66
  "May select outliers on very noisy datasets.",
56
67
  ]
57
68
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import numpy as np
3
3
  import pandas as pd
4
- from sklearn.linear_model import LogisticRegression
4
+ from datapruning.algorithms.oof_utils import compute_oof_logits, logits_to_margin
5
5
  from datapruning.algorithms.base import BaseOptimizer
6
6
  from datapruning.algorithms.registry import register
7
7
 
@@ -15,18 +15,16 @@ class MarginScorerOptimizer(BaseOptimizer):
15
15
  self._scores = None
16
16
 
17
17
  def fit(self, X: pd.DataFrame, y: pd.Series) -> "MarginScorerOptimizer":
18
- X_arr, y_arr = X.values.astype(np.float64), y.values
18
+ X_arr = X.values.astype(np.float64)
19
+ y_arr = y.values.astype(int)
19
20
  classes = np.unique(y_arr)
20
21
  if len(classes) < 2:
21
22
  self._scores = pd.Series(np.zeros(len(y_arr)), index=y.index)
22
23
  return self
23
24
  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)
25
+ logits = compute_oof_logits(X_arr, y_arr)
26
+ margin = logits_to_margin(logits, y_arr)
27
+ self._scores = pd.Series(margin, index=y.index)
30
28
  except Exception:
31
29
  feat_norms = np.linalg.norm(X_arr, axis=1)
32
30
  self._scores = pd.Series(feat_norms, index=y.index)
@@ -45,19 +43,21 @@ class MarginScorerOptimizer(BaseOptimizer):
45
43
  n_total_target = max(1, int(len(y) * keep_ratio))
46
44
  n_minority = min(len(minority_idx), int(n_total_target * 0.8))
47
45
  n_majority_target = max(n_total_target - n_minority, 0)
48
- majority_scores = self._scores.loc[majority_idx].sort_values(ascending=True)
46
+ majority_scores = self._scores.loc[majority_idx].sort_values(ascending=False)
49
47
  kept_majority_idx = majority_scores.index[:n_majority_target]
50
- kept_minority_idx = minority_idx[:n_minority]
48
+ kept_minority_idx = self._scores.loc[minority_idx].sort_values(ascending=False).index[:n_minority]
51
49
  return kept_minority_idx.append(kept_majority_idx)
52
50
 
53
51
  def why_selected(self) -> str:
54
- return ("Analyzes how confidently each data point is classified, keeping the most "
55
- "informative samples near decision boundaries. Automatically preserves all "
56
- "minority-class examples to maintain balanced training data.")
52
+ return ("Analyzes how confidently each data point is classified by looking at "
53
+ "the gap between the top two predicted classes across cross-validation "
54
+ "folds. Keeps the most informative samples near decision boundaries. "
55
+ "Automatically preserves all minority-class examples to maintain "
56
+ "balanced training data.")
57
57
 
58
58
  def limitations(self) -> list[str]:
59
59
  return [
60
- "Requires a quick model fitting step during analysis.",
60
+ "Requires a quick cross-validation step during analysis.",
61
61
  "May be less effective on extremely high-dimensional datasets.",
62
62
  ]
63
63
 
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from sklearn.linear_model import LogisticRegression
4
+ from sklearn.model_selection import StratifiedKFold
5
+
6
+
7
+ def compute_oof_logits(
8
+ X: np.ndarray,
9
+ y: np.ndarray,
10
+ n_folds: int = 5,
11
+ seed: int = 0,
12
+ C: float = 0.1,
13
+ max_iter: int = 300,
14
+ ) -> np.ndarray:
15
+ """Compute out-of-fold log-probabilities via stratified K-fold CV.
16
+
17
+ Each sample is scored by a model that never saw it during training,
18
+ producing unbiased per-sample logits. This is the correct input for
19
+ EL2N, margin, and sliding-window difficulty scores.
20
+
21
+ Returns
22
+ -------
23
+ logits : ndarray of shape (n_samples, n_classes)
24
+ Log-probability of each class for every sample.
25
+ """
26
+ X = np.asarray(X, dtype=np.float64)
27
+ y = np.asarray(y).astype(int)
28
+ n_classes = len(np.unique(y))
29
+ out = np.zeros((len(X), n_classes), dtype=np.float64)
30
+
31
+ skf = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=seed)
32
+ for tr_idx, va_idx in skf.split(X, y):
33
+ model = LogisticRegression(
34
+ max_iter=max_iter, C=C, random_state=seed, solver="lbfgs"
35
+ )
36
+ model.fit(X[tr_idx], y[tr_idx])
37
+ log_proba = model.predict_log_proba(X[va_idx])
38
+ class_cols = model.classes_.astype(int)
39
+ out[np.ix_(va_idx, class_cols)] = log_proba
40
+
41
+ return out
42
+
43
+
44
+ def logits_to_el2n(logits: np.ndarray, y: np.ndarray) -> np.ndarray:
45
+ """EL2N score from model logits: ||softmax(z) - onehot(y)||.
46
+
47
+ Higher values indicate harder / more informative samples.
48
+ """
49
+ z = logits - logits.max(axis=1, keepdims=True)
50
+ p = np.exp(z)
51
+ p /= p.sum(axis=1, keepdims=True)
52
+ oh = np.zeros_like(p)
53
+ oh[np.arange(len(y)), y] = 1.0
54
+ return np.linalg.norm(p - oh, axis=1)
55
+
56
+
57
+ def logits_to_margin(logits: np.ndarray, y: np.ndarray) -> np.ndarray:
58
+ """Margin score from model logits: confidence gap between top-1 and top-2.
59
+
60
+ Lower values indicate harder / more ambiguous samples (near decision
61
+ boundary). The score is returned as (1 - margin) so that higher
62
+ values consistently mean "harder" across all scorers.
63
+ """
64
+ z = logits - logits.max(axis=1, keepdims=True)
65
+ p = np.exp(z)
66
+ p /= p.sum(axis=1, keepdims=True)
67
+ n = len(y)
68
+ margins = np.zeros(n, dtype=np.float64)
69
+ for i in range(n):
70
+ sorted_p = np.sort(p[i])[::-1]
71
+ if len(sorted_p) >= 2:
72
+ margins[i] = sorted_p[0] - sorted_p[1]
73
+ else:
74
+ margins[i] = sorted_p[0]
75
+ return 1.0 - margins
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import numpy as np
3
3
  import pandas as pd
4
- from sklearn.linear_model import LogisticRegression
4
+ from datapruning.algorithms.oof_utils import compute_oof_logits, logits_to_el2n
5
5
  from datapruning.research_pruning.selection import sliding_window_select
6
6
  from datapruning.algorithms.base import BaseOptimizer
7
7
  from datapruning.algorithms.registry import register
@@ -16,18 +16,15 @@ class SlidingWindowOptimizer(BaseOptimizer):
16
16
  self._scores = None
17
17
 
18
18
  def fit(self, X: pd.DataFrame, y: pd.Series) -> "SlidingWindowOptimizer":
19
- X_arr, y_arr = X.values.astype(np.float64), y.values
19
+ X_arr = X.values.astype(np.float64)
20
+ y_arr = y.values.astype(int)
20
21
  classes = np.unique(y_arr)
21
22
  if len(classes) < 2:
22
23
  self._scores = np.zeros(len(y_arr))
23
24
  return self
24
25
  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
26
+ logits = compute_oof_logits(X_arr, y_arr)
27
+ self._scores = logits_to_el2n(logits, y_arr)
31
28
  except Exception:
32
29
  self._scores = np.linalg.norm(X_arr, axis=1)
33
30
  return self
@@ -56,17 +53,18 @@ class SlidingWindowOptimizer(BaseOptimizer):
56
53
  n_target = max(1, int(len(y) * keep_ratio))
57
54
  if len(all_idx) > n_target:
58
55
  scores_at_idx = pd.Series(self._scores, index=X.index).loc[all_idx]
59
- all_idx = scores_at_idx.sort_values(ascending=True).index[:n_target]
56
+ all_idx = scores_at_idx.sort_values(ascending=False).index[:n_target]
60
57
  return all_idx
61
58
 
62
59
  def why_selected(self) -> str:
63
60
  return ("Filters out both the easiest (redundant) and hardest (potentially mislabeled) "
64
- "samples, keeping only the informative middle ground. Best for cleaning "
61
+ "samples, keeping only the informative middle ground. Uses cross-validation "
62
+ "to score samples by model prediction difficulty. Best for cleaning "
65
63
  "datasets that may contain labeling errors or noisy data.")
66
64
 
67
65
  def limitations(self) -> list[str]:
68
66
  return [
69
- "Requires a quick model fitting step during analysis.",
67
+ "Requires a quick cross-validation step during analysis.",
70
68
  "May occasionally remove genuinely hard but correct samples.",
71
69
  ]
72
70
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.1.3
3
+ Version: 2.1.5
4
4
  Summary: Intelligent dataset optimization for cleaner, smaller training data
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -104,12 +104,14 @@ DataPruning selects the best algorithm automatically based on dataset characteri
104
104
 
105
105
  | Method | What it does | Best for |
106
106
  |--------|-------------|----------|
107
- | **Margin Optimization** | Analyzes classification confidence, keeps boundary samples | General tabular data |
108
- | **Importance Scoring** | Measures sample difficulty via feature distances | Quick baseline, any dataset |
107
+ | **Margin Optimization** | Analyzes classification confidence gap via cross-validation, keeps boundary samples | General tabular data |
108
+ | **Importance Scoring** | Measures sample difficulty via model prediction confidence across CV folds | Quick baseline, any dataset |
109
109
  | **Diversity Selection** | Picks the most representative subset (K-Center) | High-dimensional data |
110
- | **Noise Filtering** | Drops redundant and potentially mislabeled samples | Noisy/messy data |
110
+ | **Noise Filtering** | Drops redundant and potentially mislabeled samples via CV-based difficulty | Noisy/messy data |
111
111
  | **Full Pipeline** | Combines all signals for maximum quality | Complex datasets |
112
112
 
113
+ All scoring methods use **out-of-fold (OOF) model logits** — each sample is scored by a model that never saw it during training, producing unbiased difficulty estimates. The optimizer also includes a **"must beat random" gate** that validates the selected strategy actually improves over random subsampling on an internal holdout; if not, it safely falls back to random selection.
114
+
113
115
  ## Limits
114
116
 
115
117
  - Minimum: 1,000 rows (tabular) or 10 images
@@ -18,6 +18,7 @@ datapruning/algorithms/el2n_scorer.py
18
18
  datapruning/algorithms/fusion_pipeline.py
19
19
  datapruning/algorithms/kcenter_select.py
20
20
  datapruning/algorithms/margin_scorer.py
21
+ datapruning/algorithms/oof_utils.py
21
22
  datapruning/algorithms/registry.py
22
23
  datapruning/algorithms/sliding_window.py
23
24
  datapruning/engine/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datapruning"
7
- version = "2.1.3"
7
+ version = "2.1.5"
8
8
  description = "Intelligent dataset optimization for cleaner, smaller training data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes