datapruning 1.0.6__tar.gz → 1.0.7__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 (34) hide show
  1. {datapruning-1.0.6/datapruning.egg-info → datapruning-1.0.7}/PKG-INFO +4 -1
  2. {datapruning-1.0.6 → datapruning-1.0.7}/datapruning/__init__.py +21 -1
  3. datapruning-1.0.7/datapruning/algorithms/__init__.py +7 -0
  4. datapruning-1.0.7/datapruning/algorithms/base.py +98 -0
  5. datapruning-1.0.7/datapruning/algorithms/cluster_centroids.py +71 -0
  6. datapruning-1.0.7/datapruning/algorithms/density_pruner.py +61 -0
  7. datapruning-1.0.7/datapruning/algorithms/duplicate_aware.py +50 -0
  8. datapruning-1.0.7/datapruning/algorithms/instance_hardness.py +74 -0
  9. datapruning-1.0.7/datapruning/algorithms/random_undersampler.py +57 -0
  10. datapruning-1.0.7/datapruning/algorithms/registry.py +45 -0
  11. datapruning-1.0.7/datapruning/engine/__init__.py +0 -0
  12. datapruning-1.0.7/datapruning/engine/benchmark.py +70 -0
  13. datapruning-1.0.7/datapruning/engine/explainability.py +26 -0
  14. datapruning-1.0.7/datapruning/engine/intelligence.py +119 -0
  15. datapruning-1.0.7/datapruning/engine/optimizer.py +8 -0
  16. datapruning-1.0.7/datapruning/engine/scanner.py +48 -0
  17. datapruning-1.0.7/datapruning/engine/strategy_selector.py +35 -0
  18. {datapruning-1.0.6 → datapruning-1.0.7}/datapruning/pipeline.py +0 -1
  19. datapruning-1.0.7/datapruning/reports/__init__.py +0 -0
  20. datapruning-1.0.7/datapruning/reports/exporter.py +24 -0
  21. datapruning-1.0.7/datapruning/sdk.py +80 -0
  22. {datapruning-1.0.6 → datapruning-1.0.7/datapruning.egg-info}/PKG-INFO +4 -1
  23. datapruning-1.0.7/datapruning.egg-info/SOURCES.txt +31 -0
  24. {datapruning-1.0.6 → datapruning-1.0.7}/datapruning.egg-info/requires.txt +1 -0
  25. {datapruning-1.0.6 → datapruning-1.0.7}/pyproject.toml +2 -4
  26. datapruning-1.0.6/datapruning.egg-info/SOURCES.txt +0 -13
  27. {datapruning-1.0.6 → datapruning-1.0.7}/LICENSE +0 -0
  28. {datapruning-1.0.6 → datapruning-1.0.7}/MANIFEST.in +0 -0
  29. {datapruning-1.0.6 → datapruning-1.0.7}/README.md +0 -0
  30. {datapruning-1.0.6 → datapruning-1.0.7}/build_src/_core.c +0 -0
  31. {datapruning-1.0.6 → datapruning-1.0.7}/datapruning.egg-info/dependency_links.txt +0 -0
  32. {datapruning-1.0.6 → datapruning-1.0.7}/datapruning.egg-info/top_level.txt +0 -0
  33. {datapruning-1.0.6 → datapruning-1.0.7}/setup.cfg +0 -0
  34. {datapruning-1.0.6 → datapruning-1.0.7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: Intelligent data pruning for ML datasets
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -16,9 +16,12 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
+ License-File: LICENSE
19
20
  Requires-Dist: numpy>=1.24.0
20
21
  Requires-Dist: pandas>=2.0.0
22
+ Requires-Dist: scikit-learn>=1.3.0
21
23
  Requires-Dist: torch>=2.0.0
24
+ Dynamic: license-file
22
25
 
23
26
  # DataPruning
24
27
 
@@ -3,15 +3,29 @@ import numpy as np
3
3
  import pandas as pd
4
4
 
5
5
  from . import pipeline
6
- from ._core import analyze_data as _analyze_data
6
+ from .sdk import DatasetEngine
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
  MIN_ROWS = 1000
11
11
  MAX_ROWS = 300_000
12
12
 
13
+ try:
14
+ from ._core import analyze_data as _analyze_data
15
+ HAS_CORE = True
16
+ except ImportError:
17
+ _analyze_data = None
18
+ HAS_CORE = False
19
+ logger.warning("Compiled _core module not available — legacy prune_dataframe/Pruner disabled")
20
+
13
21
 
14
22
  def prune_dataframe(df, target_col=None, keep_ratio=0.5, num_steps=400, verbose=False):
23
+ if not HAS_CORE:
24
+ raise RuntimeError(
25
+ "prune_dataframe requires the compiled _core module, "
26
+ "which is not available on this platform. "
27
+ "Use DatasetEngine instead (pure Python, all platforms)."
28
+ )
15
29
  if verbose:
16
30
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
17
31
 
@@ -53,6 +67,12 @@ class Pruner:
53
67
  def __init__(self, df):
54
68
  if not isinstance(df, pd.DataFrame):
55
69
  raise TypeError("Expected a pandas DataFrame")
70
+ if not HAS_CORE:
71
+ raise RuntimeError(
72
+ "Pruner requires the compiled _core module, "
73
+ "which is not available on this platform. "
74
+ "Use DatasetEngine instead (pure Python, all platforms)."
75
+ )
56
76
  self._df = df
57
77
 
58
78
  def prune(self, target_col=None, keep_ratio=0.5, num_steps=400, verbose=False):
@@ -0,0 +1,7 @@
1
+ from datapruning.algorithms import random_undersampler
2
+ from datapruning.algorithms import cluster_centroids
3
+ from datapruning.algorithms import instance_hardness
4
+ from datapruning.algorithms import duplicate_aware
5
+ from datapruning.algorithms import density_pruner
6
+
7
+ from datapruning.algorithms.registry import registry
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+ import pandas as pd
6
+
7
+
8
+ @dataclass
9
+ class OptimizationResult:
10
+ optimized_df: pd.DataFrame
11
+ rows_before: int
12
+ rows_after: int
13
+ cols_before: int
14
+ cols_after: int
15
+ minority_count_before: Optional[int]
16
+ minority_count_after: Optional[int]
17
+ runtime_seconds: float
18
+
19
+
20
+ @dataclass
21
+ class Explanation:
22
+ algorithm: str
23
+ why_selected: str
24
+ rows_removed: int
25
+ reduction_pct: float
26
+ minority_preserved_pct: Optional[float]
27
+ estimated_training_gain_pct: float
28
+ memory_saved_pct: float
29
+ limitations: list[str] = field(default_factory=list)
30
+
31
+
32
+ class BaseOptimizer(ABC):
33
+ key: str = ""
34
+ display_name: str = ""
35
+
36
+ @abstractmethod
37
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "BaseOptimizer":
38
+ raise NotImplementedError
39
+
40
+ @abstractmethod
41
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
42
+ raise NotImplementedError
43
+
44
+ def transform(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float):
45
+ keep_idx = self._select_rows(X, y, keep_ratio)
46
+ return X.loc[keep_idx], y.loc[keep_idx]
47
+
48
+ def optimize(self, df: pd.DataFrame, target_col: str, keep_ratio: float = 0.5) -> OptimizationResult:
49
+ import time
50
+ X, y = df.drop(columns=[target_col]), df[target_col]
51
+ minority_before = int(y.value_counts().min()) if y.nunique() > 1 else None
52
+ t0 = time.time()
53
+ self.fit(X, y)
54
+ X_res, y_res = self.transform(X, y, keep_ratio)
55
+ runtime = time.time() - t0
56
+ minority_after = int(y_res.value_counts().min()) if y_res.nunique() > 1 else None
57
+ optimized_df = X_res.copy()
58
+ optimized_df[target_col] = y_res.values
59
+ return OptimizationResult(
60
+ optimized_df=optimized_df,
61
+ rows_before=len(df),
62
+ rows_after=len(optimized_df),
63
+ cols_before=df.shape[1],
64
+ cols_after=optimized_df.shape[1],
65
+ minority_count_before=minority_before,
66
+ minority_count_after=minority_after,
67
+ runtime_seconds=runtime,
68
+ )
69
+
70
+ def explain(self, result: OptimizationResult) -> Explanation:
71
+ rows_removed = result.rows_before - result.rows_after
72
+ reduction_pct = round(rows_removed / result.rows_before * 100, 1) if result.rows_before else 0.0
73
+ minority_preserved_pct = None
74
+ if result.minority_count_before:
75
+ minority_preserved_pct = round(
76
+ (result.minority_count_after or 0) / result.minority_count_before * 100, 1
77
+ )
78
+ return Explanation(
79
+ algorithm=self.display_name,
80
+ why_selected=self.why_selected(),
81
+ rows_removed=rows_removed,
82
+ reduction_pct=reduction_pct,
83
+ minority_preserved_pct=minority_preserved_pct,
84
+ estimated_training_gain_pct=reduction_pct,
85
+ memory_saved_pct=reduction_pct,
86
+ limitations=self.limitations(),
87
+ )
88
+
89
+ @staticmethod
90
+ @abstractmethod
91
+ def fit_score(analysis: dict) -> float:
92
+ raise NotImplementedError
93
+
94
+ def why_selected(self) -> str:
95
+ return f"{self.display_name} was selected as the best fit for this dataset."
96
+
97
+ def limitations(self) -> list[str]:
98
+ return []
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.cluster import KMeans
5
+ from datapruning.algorithms.base import BaseOptimizer
6
+ from datapruning.algorithms.registry import register
7
+
8
+
9
+ @register
10
+ class ClusterCentroidsOptimizer(BaseOptimizer):
11
+ key = "cluster"
12
+ display_name = "Cluster Centroids"
13
+
14
+ def __init__(self, random_state: int = 42):
15
+ self.random_state = random_state
16
+ self._centroid_rows = None
17
+
18
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "ClusterCentroidsOptimizer":
19
+ return self
20
+
21
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
22
+ minority_class = y.value_counts().idxmin()
23
+ minority_idx = y[y == minority_class].index
24
+ majority_idx = y[y != minority_class].index
25
+ n_total_target = int(len(y) * keep_ratio)
26
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
27
+ n_majority_target = min(n_majority_target, len(majority_idx))
28
+ if n_majority_target == 0:
29
+ return minority_idx
30
+ numeric_X = X.loc[majority_idx].select_dtypes(include=[np.number]).fillna(0)
31
+ k = max(min(n_majority_target, len(majority_idx), 3000), 1)
32
+ km = KMeans(n_clusters=k, random_state=self.random_state, n_init=3)
33
+ km.fit(numeric_X)
34
+ labels = km.labels_
35
+ per_cluster_quota = max(n_majority_target // k, 1)
36
+ kept_rows = []
37
+ numeric_vals = numeric_X.values
38
+ for cluster_id in range(k):
39
+ cluster_positions = np.where(labels == cluster_id)[0]
40
+ if len(cluster_positions) == 0:
41
+ continue
42
+ centroid = km.cluster_centers_[cluster_id]
43
+ dists = np.linalg.norm(numeric_vals[cluster_positions] - centroid, axis=1)
44
+ order = np.argsort(dists)
45
+ chosen_positions = cluster_positions[order[:per_cluster_quota]]
46
+ kept_rows.extend(numeric_X.index[chosen_positions].tolist())
47
+ keep_idx = pd.Index(kept_rows).append(minority_idx)
48
+ return keep_idx
49
+
50
+ def why_selected(self) -> str:
51
+ return ("Large balanced-to-moderately-imbalanced dataset where structural "
52
+ "(density-based) reduction outperforms blind random sampling.")
53
+
54
+ def limitations(self) -> list[str]:
55
+ return ["O(n*k) clustering cost — slow on very large datasets (100K+ rows).",
56
+ "Less effective than Instance Hardness Threshold on severe imbalance."]
57
+
58
+ @staticmethod
59
+ def fit_score(analysis: dict) -> float:
60
+ scan = analysis.get("scan", analysis)
61
+ task_type = scan.get("task_type", "unknown")
62
+ imbalance_ratio = scan.get("imbalance_ratio", 1.0)
63
+ n_rows = scan.get("rows", 0)
64
+
65
+ if n_rows < 1000 or task_type == "regression":
66
+ return 0.0
67
+ if n_rows > 200_000:
68
+ return 0.1
69
+ if 3 <= imbalance_ratio < 10:
70
+ return 0.6
71
+ return 0.3
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.algorithms.base import BaseOptimizer
5
+ from datapruning.algorithms.registry import register
6
+
7
+
8
+ @register
9
+ class DensityPrunerOptimizer(BaseOptimizer):
10
+ key = "density"
11
+ display_name = "Density-Based Pruner (Legacy)"
12
+
13
+ def __init__(self, random_state: int = 42):
14
+ self.random_state = random_state
15
+
16
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "DensityPrunerOptimizer":
17
+ return self
18
+
19
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
20
+ try:
21
+ from datapruning._core import analyze_data as core_prune
22
+ n = len(y)
23
+ n_keep = int(n * keep_ratio)
24
+ X_arr = np.ascontiguousarray(X.values.astype(np.float32))
25
+ result = core_prune(X_arr, y.values.copy(), 200, keep_ratio)
26
+ if result is not None and len(result) > 0:
27
+ kept = result[:n_keep]
28
+ return pd.Index(kept)
29
+ except Exception:
30
+ pass
31
+ rng = np.random.RandomState(self.random_state)
32
+ minority_class = y.value_counts().idxmin()
33
+ minority_idx = y[y == minority_class].index
34
+ majority_idx = y[y != minority_class].index
35
+ n_total_target = int(len(y) * keep_ratio)
36
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
37
+ chosen_majority = rng.choice(majority_idx.to_numpy(),
38
+ size=min(n_majority_target, len(majority_idx)),
39
+ replace=False)
40
+ return minority_idx.append(pd.Index(chosen_majority))
41
+
42
+ def why_selected(self) -> str:
43
+ return "Dataset has structural density patterns that the legacy pruner can exploit."
44
+
45
+ def limitations(self) -> list[str]:
46
+ return ["Legacy algorithm — may not perform as well as newer methods.",
47
+ "Falls back to random undersampling if the compiled core is unavailable."]
48
+
49
+ @staticmethod
50
+ def fit_score(analysis: dict) -> float:
51
+ scan = analysis.get("scan", analysis)
52
+ n_rows = scan.get("rows", 0)
53
+ if n_rows < 1000:
54
+ return 0.0
55
+ task_type = scan.get("task_type", "unknown")
56
+ if task_type == "regression":
57
+ return 0.0
58
+ imbalance_ratio = scan.get("imbalance_ratio", 1.0)
59
+ if 1.5 <= imbalance_ratio < 5:
60
+ return 0.65
61
+ return 0.35
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.algorithms.base import BaseOptimizer
5
+ from datapruning.algorithms.registry import register
6
+
7
+
8
+ @register
9
+ class DuplicateAwareOptimizer(BaseOptimizer):
10
+ key = "duplicate_aware"
11
+ display_name = "Duplicate-Aware Reduction"
12
+
13
+ def __init__(self, random_state: int = 42):
14
+ self.random_state = random_state
15
+
16
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "DuplicateAwareOptimizer":
17
+ return self
18
+
19
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
20
+ rng = np.random.RandomState(self.random_state)
21
+ dedup_mask = ~X.duplicated()
22
+ deduped_idx = X.index[dedup_mask]
23
+ n_target = int(len(y) * keep_ratio)
24
+ if len(deduped_idx) <= n_target:
25
+ return deduped_idx
26
+ minority_class = y.value_counts().idxmin()
27
+ minority_idx = deduped_idx.intersection(y[y == minority_class].index)
28
+ majority_idx = deduped_idx.difference(minority_idx)
29
+ n_majority_target = max(n_target - len(minority_idx), 0)
30
+ chosen_majority = rng.choice(majority_idx.to_numpy(),
31
+ size=min(n_majority_target, len(majority_idx)),
32
+ replace=False)
33
+ return minority_idx.append(pd.Index(chosen_majority))
34
+
35
+ def why_selected(self) -> str:
36
+ return "Dataset has a high duplicate-row percentage; deduplication is a safe first reduction."
37
+
38
+ def limitations(self) -> list[str]:
39
+ return ["Only effective when duplicate percentage is meaningfully high."]
40
+
41
+ @staticmethod
42
+ def fit_score(analysis: dict) -> float:
43
+ scan = analysis.get("scan", analysis)
44
+ dup_pct = scan.get("duplicate_pct", 0)
45
+ n_rows = scan.get("rows", 0)
46
+ if n_rows < 1000:
47
+ return 0.0
48
+ if dup_pct >= 5:
49
+ return 0.9
50
+ return 0.05
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.model_selection import StratifiedKFold
5
+ from sklearn.base import clone
6
+ from sklearn.linear_model import LogisticRegression
7
+ from datapruning.algorithms.base import BaseOptimizer
8
+ from datapruning.algorithms.registry import register
9
+
10
+
11
+ @register
12
+ class InstanceHardnessOptimizer(BaseOptimizer):
13
+ key = "iht"
14
+ display_name = "Instance Hardness Threshold"
15
+
16
+ def __init__(self, estimator=None, n_folds: int = 5, random_state: int = 42):
17
+ self.estimator = estimator if estimator is not None else LogisticRegression(max_iter=1000)
18
+ self.n_folds = n_folds
19
+ self.random_state = random_state
20
+ self._hardness = None
21
+
22
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "InstanceHardnessOptimizer":
23
+ skf = StratifiedKFold(n_splits=self.n_folds, shuffle=True, random_state=self.random_state)
24
+ X_arr, y_arr = X.values, y.values
25
+ hardness = np.zeros(len(y_arr))
26
+ for train_idx, val_idx in skf.split(X_arr, y_arr):
27
+ model = clone(self.estimator)
28
+ model.fit(X_arr[train_idx], y_arr[train_idx])
29
+ probs = model.predict_proba(X_arr[val_idx])
30
+ class_to_col = {c: i for i, c in enumerate(model.classes_)}
31
+ true_probs = np.array(
32
+ [probs[i, class_to_col[y_arr[val_idx][i]]] for i in range(len(val_idx))]
33
+ )
34
+ hardness[val_idx] = 1.0 - true_probs
35
+ self._hardness = pd.Series(hardness, index=y.index)
36
+ return self
37
+
38
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
39
+ if self._hardness is None:
40
+ self.fit(X, y)
41
+ minority_class = y.value_counts().idxmin()
42
+ minority_idx = y[y == minority_class].index
43
+ majority_idx = y[y != minority_class].index
44
+ n_total_target = int(len(y) * keep_ratio)
45
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
46
+ majority_hardness = self._hardness.loc[majority_idx].sort_values(ascending=False)
47
+ kept_majority_idx = majority_hardness.index[:n_majority_target]
48
+ return minority_idx.append(kept_majority_idx)
49
+
50
+ def why_selected(self) -> str:
51
+ return ("Severe class imbalance detected. Instance Hardness Threshold removes "
52
+ "only the majority-class rows the model already classifies confidently, "
53
+ "preserving 100% of the minority class and all decision-boundary rows.")
54
+
55
+ def limitations(self) -> list[str]:
56
+ return ["Requires training a quick cross-validated model — slower than pure random.",
57
+ "Quality depends on the chosen base estimator; weak estimators give a noisier hardness signal."]
58
+
59
+ @staticmethod
60
+ def fit_score(analysis: dict) -> float:
61
+ scan = analysis.get("scan", analysis)
62
+ task_type = scan.get("task_type", "unknown")
63
+ imbalance_ratio = scan.get("imbalance_ratio", 1.0)
64
+ n_rows = scan.get("rows", 0)
65
+
66
+ if n_rows < 3000 or task_type == "regression":
67
+ return 0.0
68
+ if n_rows > 200_000:
69
+ return 0.3
70
+ if imbalance_ratio >= 10:
71
+ return 0.95
72
+ if imbalance_ratio >= 3:
73
+ return 0.7
74
+ return 0.4
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ import pandas as pd
4
+ from datapruning.algorithms.base import BaseOptimizer
5
+ from datapruning.algorithms.registry import register
6
+
7
+
8
+ @register
9
+ class RandomUnderSamplerOptimizer(BaseOptimizer):
10
+ key = "random"
11
+ display_name = "Random UnderSampler"
12
+
13
+ def __init__(self, random_state: int = 42):
14
+ self.random_state = random_state
15
+
16
+ def fit(self, X: pd.DataFrame, y: pd.Series) -> "RandomUnderSamplerOptimizer":
17
+ return self
18
+
19
+ def _select_rows(self, X: pd.DataFrame, y: pd.Series, keep_ratio: float) -> pd.Index:
20
+ rng = np.random.RandomState(self.random_state)
21
+ classes = y.unique()
22
+ minority_class = y.value_counts().idxmin()
23
+ minority_idx = y[y == minority_class].index
24
+ n_total_target = int(len(y) * keep_ratio)
25
+ n_majority_target = max(n_total_target - len(minority_idx), 0)
26
+ keep_idx = list(minority_idx)
27
+ for c in classes:
28
+ if c == minority_class:
29
+ continue
30
+ class_idx = y[y == c].index.to_numpy()
31
+ n_c_target = min(n_majority_target, len(class_idx))
32
+ chosen = rng.choice(class_idx, size=n_c_target, replace=False)
33
+ keep_idx.extend(chosen.tolist())
34
+ return pd.Index(keep_idx)
35
+
36
+ def why_selected(self) -> str:
37
+ return ("Large, balanced or mildly imbalanced dataset where blind random "
38
+ "reduction is acceptable and the fastest option.")
39
+
40
+ def limitations(self) -> list[str]:
41
+ return ["Removes rows with no regard for how informative they are.",
42
+ "Can discard rare-but-important patterns in the majority class."]
43
+
44
+ @staticmethod
45
+ def fit_score(analysis: dict) -> float:
46
+ scan = analysis.get("scan", analysis)
47
+ task_type = scan.get("task_type", "unknown")
48
+ imbalance_ratio = scan.get("imbalance_ratio", 1.0)
49
+ n_rows = scan.get("rows", 0)
50
+
51
+ if n_rows < 1000:
52
+ return 0.0
53
+ if task_type == "regression":
54
+ return 0.0
55
+ if imbalance_ratio < 3:
56
+ return 0.55
57
+ return 0.25
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+ from typing import Type, Dict
3
+ from datapruning.algorithms.base import BaseOptimizer
4
+
5
+
6
+ class OptimizerRegistry:
7
+ def __init__(self):
8
+ self._registry: Dict[str, Type[BaseOptimizer]] = {}
9
+
10
+ def register(self, optimizer_cls: Type[BaseOptimizer]) -> Type[BaseOptimizer]:
11
+ if not optimizer_cls.key:
12
+ raise ValueError(f"{optimizer_cls.__name__} must set a non-empty `key`.")
13
+ self._registry[optimizer_cls.key] = optimizer_cls
14
+ return optimizer_cls
15
+
16
+ def get(self, key: str) -> BaseOptimizer:
17
+ if key not in self._registry:
18
+ raise KeyError(f"No optimizer registered under key '{key}'. "
19
+ f"Available: {list(self._registry.keys())}")
20
+ return self._registry[key]()
21
+
22
+ def all(self) -> Dict[str, Type[BaseOptimizer]]:
23
+ return dict(self._registry)
24
+
25
+ def get_best(self, analysis: dict) -> tuple[BaseOptimizer, str, float, list[dict]]:
26
+ if not self._registry:
27
+ raise RuntimeError("No optimizers registered.")
28
+ scanned = []
29
+ for key, cls in self._registry.items():
30
+ score = cls.fit_score(analysis)
31
+ scanned.append((key, cls, score))
32
+ scanned.sort(key=lambda t: -t[2])
33
+ best_key, best_cls, best_score = scanned[0]
34
+ alternatives = [
35
+ {"key": k, "display_name": cls.display_name, "score": round(s, 3)}
36
+ for k, cls, s in scanned[1:]
37
+ ]
38
+ return best_cls(), best_key, best_score, alternatives
39
+
40
+
41
+ registry = OptimizerRegistry()
42
+
43
+
44
+ def register(cls: Type[BaseOptimizer]) -> Type[BaseOptimizer]:
45
+ return registry.register(cls)
File without changes
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+ import time
3
+ import pandas as pd
4
+ import numpy as np
5
+ from datapruning.algorithms.base import BaseOptimizer
6
+ from datapruning.algorithms.registry import registry
7
+
8
+
9
+ def benchmark_algorithms(df: pd.DataFrame, target_col: str,
10
+ algorithms: list[str] = None,
11
+ keep_ratio: float = 0.5) -> dict:
12
+ if algorithms is None:
13
+ algorithms = list(registry.all().keys())
14
+ results = {}
15
+ for key in algorithms:
16
+ try:
17
+ optimizer = registry.get(key)
18
+ t0 = time.time()
19
+ result = optimizer.optimize(df, target_col=target_col, keep_ratio=keep_ratio)
20
+ elapsed = time.time() - t0
21
+ results[key] = {
22
+ "display_name": optimizer.display_name,
23
+ "rows_before": result.rows_before,
24
+ "rows_after": result.rows_after,
25
+ "reduction_pct": round((1 - result.rows_after / result.rows_before) * 100, 2) if result.rows_before else 0,
26
+ "runtime_seconds": round(elapsed, 3),
27
+ "minority_count_before": result.minority_count_before,
28
+ "minority_count_after": result.minority_count_after,
29
+ }
30
+ except Exception as e:
31
+ results[key] = {"display_name": key, "error": str(e)}
32
+ return results
33
+
34
+
35
+ def compare_to_full(optimized_df: pd.DataFrame, original_df: pd.DataFrame,
36
+ target_col: str) -> dict:
37
+ from datapruning.engine.scanner import scan
38
+ full_scan = scan(original_df, target_col=target_col)
39
+ opt_scan = scan(optimized_df, target_col=target_col)
40
+ return {
41
+ "original": {
42
+ "rows": full_scan["rows"],
43
+ "memory_mb": full_scan["memory_mb"],
44
+ "missing_pct": full_scan["missing_pct"],
45
+ "duplicate_pct": full_scan["duplicate_pct"],
46
+ "estimated_training_cost": full_scan["estimated_training_cost"],
47
+ },
48
+ "optimized": {
49
+ "rows": opt_scan["rows"],
50
+ "memory_mb": opt_scan["memory_mb"],
51
+ "missing_pct": opt_scan["missing_pct"],
52
+ "duplicate_pct": opt_scan["duplicate_pct"],
53
+ "estimated_training_cost": opt_scan["estimated_training_cost"],
54
+ },
55
+ "savings": {
56
+ "row_reduction_pct": round((1 - opt_scan["rows"] / full_scan["rows"]) * 100, 2) if full_scan["rows"] else 0,
57
+ "memory_saved_mb": round(full_scan["memory_mb"] - opt_scan["memory_mb"], 2),
58
+ "cost_reduction": _cost_reduction(full_scan["estimated_training_cost"], opt_scan["estimated_training_cost"]),
59
+ },
60
+ }
61
+
62
+
63
+ def _cost_reduction(before: str, after: str) -> str:
64
+ rank = {"low": 0, "medium": 1, "high": 2, "very_high": 3}
65
+ diff = rank.get(before, 0) - rank.get(after, 0)
66
+ if diff >= 2:
67
+ return "significant"
68
+ elif diff == 1:
69
+ return "moderate"
70
+ return "minimal"
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ from datapruning.algorithms.base import BaseOptimizer, OptimizationResult, Explanation
4
+
5
+
6
+ def build_report(optimizer: BaseOptimizer, result: OptimizationResult,
7
+ df_before: pd.DataFrame) -> dict:
8
+ explanation: Explanation = optimizer.explain(result)
9
+ missing_pct_after = (result.optimized_df.isna().sum().sum() / result.optimized_df.size) * 100 \
10
+ if result.optimized_df.size else 0
11
+ dup_pct_after = (result.optimized_df.duplicated().sum() / len(result.optimized_df)) * 100 \
12
+ if len(result.optimized_df) else 0
13
+ quality = float(max(0, 100 - missing_pct_after * 2 - dup_pct_after * 2))
14
+ size_reduction = float(1 - (result.rows_after / result.rows_before)) if result.rows_before else 0.0
15
+ training_readiness = float(min(100, 70 + size_reduction * 60))
16
+ overall_health = round((quality + training_readiness) / 2, 1)
17
+ return {
18
+ "explanation": explanation.__dict__,
19
+ "dataset_score": {
20
+ "overall_health": overall_health,
21
+ "quality": round(quality, 1),
22
+ "training_readiness": round(training_readiness, 1),
23
+ "optimization_potential": round(size_reduction * 100, 1),
24
+ },
25
+ "runtime_seconds": round(result.runtime_seconds, 3),
26
+ }
@@ -0,0 +1,119 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def analyze_intelligence(df: pd.DataFrame, target_col: str = None) -> dict:
6
+ findings = {
7
+ "id_columns": _detect_id_columns(df),
8
+ "date_columns": _detect_date_columns(df),
9
+ "constant_columns": _detect_constant_columns(df),
10
+ "near_constant_columns": _detect_near_constant_columns(df),
11
+ "high_cardinality_columns": _detect_high_cardinality(df),
12
+ "highly_correlated_pairs": _detect_correlated_features(df, target_col),
13
+ "outlier_summary": _detect_outliers(df),
14
+ "complexity_score": None,
15
+ "leakage_risk_columns": [],
16
+ }
17
+ if target_col and target_col in df.columns:
18
+ findings["leakage_risk_columns"] = _detect_leakage_risk(df, target_col)
19
+ findings["complexity_score"] = _complexity_score(df, findings)
20
+ return findings
21
+
22
+
23
+ def _detect_id_columns(df, uniqueness_threshold=0.98):
24
+ out = []
25
+ for col in df.columns:
26
+ dtype = df[col].dtype
27
+ is_id_like_dtype = pd.api.types.is_integer_dtype(dtype) or dtype == object
28
+ if is_id_like_dtype and df[col].nunique() / len(df) >= uniqueness_threshold:
29
+ out.append(col)
30
+ return out
31
+
32
+
33
+ def _detect_date_columns(df):
34
+ out = []
35
+ for col in df.select_dtypes(include=["object", "datetime64[ns]"]).columns:
36
+ if pd.api.types.is_datetime64_any_dtype(df[col]):
37
+ out.append(col)
38
+ continue
39
+ sample = df[col].dropna().head(50)
40
+ try:
41
+ parsed = pd.to_datetime(sample, errors="coerce")
42
+ if parsed.notna().mean() > 0.9:
43
+ out.append(col)
44
+ except Exception:
45
+ pass
46
+ return out
47
+
48
+
49
+ def _detect_constant_columns(df):
50
+ return [c for c in df.columns if df[c].nunique(dropna=False) <= 1]
51
+
52
+
53
+ def _detect_near_constant_columns(df, threshold=0.98):
54
+ out = []
55
+ for c in df.columns:
56
+ top_freq = df[c].value_counts(normalize=True, dropna=False).iloc[0]
57
+ if top_freq >= threshold and df[c].nunique() > 1:
58
+ out.append(c)
59
+ return out
60
+
61
+
62
+ def _detect_high_cardinality(df, threshold=0.5):
63
+ out = []
64
+ for c in df.select_dtypes(include=["object", "category"]).columns:
65
+ if df[c].nunique() / len(df) >= threshold:
66
+ out.append(c)
67
+ return out
68
+
69
+
70
+ def _detect_correlated_features(df, target_col=None, threshold=0.95):
71
+ numeric_df = df.select_dtypes(include=[np.number])
72
+ if target_col in numeric_df.columns:
73
+ numeric_df = numeric_df.drop(columns=[target_col])
74
+ if numeric_df.shape[1] < 2:
75
+ return []
76
+ corr = numeric_df.corr().abs()
77
+ pairs = []
78
+ cols = corr.columns
79
+ for i in range(len(cols)):
80
+ for j in range(i + 1, len(cols)):
81
+ val = corr.iloc[i, j]
82
+ if val >= threshold:
83
+ pairs.append({"col_a": cols[i], "col_b": cols[j], "correlation": round(float(val), 3)})
84
+ return sorted(pairs, key=lambda x: -x["correlation"])[:25]
85
+
86
+
87
+ def _detect_outliers(df, z_thresh=4.0):
88
+ numeric_df = df.select_dtypes(include=[np.number])
89
+ summary = {}
90
+ for c in numeric_df.columns:
91
+ col = numeric_df[c].dropna()
92
+ if col.std() == 0 or len(col) == 0:
93
+ continue
94
+ z = (col - col.mean()) / col.std()
95
+ pct = (z.abs() > z_thresh).mean() * 100
96
+ if pct > 0:
97
+ summary[c] = round(float(pct), 2)
98
+ return summary
99
+
100
+
101
+ def _detect_leakage_risk(df, target_col, corr_threshold=0.98):
102
+ if target_col not in df.columns:
103
+ return []
104
+ numeric_df = df.select_dtypes(include=[np.number])
105
+ if target_col not in numeric_df.columns or numeric_df.shape[1] < 2:
106
+ return []
107
+ corrs = numeric_df.corr()[target_col].drop(target_col).abs()
108
+ return [c for c, v in corrs.items() if v >= corr_threshold]
109
+
110
+
111
+ def _complexity_score(df, findings) -> int:
112
+ score = 0
113
+ n_cols = df.shape[1]
114
+ score += min(len(findings["constant_columns"]) / max(n_cols, 1) * 100, 20)
115
+ score += min(len(findings["near_constant_columns"]) / max(n_cols, 1) * 100, 15)
116
+ score += min(len(findings["highly_correlated_pairs"]) * 2, 20)
117
+ score += min(len(findings["high_cardinality_columns"]) / max(n_cols, 1) * 100, 15)
118
+ score += min(len(findings["leakage_risk_columns"]) * 15, 30)
119
+ return int(round(min(score, 100)))
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ from datapruning.algorithms.base import BaseOptimizer, OptimizationResult
4
+
5
+
6
+ def run_optimization(df: pd.DataFrame, target_col: str, optimizer: BaseOptimizer,
7
+ keep_ratio: float = 0.5) -> OptimizationResult:
8
+ return optimizer.optimize(df, target_col=target_col, keep_ratio=keep_ratio)
@@ -0,0 +1,48 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+
5
+ def scan(df: pd.DataFrame, target_col: str = None) -> dict:
6
+ n_rows, n_cols = df.shape
7
+ memory_mb = df.memory_usage(deep=True).sum() / 1e6
8
+ missing_pct = (df.isna().sum().sum() / (n_rows * n_cols)) * 100
9
+ dup_pct = (df.duplicated().sum() / n_rows) * 100
10
+ numeric_cols = df.select_dtypes(include=[np.number]).columns
11
+ categorical_cols = df.select_dtypes(exclude=[np.number]).columns
12
+ result = {
13
+ "rows": n_rows,
14
+ "columns": n_cols,
15
+ "memory_mb": round(memory_mb, 2),
16
+ "missing_pct": round(missing_pct, 3),
17
+ "duplicate_pct": round(dup_pct, 3),
18
+ "numeric_pct": round(len(numeric_cols) / n_cols * 100, 1),
19
+ "categorical_pct": round(len(categorical_cols) / n_cols * 100, 1),
20
+ "estimated_training_cost": _estimate_cost(n_rows, n_cols),
21
+ }
22
+ if target_col and target_col in df.columns:
23
+ result["task_type"] = _infer_task_type(df[target_col])
24
+ if result["task_type"] in ("binary", "multiclass"):
25
+ vc = df[target_col].value_counts(normalize=True)
26
+ result["target_balance"] = {str(k): round(v * 100, 2) for k, v in vc.items()}
27
+ result["imbalance_ratio"] = round(vc.max() / vc.min(), 2) if len(vc) > 1 else 1.0
28
+ return result
29
+
30
+
31
+ def _infer_task_type(y: pd.Series) -> str:
32
+ nunique = y.nunique()
33
+ if pd.api.types.is_numeric_dtype(y) and nunique > 20:
34
+ return "regression"
35
+ if nunique == 2:
36
+ return "binary"
37
+ return "multiclass"
38
+
39
+
40
+ def _estimate_cost(n_rows: int, n_cols: int) -> str:
41
+ scale = n_rows * n_cols
42
+ if scale < 1e6:
43
+ return "low"
44
+ elif scale < 5e7:
45
+ return "medium"
46
+ elif scale < 5e8:
47
+ return "high"
48
+ return "very_high"
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import datapruning.algorithms
4
+ from datapruning.algorithms.registry import registry
5
+
6
+
7
+ @dataclass
8
+ class StrategyRecommendation:
9
+ algorithm_key: str
10
+ algorithm_instance: object
11
+ display_name: str
12
+ confidence: float
13
+ alternatives: list[dict]
14
+
15
+
16
+ def select_strategy(analysis: dict) -> StrategyRecommendation:
17
+ best_instance, best_key, best_score, alternatives = registry.get_best(analysis)
18
+ return StrategyRecommendation(
19
+ algorithm_key=best_key,
20
+ algorithm_instance=best_instance,
21
+ display_name=best_instance.display_name,
22
+ confidence=round(best_score, 3),
23
+ alternatives=alternatives,
24
+ )
25
+
26
+
27
+ def get_algorithm(key: str):
28
+ return registry.get(key)
29
+
30
+
31
+ def list_available_algorithms() -> list[dict]:
32
+ return [
33
+ {"key": key, "display_name": cls.display_name}
34
+ for key, cls in registry.all().items()
35
+ ]
@@ -1,7 +1,6 @@
1
1
  import logging, re
2
2
  import numpy as np
3
3
  import pandas as pd
4
- import datapruning._core as _core
5
4
 
6
5
  logger = logging.getLogger(__name__)
7
6
 
File without changes
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import pandas as pd
4
+
5
+
6
+ def export_csv(df: pd.DataFrame, path: str) -> str:
7
+ path = Path(path)
8
+ path.parent.mkdir(parents=True, exist_ok=True)
9
+ df.to_csv(path, index=False)
10
+ return str(path)
11
+
12
+
13
+ def export_parquet(df: pd.DataFrame, path: str) -> str:
14
+ path = Path(path)
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ df.to_parquet(path, index=False)
17
+ return str(path)
18
+
19
+
20
+ def export_json(df: pd.DataFrame, path: str, orient: str = "records") -> str:
21
+ path = Path(path)
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ df.to_json(path, orient=orient, indent=2)
24
+ return str(path)
@@ -0,0 +1,80 @@
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datapruning
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: Intelligent data pruning for ML datasets
5
5
  License: Proprietary
6
6
  Project-URL: Homepage, https://www.datapruning.com
@@ -16,9 +16,12 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
+ License-File: LICENSE
19
20
  Requires-Dist: numpy>=1.24.0
20
21
  Requires-Dist: pandas>=2.0.0
22
+ Requires-Dist: scikit-learn>=1.3.0
21
23
  Requires-Dist: torch>=2.0.0
24
+ Dynamic: license-file
22
25
 
23
26
  # DataPruning
24
27
 
@@ -0,0 +1,31 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ build_src/_core.c
7
+ datapruning/__init__.py
8
+ datapruning/pipeline.py
9
+ datapruning/sdk.py
10
+ datapruning.egg-info/PKG-INFO
11
+ datapruning.egg-info/SOURCES.txt
12
+ datapruning.egg-info/dependency_links.txt
13
+ datapruning.egg-info/requires.txt
14
+ datapruning.egg-info/top_level.txt
15
+ datapruning/algorithms/__init__.py
16
+ datapruning/algorithms/base.py
17
+ datapruning/algorithms/cluster_centroids.py
18
+ datapruning/algorithms/density_pruner.py
19
+ datapruning/algorithms/duplicate_aware.py
20
+ datapruning/algorithms/instance_hardness.py
21
+ datapruning/algorithms/random_undersampler.py
22
+ datapruning/algorithms/registry.py
23
+ datapruning/engine/__init__.py
24
+ datapruning/engine/benchmark.py
25
+ datapruning/engine/explainability.py
26
+ datapruning/engine/intelligence.py
27
+ datapruning/engine/optimizer.py
28
+ datapruning/engine/scanner.py
29
+ datapruning/engine/strategy_selector.py
30
+ datapruning/reports/__init__.py
31
+ datapruning/reports/exporter.py
@@ -1,3 +1,4 @@
1
1
  numpy>=1.24.0
2
2
  pandas>=2.0.0
3
+ scikit-learn>=1.3.0
3
4
  torch>=2.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datapruning"
7
- version = "1.0.6"
7
+ version = "1.0.7"
8
8
  description = "Intelligent data pruning for ML datasets"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -24,14 +24,12 @@ classifiers = [
24
24
  dependencies = [
25
25
  "numpy>=1.24.0",
26
26
  "pandas>=2.0.0",
27
+ "scikit-learn>=1.3.0",
27
28
  "torch>=2.0.0",
28
29
  ]
29
30
 
30
31
  [project.urls]
31
32
  Homepage = "https://www.datapruning.com"
32
33
 
33
- [tool.setuptools]
34
- license-files = []
35
-
36
34
  [tool.setuptools.packages.find]
37
35
  include = ["datapruning", "datapruning.*"]
@@ -1,13 +0,0 @@
1
- LICENSE
2
- MANIFEST.in
3
- README.md
4
- pyproject.toml
5
- setup.py
6
- build_src/_core.c
7
- datapruning/__init__.py
8
- datapruning/pipeline.py
9
- datapruning.egg-info/PKG-INFO
10
- datapruning.egg-info/SOURCES.txt
11
- datapruning.egg-info/dependency_links.txt
12
- datapruning.egg-info/requires.txt
13
- datapruning.egg-info/top_level.txt
File without changes
File without changes
File without changes
File without changes
File without changes