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.
datapruning/__init__.py
ADDED
|
@@ -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
|
datapruning/pipeline.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
datapruning
|