datapruning 2.0.0__tar.gz → 2.1.1__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 (63) hide show
  1. {datapruning-2.0.0/datapruning.egg-info → datapruning-2.1.1}/PKG-INFO +8 -3
  2. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/base.py +8 -0
  3. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/el2n_scorer.py +5 -6
  4. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/fusion_pipeline.py +5 -7
  5. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/kcenter_select.py +5 -7
  6. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/margin_scorer.py +5 -6
  7. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/sliding_window.py +5 -6
  8. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/strategy_display.py +16 -16
  9. datapruning-2.1.1/datapruning/multimodal/__init__.py +1 -0
  10. datapruning-2.1.1/datapruning/multimodal/clip_encoder.py +179 -0
  11. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/difficulty.py +6 -0
  12. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/pipeline.py +2 -0
  13. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/selection.py +26 -21
  14. datapruning-2.1.1/datapruning/sdk.py +289 -0
  15. {datapruning-2.0.0 → datapruning-2.1.1/datapruning.egg-info}/PKG-INFO +8 -3
  16. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning.egg-info/SOURCES.txt +2 -0
  17. datapruning-2.1.1/datapruning.egg-info/requires.txt +11 -0
  18. {datapruning-2.0.0 → datapruning-2.1.1}/pyproject.toml +11 -2
  19. datapruning-2.0.0/datapruning/sdk.py +0 -80
  20. datapruning-2.0.0/datapruning.egg-info/requires.txt +0 -4
  21. {datapruning-2.0.0 → datapruning-2.1.1}/LICENSE +0 -0
  22. {datapruning-2.0.0 → datapruning-2.1.1}/MANIFEST.in +0 -0
  23. {datapruning-2.0.0 → datapruning-2.1.1}/README.md +0 -0
  24. {datapruning-2.0.0 → datapruning-2.1.1}/build_src/_core.c +0 -0
  25. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/__init__.py +0 -0
  26. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/__init__.py +0 -0
  27. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/algorithms/registry.py +0 -0
  28. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/__init__.py +0 -0
  29. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/benchmark.py +0 -0
  30. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/explainability.py +0 -0
  31. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/intelligence.py +0 -0
  32. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/optimizer.py +0 -0
  33. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/scanner.py +0 -0
  34. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/engine/strategy_selector.py +0 -0
  35. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/pipeline.py +0 -0
  36. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/reports/__init__.py +0 -0
  37. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/reports/exporter.py +0 -0
  38. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/__init__.py +0 -0
  39. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/benchmark/__init__.py +0 -0
  40. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/benchmark/compare.py +0 -0
  41. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/benchmark/evaluation.py +0 -0
  42. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/benchmark/synthetic.py +0 -0
  43. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/embeddings/__init__.py +0 -0
  44. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/embeddings/torch_utils.py +0 -0
  45. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/fusion.py +0 -0
  46. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/noise.py +0 -0
  47. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/normalize.py +0 -0
  48. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/redundancy.py +0 -0
  49. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/__init__.py +0 -0
  50. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/aum.py +0 -0
  51. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/el2n.py +0 -0
  52. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/forgetting.py +0 -0
  53. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/grand.py +0 -0
  54. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/scores/margin.py +0 -0
  55. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/selectors/__init__.py +0 -0
  56. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/selectors/facility_location.py +0 -0
  57. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/selectors/kcenter.py +0 -0
  58. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/selectors/out_of_fold.py +0 -0
  59. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning/research_pruning/strategy.py +0 -0
  60. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning.egg-info/dependency_links.txt +0 -0
  61. {datapruning-2.0.0 → datapruning-2.1.1}/datapruning.egg-info/top_level.txt +0 -0
  62. {datapruning-2.0.0 → datapruning-2.1.1}/setup.cfg +0 -0
  63. {datapruning-2.0.0 → datapruning-2.1.1}/setup.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.0.0
4
- Summary: Intelligent data pruning for ML datasets
3
+ Version: 2.1.1
4
+ Summary: Intelligent dataset optimization for cleaner, smaller training data
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
7
7
  Classifier: Development Status :: 4 - Beta
@@ -20,7 +20,12 @@ License-File: LICENSE
20
20
  Requires-Dist: numpy>=1.24.0
21
21
  Requires-Dist: pandas>=2.0.0
22
22
  Requires-Dist: scikit-learn>=1.3.0
23
- Requires-Dist: torch>=2.0.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"
24
29
  Dynamic: license-file
25
30
 
26
31
  # DataPruning
@@ -47,7 +47,15 @@ class BaseOptimizer(ABC):
47
47
 
48
48
  def optimize(self, df: pd.DataFrame, target_col: str, keep_ratio: float = 0.5) -> OptimizationResult:
49
49
  import time
50
+ import numpy as np
50
51
  X, y = df.drop(columns=[target_col]), df[target_col]
52
+ for col in X.columns:
53
+ if X[col].dtype == object or X[col].dtype.name == "category":
54
+ from sklearn.preprocessing import LabelEncoder
55
+ X[col] = LabelEncoder().fit_transform(X[col].astype(str))
56
+ elif not np.issubdtype(X[col].dtype, np.number):
57
+ X[col] = pd.to_numeric(X[col], errors="coerce").fillna(0)
58
+ X = X.fillna(0)
51
59
  minority_before = int(y.value_counts().min()) if y.nunique() > 1 else None
52
60
  t0 = time.time()
53
61
  self.fit(X, y)
@@ -8,7 +8,7 @@ from datapruning.algorithms.registry import register
8
8
  @register
9
9
  class EL2NScorerOptimizer(BaseOptimizer):
10
10
  key = "el2n"
11
- display_name = "Difficulty Scoring"
11
+ display_name = "Importance Scoring"
12
12
 
13
13
  def __init__(self):
14
14
  self._scores = None
@@ -43,14 +43,13 @@ class EL2NScorerOptimizer(BaseOptimizer):
43
43
  return minority_idx.append(kept_majority_idx)
44
44
 
45
45
  def why_selected(self) -> str:
46
- return ("Difficulty Scoring uses feature-space distance analysis to identify "
47
- "samples that are far from their class centroid. These informative "
48
- "boundary samples help the model learn better decision regions. "
49
- "Works as a reliable fallback for any dataset shape.")
46
+ return ("Measures how difficult each sample is for the model to classify by analyzing "
47
+ "feature-space distances. Keeps the most informative boundary samples that "
48
+ "help the model learn better decision regions. Works reliably on any dataset.")
50
49
 
51
50
  def limitations(self) -> list[str]:
52
51
  return [
53
- "Uses feature norms as proxy less precise than real model logits.",
52
+ "Uses distance-based approximation slightly less precise than model-based methods.",
54
53
  "May select outliers on very noisy datasets.",
55
54
  ]
56
55
 
@@ -32,16 +32,14 @@ class FusionPipelineOptimizer(BaseOptimizer):
32
32
  return self._selected_indices
33
33
 
34
34
  def why_selected(self) -> str:
35
- return ("Full Pipeline runs the complete ResearchFusionEngine: "
36
- "multi-signal scoring (EL2N + Margin), normalization, "
37
- "adaptive fusion, noise detection, difficulty classification, "
38
- "redundancy removal, and diversity-aware selection. "
39
- "Maximum quality for complex datasets.")
35
+ return ("Runs the most comprehensive analysis: combines multiple quality signals, "
36
+ "detects noise, classifies sample difficulty, removes redundancy, and "
37
+ "ensures diversity. Maximum quality for complex or messy datasets.")
40
38
 
41
39
  def limitations(self) -> list[str]:
42
40
  return [
43
- "Slower than individual methods runs multiple scoring steps.",
44
- "May be overkill for simple, clean datasets.",
41
+ "Takes longer than simpler methods due to multiple analysis steps.",
42
+ "May be overkill for small, clean datasets.",
45
43
  ]
46
44
 
47
45
  @staticmethod
@@ -30,16 +30,14 @@ class KCenterSelectOptimizer(BaseOptimizer):
30
30
  return self._selected_indices
31
31
 
32
32
  def why_selected(self) -> str:
33
- return ("Diversity Selection uses K-Center Greedy to pick the most "
34
- "representative and diverse subset. It ensures broad coverage "
35
- "of the feature space, avoiding redundancy. Ideal for "
36
- "high-dimensional data or when you need maximum data variety.")
33
+ return ("Selects the most diverse and representative subset of your data by ensuring "
34
+ "broad coverage of the feature space. Eliminates redundant similar samples "
35
+ "while preserving maximum variety.")
37
36
 
38
37
  def limitations(self) -> list[str]:
39
38
  return [
40
- "Does not consider class labels directly — relies on feature diversity.",
41
- "May underperform on highly imbalanced datasets without minority preservation.",
42
- "Slower than scoring-based methods on very large datasets.",
39
+ "Focuses on diversity rather than class labels directly.",
40
+ "May be less effective on highly imbalanced datasets.",
43
41
  ]
44
42
 
45
43
  @staticmethod
@@ -49,15 +49,14 @@ class MarginScorerOptimizer(BaseOptimizer):
49
49
  return minority_idx.append(kept_majority_idx)
50
50
 
51
51
  def why_selected(self) -> str:
52
- return ("Margin Optimization uses logistic regression to measure how confidently "
53
- "each sample is classified. It keeps the most informative majority-class "
54
- "samples (high margin = clear signal) and preserves 100% of the minority class. "
55
- "Best for general tabular data with clear class boundaries.")
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.")
56
55
 
57
56
  def limitations(self) -> list[str]:
58
57
  return [
59
- "Requires fitting a quick logistic regression model.",
60
- "May underperform on very high-dimensional data without feature selection.",
58
+ "Requires a quick model fitting step during analysis.",
59
+ "May be less effective on extremely high-dimensional datasets.",
61
60
  ]
62
61
 
63
62
  @staticmethod
@@ -60,15 +60,14 @@ class SlidingWindowOptimizer(BaseOptimizer):
60
60
  return all_idx
61
61
 
62
62
  def why_selected(self) -> str:
63
- return ("Noise Filtering drops both the easiest (redundant) and hardest "
64
- "(potentially mislabeled) samples, keeping the informative middle. "
65
- "This is the sliding window method from the EL2N/GraNd paper. "
66
- "Best for datasets with suspected label noise.")
63
+ return ("Filters out both the easiest (redundant) and hardest (potentially mislabeled) "
64
+ "samples, keeping only the informative middle ground. Best for cleaning "
65
+ "datasets that may contain labeling errors or noisy data.")
67
66
 
68
67
  def limitations(self) -> list[str]:
69
68
  return [
70
- "Requires fitting a logistic regression for scoring.",
71
- "May discard genuinely hard but correct samples on complex datasets.",
69
+ "Requires a quick model fitting step during analysis.",
70
+ "May occasionally remove genuinely hard but correct samples.",
72
71
  ]
73
72
 
74
73
  @staticmethod
@@ -2,23 +2,23 @@ STRATEGY_DISPLAY = {
2
2
  "margin": {
3
3
  "display_name": "Margin Optimization",
4
4
  "icon": "\U0001F3AF",
5
- "description": "Uses logistic regression to measure classification confidence. Keeps the most informative samples near decision boundaries while preserving all minority-class examples.",
5
+ "description": "Analyzes classification confidence to keep the most informative samples near decision boundaries while preserving all minority-class examples.",
6
6
  "best_for": "General tabular data, binary and multi-class classification",
7
7
  "advantages": [
8
8
  "Best overall accuracy on benchmarks (+12% avg improvement)",
9
- "Fast execution single logistic regression fit",
9
+ "Fast execution -- single analysis step",
10
10
  "Preserves 100% of minority class",
11
11
  ],
12
12
  "tradeoffs": [
13
- "Requires fitting a quick model",
14
- "May underperform on extremely high-dimensional data",
13
+ "Requires a quick model fitting step",
14
+ "May be less effective on extremely high-dimensional data",
15
15
  ],
16
16
  "recommended_models": ["XGBoost", "LightGBM", "Random Forest", "CatBoost"],
17
17
  },
18
18
  "el2n": {
19
- "display_name": "Difficulty Scoring",
19
+ "display_name": "Importance Scoring",
20
20
  "icon": "\U0001F4CA",
21
- "description": "Analyzes feature-space distances to identify informative boundary samples. Works as a reliable fallback for any dataset shape.",
21
+ "description": "Measures how difficult each sample is to classify by analyzing feature distances. Keeps the most informative boundary samples.",
22
22
  "best_for": "Fallback method, any dataset type, quick analysis",
23
23
  "advantages": [
24
24
  "Works on any dataset without model training",
@@ -29,12 +29,12 @@ STRATEGY_DISPLAY = {
29
29
  "Less precise than model-based scoring",
30
30
  "May select outliers on very noisy datasets",
31
31
  ],
32
- "recommended_models": ["Any model general purpose"],
32
+ "recommended_models": ["Any model -- general purpose"],
33
33
  },
34
34
  "kcenter": {
35
35
  "display_name": "Diversity Selection",
36
36
  "icon": "\U0001F308",
37
- "description": "Picks the most representative and diverse subset using K-Center Greedy. Ensures broad coverage of the feature space, avoiding redundancy.",
37
+ "description": "Picks the most representative and diverse subset, ensuring broad coverage of the feature space while avoiding redundancy.",
38
38
  "best_for": "High-dimensional data, datasets with redundancy, when variety matters",
39
39
  "advantages": [
40
40
  "Maximum feature space coverage",
@@ -50,34 +50,34 @@ STRATEGY_DISPLAY = {
50
50
  "sliding_window": {
51
51
  "display_name": "Noise Filtering",
52
52
  "icon": "\U0001F9FB",
53
- "description": "Drops both the easiest (redundant) and hardest (potentially mislabeled) samples, keeping the informative middle. Based on the EL2N/GraNd research paper.",
53
+ "description": "Drops both the easiest (redundant) and hardest (potentially mislabeled) samples, keeping the informative middle ground.",
54
54
  "best_for": "Datasets with suspected label noise, corrupted data",
55
55
  "advantages": [
56
56
  "Removes both redundant and noisy samples",
57
- "Research-backed method from top ML papers",
57
+ "Proven method for cleaning real-world data",
58
58
  "Good for cleaning real-world messy data",
59
59
  ],
60
60
  "tradeoffs": [
61
- "Requires fitting a logistic regression",
62
- "May discard genuinely hard but correct samples",
61
+ "Requires a quick model fitting step",
62
+ "May occasionally discard hard but correct samples",
63
63
  ],
64
64
  "recommended_models": ["XGBoost", "LightGBM", "Any robust model"],
65
65
  },
66
66
  "fusion": {
67
67
  "display_name": "Full Pipeline",
68
68
  "icon": "\u2728",
69
- "description": "Runs the complete ResearchFusionEngine: multi-signal scoring, normalization, adaptive fusion, noise detection, difficulty classification, redundancy removal, and diversity-aware selection. Maximum quality.",
69
+ "description": "The most comprehensive analysis: combines multiple quality signals, detects noise, classifies importance, removes redundancy, and ensures diversity.",
70
70
  "best_for": "Complex datasets, when quality matters more than speed",
71
71
  "advantages": [
72
- "Most comprehensive pruning pipeline",
73
- "Combines multiple research-backed signals",
72
+ "Most comprehensive optimization pipeline",
73
+ "Combines multiple quality signals",
74
74
  "Handles noise, redundancy, and diversity",
75
75
  ],
76
76
  "tradeoffs": [
77
77
  "Slower than individual methods",
78
78
  "May be overkill for simple clean datasets",
79
79
  ],
80
- "recommended_models": ["Any maximizes data quality"],
80
+ "recommended_models": ["Any -- maximizes data quality"],
81
81
  },
82
82
  }
83
83
 
@@ -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
@@ -32,6 +32,12 @@ def classify_difficulty(
32
32
  thresholds : dict — the score values at each boundary.
33
33
  """
34
34
  s = np.asarray(scores, dtype=np.float64)
35
+ score_range = s.max() - s.min()
36
+
37
+ if score_range < 1e-10:
38
+ labels = np.full(len(s), "medium", dtype="U6")
39
+ return labels, {"easy_below": float(s.min()), "hard_above": float(s.max())}
40
+
35
41
  easy_val = np.percentile(s, easy_threshold)
36
42
  hard_val = np.percentile(s, hard_threshold)
37
43
 
@@ -209,12 +209,14 @@ class ResearchFusionEngine:
209
209
  adjusted_scores[noise_mask] -= np.abs(adjusted_scores[noise_mask].max()) + 1
210
210
 
211
211
  if selection_method == "sliding_window":
212
+ budget = max(1, int(n * keep_ratio))
212
213
  selected = sliding_window_select(
213
214
  adjusted_scores,
214
215
  low_percentile=10.0,
215
216
  high_percentile=90.0,
216
217
  labels=y,
217
218
  preserve_minority=minority_preserve,
219
+ budget=budget,
218
220
  )
219
221
  else:
220
222
  budget = max(1, int(n * keep_ratio))
@@ -88,40 +88,45 @@ def sliding_window_select(
88
88
  high_percentile: float = 90.0,
89
89
  labels: np.ndarray | None = None,
90
90
  preserve_minority: bool = True,
91
+ budget: int | None = None,
91
92
  ) -> np.ndarray:
92
- """Sliding window selection (from Paul et al., 2021).
93
+ """Sliding window selection keep samples within a score range.
93
94
 
94
- Keep samples within a score range, excluding both easiest and hardest.
95
- This is the method described in the EL2N/GraNd paper for balancing
96
- difficulty and coverage.
95
+ Excludes both the easiest (redundant) and hardest (potentially noisy)
96
+ samples, keeping the most informative middle band.
97
97
 
98
98
  Parameters
99
99
  ----------
100
- scores : (N,) array — difficulty scores.
101
- low_percentile : float — lower bound (drop easier samples).
100
+ scores : (N,) array — importance scores.
101
+ low_percentile : float — lower bound (drop easiest samples).
102
102
  high_percentile : float — upper bound (drop hardest/noisiest samples).
103
+ labels : (N,) array — class labels (for balance enforcement).
104
+ preserve_minority : bool — ensure minority classes aren't wiped out.
105
+ budget : int, optional — if set, cap output to this many samples.
103
106
  """
104
107
  s = np.asarray(scores, dtype=np.float64)
108
+ n = len(s)
105
109
  low_val = np.percentile(s, low_percentile)
106
110
  high_val = np.percentile(s, high_percentile)
107
111
 
108
112
  in_window = (s >= low_val) & (s <= high_val)
109
113
  window_indices = np.where(in_window)[0]
110
114
 
111
- if labels is None or not preserve_minority:
112
- return window_indices
113
-
114
- # ensure minority classes are represented in the window
115
- labels = np.asarray(labels, dtype=np.int64)
116
- classes = np.unique(labels)
117
- for cls in classes:
118
- cls_in_window = window_indices[labels[window_indices] == cls]
119
- if len(cls_in_window) == 0:
120
- # no samples of this class in window → add top-scored ones
121
- cls_mask = labels == cls
122
- cls_scores = s.copy()
123
- cls_scores[~cls_mask] = -np.inf
124
- top_cls = np.argsort(cls_scores)[-1:]
125
- window_indices = np.concatenate([window_indices, top_cls])
115
+ if labels is not None and preserve_minority:
116
+ labels = np.asarray(labels, dtype=np.int64)
117
+ classes = np.unique(labels)
118
+ for cls in classes:
119
+ cls_in_window = window_indices[labels[window_indices] == cls]
120
+ if len(cls_in_window) == 0:
121
+ cls_mask = labels == cls
122
+ cls_scores = s.copy()
123
+ cls_scores[~cls_mask] = -np.inf
124
+ top_cls = np.argsort(cls_scores)[-1:]
125
+ window_indices = np.concatenate([window_indices, top_cls])
126
+
127
+ if budget is not None and len(window_indices) > budget:
128
+ cls_in = window_indices
129
+ cls_sub = cls_in[np.argsort(s[cls_in])[-budget:][::-1]]
130
+ window_indices = cls_sub
126
131
 
127
132
  return window_indices
@@ -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 optimization 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,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 2.0.0
4
- Summary: Intelligent data pruning for ML datasets
3
+ Version: 2.1.1
4
+ Summary: Intelligent dataset optimization for cleaner, smaller training data
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
7
7
  Classifier: Development Status :: 4 - Beta
@@ -20,7 +20,12 @@ License-File: LICENSE
20
20
  Requires-Dist: numpy>=1.24.0
21
21
  Requires-Dist: pandas>=2.0.0
22
22
  Requires-Dist: scikit-learn>=1.3.0
23
- Requires-Dist: torch>=2.0.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"
24
29
  Dynamic: license-file
25
30
 
26
31
  # 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
@@ -0,0 +1,11 @@
1
+ numpy>=1.24.0
2
+ pandas>=2.0.0
3
+ scikit-learn>=1.3.0
4
+
5
+ [clip]
6
+ torch>=2.0.0
7
+ transformers>=4.30.0
8
+ pillow>=9.0.0
9
+
10
+ [torch]
11
+ torch>=2.0.0
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datapruning"
7
- version = "2.0.0"
8
- description = "Intelligent data pruning for ML datasets"
7
+ version = "2.1.1"
8
+ description = "Intelligent dataset optimization for cleaner, smaller training data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = {text = "Proprietary"}
@@ -25,7 +25,16 @@ dependencies = [
25
25
  "numpy>=1.24.0",
26
26
  "pandas>=2.0.0",
27
27
  "scikit-learn>=1.3.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ torch = [
32
+ "torch>=2.0.0",
33
+ ]
34
+ clip = [
28
35
  "torch>=2.0.0",
36
+ "transformers>=4.30.0",
37
+ "pillow>=9.0.0",
29
38
  ]
30
39
 
31
40
  [project.urls]
@@ -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)
@@ -1,4 +0,0 @@
1
- numpy>=1.24.0
2
- pandas>=2.0.0
3
- scikit-learn>=1.3.0
4
- torch>=2.0.0
File without changes
File without changes
File without changes
File without changes
File without changes