universal-data-cleaner 2.1.0__tar.gz → 2.3.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.3.0}/PKG-INFO +89 -11
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/README.md +88 -10
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/pyproject.toml +44 -4
- universal_data_cleaner-2.3.0/src/data_cleaner/__init__.py +47 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/_roles.py +134 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/_version.py +4 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/artifacts.py +171 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/cleaner.py +991 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/config.py +84 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/contract.py +429 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/decisions.py +50 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/profiling.py +92 -0
- universal_data_cleaner-2.3.0/src/data_cleaner/py.typed +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/PKG-INFO +89 -11
- universal_data_cleaner-2.3.0/src/universal_data_cleaner.egg-info/SOURCES.txt +19 -0
- universal_data_cleaner-2.3.0/tests/test_data_cleaner.py +624 -0
- universal_data_cleaner-2.1.0/data_cleaner.py +0 -1376
- universal_data_cleaner-2.1.0/tests/test_data_cleaner.py +0 -299
- 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.3.0}/LICENSE +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/setup.cfg +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/requires.txt +0 -0
- {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.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.3.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,13 +234,68 @@ 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`
|
|
243
241
|
# restored = DataContract.from_yaml(contract.to_yaml())
|
|
244
242
|
```
|
|
245
243
|
|
|
244
|
+
## Audit trail, manifests, and fingerprints
|
|
245
|
+
|
|
246
|
+
Every learned or configured decision is recorded during `fit()` and exposed as
|
|
247
|
+
a tidy DataFrame:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
log = cleaner.decision_log()
|
|
251
|
+
# columns: column, action, reason, source, before, after
|
|
252
|
+
log[log["action"] == "cap_outliers"]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`source` says whether a rule was `learned` from the training data, supplied via
|
|
256
|
+
`config`, declared in a `contract`, or `inferred` by the library. Imputation
|
|
257
|
+
values, outlier bounds, datetime expansion, missing indicators, dropped
|
|
258
|
+
constants, and identifier handling are all explained instead of hidden.
|
|
259
|
+
|
|
260
|
+
Build a JSON-serializable manifest for logs or a model registry:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
import json
|
|
264
|
+
|
|
265
|
+
manifest = cleaner.to_manifest(X_train, include_fingerprint=True)
|
|
266
|
+
# library_version, artifact_version, environment, config, contract,
|
|
267
|
+
# input_schema, output_columns, fitted, decisions, fingerprint
|
|
268
|
+
|
|
269
|
+
json.dumps(manifest) # safe to ship to a registry or a log line
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`save()` embeds the manifest and the training fingerprint in the artifact, and
|
|
273
|
+
`load()` restores them:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
cleaner.save("cleaner.joblib")
|
|
277
|
+
restored = DataCleaner.load("cleaner.joblib")
|
|
278
|
+
restored.artifact_manifest()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Fingerprints are stable SHA-256 hashes of the schema plus summary statistics.
|
|
282
|
+
They never hash raw values, so they are safe to log:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from data_cleaner import (
|
|
286
|
+
assert_schema_stable,
|
|
287
|
+
compare_fingerprints,
|
|
288
|
+
fingerprint_dataframe,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
expected = fingerprint_dataframe(X_train)
|
|
292
|
+
same = compare_fingerprints(expected, fingerprint_dataframe(X_new))
|
|
293
|
+
assert_schema_stable(X_new, expected) # raises ValueError on schema/stat drift
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Set `CleanerConfig(fingerprint_on_fit=True)` to compute the training fingerprint
|
|
297
|
+
once during `fit()`, so `to_manifest(include_fingerprint=True)` needs no data.
|
|
298
|
+
|
|
246
299
|
## scikit-learn Pipeline
|
|
247
300
|
|
|
248
301
|
```python
|
|
@@ -281,11 +334,19 @@ predictions = pipeline.predict(X_test)
|
|
|
281
334
|
| --- | --- |
|
|
282
335
|
| `CleanerConfig` | Validated cleaning configuration |
|
|
283
336
|
| `DataCleaner` | Fit/transform cleaner with save/load and drift reporting |
|
|
337
|
+
| `CleaningDecision` | One auditable learned or configured decision |
|
|
284
338
|
| `ColumnSpec` | Declarative expectations and semantics for one column |
|
|
285
339
|
| `DataContract` | A set of column specs, with inference and (de)serialization |
|
|
286
340
|
| `DriftReport` | Findings from a contract check, with a Markdown summary |
|
|
287
341
|
| `DataCleaner.drift_report()` | Drift findings from the last `transform` |
|
|
288
342
|
| `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
|
|
343
|
+
| `DataCleaner.decision_log()` | Tidy DataFrame of cleaning decisions |
|
|
344
|
+
| `DataCleaner.to_manifest()` | JSON-serializable artifact manifest |
|
|
345
|
+
| `DataCleaner.artifact_manifest()` | Manifest stored in a loaded artifact |
|
|
346
|
+
| `DataCleaner.fingerprint()` | Stable schema + statistics fingerprint |
|
|
347
|
+
| `fingerprint_dataframe` | Fingerprint any DataFrame for lineage |
|
|
348
|
+
| `compare_fingerprints` | Compare two fingerprints |
|
|
349
|
+
| `assert_schema_stable` | Raise when a frame's fingerprint changed |
|
|
289
350
|
| `profile_dataframe` | Column-level missing/type/cardinality profile |
|
|
290
351
|
| `dataset_summary` | Row/column/missing/duplicate summary |
|
|
291
352
|
| `drop_duplicate_rows` | Explicit duplicate removal helper |
|
|
@@ -307,16 +368,29 @@ predictions = pipeline.predict(X_test)
|
|
|
307
368
|
represented by the generated feature NaNs plus an optional missing indicator.
|
|
308
369
|
- IQR capping is skipped automatically when the IQR is zero, so constant or
|
|
309
370
|
near-constant numeric columns are not accidentally destroyed.
|
|
371
|
+
- Persist `cleaner.to_manifest(X_train, include_fingerprint=True)` alongside the
|
|
372
|
+
model so you can audit exactly which cleaning rules produced an artifact.
|
|
373
|
+
- `decision_log()` explains every learned or configured rule; ship it to your
|
|
374
|
+
experiment tracker when a model's preprocessing changes.
|
|
310
375
|
|
|
311
376
|
## Project layout
|
|
312
377
|
|
|
313
378
|
```text
|
|
314
379
|
.
|
|
315
|
-
├── data_cleaner
|
|
380
|
+
├── src/data_cleaner/ # typed, pip-installable package
|
|
381
|
+
│ ├── __init__.py # public API re-exports
|
|
382
|
+
│ ├── _version.py # version metadata
|
|
383
|
+
│ ├── _roles.py # semantic roles and dtype helpers
|
|
384
|
+
│ ├── contract.py # ColumnSpec / DataContract / DriftReport
|
|
385
|
+
│ ├── config.py # CleanerConfig
|
|
386
|
+
│ ├── cleaner.py # DataCleaner
|
|
387
|
+
│ ├── profiling.py # profiling and validation helpers
|
|
388
|
+
│ └── py.typed # PEP 561 marker
|
|
316
389
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
317
390
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
318
391
|
├── pyproject.toml # package metadata and dev tooling
|
|
319
392
|
├── uv.lock # reproducible dependency lock
|
|
393
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
320
394
|
├── README.md
|
|
321
395
|
├── ROADMAP.md
|
|
322
396
|
├── CHANGELOG.md
|
|
@@ -325,6 +399,10 @@ predictions = pipeline.predict(X_test)
|
|
|
325
399
|
└── LICENSE
|
|
326
400
|
```
|
|
327
401
|
|
|
402
|
+
The package ships a `py.typed` marker, so `mypy`, `pyright`, and IDE tooling
|
|
403
|
+
pick up the type annotations automatically. Linting (`ruff`), formatting, and
|
|
404
|
+
type checking (`mypy`) run in CI and via `pre-commit`.
|
|
405
|
+
|
|
328
406
|
## Contributing
|
|
329
407
|
|
|
330
408
|
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,13 +201,68 @@ 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`
|
|
210
208
|
# restored = DataContract.from_yaml(contract.to_yaml())
|
|
211
209
|
```
|
|
212
210
|
|
|
211
|
+
## Audit trail, manifests, and fingerprints
|
|
212
|
+
|
|
213
|
+
Every learned or configured decision is recorded during `fit()` and exposed as
|
|
214
|
+
a tidy DataFrame:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
log = cleaner.decision_log()
|
|
218
|
+
# columns: column, action, reason, source, before, after
|
|
219
|
+
log[log["action"] == "cap_outliers"]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`source` says whether a rule was `learned` from the training data, supplied via
|
|
223
|
+
`config`, declared in a `contract`, or `inferred` by the library. Imputation
|
|
224
|
+
values, outlier bounds, datetime expansion, missing indicators, dropped
|
|
225
|
+
constants, and identifier handling are all explained instead of hidden.
|
|
226
|
+
|
|
227
|
+
Build a JSON-serializable manifest for logs or a model registry:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import json
|
|
231
|
+
|
|
232
|
+
manifest = cleaner.to_manifest(X_train, include_fingerprint=True)
|
|
233
|
+
# library_version, artifact_version, environment, config, contract,
|
|
234
|
+
# input_schema, output_columns, fitted, decisions, fingerprint
|
|
235
|
+
|
|
236
|
+
json.dumps(manifest) # safe to ship to a registry or a log line
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`save()` embeds the manifest and the training fingerprint in the artifact, and
|
|
240
|
+
`load()` restores them:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
cleaner.save("cleaner.joblib")
|
|
244
|
+
restored = DataCleaner.load("cleaner.joblib")
|
|
245
|
+
restored.artifact_manifest()
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Fingerprints are stable SHA-256 hashes of the schema plus summary statistics.
|
|
249
|
+
They never hash raw values, so they are safe to log:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from data_cleaner import (
|
|
253
|
+
assert_schema_stable,
|
|
254
|
+
compare_fingerprints,
|
|
255
|
+
fingerprint_dataframe,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
expected = fingerprint_dataframe(X_train)
|
|
259
|
+
same = compare_fingerprints(expected, fingerprint_dataframe(X_new))
|
|
260
|
+
assert_schema_stable(X_new, expected) # raises ValueError on schema/stat drift
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Set `CleanerConfig(fingerprint_on_fit=True)` to compute the training fingerprint
|
|
264
|
+
once during `fit()`, so `to_manifest(include_fingerprint=True)` needs no data.
|
|
265
|
+
|
|
213
266
|
## scikit-learn Pipeline
|
|
214
267
|
|
|
215
268
|
```python
|
|
@@ -248,11 +301,19 @@ predictions = pipeline.predict(X_test)
|
|
|
248
301
|
| --- | --- |
|
|
249
302
|
| `CleanerConfig` | Validated cleaning configuration |
|
|
250
303
|
| `DataCleaner` | Fit/transform cleaner with save/load and drift reporting |
|
|
304
|
+
| `CleaningDecision` | One auditable learned or configured decision |
|
|
251
305
|
| `ColumnSpec` | Declarative expectations and semantics for one column |
|
|
252
306
|
| `DataContract` | A set of column specs, with inference and (de)serialization |
|
|
253
307
|
| `DriftReport` | Findings from a contract check, with a Markdown summary |
|
|
254
308
|
| `DataCleaner.drift_report()` | Drift findings from the last `transform` |
|
|
255
309
|
| `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
|
|
310
|
+
| `DataCleaner.decision_log()` | Tidy DataFrame of cleaning decisions |
|
|
311
|
+
| `DataCleaner.to_manifest()` | JSON-serializable artifact manifest |
|
|
312
|
+
| `DataCleaner.artifact_manifest()` | Manifest stored in a loaded artifact |
|
|
313
|
+
| `DataCleaner.fingerprint()` | Stable schema + statistics fingerprint |
|
|
314
|
+
| `fingerprint_dataframe` | Fingerprint any DataFrame for lineage |
|
|
315
|
+
| `compare_fingerprints` | Compare two fingerprints |
|
|
316
|
+
| `assert_schema_stable` | Raise when a frame's fingerprint changed |
|
|
256
317
|
| `profile_dataframe` | Column-level missing/type/cardinality profile |
|
|
257
318
|
| `dataset_summary` | Row/column/missing/duplicate summary |
|
|
258
319
|
| `drop_duplicate_rows` | Explicit duplicate removal helper |
|
|
@@ -274,16 +335,29 @@ predictions = pipeline.predict(X_test)
|
|
|
274
335
|
represented by the generated feature NaNs plus an optional missing indicator.
|
|
275
336
|
- IQR capping is skipped automatically when the IQR is zero, so constant or
|
|
276
337
|
near-constant numeric columns are not accidentally destroyed.
|
|
338
|
+
- Persist `cleaner.to_manifest(X_train, include_fingerprint=True)` alongside the
|
|
339
|
+
model so you can audit exactly which cleaning rules produced an artifact.
|
|
340
|
+
- `decision_log()` explains every learned or configured rule; ship it to your
|
|
341
|
+
experiment tracker when a model's preprocessing changes.
|
|
277
342
|
|
|
278
343
|
## Project layout
|
|
279
344
|
|
|
280
345
|
```text
|
|
281
346
|
.
|
|
282
|
-
├── data_cleaner
|
|
347
|
+
├── src/data_cleaner/ # typed, pip-installable package
|
|
348
|
+
│ ├── __init__.py # public API re-exports
|
|
349
|
+
│ ├── _version.py # version metadata
|
|
350
|
+
│ ├── _roles.py # semantic roles and dtype helpers
|
|
351
|
+
│ ├── contract.py # ColumnSpec / DataContract / DriftReport
|
|
352
|
+
│ ├── config.py # CleanerConfig
|
|
353
|
+
│ ├── cleaner.py # DataCleaner
|
|
354
|
+
│ ├── profiling.py # profiling and validation helpers
|
|
355
|
+
│ └── py.typed # PEP 561 marker
|
|
283
356
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
284
357
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
285
358
|
├── pyproject.toml # package metadata and dev tooling
|
|
286
359
|
├── uv.lock # reproducible dependency lock
|
|
360
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
287
361
|
├── README.md
|
|
288
362
|
├── ROADMAP.md
|
|
289
363
|
├── CHANGELOG.md
|
|
@@ -292,6 +366,10 @@ predictions = pipeline.predict(X_test)
|
|
|
292
366
|
└── LICENSE
|
|
293
367
|
```
|
|
294
368
|
|
|
369
|
+
The package ships a `py.typed` marker, so `mypy`, `pyright`, and IDE tooling
|
|
370
|
+
pick up the type annotations automatically. Linting (`ruff`), formatting, and
|
|
371
|
+
type checking (`mypy`) run in CI and via `pre-commit`.
|
|
372
|
+
|
|
295
373
|
## Contributing
|
|
296
374
|
|
|
297
375
|
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.3.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,47 @@
|
|
|
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 .artifacts import (
|
|
17
|
+
assert_schema_stable,
|
|
18
|
+
compare_fingerprints,
|
|
19
|
+
fingerprint_dataframe,
|
|
20
|
+
)
|
|
21
|
+
from .cleaner import DataCleaner
|
|
22
|
+
from .config import CleanerConfig
|
|
23
|
+
from .contract import ColumnSpec, DataContract, DriftReport
|
|
24
|
+
from .decisions import CleaningDecision
|
|
25
|
+
from .profiling import (
|
|
26
|
+
assert_frame_clean,
|
|
27
|
+
dataset_summary,
|
|
28
|
+
drop_duplicate_rows,
|
|
29
|
+
profile_dataframe,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CleanerConfig",
|
|
34
|
+
"CleaningDecision",
|
|
35
|
+
"ColumnSpec",
|
|
36
|
+
"DataCleaner",
|
|
37
|
+
"DataContract",
|
|
38
|
+
"DriftReport",
|
|
39
|
+
"__version__",
|
|
40
|
+
"assert_frame_clean",
|
|
41
|
+
"assert_schema_stable",
|
|
42
|
+
"compare_fingerprints",
|
|
43
|
+
"dataset_summary",
|
|
44
|
+
"drop_duplicate_rows",
|
|
45
|
+
"fingerprint_dataframe",
|
|
46
|
+
"profile_dataframe",
|
|
47
|
+
]
|
|
@@ -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)
|