datapruning 1.0.3__cp314-cp314-win_amd64.whl

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.
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+ from . import pipeline
6
+ from ._core import analyze_data as _analyze_data
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ MIN_ROWS = 1000
11
+ MAX_ROWS = 300_000
12
+
13
+
14
+ def prune_dataframe(df, target_col=None, keep_ratio=0.5, num_steps=400, verbose=False):
15
+ if verbose:
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
17
+
18
+ if len(df) < MIN_ROWS:
19
+ raise ValueError(
20
+ f"Dataset too small for pruning. Minimum {MIN_ROWS:,} rows required, "
21
+ f"got {len(df):,}."
22
+ )
23
+ if len(df) > MAX_ROWS:
24
+ raise ValueError(
25
+ f"Dataset exceeds maximum of {MAX_ROWS:,} rows (got {len(df):,})."
26
+ )
27
+
28
+ if target_col is None:
29
+ target_col = pipeline.detect_target_column(df)
30
+ logger.info("Auto-detected target column: '%s'", target_col)
31
+
32
+ if target_col not in df.columns:
33
+ raise ValueError(f"Column '{target_col}' not found in DataFrame")
34
+
35
+ info = pipeline.analyze_dataset(df, target_col)
36
+ logger.info("Task: %s, %d rows, %d features, %d classes",
37
+ info["task_type"], info["row_count"],
38
+ info["feature_count"], info["num_classes"] or "N/A")
39
+
40
+ X, y, meta = pipeline.translate_df(df, target_col)
41
+
42
+ X_f32 = np.ascontiguousarray(X, dtype=np.float32)
43
+ indices = _analyze_data(X_f32, y.copy(), num_steps, keep_ratio)
44
+
45
+ indices, adjustments = pipeline.safety_layer(indices, y, original_count=len(df))
46
+ if adjustments:
47
+ logger.info("Safety adjustments: %s", adjustments)
48
+
49
+ return df.iloc[indices].reset_index(drop=True)
50
+
51
+
52
+ class Pruner:
53
+ def __init__(self, df):
54
+ if not isinstance(df, pd.DataFrame):
55
+ raise TypeError("Expected a pandas DataFrame")
56
+ self._df = df
57
+
58
+ def prune(self, target_col=None, keep_ratio=0.5, num_steps=400, verbose=False):
59
+ return prune_dataframe(self._df, target_col=target_col,
60
+ keep_ratio=keep_ratio, num_steps=num_steps,
61
+ verbose=verbose)
Binary file
@@ -0,0 +1,188 @@
1
+ import logging, re
2
+ import numpy as np
3
+ import pandas as pd
4
+ import datapruning._core as _core
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ SMALL_DATASET_THRESHOLD = 3000
9
+ HIGH_DIM_THRESHOLD = 50
10
+ IMBALANCE_RATIO_THRESHOLD = 0.1
11
+ MAX_PRUNING = 0.60
12
+ MIN_ROWS = 1000
13
+ MAX_ROWS = 300_000
14
+
15
+ SUSPICIOUS_NAMES = [
16
+ "target", "class", "label", "outcome",
17
+ "y", "diagnosis", "income",
18
+ "category", "prediction",
19
+ ]
20
+ BOOL_MAPS = {
21
+ frozenset({"yes", "no"}): {"yes": 1, "no": 0},
22
+ frozenset({"true", "false"}): {"true": 1, "false": 0},
23
+ frozenset({"male", "female"}): {"male": 1, "female": 0},
24
+ frozenset({"y", "n"}): {"y": 1, "n": 0},
25
+ }
26
+
27
+
28
+ def detect_target_column(df):
29
+ candidates = []
30
+ for col in df.columns:
31
+ score = 0.0
32
+ series = df[col]
33
+ unique_count = series.nunique(dropna=True)
34
+ unique_ratio = unique_count / max(len(series), 1)
35
+ is_numeric = pd.api.types.is_numeric_dtype(series)
36
+ col_lower = str(col).lower()
37
+ tokens = set(re.split(r'[_\-\.\s]+', col_lower))
38
+ if tokens & set(SUSPICIOUS_NAMES):
39
+ score += 50
40
+ if unique_count <= 20:
41
+ score += 35
42
+ elif unique_count <= 50:
43
+ score += 15
44
+ if unique_count == 2:
45
+ score += 25
46
+ if unique_ratio > 0.9:
47
+ score -= 40
48
+ if "id" in col_lower:
49
+ score -= 50
50
+ if "date" in col_lower or "time" in col_lower:
51
+ score -= 30
52
+ if is_numeric and unique_count > 100:
53
+ score -= 20
54
+ if col == df.columns[0]:
55
+ score += 10
56
+ if col == df.columns[-1]:
57
+ score += 15
58
+ candidates.append({"column": col, "score": score})
59
+ candidates.sort(key=lambda x: x["score"], reverse=True)
60
+ return candidates[0]["column"]
61
+
62
+
63
+ def translate_df(df, target_column):
64
+ feature_names = [c for c in df.columns if c != target_column]
65
+ target_name = target_column
66
+
67
+ df = df.dropna(subset=[target_name]).copy()
68
+ if df.empty:
69
+ raise ValueError("All rows have missing target values")
70
+
71
+ features = df[feature_names].copy()
72
+
73
+ for col in features.columns:
74
+ uniq = set(features[col].dropna().astype(str).str.lower().unique())
75
+ for pattern, mapping in BOOL_MAPS.items():
76
+ if uniq and (uniq == pattern or uniq.issubset(pattern)):
77
+ features[col] = features[col].astype(str).str.lower().map(mapping).fillna(0).astype(int)
78
+ break
79
+
80
+ num_cols = features.select_dtypes(include=[np.number]).columns.tolist()
81
+ cat_cols = [c for c in features.columns if c not in num_cols]
82
+
83
+ for col in num_cols:
84
+ features[col] = pd.to_numeric(features[col], errors="coerce")
85
+ med = features[col].median()
86
+ features[col] = features[col].fillna(med if pd.notna(med) else 0.0)
87
+
88
+ for col in cat_cols:
89
+ features[col] = features[col].astype(str).replace("nan", pd.NA)
90
+ most_common = features[col].mode()
91
+ fill_val = most_common.iloc[0] if not most_common.empty else "missing"
92
+ features[col] = features[col].fillna(fill_val)
93
+
94
+ if cat_cols:
95
+ features = pd.get_dummies(features, columns=cat_cols, prefix=cat_cols, dummy_na=False)
96
+
97
+ for col in features.columns:
98
+ features[col] = pd.to_numeric(features[col], errors="coerce").fillna(0.0)
99
+
100
+ target_series = df[target_name].fillna("missing").astype(str)
101
+ classes = sorted(target_series.unique())
102
+ le = {v: i for i, v in enumerate(classes)}
103
+ y = target_series.map(le).fillna(-1).astype(np.int64).values
104
+ X = features.values.astype(np.float64)
105
+
106
+ metadata = {
107
+ "feature_names": features.columns.tolist(),
108
+ "target_name": target_name,
109
+ "num_features": X.shape[1],
110
+ "num_classes": len(classes),
111
+ "class_mapping": le,
112
+ }
113
+
114
+ return X, y, metadata
115
+
116
+
117
+ def analyze_dataset(df, target_column):
118
+ feature_count = df.shape[1] - 1
119
+ row_count = len(df)
120
+ target_series = df[target_column].dropna()
121
+ unique_classes = target_series.unique()
122
+ num_classes = len(unique_classes)
123
+ is_numeric_target = pd.api.types.is_numeric_dtype(target_series)
124
+ unique_ratio = num_classes / max(len(target_series), 1)
125
+
126
+ if num_classes <= 20 and (not is_numeric_target or unique_ratio < 0.01):
127
+ task_type = "classification"
128
+ elif is_numeric_target and num_classes > 20:
129
+ task_type = "regression"
130
+ else:
131
+ task_type = "classification"
132
+
133
+ risks = []
134
+ if row_count < SMALL_DATASET_THRESHOLD:
135
+ risks.append({"type": "small_dataset", "message": "Small dataset detected.", "severity": "info"})
136
+ if feature_count >= HIGH_DIM_THRESHOLD:
137
+ risks.append({"type": "high_dimensional", "message": "High-dimensional dataset detected.", "severity": "info"})
138
+
139
+ has_imbalance = False
140
+ class_distribution = None
141
+ if task_type == "classification" and num_classes >= 2:
142
+ counts = target_series.value_counts()
143
+ class_distribution = {str(k): int(v) for k, v in counts.items()}
144
+ min_count = counts.min()
145
+ max_count = counts.max()
146
+ if min_count > 0 and (min_count / max_count) < IMBALANCE_RATIO_THRESHOLD:
147
+ has_imbalance = True
148
+ risks.append({"type": "imbalance", "message": "Class imbalance detected.", "severity": "info"})
149
+
150
+ return {
151
+ "row_count": row_count,
152
+ "feature_count": feature_count,
153
+ "task_type": task_type,
154
+ "num_classes": num_classes if task_type == "classification" else None,
155
+ "class_distribution": class_distribution,
156
+ "has_imbalance": has_imbalance,
157
+ "risks": risks,
158
+ }
159
+
160
+
161
+ def safety_layer(kept_indices, y, original_count=None, min_per_class=3):
162
+ adjustments = []
163
+
164
+ if original_count is not None:
165
+ min_kept = max(1, int(original_count * (1 - MAX_PRUNING)))
166
+ if len(kept_indices) < min_kept:
167
+ kept_set = set(kept_indices)
168
+ all_set = set(range(original_count))
169
+ removed = all_set - kept_set
170
+ to_add = sorted(removed)[:min_kept - len(kept_indices)]
171
+ kept_indices = np.array(sorted(kept_set | set(to_add)))
172
+ adjustments.append("max_pruning_cap_applied")
173
+
174
+ kept_set = set(kept_indices)
175
+ unique_classes = np.unique(y)
176
+ result = list(kept_indices)
177
+ for cls in unique_classes:
178
+ cls_indices = np.where(y == cls)[0]
179
+ cls_kept = [int(i) for i in cls_indices if i in kept_set]
180
+ if len(cls_kept) < min_per_class:
181
+ to_add = min_per_class - len(cls_kept)
182
+ candidates = [int(i) for i in cls_indices if i not in kept_set]
183
+ result.extend(candidates[:to_add])
184
+ if len(result) != len(kept_indices):
185
+ adjustments.append("class_preservation_applied")
186
+ kept_indices = np.array(sorted(result))
187
+
188
+ return kept_indices, adjustments
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: datapruning
3
+ Version: 1.0.3
4
+ Summary: Intelligent data pruning for ML datasets
5
+ License: Proprietary
6
+ Project-URL: Homepage, https://www.datapruning.com
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: numpy>=1.24.0
20
+ Requires-Dist: pandas>=2.0.0
21
+ Requires-Dist: torch>=2.0.0
22
+
23
+ # DataPruning
24
+
25
+ Intelligent dataset pruning for ML — reduces dataset size by selecting the most informative rows.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install datapruning
31
+ ```
32
+
33
+ **Note:** PyTorch (~2 GB) is a required dependency.
34
+
35
+ ## Requirements
36
+
37
+ - Python >= 3.10
38
+ - PyTorch 2.0+
39
+ - Pandas 2.0+
40
+ - NumPy 1.24+
41
+
42
+ ## Limits
43
+
44
+ - Minimum: 1,000 rows per dataset
45
+ - Maximum: 300,000 rows per dataset
46
+
47
+ ## Usage
48
+
49
+ ```python
50
+ import pandas as pd
51
+ from datapruning import Pruner
52
+
53
+ df = pd.read_csv("dataset.csv")
54
+ p = Pruner(df)
55
+ filtered = p.prune(target_col="label", keep_ratio=0.5)
56
+
57
+ print(f"Kept {len(filtered)} / {len(df)} rows")
58
+ ```
59
+
60
+ ## Features
61
+
62
+ - Runs locally, no data upload
63
+ - Pre-training dataset optimization
64
+ - Pandas DataFrame input / output
65
+ - Compiled processing module
66
+
67
+ ## Links
68
+
69
+ - Website: [datapruning.com](https://www.datapruning.com)
70
+
71
+ ## License
72
+
73
+ Proprietary. See LICENSE file.
@@ -0,0 +1,7 @@
1
+ datapruning/__init__.py,sha256=4iQ1n8K0FTPSwbC1T3Oh4oFfzwkQI9H5ac3h_N3kwGY,2127
2
+ datapruning/_core.cp314-win_amd64.pyd,sha256=TvIKgYUtOxS-Euwh60H36gauUUB4kDY1BeQraFNgHoI,348160
3
+ datapruning/pipeline.py,sha256=T78LTTq5PFSfW7S4rKPXvupdEenkFK9tXW6AkHY8lDM,6973
4
+ datapruning-1.0.3.dist-info/METADATA,sha256=dCenCUNPq2MNO6ds5r8kA53O0KysD0Bbo6NIkNaXKEE,1779
5
+ datapruning-1.0.3.dist-info/WHEEL,sha256=jhxrRP3tdEfm7toB1X7dl5x6f0bu6Tve2cWwIdkbL8U,101
6
+ datapruning-1.0.3.dist-info/top_level.txt,sha256=HHitOoBY_0KeZGXmSViQTKsbSMkUJNPSc7oUMAoxlF8,12
7
+ datapruning-1.0.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-win_amd64
5
+
@@ -0,0 +1 @@
1
+ datapruning