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.
Files changed (21) hide show
  1. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/PKG-INFO +22 -11
  2. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/README.md +21 -10
  3. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/pyproject.toml +44 -4
  4. universal_data_cleaner-2.2.0/src/data_cleaner/__init__.py +37 -0
  5. universal_data_cleaner-2.2.0/src/data_cleaner/_roles.py +134 -0
  6. universal_data_cleaner-2.2.0/src/data_cleaner/_version.py +4 -0
  7. universal_data_cleaner-2.2.0/src/data_cleaner/cleaner.py +659 -0
  8. universal_data_cleaner-2.2.0/src/data_cleaner/config.py +83 -0
  9. universal_data_cleaner-2.2.0/src/data_cleaner/contract.py +429 -0
  10. universal_data_cleaner-2.2.0/src/data_cleaner/profiling.py +92 -0
  11. universal_data_cleaner-2.2.0/src/data_cleaner/py.typed +0 -0
  12. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/PKG-INFO +22 -11
  13. universal_data_cleaner-2.2.0/src/universal_data_cleaner.egg-info/SOURCES.txt +17 -0
  14. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/tests/test_data_cleaner.py +131 -15
  15. universal_data_cleaner-2.1.0/data_cleaner.py +0 -1376
  16. universal_data_cleaner-2.1.0/universal_data_cleaner.egg-info/SOURCES.txt +0 -10
  17. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/LICENSE +0 -0
  18. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0}/setup.cfg +0 -0
  19. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
  20. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/requires.txt +0 -0
  21. {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.1.0
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",), # 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
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) # uses train statistics only
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) # drift is detected, never mutates
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() # store it with the model artifact
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.py # reusable library module
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",), # 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
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) # uses train statistics only
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) # drift is detected, never mutates
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() # store it with the model artifact
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.py # reusable library module
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.1.0"
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
- py-modules = ["data_cleaner"]
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)
@@ -0,0 +1,4 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "2.2.0"
4
+ _ARTIFACT_VERSION = 1