datapruning 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {datapruning-2.0.0/datapruning.egg-info → datapruning-2.1.0}/PKG-INFO +4 -1
  2. datapruning-2.1.0/datapruning/multimodal/__init__.py +1 -0
  3. datapruning-2.1.0/datapruning/multimodal/clip_encoder.py +179 -0
  4. datapruning-2.1.0/datapruning/sdk.py +289 -0
  5. {datapruning-2.0.0 → datapruning-2.1.0/datapruning.egg-info}/PKG-INFO +4 -1
  6. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/SOURCES.txt +2 -0
  7. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/requires.txt +4 -0
  8. {datapruning-2.0.0 → datapruning-2.1.0}/pyproject.toml +7 -1
  9. datapruning-2.0.0/datapruning/sdk.py +0 -80
  10. {datapruning-2.0.0 → datapruning-2.1.0}/LICENSE +0 -0
  11. {datapruning-2.0.0 → datapruning-2.1.0}/MANIFEST.in +0 -0
  12. {datapruning-2.0.0 → datapruning-2.1.0}/README.md +0 -0
  13. {datapruning-2.0.0 → datapruning-2.1.0}/build_src/_core.c +0 -0
  14. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/__init__.py +0 -0
  15. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/__init__.py +0 -0
  16. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/base.py +0 -0
  17. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/el2n_scorer.py +0 -0
  18. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/fusion_pipeline.py +0 -0
  19. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/kcenter_select.py +0 -0
  20. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/margin_scorer.py +0 -0
  21. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/registry.py +0 -0
  22. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/algorithms/sliding_window.py +0 -0
  23. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/__init__.py +0 -0
  24. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/benchmark.py +0 -0
  25. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/explainability.py +0 -0
  26. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/intelligence.py +0 -0
  27. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/optimizer.py +0 -0
  28. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/scanner.py +0 -0
  29. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/strategy_display.py +0 -0
  30. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/engine/strategy_selector.py +0 -0
  31. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/pipeline.py +0 -0
  32. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/reports/__init__.py +0 -0
  33. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/reports/exporter.py +0 -0
  34. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/__init__.py +0 -0
  35. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/__init__.py +0 -0
  36. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/compare.py +0 -0
  37. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
  38. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
  39. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/difficulty.py +0 -0
  40. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/embeddings/__init__.py +0 -0
  41. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
  42. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/fusion.py +0 -0
  43. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/noise.py +0 -0
  44. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/normalize.py +0 -0
  45. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/pipeline.py +0 -0
  46. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/redundancy.py +0 -0
  47. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/__init__.py +0 -0
  48. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/aum.py +0 -0
  49. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/el2n.py +0 -0
  50. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/forgetting.py +0 -0
  51. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/grand.py +0 -0
  52. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/scores/margin.py +0 -0
  53. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selection.py +0 -0
  54. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/__init__.py +0 -0
  55. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/facility_location.py +0 -0
  56. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/kcenter.py +0 -0
  57. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
  58. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning/research_pruning/strategy.py +0 -0
  59. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/dependency_links.txt +0 -0
  60. {datapruning-2.0.0 → datapruning-2.1.0}/datapruning.egg-info/top_level.txt +0 -0
  61. {datapruning-2.0.0 → datapruning-2.1.0}/setup.cfg +0 -0
  62. {datapruning-2.0.0 → datapruning-2.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Intelligent data pruning for ML datasets
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -21,6 +21,9 @@ Requires-Dist: numpy>=1.24.0
21
21
  Requires-Dist: pandas>=2.0.0
22
22
  Requires-Dist: scikit-learn>=1.3.0
23
23
  Requires-Dist: torch>=2.0.0
24
+ Provides-Extra: clip
25
+ Requires-Dist: transformers>=4.30.0; extra == "clip"
26
+ Requires-Dist: pillow>=9.0.0; extra == "clip"
24
27
  Dynamic: license-file
25
28
 
26
29
  # DataPruning
@@ -0,0 +1 @@
1
+ """Multimodal pruning support — encode images and text with CLIP, then prune."""
@@ -0,0 +1,179 @@
1
+ """CLIP-based encoder for images and text.
2
+
3
+ Turns images and text into feature vectors that the pruning algorithms
4
+ can work with. Uses HuggingFace transformers for CLIP.
5
+
6
+ Requirements:
7
+ pip install datapruning[clip]
8
+ # or manually: pip install transformers pillow
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ import numpy as np
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ CLIP_MODEL = "openai/clip-vit-base-patch32"
23
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
24
+
25
+ try:
26
+ import torch
27
+ from transformers import CLIPModel, CLIPTokenizer, CLIPProcessor
28
+
29
+ HAS_CLIP = True
30
+ except ImportError:
31
+ HAS_CLIP = False
32
+
33
+
34
+ def _require_clip():
35
+ if not HAS_CLIP:
36
+ raise ImportError(
37
+ "CLIP dependencies not installed. "
38
+ "Install with: pip install datapruning[clip] "
39
+ "or manually: pip install transformers pillow"
40
+ )
41
+
42
+
43
+ def _to_tensor(x):
44
+ """Extract a torch.Tensor from various CLIP output types."""
45
+ if isinstance(x, torch.Tensor):
46
+ return x
47
+ if hasattr(x, "pooler_output") and x.pooler_output is not None:
48
+ return x.pooler_output
49
+ if hasattr(x, "last_hidden_state"):
50
+ return x.last_hidden_state[:, 0, :]
51
+ if hasattr(x, "text_embeds"):
52
+ return x.text_embeds
53
+ if hasattr(x, "image_embeds"):
54
+ return x.image_embeds
55
+ raise TypeError(f"Cannot extract tensor from {type(x).__name__}")
56
+
57
+
58
+ class CLIPEncoder:
59
+ """Encode images and text into CLIP feature vectors."""
60
+
61
+ def __init__(self, model_name: str = CLIP_MODEL, device: Optional[str] = None):
62
+ _require_clip()
63
+ self.model_name = model_name
64
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
65
+ self._model = None
66
+ self._processor = None
67
+ self._tokenizer = None
68
+
69
+ def _load(self):
70
+ if self._model is not None:
71
+ return
72
+ logger.info("Loading CLIP model: %s (device=%s)", self.model_name, self.device)
73
+ self._model = CLIPModel.from_pretrained(self.model_name).to(self.device)
74
+ self._model.eval()
75
+ self._processor = CLIPProcessor.from_pretrained(self.model_name)
76
+ self._tokenizer = CLIPTokenizer.from_pretrained(self.model_name)
77
+ logger.info("CLIP model loaded successfully")
78
+
79
+ def encode_images(
80
+ self, image_paths: list[str], batch_size: int = 32
81
+ ) -> np.ndarray:
82
+ """Encode a list of image file paths into CLIP embeddings.
83
+
84
+ Parameters
85
+ ----------
86
+ image_paths : list of str — paths to image files.
87
+ batch_size : int — encoding batch size.
88
+
89
+ Returns
90
+ -------
91
+ embeddings : (N, 512) float32 array — CLIP image features.
92
+ """
93
+ _require_clip()
94
+ self._load()
95
+
96
+ from PIL import Image
97
+
98
+ all_embeddings = []
99
+ for i in range(0, len(image_paths), batch_size):
100
+ batch_paths = image_paths[i : i + batch_size]
101
+ images = []
102
+ for p in batch_paths:
103
+ try:
104
+ img = Image.open(p).convert("RGB")
105
+ images.append(img)
106
+ except Exception as e:
107
+ logger.warning("Failed to load %s: %s", p, e)
108
+ images.append(Image.new("RGB", (224, 224), (0, 0, 0)))
109
+
110
+ inputs = self._processor(images=images, return_tensors="pt", padding=True)
111
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
112
+
113
+ with torch.no_grad():
114
+ features = self._model.get_image_features(**inputs)
115
+ features = _to_tensor(features)
116
+ features = features / features.norm(dim=-1, keepdim=True)
117
+
118
+ all_embeddings.append(features.cpu().numpy().astype(np.float32))
119
+
120
+ return np.concatenate(all_embeddings, axis=0)
121
+
122
+ def encode_texts(
123
+ self, texts: list[str], batch_size: int = 64
124
+ ) -> np.ndarray:
125
+ """Encode a list of text strings into CLIP embeddings.
126
+
127
+ Parameters
128
+ ----------
129
+ texts : list of str — text strings to encode.
130
+ batch_size : int — encoding batch size.
131
+
132
+ Returns
133
+ -------
134
+ embeddings : (N, 512) float32 array — CLIP text features.
135
+ """
136
+ _require_clip()
137
+ self._load()
138
+
139
+ all_embeddings = []
140
+ for i in range(0, len(texts), batch_size):
141
+ batch = texts[i : i + batch_size]
142
+ inputs = self._tokenizer(
143
+ batch, padding=True, truncation=True, return_tensors="pt"
144
+ )
145
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
146
+
147
+ with torch.no_grad():
148
+ features = self._model.get_text_features(**inputs)
149
+ features = _to_tensor(features)
150
+ features = features / features.norm(dim=-1, keepdim=True)
151
+
152
+ all_embeddings.append(features.cpu().numpy().astype(np.float32))
153
+
154
+ return np.concatenate(all_embeddings, axis=0)
155
+
156
+
157
+ def scan_images(folder_path: str) -> list[str]:
158
+ """Recursively find all image files in a folder.
159
+
160
+ Returns sorted list of absolute paths.
161
+ """
162
+ folder = Path(folder_path)
163
+ if not folder.exists():
164
+ raise FileNotFoundError(f"Folder not found: {folder_path}")
165
+ if not folder.is_dir():
166
+ raise NotADirectoryError(f"Not a directory: {folder_path}")
167
+
168
+ paths = []
169
+ for ext in IMAGE_EXTENSIONS:
170
+ paths.extend(folder.rglob(f"*{ext}"))
171
+ paths.extend(folder.rglob(f"*{ext.upper()}"))
172
+
173
+ paths = sorted(set(str(p) for p in paths))
174
+ if not paths:
175
+ raise ValueError(
176
+ f"No image files found in {folder_path}. "
177
+ f"Supported formats: {', '.join(sorted(IMAGE_EXTENSIONS))}"
178
+ )
179
+ return paths
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import logging
4
+ import numpy as np
5
+ import pandas as pd
6
+ from pathlib import Path
7
+ from datapruning.engine.scanner import scan
8
+ from datapruning.engine.intelligence import analyze_intelligence
9
+ from datapruning.engine.strategy_selector import select_strategy, get_algorithm, list_available_algorithms
10
+ from datapruning.engine.optimizer import run_optimization
11
+ from datapruning.engine.explainability import build_report
12
+ from datapruning.engine.benchmark import benchmark_algorithms, compare_to_full
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DatasetEngine:
18
+ def __init__(self, df: pd.DataFrame, target_col: str = None):
19
+ self.df = df
20
+ self.target_col = target_col
21
+ self._analysis = None
22
+ self._last_result = None
23
+ self._last_optimizer = None
24
+ self._embeddings = None
25
+ self._input_mode = "tabular"
26
+
27
+ @classmethod
28
+ def from_images(
29
+ cls,
30
+ folder_path: str,
31
+ labels: list[int],
32
+ keep_ratio: float = 0.5,
33
+ batch_size: int = 32,
34
+ ) -> "DatasetEngine":
35
+ """Create a DatasetEngine from a folder of images.
36
+
37
+ Encodes all images with CLIP into feature vectors, then wraps
38
+ them in a DataFrame so the existing pruning algorithms work
39
+ unchanged.
40
+
41
+ Parameters
42
+ ----------
43
+ folder_path : str — path to folder containing images.
44
+ labels : list of int — integer label for each image (same order).
45
+ keep_ratio : float — default keep ratio for optimize().
46
+ batch_size : int — CLIP encoding batch size.
47
+
48
+ Returns
49
+ -------
50
+ DatasetEngine — ready to call analyze(), recommend(), optimize().
51
+ """
52
+ from datapruning.multimodal.clip_encoder import CLIPEncoder, scan_images
53
+
54
+ image_paths = scan_images(folder_path)
55
+ if len(image_paths) != len(labels):
56
+ raise ValueError(
57
+ f"Number of images ({len(image_paths)}) != "
58
+ f"number of labels ({len(labels)})"
59
+ )
60
+
61
+ encoder = CLIPEncoder()
62
+ embeddings = encoder.encode_images(image_paths, batch_size=batch_size)
63
+
64
+ feat_cols = [f"clip_{i}" for i in range(embeddings.shape[1])]
65
+ df = pd.DataFrame(embeddings, columns=feat_cols)
66
+ df["label"] = labels
67
+ df["filepath"] = image_paths
68
+
69
+ engine = cls(df, target_col="label")
70
+ engine._embeddings = embeddings
71
+ engine._input_mode = "images"
72
+ return engine
73
+
74
+ @classmethod
75
+ def from_texts(
76
+ cls,
77
+ texts: list[str],
78
+ labels: list[int],
79
+ batch_size: int = 64,
80
+ ) -> "DatasetEngine":
81
+ """Create a DatasetEngine from a list of text strings.
82
+
83
+ Encodes all texts with CLIP into feature vectors.
84
+
85
+ Parameters
86
+ ----------
87
+ texts : list of str — text strings to encode.
88
+ labels : list of int — integer label for each text.
89
+ batch_size : int — CLIP encoding batch size.
90
+
91
+ Returns
92
+ -------
93
+ DatasetEngine — ready to call analyze(), recommend(), optimize().
94
+ """
95
+ from datapruning.multimodal.clip_encoder import CLIPEncoder
96
+
97
+ if len(texts) != len(labels):
98
+ raise ValueError(
99
+ f"Number of texts ({len(texts)}) != "
100
+ f"number of labels ({len(labels)})"
101
+ )
102
+
103
+ encoder = CLIPEncoder()
104
+ embeddings = encoder.encode_texts(texts, batch_size=batch_size)
105
+
106
+ feat_cols = [f"clip_{i}" for i in range(embeddings.shape[1])]
107
+ df = pd.DataFrame(embeddings, columns=feat_cols)
108
+ df["label"] = labels
109
+ df["text"] = texts
110
+
111
+ engine = cls(df, target_col="label")
112
+ engine._embeddings = embeddings
113
+ engine._input_mode = "texts"
114
+ return engine
115
+
116
+ def _build_multimodal_analysis(self) -> dict:
117
+ """Build analysis dict from CLIP embeddings for strategy selection."""
118
+ X = self._embeddings
119
+ y = self.df[self.target_col].values
120
+ n_samples, n_features = X.shape
121
+ classes, counts = np.unique(y, return_counts=True)
122
+ imbalance = float(counts.max() / max(counts.min(), 1))
123
+
124
+ scan_result = {
125
+ "rows": n_samples,
126
+ "columns": n_features,
127
+ "memory_mb": round(X.nbytes / 1024 / 1024, 2),
128
+ "missing_pct": 0.0,
129
+ "duplicate_pct": round(
130
+ 1.0 - len(np.unique(X, axis=0)) / n_samples, 4
131
+ ),
132
+ "numeric_pct": 100.0,
133
+ "categorical_pct": 0.0,
134
+ "estimated_training_cost": "medium",
135
+ "task_type": "binary" if len(classes) == 2 else "multiclass",
136
+ "target_balance": classes.tolist(),
137
+ "imbalance_ratio": imbalance,
138
+ "input_mode": self._input_mode,
139
+ "embedding_dim": n_features,
140
+ }
141
+
142
+ intelligence_result = {
143
+ "id_columns": [],
144
+ "date_columns": [],
145
+ "constant_columns": [],
146
+ "near_constant_columns": [],
147
+ "high_cardinality_columns": [],
148
+ "highly_correlated_pairs": [],
149
+ "outlier_summary": [],
150
+ "complexity_score": min(100, int(n_features * 0.1 + n_samples * 0.001)),
151
+ "leakage_risk_columns": [],
152
+ }
153
+
154
+ return {"scan": scan_result, "intelligence": intelligence_result}
155
+
156
+ def analyze(self) -> dict:
157
+ if self._embeddings is not None:
158
+ self._analysis = self._build_multimodal_analysis()
159
+ else:
160
+ scan_result = scan(self.df, target_col=self.target_col)
161
+ intelligence_result = analyze_intelligence(self.df, target_col=self.target_col)
162
+ self._analysis = {"scan": scan_result, "intelligence": intelligence_result}
163
+ return self._analysis
164
+
165
+ def report(self, as_json: bool = False):
166
+ if self._analysis is None:
167
+ self.analyze()
168
+ if as_json:
169
+ return json.dumps(self._analysis, indent=2, default=str)
170
+ s, i = self._analysis["scan"], self._analysis["intelligence"]
171
+ print(f"Rows: {s['rows']:,} | Columns: {s['columns']} | Memory: {s['memory_mb']} MB")
172
+ print(f"Missing: {s['missing_pct']}% | Duplicates: {s['duplicate_pct']}%")
173
+ if "task_type" in s:
174
+ print(f"Task type: {s['task_type']}")
175
+ if "imbalance_ratio" in s:
176
+ print(f"Imbalance ratio: {s['imbalance_ratio']}:1")
177
+ if i["leakage_risk_columns"]:
178
+ print(f"Possible leakage columns: {i['leakage_risk_columns']}")
179
+ print(f"Complexity score: {i['complexity_score']}/100")
180
+ return self._analysis
181
+
182
+ def recommend(self):
183
+ if self._analysis is None:
184
+ self.analyze()
185
+ rec = select_strategy(self._analysis)
186
+ if self._embeddings is not None:
187
+ rec._input_mode = self._input_mode
188
+ rec._embedding_dim = self._embeddings.shape[1]
189
+ return rec
190
+
191
+ def available_algorithms(self) -> list[dict]:
192
+ return list_available_algorithms()
193
+
194
+ def optimize(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
195
+ if self.target_col is None:
196
+ raise ValueError("target_col is required to optimize.")
197
+
198
+ if self._embeddings is not None:
199
+ return self._optimize_multimodal(algorithm_key, keep_ratio)
200
+
201
+ if algorithm_key is None:
202
+ rec = self.recommend()
203
+ optimizer = rec.algorithm_instance
204
+ else:
205
+ optimizer = get_algorithm(algorithm_key)
206
+ result = run_optimization(self.df, self.target_col, optimizer, keep_ratio=keep_ratio)
207
+ report = build_report(optimizer, result, self.df)
208
+ self._last_result = result
209
+ self._last_optimizer = optimizer
210
+ return {
211
+ "optimized_df": result.optimized_df,
212
+ "explainability": report["explanation"],
213
+ "dataset_score": report["dataset_score"],
214
+ "runtime_seconds": report["runtime_seconds"],
215
+ }
216
+
217
+ def _optimize_multimodal(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
218
+ """Optimize using CLIP embeddings directly."""
219
+ import time
220
+ from datapruning.research_pruning.selectors.kcenter import kcenter_greedy
221
+ from datapruning.research_pruning.pipeline import ResearchFusionEngine
222
+
223
+ X = self._embeddings.astype(np.float64)
224
+ y = self.df[self.target_col].values.astype(np.int64)
225
+
226
+ t0 = time.time()
227
+
228
+ if algorithm_key is None:
229
+ rec = self.recommend()
230
+ algorithm_key = rec.algorithm_key
231
+
232
+ n_keep = max(1, int(len(X) * keep_ratio))
233
+
234
+ if algorithm_key == "kcenter":
235
+ selected = kcenter_greedy(X, budget=n_keep)
236
+ elif algorithm_key == "fusion":
237
+ engine = ResearchFusionEngine({"keep_ratio": keep_ratio})
238
+ _, _, report = engine.run(X, y, keep_ratio=keep_ratio, auto_strategy=True)
239
+ selected = np.array(report.get("selected_indices", list(range(n_keep))))
240
+ else:
241
+ from datapruning.research_pruning.scores.el2n import el2n_score
242
+ from datapruning.research_pruning.scores.margin import margin_score
243
+
244
+ scores = np.zeros(len(X))
245
+ try:
246
+ e_scores = el2n_score(X, y)
247
+ scores += e_scores
248
+ except Exception:
249
+ scores += np.linalg.norm(X, axis=1)
250
+ try:
251
+ m_scores = margin_score(X, y)
252
+ scores += m_scores
253
+ except Exception:
254
+ pass
255
+
256
+ selected = np.argsort(scores)[::-1][:n_keep]
257
+
258
+ runtime = time.time() - t0
259
+ pruned_df = self.df.iloc[selected].reset_index(drop=True)
260
+
261
+ self._last_result = type("Result", (), {"optimized_df": pruned_df})()
262
+
263
+ return {
264
+ "optimized_df": pruned_df,
265
+ "explainability": {
266
+ "algorithm": algorithm_key,
267
+ "input_mode": self._input_mode,
268
+ "embedding_dim": self._embeddings.shape[1],
269
+ "rows_removed": len(self.df) - len(pruned_df),
270
+ "reduction_pct": round((1 - len(pruned_df) / len(self.df)) * 100, 1),
271
+ },
272
+ "dataset_score": {
273
+ "overall_health": 100.0,
274
+ "input_mode": self._input_mode,
275
+ },
276
+ "runtime_seconds": round(runtime, 4),
277
+ }
278
+
279
+ def export(self, path: str, result: dict = None):
280
+ result = result or {"optimized_df": self._last_result.optimized_df}
281
+ result["optimized_df"].to_csv(path, index=False)
282
+ print(f"Exported optimized dataset -> {path} ({len(result['optimized_df']):,} rows)")
283
+ return path
284
+
285
+ def benchmark(self, algorithms: list[str] = None, keep_ratio: float = 0.5) -> dict:
286
+ return benchmark_algorithms(self.df, self.target_col, algorithms=algorithms, keep_ratio=keep_ratio)
287
+
288
+ def compare(self, optimized_df: pd.DataFrame) -> dict:
289
+ return compare_to_full(optimized_df, self.df, self.target_col)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Intelligent data pruning for ML datasets
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -21,6 +21,9 @@ Requires-Dist: numpy>=1.24.0
21
21
  Requires-Dist: pandas>=2.0.0
22
22
  Requires-Dist: scikit-learn>=1.3.0
23
23
  Requires-Dist: torch>=2.0.0
24
+ Provides-Extra: clip
25
+ Requires-Dist: transformers>=4.30.0; extra == "clip"
26
+ Requires-Dist: pillow>=9.0.0; extra == "clip"
24
27
  Dynamic: license-file
25
28
 
26
29
  # DataPruning
@@ -28,6 +28,8 @@ datapruning/engine/optimizer.py
28
28
  datapruning/engine/scanner.py
29
29
  datapruning/engine/strategy_display.py
30
30
  datapruning/engine/strategy_selector.py
31
+ datapruning/multimodal/__init__.py
32
+ datapruning/multimodal/clip_encoder.py
31
33
  datapruning/reports/__init__.py
32
34
  datapruning/reports/exporter.py
33
35
  datapruning/research_pruning/__init__.py
@@ -2,3 +2,7 @@ numpy>=1.24.0
2
2
  pandas>=2.0.0
3
3
  scikit-learn>=1.3.0
4
4
  torch>=2.0.0
5
+
6
+ [clip]
7
+ transformers>=4.30.0
8
+ pillow>=9.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datapruning"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  description = "Intelligent data pruning for ML datasets"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -28,6 +28,12 @@ dependencies = [
28
28
  "torch>=2.0.0",
29
29
  ]
30
30
 
31
+ [project.optional-dependencies]
32
+ clip = [
33
+ "transformers>=4.30.0",
34
+ "pillow>=9.0.0",
35
+ ]
36
+
31
37
  [project.urls]
32
38
  Homepage = "https://www.datapruning.com"
33
39
 
@@ -1,80 +0,0 @@
1
- from __future__ import annotations
2
- import json
3
- import pandas as pd
4
- from datapruning.engine.scanner import scan
5
- from datapruning.engine.intelligence import analyze_intelligence
6
- from datapruning.engine.strategy_selector import select_strategy, get_algorithm, list_available_algorithms
7
- from datapruning.engine.optimizer import run_optimization
8
- from datapruning.engine.explainability import build_report
9
- from datapruning.engine.benchmark import benchmark_algorithms, compare_to_full
10
-
11
-
12
- class DatasetEngine:
13
- def __init__(self, df: pd.DataFrame, target_col: str = None):
14
- self.df = df
15
- self.target_col = target_col
16
- self._analysis = None
17
- self._last_result = None
18
- self._last_optimizer = None
19
-
20
- def analyze(self) -> dict:
21
- scan_result = scan(self.df, target_col=self.target_col)
22
- intelligence_result = analyze_intelligence(self.df, target_col=self.target_col)
23
- self._analysis = {"scan": scan_result, "intelligence": intelligence_result}
24
- return self._analysis
25
-
26
- def report(self, as_json: bool = False):
27
- if self._analysis is None:
28
- self.analyze()
29
- if as_json:
30
- return json.dumps(self._analysis, indent=2, default=str)
31
- s, i = self._analysis["scan"], self._analysis["intelligence"]
32
- print(f"Rows: {s['rows']:,} | Columns: {s['columns']} | Memory: {s['memory_mb']} MB")
33
- print(f"Missing: {s['missing_pct']}% | Duplicates: {s['duplicate_pct']}%")
34
- if "task_type" in s:
35
- print(f"Task type: {s['task_type']}")
36
- if "imbalance_ratio" in s:
37
- print(f"Imbalance ratio: {s['imbalance_ratio']}:1")
38
- if i["leakage_risk_columns"]:
39
- print(f"Possible leakage columns: {i['leakage_risk_columns']}")
40
- print(f"Complexity score: {i['complexity_score']}/100")
41
- return self._analysis
42
-
43
- def recommend(self):
44
- if self._analysis is None:
45
- self.analyze()
46
- return select_strategy(self._analysis)
47
-
48
- def available_algorithms(self) -> list[dict]:
49
- return list_available_algorithms()
50
-
51
- def optimize(self, algorithm_key: str = None, keep_ratio: float = 0.5) -> dict:
52
- if self.target_col is None:
53
- raise ValueError("target_col is required to optimize.")
54
- if algorithm_key is None:
55
- rec = self.recommend()
56
- optimizer = rec.algorithm_instance
57
- else:
58
- optimizer = get_algorithm(algorithm_key)
59
- result = run_optimization(self.df, self.target_col, optimizer, keep_ratio=keep_ratio)
60
- report = build_report(optimizer, result, self.df)
61
- self._last_result = result
62
- self._last_optimizer = optimizer
63
- return {
64
- "optimized_df": result.optimized_df,
65
- "explainability": report["explanation"],
66
- "dataset_score": report["dataset_score"],
67
- "runtime_seconds": report["runtime_seconds"],
68
- }
69
-
70
- def export(self, path: str, result: dict = None):
71
- result = result or {"optimized_df": self._last_result.optimized_df}
72
- result["optimized_df"].to_csv(path, index=False)
73
- print(f"Exported optimized dataset -> {path} ({len(result['optimized_df']):,} rows)")
74
- return path
75
-
76
- def benchmark(self, algorithms: list[str] = None, keep_ratio: float = 0.5) -> dict:
77
- return benchmark_algorithms(self.df, self.target_col, algorithms=algorithms, keep_ratio=keep_ratio)
78
-
79
- def compare(self, optimized_df: pd.DataFrame) -> dict:
80
- return compare_to_full(optimized_df, self.df, self.target_col)
File without changes
File without changes
File without changes
File without changes
File without changes