datapruning 2.1.3__tar.gz → 2.1.4__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.4}/PKG-INFO +6 -4
  2. {datapruning-2.1.3 → datapruning-2.1.4}/README.md +5 -3
  3. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/base.py +78 -3
  4. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/el2n_scorer.py +22 -11
  5. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/margin_scorer.py +12 -12
  6. datapruning-2.1.4/datapruning/algorithms/oof_utils.py +75 -0
  7. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/sliding_window.py +8 -10
  8. {datapruning-2.1.3 → datapruning-2.1.4/datapruning.egg-info}/PKG-INFO +6 -4
  9. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning.egg-info/SOURCES.txt +1 -0
  10. {datapruning-2.1.3 → datapruning-2.1.4}/pyproject.toml +1 -1
  11. {datapruning-2.1.3 → datapruning-2.1.4}/LICENSE +0 -0
  12. {datapruning-2.1.3 → datapruning-2.1.4}/MANIFEST.in +0 -0
  13. {datapruning-2.1.3 → datapruning-2.1.4}/build_src/_core.c +0 -0
  14. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/__init__.py +0 -0
  15. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/__init__.py +0 -0
  16. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/fusion_pipeline.py +0 -0
  17. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/kcenter_select.py +0 -0
  18. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/algorithms/registry.py +0 -0
  19. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/__init__.py +0 -0
  20. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/benchmark.py +0 -0
  21. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/explainability.py +0 -0
  22. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/intelligence.py +0 -0
  23. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/optimizer.py +0 -0
  24. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/scanner.py +0 -0
  25. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/strategy_display.py +0 -0
  26. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/engine/strategy_selector.py +0 -0
  27. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/multimodal/__init__.py +0 -0
  28. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/multimodal/clip_encoder.py +0 -0
  29. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/pipeline.py +0 -0
  30. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/reports/__init__.py +0 -0
  31. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/reports/exporter.py +0 -0
  32. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/__init__.py +0 -0
  33. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/__init__.py +0 -0
  34. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/compare.py +0 -0
  35. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
  36. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
  37. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/difficulty.py +0 -0
  38. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/__init__.py +0 -0
  39. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
  40. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/fusion.py +0 -0
  41. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/noise.py +0 -0
  42. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/normalize.py +0 -0
  43. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/pipeline.py +0 -0
  44. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/redundancy.py +0 -0
  45. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/__init__.py +0 -0
  46. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/aum.py +0 -0
  47. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/el2n.py +0 -0
  48. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/forgetting.py +0 -0
  49. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/grand.py +0 -0
  50. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/scores/margin.py +0 -0
  51. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/selection.py +0 -0
  52. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/selectors/__init__.py +0 -0
  53. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/selectors/facility_location.py +0 -0
  54. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/selectors/kcenter.py +0 -0
  55. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
  56. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/research_pruning/strategy.py +0 -0
  57. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning/sdk.py +0 -0
  58. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning.egg-info/dependency_links.txt +0 -0
  59. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning.egg-info/requires.txt +0 -0
  60. {datapruning-2.1.3 → datapruning-2.1.4}/datapruning.egg-info/top_level.txt +0 -0
  61. {datapruning-2.1.3 → datapruning-2.1.4}/setup.cfg +0 -0
  62. {datapruning-2.1.3 → datapruning-2.1.4}/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.4
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,77 @@ 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 = 3,
58
+ test_size: float = 0.3,
59
+ seed: int = 0,
60
+ ) -> bool:
61
+ """Check whether this strategy beats random on an internal holdout.
62
+
63
+ Splits the data 70/30, trains the strategy on the 70% subset,
64
+ validates on the 30% holdout, and compares with random subsets.
65
+ Returns True if the strategy is beneficial.
66
+ """
67
+ from sklearn.model_selection import train_test_split
68
+ from sklearn.linear_model import LogisticRegression
69
+
70
+ if len(y) < 50:
71
+ return True
72
+ y_vals = y.values if hasattr(y, 'values') else np.asarray(y)
73
+ n_unique = len(np.unique(y_vals))
74
+ if n_unique < 2 or n_unique >= len(y_vals) * 0.8:
75
+ return True
76
+
77
+ tr_idx, va_idx = train_test_split(
78
+ np.arange(len(y)), test_size=test_size,
79
+ random_state=seed, stratify=y.values,
80
+ )
81
+ X_tr, y_tr = X.iloc[tr_idx], y.iloc[tr_idx]
82
+ X_va, y_va = X.iloc[va_idx], y.iloc[va_idx]
83
+ n_keep = max(1, int(len(tr_idx) * keep_ratio))
84
+
85
+ try:
86
+ self.fit(X_tr, y_tr)
87
+ s_keep = self._select_rows(X_tr, y_tr, keep_ratio)
88
+ m = LogisticRegression(max_iter=300, C=0.1, random_state=seed)
89
+ m.fit(X_tr.loc[s_keep], y_tr.loc[s_keep])
90
+ strat_acc = m.score(X_va, y_va)
91
+
92
+ rng = np.random.RandomState(seed)
93
+ random_accs = []
94
+ for s in range(n_random):
95
+ r_idx = rng.choice(len(tr_idx), n_keep, replace=False)
96
+ r_keep = X_tr.index[r_idx]
97
+ m2 = LogisticRegression(max_iter=300, C=0.1, random_state=seed)
98
+ m2.fit(X_tr.loc[r_keep], y_tr.loc[r_keep])
99
+ random_accs.append(m2.score(X_va, y_va))
100
+
101
+ return strat_acc >= np.mean(random_accs)
102
+ except Exception:
103
+ return True
104
+
105
+ def _random_select(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float, seed: int = 42) -> pd.Index:
106
+ n_target = max(1, int(len(y) * keep_ratio))
107
+ rng = np.random.RandomState(seed)
108
+ indices = rng.choice(len(y), n_target, replace=False)
109
+ return X.index[indices]
110
+
48
111
  def optimize(self, df: pd.DataFrame, target_col: str, keep_ratio: float = 0.5) -> OptimizationResult:
49
112
  import time
50
- import numpy as np
51
- from sklearn.preprocessing import LabelEncoder
52
113
 
53
114
  X, y = df.drop(columns=[target_col]), df[target_col].copy()
54
115
 
55
116
  for col in X.columns:
56
117
  if not np.issubdtype(X[col].dtype, np.number):
118
+ from sklearn.preprocessing import LabelEncoder
57
119
  X[col] = LabelEncoder().fit_transform(X[col].astype(str))
58
120
 
59
121
  if not np.issubdtype(y.dtype, np.number):
122
+ from sklearn.preprocessing import LabelEncoder
60
123
  y = pd.Series(LabelEncoder().fit_transform(y.astype(str)), index=y.index)
61
124
 
62
125
  X = X.fillna(0).astype(np.float64)
@@ -65,6 +128,15 @@ class BaseOptimizer(ABC):
65
128
  self.fit(X, y)
66
129
  X_res, y_res = self.transform(X, y, keep_ratio)
67
130
  runtime = time.time() - t0
131
+
132
+ beneficial = self._beat_random_gate(X, y, keep_ratio)
133
+ if not beneficial:
134
+ keep_idx = self._random_select(X, y, keep_ratio)
135
+ X_res, y_res = X.loc[keep_idx], y.loc[keep_idx]
136
+ strategy_method = "random (fallback)"
137
+ else:
138
+ strategy_method = self.display_name
139
+
68
140
  minority_after = int(y_res.value_counts().min()) if y_res.nunique() > 1 else None
69
141
  optimized_df = X_res.copy()
70
142
  optimized_df[target_col] = y_res.values
@@ -77,6 +149,8 @@ class BaseOptimizer(ABC):
77
149
  minority_count_before=minority_before,
78
150
  minority_count_after=minority_after,
79
151
  runtime_seconds=runtime,
152
+ beneficial=beneficial,
153
+ strategy_method=strategy_method,
80
154
  )
81
155
 
82
156
  def explain(self, result: OptimizationResult) -> Explanation:
@@ -88,7 +162,7 @@ class BaseOptimizer(ABC):
88
162
  (result.minority_count_after or 0) / result.minority_count_before * 100, 1
89
163
  )
90
164
  return Explanation(
91
- algorithm=self.display_name,
165
+ algorithm=result.strategy_method or self.display_name,
92
166
  why_selected=self.why_selected(),
93
167
  rows_removed=rows_removed,
94
168
  reduction_pct=reduction_pct,
@@ -96,6 +170,7 @@ class BaseOptimizer(ABC):
96
170
  estimated_training_gain_pct=reduction_pct,
97
171
  memory_saved_pct=reduction_pct,
98
172
  limitations=self.limitations(),
173
+ beneficial=result.beneficial,
99
174
  )
100
175
 
101
176
  @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)
@@ -51,13 +49,15 @@ class MarginScorerOptimizer(BaseOptimizer):
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
@@ -61,12 +58,13 @@ class SlidingWindowOptimizer(BaseOptimizer):
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.4
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.4"
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