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.
- {datapruning-2.1.3/datapruning.egg-info → datapruning-2.1.5}/PKG-INFO +6 -4
- {datapruning-2.1.3 → datapruning-2.1.5}/README.md +5 -3
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/base.py +84 -3
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/el2n_scorer.py +22 -11
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/margin_scorer.py +14 -14
- datapruning-2.1.5/datapruning/algorithms/oof_utils.py +75 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/sliding_window.py +9 -11
- {datapruning-2.1.3 → datapruning-2.1.5/datapruning.egg-info}/PKG-INFO +6 -4
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/SOURCES.txt +1 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/pyproject.toml +1 -1
- {datapruning-2.1.3 → datapruning-2.1.5}/LICENSE +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/MANIFEST.in +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/build_src/_core.c +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/fusion_pipeline.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/kcenter_select.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/algorithms/registry.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/benchmark.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/explainability.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/intelligence.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/optimizer.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/scanner.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/strategy_display.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/engine/strategy_selector.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/multimodal/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/multimodal/clip_encoder.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/pipeline.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/reports/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/reports/exporter.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/compare.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/difficulty.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/embeddings/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/fusion.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/noise.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/normalize.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/pipeline.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/redundancy.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/aum.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/el2n.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/forgetting.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/grand.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/scores/margin.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selection.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/__init__.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/facility_location.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/kcenter.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/strategy.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning/sdk.py +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/dependency_links.txt +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/requires.txt +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/datapruning.egg-info/top_level.txt +0 -0
- {datapruning-2.1.3 → datapruning-2.1.5}/setup.cfg +0 -0
- {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
|
+
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
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
"
|
|
50
|
-
"help the model learn better decision regions.
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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=
|
|
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
|
|
55
|
-
"
|
|
56
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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=
|
|
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.
|
|
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
|
|
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
|
+
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/benchmark/evaluation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/embeddings/torch_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/facility_location.py
RENAMED
|
File without changes
|
|
File without changes
|
{datapruning-2.1.3 → datapruning-2.1.5}/datapruning/research_pruning/selectors/out_of_fold.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|