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.
- datapruning-2.1.4/PKG-INFO +150 -0
- datapruning-2.1.4/README.md +120 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/__init__.py +2 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/base.py +78 -3
- datapruning-2.1.4/datapruning/algorithms/el2n_scorer.py +79 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/kcenter_select.py +6 -4
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/margin_scorer.py +22 -25
- datapruning-2.1.4/datapruning/algorithms/oof_utils.py +75 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/sliding_window.py +12 -14
- datapruning-2.1.4/datapruning.egg-info/PKG-INFO +150 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/SOURCES.txt +1 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/pyproject.toml +1 -1
- datapruning-2.1.2/PKG-INFO +0 -81
- datapruning-2.1.2/README.md +0 -51
- datapruning-2.1.2/datapruning/algorithms/el2n_scorer.py +0 -66
- datapruning-2.1.2/datapruning.egg-info/PKG-INFO +0 -81
- {datapruning-2.1.2 → datapruning-2.1.4}/LICENSE +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/MANIFEST.in +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/build_src/_core.c +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/fusion_pipeline.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/algorithms/registry.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/benchmark.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/explainability.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/intelligence.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/optimizer.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/scanner.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/strategy_display.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/engine/strategy_selector.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/multimodal/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/multimodal/clip_encoder.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/pipeline.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/reports/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/reports/exporter.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/compare.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/difficulty.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/fusion.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/noise.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/normalize.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/pipeline.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/redundancy.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/aum.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/el2n.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/forgetting.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/grand.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/scores/margin.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selection.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/__init__.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/facility_location.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/kcenter.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/research_pruning/strategy.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning/sdk.py +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/dependency_links.txt +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/requires.txt +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/datapruning.egg-info/top_level.txt +0 -0
- {datapruning-2.1.2 → datapruning-2.1.4}/setup.cfg +0 -0
- {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.
|
|
@@ -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.
|
|
51
|
+
return 0.3
|
|
52
|
+
if n_rows < 500:
|
|
53
|
+
return 0.6
|
|
52
54
|
if n_cols > n_rows:
|
|
53
|
-
return 0.
|
|
55
|
+
return 0.9
|
|
54
56
|
if n_cols > 50:
|
|
55
|
-
return 0.
|
|
56
|
-
return 0.
|
|
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
|
|
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)
|
|
@@ -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.
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
"
|
|
54
|
-
"
|
|
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
|
|
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.
|
|
73
|
-
|
|
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
|