datapruning 2.1.2__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 (66) hide show
  1. datapruning-2.1.4/PKG-INFO +150 -0
  2. datapruning-2.1.4/README.md +120 -0
  3. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/__init__.py +2 -0
  4. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/base.py +78 -3
  5. datapruning-2.1.4/datapruning/algorithms/el2n_scorer.py +79 -0
  6. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/kcenter_select.py +6 -4
  7. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/margin_scorer.py +22 -25
  8. datapruning-2.1.4/datapruning/algorithms/oof_utils.py +75 -0
  9. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/sliding_window.py +12 -14
  10. datapruning-2.1.4/datapruning.egg-info/PKG-INFO +150 -0
  11. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/SOURCES.txt +1 -0
  12. {datapruning-2.1.2 → datapruning-2.1.4}/pyproject.toml +1 -1
  13. datapruning-2.1.2/PKG-INFO +0 -81
  14. datapruning-2.1.2/README.md +0 -51
  15. datapruning-2.1.2/datapruning/algorithms/el2n_scorer.py +0 -66
  16. datapruning-2.1.2/datapruning.egg-info/PKG-INFO +0 -81
  17. {datapruning-2.1.2 → datapruning-2.1.4}/LICENSE +0 -0
  18. {datapruning-2.1.2 → datapruning-2.1.4}/MANIFEST.in +0 -0
  19. {datapruning-2.1.2 → datapruning-2.1.4}/build_src/_core.c +0 -0
  20. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/__init__.py +0 -0
  21. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/fusion_pipeline.py +0 -0
  22. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/registry.py +0 -0
  23. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/__init__.py +0 -0
  24. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/benchmark.py +0 -0
  25. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/explainability.py +0 -0
  26. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/intelligence.py +0 -0
  27. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/optimizer.py +0 -0
  28. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/scanner.py +0 -0
  29. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/strategy_display.py +0 -0
  30. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/strategy_selector.py +0 -0
  31. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/multimodal/__init__.py +0 -0
  32. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/multimodal/clip_encoder.py +0 -0
  33. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/pipeline.py +0 -0
  34. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/reports/__init__.py +0 -0
  35. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/reports/exporter.py +0 -0
  36. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/__init__.py +0 -0
  37. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/__init__.py +0 -0
  38. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/compare.py +0 -0
  39. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
  40. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
  41. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/difficulty.py +0 -0
  42. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/__init__.py +0 -0
  43. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
  44. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/fusion.py +0 -0
  45. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/noise.py +0 -0
  46. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/normalize.py +0 -0
  47. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/pipeline.py +0 -0
  48. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/redundancy.py +0 -0
  49. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/__init__.py +0 -0
  50. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/aum.py +0 -0
  51. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/el2n.py +0 -0
  52. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/forgetting.py +0 -0
  53. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/grand.py +0 -0
  54. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/margin.py +0 -0
  55. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selection.py +0 -0
  56. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/__init__.py +0 -0
  57. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/facility_location.py +0 -0
  58. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/kcenter.py +0 -0
  59. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
  60. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/strategy.py +0 -0
  61. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/sdk.py +0 -0
  62. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/dependency_links.txt +0 -0
  63. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/requires.txt +0 -0
  64. {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/top_level.txt +0 -0
  65. {datapruning-2.1.2 → datapruning-2.1.4}/setup.cfg +0 -0
  66. {datapruning-2.1.2 → datapruning-2.1.4}/setup.py +0 -0
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: datapruning
3
+ Version: 2.1.4
4
+ Summary: Intelligent dataset optimization for cleaner, smaller training data
5
+ License: Proprietary
6
+ Project-URL: Homepage, https://www.datapruning.com
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24.0
21
+ Requires-Dist: pandas>=2.0.0
22
+ Requires-Dist: scikit-learn>=1.3.0
23
+ Provides-Extra: torch
24
+ Requires-Dist: torch>=2.0.0; extra == "torch"
25
+ Provides-Extra: clip
26
+ Requires-Dist: torch>=2.0.0; extra == "clip"
27
+ Requires-Dist: transformers>=4.30.0; extra == "clip"
28
+ Requires-Dist: pillow>=9.0.0; extra == "clip"
29
+ Dynamic: license-file
30
+
31
+ # DataPruning
32
+
33
+ Intelligent dataset optimization — reduces dataset size by selecting the most informative rows while preserving model quality.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install datapruning
39
+ ```
40
+
41
+ For image/text multimodal support (CLIP encoding):
42
+
43
+ ```bash
44
+ pip install datapruning[clip]
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ### CSV / Tabular Data
50
+
51
+ ```python
52
+ import pandas as pd
53
+ from datapruning.sdk import DatasetEngine
54
+
55
+ df = pd.read_csv("dataset.csv")
56
+ engine = DatasetEngine(df, target_col="label")
57
+
58
+ # Analyze the dataset
59
+ analysis = engine.analyze()
60
+
61
+ # Get a recommendation
62
+ rec = engine.recommend()
63
+ print(f"Best method: {rec.display_name} ({rec.confidence}% confidence)")
64
+
65
+ # Optimize — keep 50% of most informative rows
66
+ result = engine.optimize(keep_ratio=0.5)
67
+ print(f"Kept {result['runtime_seconds']:.1f}s — {result['explainability']['rows_removed']} rows removed")
68
+
69
+ # Download the optimized dataset
70
+ result["optimized_df"].to_csv("optimized.csv", index=False)
71
+ ```
72
+
73
+ ### Image Datasets
74
+
75
+ ```python
76
+ from datapruning.sdk import DatasetEngine
77
+
78
+ # Create engine from a folder of images
79
+ engine = DatasetEngine.from_images(
80
+ folder_path="./photos",
81
+ labels=[0, 1, 0, 1, ...], # one label per image
82
+ keep_ratio=0.5,
83
+ )
84
+
85
+ # Analyze and optimize
86
+ analysis = engine.analyze()
87
+ result = engine.optimize(keep_ratio=0.5)
88
+ result["optimized_df"].to_csv("optimized_images.csv", index=False)
89
+ ```
90
+
91
+ ### Text Datasets
92
+
93
+ ```python
94
+ engine = DatasetEngine.from_texts(
95
+ texts=["label A sample", "label B sample", ...],
96
+ labels=[0, 1, 0, 1, ...],
97
+ )
98
+ result = engine.optimize(keep_ratio=0.5)
99
+ ```
100
+
101
+ ## Algorithms
102
+
103
+ DataPruning selects the best algorithm automatically based on dataset characteristics:
104
+
105
+ | Method | What it does | Best for |
106
+ |--------|-------------|----------|
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
+ | **Diversity Selection** | Picks the most representative subset (K-Center) | High-dimensional data |
110
+ | **Noise Filtering** | Drops redundant and potentially mislabeled samples via CV-based difficulty | Noisy/messy data |
111
+ | **Full Pipeline** | Combines all signals for maximum quality | Complex datasets |
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
+
115
+ ## Limits
116
+
117
+ - Minimum: 1,000 rows (tabular) or 10 images
118
+ - Maximum: 300,000 rows (tabular) or 1,000 images
119
+ - Supported image formats: JPG, PNG, BMP, GIF, TIFF, WebP
120
+
121
+ ## Requirements
122
+
123
+ - Python >= 3.10
124
+ - Pandas 2.0+
125
+ - NumPy 1.24+
126
+ - scikit-learn 1.3+
127
+
128
+ Optional (for image/text support):
129
+ - PyTorch 2.0+
130
+ - transformers 4.30+
131
+ - Pillow 9.0+
132
+
133
+ ## Features
134
+
135
+ - Runs locally — no data uploaded anywhere
136
+ - Auto-detects task type (classification/regression) and target column
137
+ - Auto-encodes categorical/string columns
138
+ - 5 optimization algorithms with automatic selection
139
+ - Preserves minority class balance
140
+ - CSV and image dataset support
141
+ - Pure Python — works on any platform
142
+
143
+ ## Links
144
+
145
+ - Website: [datapruning.com](https://www.datapruning.com)
146
+ - PyPI: [pypi.org/project/datapruning](https://pypi.org/project/datapruning/)
147
+
148
+ ## License
149
+
150
+ Proprietary. See LICENSE file.
@@ -0,0 +1,120 @@
1
+ # DataPruning
2
+
3
+ Intelligent dataset optimization — reduces dataset size by selecting the most informative rows while preserving model quality.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install datapruning
9
+ ```
10
+
11
+ For image/text multimodal support (CLIP encoding):
12
+
13
+ ```bash
14
+ pip install datapruning[clip]
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### CSV / Tabular Data
20
+
21
+ ```python
22
+ import pandas as pd
23
+ from datapruning.sdk import DatasetEngine
24
+
25
+ df = pd.read_csv("dataset.csv")
26
+ engine = DatasetEngine(df, target_col="label")
27
+
28
+ # Analyze the dataset
29
+ analysis = engine.analyze()
30
+
31
+ # Get a recommendation
32
+ rec = engine.recommend()
33
+ print(f"Best method: {rec.display_name} ({rec.confidence}% confidence)")
34
+
35
+ # Optimize — keep 50% of most informative rows
36
+ result = engine.optimize(keep_ratio=0.5)
37
+ print(f"Kept {result['runtime_seconds']:.1f}s — {result['explainability']['rows_removed']} rows removed")
38
+
39
+ # Download the optimized dataset
40
+ result["optimized_df"].to_csv("optimized.csv", index=False)
41
+ ```
42
+
43
+ ### Image Datasets
44
+
45
+ ```python
46
+ from datapruning.sdk import DatasetEngine
47
+
48
+ # Create engine from a folder of images
49
+ engine = DatasetEngine.from_images(
50
+ folder_path="./photos",
51
+ labels=[0, 1, 0, 1, ...], # one label per image
52
+ keep_ratio=0.5,
53
+ )
54
+
55
+ # Analyze and optimize
56
+ analysis = engine.analyze()
57
+ result = engine.optimize(keep_ratio=0.5)
58
+ result["optimized_df"].to_csv("optimized_images.csv", index=False)
59
+ ```
60
+
61
+ ### Text Datasets
62
+
63
+ ```python
64
+ engine = DatasetEngine.from_texts(
65
+ texts=["label A sample", "label B sample", ...],
66
+ labels=[0, 1, 0, 1, ...],
67
+ )
68
+ result = engine.optimize(keep_ratio=0.5)
69
+ ```
70
+
71
+ ## Algorithms
72
+
73
+ DataPruning selects the best algorithm automatically based on dataset characteristics:
74
+
75
+ | Method | What it does | Best for |
76
+ |--------|-------------|----------|
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
+ | **Diversity Selection** | Picks the most representative subset (K-Center) | High-dimensional data |
80
+ | **Noise Filtering** | Drops redundant and potentially mislabeled samples via CV-based difficulty | Noisy/messy data |
81
+ | **Full Pipeline** | Combines all signals for maximum quality | Complex datasets |
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
+
85
+ ## Limits
86
+
87
+ - Minimum: 1,000 rows (tabular) or 10 images
88
+ - Maximum: 300,000 rows (tabular) or 1,000 images
89
+ - Supported image formats: JPG, PNG, BMP, GIF, TIFF, WebP
90
+
91
+ ## Requirements
92
+
93
+ - Python >= 3.10
94
+ - Pandas 2.0+
95
+ - NumPy 1.24+
96
+ - scikit-learn 1.3+
97
+
98
+ Optional (for image/text support):
99
+ - PyTorch 2.0+
100
+ - transformers 4.30+
101
+ - Pillow 9.0+
102
+
103
+ ## Features
104
+
105
+ - Runs locally — no data uploaded anywhere
106
+ - Auto-detects task type (classification/regression) and target column
107
+ - Auto-encodes categorical/string columns
108
+ - 5 optimization algorithms with automatic selection
109
+ - Preserves minority class balance
110
+ - CSV and image dataset support
111
+ - Pure Python — works on any platform
112
+
113
+ ## Links
114
+
115
+ - Website: [datapruning.com](https://www.datapruning.com)
116
+ - PyPI: [pypi.org/project/datapruning](https://pypi.org/project/datapruning/)
117
+
118
+ ## License
119
+
120
+ Proprietary. See LICENSE file.
@@ -5,6 +5,8 @@ import pandas as pd
5
5
  from . import pipeline
6
6
  from .sdk import DatasetEngine
7
7
 
8
+ __all__ = ["DatasetEngine", "Pruner", "prune_dataframe"]
9
+
8
10
  logger = logging.getLogger(__name__)
9
11
 
10
12
  MIN_ROWS = 1000
@@ -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
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.algorithms.oof_utils import compute_oof_logits, logits_to_el2n
5
+ from datapruning.algorithms.base import BaseOptimizer
6
+ from datapruning.algorithms.registry import register
7
+
8
+
9
+ @register
10
+ class EL2NScorerOptimizer(BaseOptimizer):
11
+ key = "el2n"
12
+ display_name = "Importance Scoring"
13
+
14
+ def __init__(self):
15
+ self._scores = None
16
+
17
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "EL2NScorerOptimizer":
18
+ X_arr = X.values.astype(np.float64)
19
+ y_arr = y.values.astype(int)
20
+ classes = np.unique(y_arr)
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)
37
+ return self
38
+
39
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
40
+ if self._scores is None:
41
+ self.fit(X, y)
42
+ classes = y.unique()
43
+ if len(classes) < 2:
44
+ n_target = max(1, int(len(y) * keep_ratio))
45
+ return y.sample(n=n_target, random_state=42).index
46
+ minority_class = y.value_counts().idxmin()
47
+ minority_idx = y[y == minority_class].index
48
+ majority_idx = y[y != minority_class].index
49
+ n_total_target = max(1, int(len(y) * keep_ratio))
50
+ n_minority = min(len(minority_idx), int(n_total_target * 0.8))
51
+ n_majority_target = max(n_total_target - n_minority, 0)
52
+ majority_scores = self._scores.loc[majority_idx].sort_values(ascending=False)
53
+ kept_majority_idx = majority_scores.index[:n_majority_target]
54
+ kept_minority_idx = minority_idx[:n_minority]
55
+ return kept_minority_idx.append(kept_majority_idx)
56
+
57
+ def why_selected(self) -> str:
58
+ return ("Measures how difficult each sample is for the model to classify by analyzing "
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.")
62
+
63
+ def limitations(self) -> list[str]:
64
+ return [
65
+ "Requires a quick cross-validation step during analysis.",
66
+ "May select outliers on very noisy datasets.",
67
+ ]
68
+
69
+ @staticmethod
70
+ def fit_score(analysis: dict) -> float:
71
+ scan = analysis.get("scan", analysis)
72
+ task_type = scan.get("task_type", "unknown")
73
+ n_rows = scan.get("rows", 0)
74
+
75
+ if task_type == "regression":
76
+ return 0.0
77
+ if n_rows < 200:
78
+ return 0.2
79
+ return 0.4
@@ -48,9 +48,11 @@ class KCenterSelectOptimizer(BaseOptimizer):
48
48
  n_cols = scan.get("columns", 0)
49
49
 
50
50
  if task_type == "regression":
51
- return 0.2
51
+ return 0.3
52
+ if n_rows < 500:
53
+ return 0.6
52
54
  if n_cols > n_rows:
53
- return 0.8
55
+ return 0.9
54
56
  if n_cols > 50:
55
- return 0.65
56
- return 0.4
57
+ return 0.85
58
+ return 0.8
@@ -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)
@@ -37,25 +35,29 @@ class MarginScorerOptimizer(BaseOptimizer):
37
35
  self.fit(X, y)
38
36
  classes = y.unique()
39
37
  if len(classes) < 2:
40
- n_target = int(len(y) * keep_ratio)
41
- return y.nlargest(n_target).index
38
+ n_target = max(1, int(len(y) * keep_ratio))
39
+ return y.sample(n=n_target, random_state=42).index
42
40
  minority_class = y.value_counts().idxmin()
43
41
  minority_idx = y[y == minority_class].index
44
42
  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)
43
+ n_total_target = max(1, int(len(y) * keep_ratio))
44
+ n_minority = min(len(minority_idx), int(n_total_target * 0.8))
45
+ n_majority_target = max(n_total_target - n_minority, 0)
46
+ majority_scores = self._scores.loc[majority_idx].sort_values(ascending=True)
48
47
  kept_majority_idx = majority_scores.index[:n_majority_target]
49
- return minority_idx.append(kept_majority_idx)
48
+ kept_minority_idx = minority_idx[:n_minority]
49
+ return kept_minority_idx.append(kept_majority_idx)
50
50
 
51
51
  def why_selected(self) -> str:
52
- return ("Analyzes how confidently each data point is classified, keeping the most "
53
- "informative samples near decision boundaries. Automatically preserves all "
54
- "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.")
55
57
 
56
58
  def limitations(self) -> list[str]:
57
59
  return [
58
- "Requires a quick model fitting step during analysis.",
60
+ "Requires a quick cross-validation step during analysis.",
59
61
  "May be less effective on extremely high-dimensional datasets.",
60
62
  ]
61
63
 
@@ -64,14 +66,9 @@ class MarginScorerOptimizer(BaseOptimizer):
64
66
  scan = analysis.get("scan", analysis)
65
67
  task_type = scan.get("task_type", "unknown")
66
68
  n_rows = scan.get("rows", 0)
67
- n_cols = scan.get("columns", 0)
68
69
 
69
70
  if task_type == "regression":
70
71
  return 0.0
71
72
  if n_rows < 500:
72
- return 0.1
73
- if n_rows > 200_000:
74
- return 0.6
75
- if n_cols > 100:
76
- return 0.7
77
- return 0.85
73
+ return 0.2
74
+ return 0.3
@@ -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