universal-data-cleaner 2.0.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.0.0 → universal_data_cleaner-2.2.0}/PKG-INFO +94 -7
- {universal_data_cleaner-2.0.0 → universal_data_cleaner-2.2.0}/README.md +93 -6
- {universal_data_cleaner-2.0.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.0.0/data_cleaner.py → universal_data_cleaner-2.2.0/src/data_cleaner/cleaner.py +127 -222
- 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.0.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/PKG-INFO +94 -7
- universal_data_cleaner-2.2.0/src/universal_data_cleaner.egg-info/SOURCES.txt +17 -0
- universal_data_cleaner-2.2.0/tests/test_data_cleaner.py +415 -0
- universal_data_cleaner-2.0.0/tests/test_data_cleaner.py +0 -195
- universal_data_cleaner-2.0.0/universal_data_cleaner.egg-info/SOURCES.txt +0 -10
- {universal_data_cleaner-2.0.0 → universal_data_cleaner-2.2.0}/LICENSE +0 -0
- {universal_data_cleaner-2.0.0 → universal_data_cleaner-2.2.0}/setup.cfg +0 -0
- {universal_data_cleaner-2.0.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
- {universal_data_cleaner-2.0.0 → universal_data_cleaner-2.2.0/src}/universal_data_cleaner.egg-info/requires.txt +0 -0
- {universal_data_cleaner-2.0.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
|
|
@@ -69,6 +69,10 @@ breaks at inference time because the median is recomputed on the new batch.
|
|
|
69
69
|
- optional missing-value indicator columns
|
|
70
70
|
- optional constant-column dropping
|
|
71
71
|
- stable output schema and column order
|
|
72
|
+
- declarative column contracts (roles, allowed values, ranges, patterns, uniqueness)
|
|
73
|
+
- automatic train-schema contract inference and transform-time drift reports
|
|
74
|
+
- `on_drift` policy: `raise`, `warn`, or `ignore`
|
|
75
|
+
- identifier columns that are never numeric-coerced, median-imputed, or capped
|
|
72
76
|
- joblib save / load with artifact versioning
|
|
73
77
|
- scikit-learn `Pipeline`, `clone`, and `get_feature_names_out` compatibility
|
|
74
78
|
|
|
@@ -149,9 +153,9 @@ For a column that should be numeric even though it arrived as dirty strings:
|
|
|
149
153
|
|
|
150
154
|
```python
|
|
151
155
|
config = CleanerConfig(
|
|
152
|
-
numeric_columns=("salary",),
|
|
153
|
-
datetime_columns=("joined",),
|
|
154
|
-
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
|
|
155
159
|
)
|
|
156
160
|
```
|
|
157
161
|
|
|
@@ -167,7 +171,7 @@ X_train, X_test, y_train, y_test = train_test_split(
|
|
|
167
171
|
cleaner = DataCleaner(config).fit(X_train)
|
|
168
172
|
|
|
169
173
|
X_train_clean = cleaner.transform(X_train)
|
|
170
|
-
X_test_clean = cleaner.transform(X_test)
|
|
174
|
+
X_test_clean = cleaner.transform(X_test) # uses train statistics only
|
|
171
175
|
```
|
|
172
176
|
|
|
173
177
|
Save the fitted cleaner with the model artifact:
|
|
@@ -177,6 +181,66 @@ cleaner.save("artifacts/data_cleaner.joblib")
|
|
|
177
181
|
restored = DataCleaner.load("artifacts/data_cleaner.joblib")
|
|
178
182
|
```
|
|
179
183
|
|
|
184
|
+
## Column contracts and drift detection
|
|
185
|
+
|
|
186
|
+
A contract makes the assumptions about your data explicit and checks them again
|
|
187
|
+
on every batch.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from data_cleaner import ColumnSpec, DataContract, CleanerConfig, DataCleaner
|
|
191
|
+
|
|
192
|
+
contract = DataContract(
|
|
193
|
+
columns=[
|
|
194
|
+
ColumnSpec("customer_id", role="id"),
|
|
195
|
+
ColumnSpec("age", role="numeric", min_value=0, max_value=120),
|
|
196
|
+
ColumnSpec("plan", role="category", allowed_values=("free", "pro", "team")),
|
|
197
|
+
],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
cleaner = DataCleaner(CleanerConfig(contract=contract, on_drift="warn")).fit(X_train)
|
|
201
|
+
|
|
202
|
+
X_test_clean = cleaner.transform(X_test) # drift is detected, never mutates
|
|
203
|
+
print(cleaner.drift_report())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If you do not pass a contract, a conservative one is inferred from the training
|
|
207
|
+
schema automatically. Drift is then reported without any configuration:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
cleaner = DataCleaner().fit(X_train)
|
|
211
|
+
cleaner.transform(X_test)
|
|
212
|
+
report = cleaner.drift_report()
|
|
213
|
+
|
|
214
|
+
if not report.empty:
|
|
215
|
+
print(report[["column", "check", "severity", "message"]])
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Supported drift checks: `missing_column`, `unexpected_column`, `dtype_change`,
|
|
219
|
+
`unexpected_missing`, `unexpected_category`, `value_out_of_range`,
|
|
220
|
+
`pattern_mismatch`, and `duplicate_values`.
|
|
221
|
+
|
|
222
|
+
Control what happens when drift is found with `CleanerConfig.on_drift`:
|
|
223
|
+
|
|
224
|
+
| `on_drift` | Behavior |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `"warn"` (default) | Log the drift report and continue |
|
|
227
|
+
| `"raise"` | Raise `ValueError` when any error-severity finding exists |
|
|
228
|
+
| `"ignore"` | Record findings in `drift_report()` without logging |
|
|
229
|
+
|
|
230
|
+
`role="id"` is worth calling out: identifier columns are treated as text, never
|
|
231
|
+
numeric-imputed, never outlier-capped, and rendered without a trailing `.0`.
|
|
232
|
+
Use `role="ignore"` to drop operational columns from the output.
|
|
233
|
+
|
|
234
|
+
Contracts can be version-controlled and shared between training and serving:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
contract.to_json() # store it with the model artifact
|
|
238
|
+
restored = DataContract.from_json(payload)
|
|
239
|
+
|
|
240
|
+
# Optional YAML support requires `pip install pyyaml`
|
|
241
|
+
# restored = DataContract.from_yaml(contract.to_yaml())
|
|
242
|
+
```
|
|
243
|
+
|
|
180
244
|
## scikit-learn Pipeline
|
|
181
245
|
|
|
182
246
|
```python
|
|
@@ -214,7 +278,12 @@ predictions = pipeline.predict(X_test)
|
|
|
214
278
|
| Name | Purpose |
|
|
215
279
|
| --- | --- |
|
|
216
280
|
| `CleanerConfig` | Validated cleaning configuration |
|
|
217
|
-
| `DataCleaner` | Fit/transform cleaner with save/load |
|
|
281
|
+
| `DataCleaner` | Fit/transform cleaner with save/load and drift reporting |
|
|
282
|
+
| `ColumnSpec` | Declarative expectations and semantics for one column |
|
|
283
|
+
| `DataContract` | A set of column specs, with inference and (de)serialization |
|
|
284
|
+
| `DriftReport` | Findings from a contract check, with a Markdown summary |
|
|
285
|
+
| `DataCleaner.drift_report()` | Drift findings from the last `transform` |
|
|
286
|
+
| `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
|
|
218
287
|
| `profile_dataframe` | Column-level missing/type/cardinality profile |
|
|
219
288
|
| `dataset_summary` | Row/column/missing/duplicate summary |
|
|
220
289
|
| `drop_duplicate_rows` | Explicit duplicate removal helper |
|
|
@@ -224,6 +293,10 @@ predictions = pipeline.predict(X_test)
|
|
|
224
293
|
|
|
225
294
|
- Fit the cleaner **after** splitting and **only** on training data.
|
|
226
295
|
- Do not pass IDs, free-text notes, or the target column to the cleaner.
|
|
296
|
+
- Mark identifier columns with `ColumnSpec(..., role="id")` so they are never
|
|
297
|
+
treated as numeric measurements.
|
|
298
|
+
- Set `on_drift="raise"` in production when an unexpected schema change should
|
|
299
|
+
stop the job instead of silently cleaning it.
|
|
227
300
|
- Keep `DataSource`-style constant columns out of the model, or let
|
|
228
301
|
`drop_constant_columns=True` remove them.
|
|
229
302
|
- `unseen_category="keep"` is the default because one-hot encoders with
|
|
@@ -237,18 +310,32 @@ predictions = pipeline.predict(X_test)
|
|
|
237
310
|
|
|
238
311
|
```text
|
|
239
312
|
.
|
|
240
|
-
├── 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
|
|
241
322
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
242
323
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
243
324
|
├── pyproject.toml # package metadata and dev tooling
|
|
244
325
|
├── uv.lock # reproducible dependency lock
|
|
326
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
245
327
|
├── README.md
|
|
328
|
+
├── ROADMAP.md
|
|
246
329
|
├── CHANGELOG.md
|
|
247
330
|
├── CONTRIBUTING.md
|
|
248
331
|
├── SECURITY.md
|
|
249
332
|
└── LICENSE
|
|
250
333
|
```
|
|
251
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
|
+
|
|
252
339
|
## Contributing
|
|
253
340
|
|
|
254
341
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local
|
|
@@ -36,6 +36,10 @@ breaks at inference time because the median is recomputed on the new batch.
|
|
|
36
36
|
- optional missing-value indicator columns
|
|
37
37
|
- optional constant-column dropping
|
|
38
38
|
- stable output schema and column order
|
|
39
|
+
- declarative column contracts (roles, allowed values, ranges, patterns, uniqueness)
|
|
40
|
+
- automatic train-schema contract inference and transform-time drift reports
|
|
41
|
+
- `on_drift` policy: `raise`, `warn`, or `ignore`
|
|
42
|
+
- identifier columns that are never numeric-coerced, median-imputed, or capped
|
|
39
43
|
- joblib save / load with artifact versioning
|
|
40
44
|
- scikit-learn `Pipeline`, `clone`, and `get_feature_names_out` compatibility
|
|
41
45
|
|
|
@@ -116,9 +120,9 @@ For a column that should be numeric even though it arrived as dirty strings:
|
|
|
116
120
|
|
|
117
121
|
```python
|
|
118
122
|
config = CleanerConfig(
|
|
119
|
-
numeric_columns=("salary",),
|
|
120
|
-
datetime_columns=("joined",),
|
|
121
|
-
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
|
|
122
126
|
)
|
|
123
127
|
```
|
|
124
128
|
|
|
@@ -134,7 +138,7 @@ X_train, X_test, y_train, y_test = train_test_split(
|
|
|
134
138
|
cleaner = DataCleaner(config).fit(X_train)
|
|
135
139
|
|
|
136
140
|
X_train_clean = cleaner.transform(X_train)
|
|
137
|
-
X_test_clean = cleaner.transform(X_test)
|
|
141
|
+
X_test_clean = cleaner.transform(X_test) # uses train statistics only
|
|
138
142
|
```
|
|
139
143
|
|
|
140
144
|
Save the fitted cleaner with the model artifact:
|
|
@@ -144,6 +148,66 @@ cleaner.save("artifacts/data_cleaner.joblib")
|
|
|
144
148
|
restored = DataCleaner.load("artifacts/data_cleaner.joblib")
|
|
145
149
|
```
|
|
146
150
|
|
|
151
|
+
## Column contracts and drift detection
|
|
152
|
+
|
|
153
|
+
A contract makes the assumptions about your data explicit and checks them again
|
|
154
|
+
on every batch.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from data_cleaner import ColumnSpec, DataContract, CleanerConfig, DataCleaner
|
|
158
|
+
|
|
159
|
+
contract = DataContract(
|
|
160
|
+
columns=[
|
|
161
|
+
ColumnSpec("customer_id", role="id"),
|
|
162
|
+
ColumnSpec("age", role="numeric", min_value=0, max_value=120),
|
|
163
|
+
ColumnSpec("plan", role="category", allowed_values=("free", "pro", "team")),
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
cleaner = DataCleaner(CleanerConfig(contract=contract, on_drift="warn")).fit(X_train)
|
|
168
|
+
|
|
169
|
+
X_test_clean = cleaner.transform(X_test) # drift is detected, never mutates
|
|
170
|
+
print(cleaner.drift_report())
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
If you do not pass a contract, a conservative one is inferred from the training
|
|
174
|
+
schema automatically. Drift is then reported without any configuration:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
cleaner = DataCleaner().fit(X_train)
|
|
178
|
+
cleaner.transform(X_test)
|
|
179
|
+
report = cleaner.drift_report()
|
|
180
|
+
|
|
181
|
+
if not report.empty:
|
|
182
|
+
print(report[["column", "check", "severity", "message"]])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Supported drift checks: `missing_column`, `unexpected_column`, `dtype_change`,
|
|
186
|
+
`unexpected_missing`, `unexpected_category`, `value_out_of_range`,
|
|
187
|
+
`pattern_mismatch`, and `duplicate_values`.
|
|
188
|
+
|
|
189
|
+
Control what happens when drift is found with `CleanerConfig.on_drift`:
|
|
190
|
+
|
|
191
|
+
| `on_drift` | Behavior |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `"warn"` (default) | Log the drift report and continue |
|
|
194
|
+
| `"raise"` | Raise `ValueError` when any error-severity finding exists |
|
|
195
|
+
| `"ignore"` | Record findings in `drift_report()` without logging |
|
|
196
|
+
|
|
197
|
+
`role="id"` is worth calling out: identifier columns are treated as text, never
|
|
198
|
+
numeric-imputed, never outlier-capped, and rendered without a trailing `.0`.
|
|
199
|
+
Use `role="ignore"` to drop operational columns from the output.
|
|
200
|
+
|
|
201
|
+
Contracts can be version-controlled and shared between training and serving:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
contract.to_json() # store it with the model artifact
|
|
205
|
+
restored = DataContract.from_json(payload)
|
|
206
|
+
|
|
207
|
+
# Optional YAML support requires `pip install pyyaml`
|
|
208
|
+
# restored = DataContract.from_yaml(contract.to_yaml())
|
|
209
|
+
```
|
|
210
|
+
|
|
147
211
|
## scikit-learn Pipeline
|
|
148
212
|
|
|
149
213
|
```python
|
|
@@ -181,7 +245,12 @@ predictions = pipeline.predict(X_test)
|
|
|
181
245
|
| Name | Purpose |
|
|
182
246
|
| --- | --- |
|
|
183
247
|
| `CleanerConfig` | Validated cleaning configuration |
|
|
184
|
-
| `DataCleaner` | Fit/transform cleaner with save/load |
|
|
248
|
+
| `DataCleaner` | Fit/transform cleaner with save/load and drift reporting |
|
|
249
|
+
| `ColumnSpec` | Declarative expectations and semantics for one column |
|
|
250
|
+
| `DataContract` | A set of column specs, with inference and (de)serialization |
|
|
251
|
+
| `DriftReport` | Findings from a contract check, with a Markdown summary |
|
|
252
|
+
| `DataCleaner.drift_report()` | Drift findings from the last `transform` |
|
|
253
|
+
| `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
|
|
185
254
|
| `profile_dataframe` | Column-level missing/type/cardinality profile |
|
|
186
255
|
| `dataset_summary` | Row/column/missing/duplicate summary |
|
|
187
256
|
| `drop_duplicate_rows` | Explicit duplicate removal helper |
|
|
@@ -191,6 +260,10 @@ predictions = pipeline.predict(X_test)
|
|
|
191
260
|
|
|
192
261
|
- Fit the cleaner **after** splitting and **only** on training data.
|
|
193
262
|
- Do not pass IDs, free-text notes, or the target column to the cleaner.
|
|
263
|
+
- Mark identifier columns with `ColumnSpec(..., role="id")` so they are never
|
|
264
|
+
treated as numeric measurements.
|
|
265
|
+
- Set `on_drift="raise"` in production when an unexpected schema change should
|
|
266
|
+
stop the job instead of silently cleaning it.
|
|
194
267
|
- Keep `DataSource`-style constant columns out of the model, or let
|
|
195
268
|
`drop_constant_columns=True` remove them.
|
|
196
269
|
- `unseen_category="keep"` is the default because one-hot encoders with
|
|
@@ -204,18 +277,32 @@ predictions = pipeline.predict(X_test)
|
|
|
204
277
|
|
|
205
278
|
```text
|
|
206
279
|
.
|
|
207
|
-
├── 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
|
|
208
289
|
├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
|
|
209
290
|
├── tests/test_data_cleaner.py # unit and integration tests
|
|
210
291
|
├── pyproject.toml # package metadata and dev tooling
|
|
211
292
|
├── uv.lock # reproducible dependency lock
|
|
293
|
+
├── .pre-commit-config.yaml # local lint/format/type hooks
|
|
212
294
|
├── README.md
|
|
295
|
+
├── ROADMAP.md
|
|
213
296
|
├── CHANGELOG.md
|
|
214
297
|
├── CONTRIBUTING.md
|
|
215
298
|
├── SECURITY.md
|
|
216
299
|
└── LICENSE
|
|
217
300
|
```
|
|
218
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
|
+
|
|
219
306
|
## Contributing
|
|
220
307
|
|
|
221
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)
|