universal-data-cleaner 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/PKG-INFO +22 -11
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/README.md +21 -10
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/pyproject.toml +44 -4
- universal_data_cleaner-2.2.0/src/data_cleaner/__init__.py +37 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/_roles.py +134 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/_version.py +4 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/cleaner.py +659 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/config.py +83 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/contract.py +429 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/profiling.py +92 -0
- universal_data_cleaner-2.2.0/src/data_cleaner/py.typed +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/PKG-INFO +22 -11
- universal_data_cleaner-2.2.0/src/universal_data_cleaner.egg-info/SOURCES.txt +17 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/tests/test_data_cleaner.py +131 -15
- universal_data_cleaner-2.1.0/data_cleaner.py +0 -1376
- universal_data_cleaner-2.1.0/universal_data_cleaner.egg-info/SOURCES.txt +0 -10
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/LICENSE +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/setup.cfg +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/requires.txt +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: universal-data-cleaner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Production-ready, scikit-learn-compatible data cleaning transformer for pandas.
|
|
5
5
|
Author: Haseeb Khan
|
|
6
6
|
License-Expression: MIT
|
|
@@ -153,9 +153,9 @@ For a column that should be numeric even though it arrived as dirty strings:
|
|
|
153
153
|
|
|
154
154
|
```python
|
|
155
155
|
config = CleanerConfig(
|
|
156
|
-
numeric_columns=("salary",),
|
|
157
|
-
datetime_columns=("joined",),
|
|
158
|
-
unseen_category="keep",
|
|
156
|
+
numeric_columns=("salary",), # force numeric parsing
|
|
157
|
+
datetime_columns=("joined",), # parse dates instead of treating them as categories
|
|
158
|
+
unseen_category="keep", # or "fill" to replace unseen labels
|
|
159
159
|
)
|
|
160
160
|
```
|
|
161
161
|
|
|
@@ -171,7 +171,7 @@ X_train, X_test, y_train, y_test = train_test_split(
|
|
|
171
171
|
cleaner = DataCleaner(config).fit(X_train)
|
|
172
172
|
|
|
173
173
|
X_train_clean = cleaner.transform(X_train)
|
|
174
|
-
X_test_clean = cleaner.transform(X_test)
|
|
174
|
+
X_test_clean = cleaner.transform(X_test) # uses train statistics only
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
Save the fitted cleaner with the model artifact:
|
|
@@ -197,11 +197,9 @@ contract = DataContract(
|
|
|
197
197
|
],
|
|
198
198
|
)
|
|
199
199
|
|
|
200
|
-
cleaner = DataCleaner(
|
|
201
|
-
CleanerConfig(contract=contract, on_drift="warn")
|
|
202
|
-
).fit(X_train)
|
|
200
|
+
cleaner = DataCleaner(CleanerConfig(contract=contract, on_drift="warn")).fit(X_train)
|
|
203
201
|
|
|
204
|
-
X_test_clean = cleaner.transform(X_test)
|
|
202
|
+
X_test_clean = cleaner.transform(X_test) # drift is detected, never mutates
|
|
205
203
|
print(cleaner.drift_report())
|
|
206
204
|
```
|
|
207
205
|
|
|
@@ -236,7 +234,7 @@ Use `role="ignore"` to drop operational columns from the output.
|
|
|
236
234
|
Contracts can be version-controlled and shared between training and serving:
|
|
237
235
|
|
|
238
236
|
```python
|
|
239
|
-
contract.to_json()
|
|
237
|
+
contract.to_json() # store it with the model artifact
|
|
240
238
|
restored = DataContract.from_json(payload)
|
|
241
239
|
|
|
242
240
|
# Optional YAML support requires `pip install pyyaml`
|
|
@@ -312,11 +310,20 @@ predictions = pipeline.predict(X_test)
|
|
|
312
310
|
|
|
313
311
|
```text
|
|
314
312
|
.
|
|
315
|
-
├── data_cleaner
|
|
313
|
+
├── src/data_cleaner/ # typed, pip-installable package
|
|
314
|
+
│ ├── __init__.py # public API re-exports
|
|
315
|
+
│ ├── _version.py # version metadata
|
|
316
|
+
│ ├── _roles.py # semantic roles and dtype helpers
|
|
317
|
+
│ ├── contract.py # ColumnSpec / DataContract / DriftReport
|
|
318
|
+
│ ├── config.py # CleanerConfig
|
|
319
|
+
│ ├── cleaner.py # DataCleaner
|
|
320
|
+
│ ├── profiling.py # profiling and validation helpers
|
|
321
|
+
│ └── py.typed # PEP 561 marker
|
|
316
322
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
317
323
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
318
324
|
├── pyproject.toml # package metadata and dev tooling
|
|
319
325
|
├── uv.lock # reproducible dependency lock
|
|
326
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
320
327
|
├── README.md
|
|
321
328
|
├── ROADMAP.md
|
|
322
329
|
├── CHANGELOG.md
|
|
@@ -325,6 +332,10 @@ predictions = pipeline.predict(X_test)
|
|
|
325
332
|
└── LICENSE
|
|
326
333
|
```
|
|
327
334
|
|
|
335
|
+
The package ships a `py.typed` marker, so `mypy`, `pyright`, and IDE tooling
|
|
336
|
+
pick up the type annotations automatically. Linting (`ruff`), formatting, and
|
|
337
|
+
type checking (`mypy`) run in CI and via `pre-commit`.
|
|
338
|
+
|
|
328
339
|
## Contributing
|
|
329
340
|
|
|
330
341
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local
|
|
@@ -120,9 +120,9 @@ For a column that should be numeric even though it arrived as dirty strings:
|
|
|
120
120
|
|
|
121
121
|
```python
|
|
122
122
|
config = CleanerConfig(
|
|
123
|
-
numeric_columns=("salary",),
|
|
124
|
-
datetime_columns=("joined",),
|
|
125
|
-
unseen_category="keep",
|
|
123
|
+
numeric_columns=("salary",), # force numeric parsing
|
|
124
|
+
datetime_columns=("joined",), # parse dates instead of treating them as categories
|
|
125
|
+
unseen_category="keep", # or "fill" to replace unseen labels
|
|
126
126
|
)
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -138,7 +138,7 @@ X_train, X_test, y_train, y_test = train_test_split(
|
|
|
138
138
|
cleaner = DataCleaner(config).fit(X_train)
|
|
139
139
|
|
|
140
140
|
X_train_clean = cleaner.transform(X_train)
|
|
141
|
-
X_test_clean = cleaner.transform(X_test)
|
|
141
|
+
X_test_clean = cleaner.transform(X_test) # uses train statistics only
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
Save the fitted cleaner with the model artifact:
|
|
@@ -164,11 +164,9 @@ contract = DataContract(
|
|
|
164
164
|
],
|
|
165
165
|
)
|
|
166
166
|
|
|
167
|
-
cleaner = DataCleaner(
|
|
168
|
-
CleanerConfig(contract=contract, on_drift="warn")
|
|
169
|
-
).fit(X_train)
|
|
167
|
+
cleaner = DataCleaner(CleanerConfig(contract=contract, on_drift="warn")).fit(X_train)
|
|
170
168
|
|
|
171
|
-
X_test_clean = cleaner.transform(X_test)
|
|
169
|
+
X_test_clean = cleaner.transform(X_test) # drift is detected, never mutates
|
|
172
170
|
print(cleaner.drift_report())
|
|
173
171
|
```
|
|
174
172
|
|
|
@@ -203,7 +201,7 @@ Use `role="ignore"` to drop operational columns from the output.
|
|
|
203
201
|
Contracts can be version-controlled and shared between training and serving:
|
|
204
202
|
|
|
205
203
|
```python
|
|
206
|
-
contract.to_json()
|
|
204
|
+
contract.to_json() # store it with the model artifact
|
|
207
205
|
restored = DataContract.from_json(payload)
|
|
208
206
|
|
|
209
207
|
# Optional YAML support requires `pip install pyyaml`
|
|
@@ -279,11 +277,20 @@ predictions = pipeline.predict(X_test)
|
|
|
279
277
|
|
|
280
278
|
```text
|
|
281
279
|
.
|
|
282
|
-
├── data_cleaner
|
|
280
|
+
├── src/data_cleaner/ # typed, pip-installable package
|
|
281
|
+
│ ├── __init__.py # public API re-exports
|
|
282
|
+
│ ├── _version.py # version metadata
|
|
283
|
+
│ ├── _roles.py # semantic roles and dtype helpers
|
|
284
|
+
│ ├── contract.py # ColumnSpec / DataContract / DriftReport
|
|
285
|
+
│ ├── config.py # CleanerConfig
|
|
286
|
+
│ ├── cleaner.py # DataCleaner
|
|
287
|
+
│ ├── profiling.py # profiling and validation helpers
|
|
288
|
+
│ └── py.typed # PEP 561 marker
|
|
283
289
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
284
290
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
285
291
|
├── pyproject.toml # package metadata and dev tooling
|
|
286
292
|
├── uv.lock # reproducible dependency lock
|
|
293
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
287
294
|
├── README.md
|
|
288
295
|
├── ROADMAP.md
|
|
289
296
|
├── CHANGELOG.md
|
|
@@ -292,6 +299,10 @@ predictions = pipeline.predict(X_test)
|
|
|
292
299
|
└── LICENSE
|
|
293
300
|
```
|
|
294
301
|
|
|
302
|
+
The package ships a `py.typed` marker, so `mypy`, `pyright`, and IDE tooling
|
|
303
|
+
pick up the type annotations automatically. Linting (`ruff`), formatting, and
|
|
304
|
+
type checking (`mypy`) run in CI and via `pre-commit`.
|
|
305
|
+
|
|
295
306
|
## Contributing
|
|
296
307
|
|
|
297
308
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "universal-data-cleaner"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Production-ready, scikit-learn-compatible data cleaning transformer for pandas."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -53,14 +53,54 @@ Changelog = "https://github.com/devlprkhan/universal-data-cleaner/blob/main/CHAN
|
|
|
53
53
|
dev = [
|
|
54
54
|
"ipykernel>=7.3.0",
|
|
55
55
|
"jupyterlab>=4.6.4",
|
|
56
|
+
"mypy>=2.4.0",
|
|
56
57
|
"nbclient>=0.11.0",
|
|
57
58
|
"nbformat>=5.11.1",
|
|
59
|
+
"pandas-stubs>=2.3.3.260113",
|
|
60
|
+
"pre-commit>=4.6.2",
|
|
58
61
|
"pytest>=9.1.1",
|
|
62
|
+
"ruff>=0.17.1",
|
|
59
63
|
]
|
|
60
64
|
|
|
61
|
-
[tool.setuptools]
|
|
62
|
-
|
|
65
|
+
[tool.setuptools.packages.find]
|
|
66
|
+
where = ["src"]
|
|
67
|
+
|
|
68
|
+
[tool.setuptools.package-data]
|
|
69
|
+
data_cleaner = ["py.typed"]
|
|
63
70
|
|
|
64
71
|
[tool.pytest.ini_options]
|
|
65
|
-
pythonpath = ["
|
|
72
|
+
pythonpath = ["src"]
|
|
66
73
|
testpaths = ["tests"]
|
|
74
|
+
addopts = "-q"
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 88
|
|
78
|
+
target-version = "py310"
|
|
79
|
+
src = ["src", "tests"]
|
|
80
|
+
extend-exclude = ["*.ipynb"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint]
|
|
83
|
+
select = ["E", "F", "I", "B", "UP", "C4", "SIM", "RUF"]
|
|
84
|
+
ignore = ["E501"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
|
87
|
+
"tests/**" = ["B011"]
|
|
88
|
+
|
|
89
|
+
[tool.ruff.format]
|
|
90
|
+
quote-style = "double"
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.10"
|
|
94
|
+
check_untyped_defs = true
|
|
95
|
+
no_implicit_optional = true
|
|
96
|
+
warn_redundant_casts = true
|
|
97
|
+
warn_unused_ignores = true
|
|
98
|
+
warn_unreachable = true
|
|
99
|
+
show_error_codes = true
|
|
100
|
+
ignore_missing_imports = true
|
|
101
|
+
mypy_path = "src"
|
|
102
|
+
files = ["src", "tests"]
|
|
103
|
+
|
|
104
|
+
[[tool.mypy.overrides]]
|
|
105
|
+
module = ["sklearn.*", "yaml", "joblib.*"]
|
|
106
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Universal Data Cleaner — production-oriented tabular cleaning toolkit.
|
|
2
|
+
|
|
3
|
+
The package is intentionally dependency-light:
|
|
4
|
+
|
|
5
|
+
- pandas + numpy for data handling
|
|
6
|
+
- scikit-learn base classes for Pipeline / clone compatibility
|
|
7
|
+
- joblib for artifact persistence
|
|
8
|
+
|
|
9
|
+
The public API is re-exported here so ``import data_cleaner`` keeps working
|
|
10
|
+
exactly as it did when the library shipped as a single module.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
from .cleaner import DataCleaner
|
|
17
|
+
from .config import CleanerConfig
|
|
18
|
+
from .contract import ColumnSpec, DataContract, DriftReport
|
|
19
|
+
from .profiling import (
|
|
20
|
+
assert_frame_clean,
|
|
21
|
+
dataset_summary,
|
|
22
|
+
drop_duplicate_rows,
|
|
23
|
+
profile_dataframe,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CleanerConfig",
|
|
28
|
+
"ColumnSpec",
|
|
29
|
+
"DataCleaner",
|
|
30
|
+
"DataContract",
|
|
31
|
+
"DriftReport",
|
|
32
|
+
"__version__",
|
|
33
|
+
"assert_frame_clean",
|
|
34
|
+
"dataset_summary",
|
|
35
|
+
"drop_duplicate_rows",
|
|
36
|
+
"profile_dataframe",
|
|
37
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Semantic roles, dtype normalization, and identifier helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from pandas.api import types as ptypes
|
|
10
|
+
|
|
11
|
+
_ALLOWED_DATETIME_FEATURES = {
|
|
12
|
+
"year",
|
|
13
|
+
"month",
|
|
14
|
+
"day",
|
|
15
|
+
"dayofweek",
|
|
16
|
+
"quarter",
|
|
17
|
+
"dayofyear",
|
|
18
|
+
"weekofyear",
|
|
19
|
+
"is_weekend",
|
|
20
|
+
"hour",
|
|
21
|
+
"minute",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_ROLE_ALIASES = {
|
|
25
|
+
"numeric": "numeric",
|
|
26
|
+
"measure": "numeric",
|
|
27
|
+
"number": "numeric",
|
|
28
|
+
"float": "numeric",
|
|
29
|
+
"int": "numeric",
|
|
30
|
+
"integer": "numeric",
|
|
31
|
+
"category": "category",
|
|
32
|
+
"categorical": "category",
|
|
33
|
+
"object": "category",
|
|
34
|
+
"string": "category",
|
|
35
|
+
"str": "category",
|
|
36
|
+
"boolean": "boolean",
|
|
37
|
+
"bool": "boolean",
|
|
38
|
+
"datetime": "datetime",
|
|
39
|
+
"date": "datetime",
|
|
40
|
+
"timestamp": "datetime",
|
|
41
|
+
"id": "id",
|
|
42
|
+
"identifier": "id",
|
|
43
|
+
"text": "text",
|
|
44
|
+
"ignore": "ignore",
|
|
45
|
+
"drop": "ignore",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_ROLE_TO_KIND = {
|
|
49
|
+
"numeric": "numeric",
|
|
50
|
+
"category": "categorical",
|
|
51
|
+
"boolean": "boolean",
|
|
52
|
+
"datetime": "datetime",
|
|
53
|
+
"id": "categorical",
|
|
54
|
+
"text": "categorical",
|
|
55
|
+
"ignore": None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_DRIFT_COLUMNS = ["column", "check", "severity", "expected", "actual", "message"]
|
|
59
|
+
|
|
60
|
+
_ID_STRING_KINDS = (int, np.integer)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalize_role(value: object) -> str | None:
|
|
64
|
+
"""Map role aliases to a canonical role name."""
|
|
65
|
+
if value is None:
|
|
66
|
+
return None
|
|
67
|
+
key = str(value).strip().lower()
|
|
68
|
+
if key not in _ROLE_ALIASES:
|
|
69
|
+
valid = sorted(set(_ROLE_ALIASES.values()))
|
|
70
|
+
raise ValueError(f"Unsupported column role '{value}'. Valid roles: {valid}")
|
|
71
|
+
return _ROLE_ALIASES[key]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _normalize_kind(value: object) -> str | None:
|
|
75
|
+
"""Map dtype strings to a broad kind: numeric/categorical/boolean/datetime."""
|
|
76
|
+
if value is None:
|
|
77
|
+
return None
|
|
78
|
+
key = str(value).strip().lower()
|
|
79
|
+
mapping = {
|
|
80
|
+
"numeric": "numeric",
|
|
81
|
+
"number": "numeric",
|
|
82
|
+
"int": "numeric",
|
|
83
|
+
"integer": "numeric",
|
|
84
|
+
"float": "numeric",
|
|
85
|
+
"double": "numeric",
|
|
86
|
+
"int64": "numeric",
|
|
87
|
+
"int32": "numeric",
|
|
88
|
+
"int16": "numeric",
|
|
89
|
+
"int8": "numeric",
|
|
90
|
+
"float64": "numeric",
|
|
91
|
+
"float32": "numeric",
|
|
92
|
+
"boolean": "boolean",
|
|
93
|
+
"bool": "boolean",
|
|
94
|
+
"datetime": "datetime",
|
|
95
|
+
"datetime64": "datetime",
|
|
96
|
+
"datetime64[ns]": "datetime",
|
|
97
|
+
"date": "datetime",
|
|
98
|
+
"timestamp": "datetime",
|
|
99
|
+
"categorical": "categorical",
|
|
100
|
+
"category": "categorical",
|
|
101
|
+
"object": "categorical",
|
|
102
|
+
"string": "categorical",
|
|
103
|
+
"str": "categorical",
|
|
104
|
+
"text": "categorical",
|
|
105
|
+
"id": "categorical",
|
|
106
|
+
}
|
|
107
|
+
return mapping.get(key, key)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _series_kind(series: pd.Series) -> str:
|
|
111
|
+
"""Return the broad semantic kind of a pandas Series."""
|
|
112
|
+
if ptypes.is_bool_dtype(series):
|
|
113
|
+
return "boolean"
|
|
114
|
+
if ptypes.is_datetime64_any_dtype(series):
|
|
115
|
+
return "datetime"
|
|
116
|
+
if ptypes.is_numeric_dtype(series):
|
|
117
|
+
return "numeric"
|
|
118
|
+
return "categorical"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _stringify_identifier(value: Any) -> object:
|
|
122
|
+
"""Render an identifier without a trailing ``.0`` while preserving NaN."""
|
|
123
|
+
if value is None:
|
|
124
|
+
return value
|
|
125
|
+
try:
|
|
126
|
+
if pd.isna(value):
|
|
127
|
+
return value
|
|
128
|
+
except (TypeError, ValueError):
|
|
129
|
+
pass
|
|
130
|
+
if isinstance(value, _ID_STRING_KINDS):
|
|
131
|
+
return str(int(value))
|
|
132
|
+
if isinstance(value, (float, np.floating)) and float(value).is_integer():
|
|
133
|
+
return str(int(value))
|
|
134
|
+
return str(value)
|