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.
Files changed (24) hide show
  1. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/PKG-INFO +89 -11
  2. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/README.md +88 -10
  3. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/pyproject.toml +44 -4
  4. universal_data_cleaner-2.3.0/src/data_cleaner/__init__.py +47 -0
  5. universal_data_cleaner-2.3.0/src/data_cleaner/_roles.py +134 -0
  6. universal_data_cleaner-2.3.0/src/data_cleaner/_version.py +4 -0
  7. universal_data_cleaner-2.3.0/src/data_cleaner/artifacts.py +171 -0
  8. universal_data_cleaner-2.3.0/src/data_cleaner/cleaner.py +991 -0
  9. universal_data_cleaner-2.3.0/src/data_cleaner/config.py +84 -0
  10. universal_data_cleaner-2.3.0/src/data_cleaner/contract.py +429 -0
  11. universal_data_cleaner-2.3.0/src/data_cleaner/decisions.py +50 -0
  12. universal_data_cleaner-2.3.0/src/data_cleaner/profiling.py +92 -0
  13. universal_data_cleaner-2.3.0/src/data_cleaner/py.typed +0 -0
  14. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/PKG-INFO +89 -11
  15. universal_data_cleaner-2.3.0/src/universal_data_cleaner.egg-info/SOURCES.txt +19 -0
  16. universal_data_cleaner-2.3.0/tests/test_data_cleaner.py +624 -0
  17. universal_data_cleaner-2.1.0/data_cleaner.py +0 -1376
  18. universal_data_cleaner-2.1.0/tests/test_data_cleaner.py +0 -299
  19. universal_data_cleaner-2.1.0/universal_data_cleaner.egg-info/SOURCES.txt +0 -10
  20. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/LICENSE +0 -0
  21. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0}/setup.cfg +0 -0
  22. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
  23. {universal_data_cleaner-2.1.0 → universal_data_cleaner-2.3.0/src}/universal_data_cleaner.egg-info/requires.txt +0 -0
  24. {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.1.0
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",), # 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,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() # 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`
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.py # reusable library module
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",), # 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,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() # 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`
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.py # reusable library module
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.1.0"
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
- 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,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)
@@ -0,0 +1,4 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "2.3.0"
4
+ _ARTIFACT_VERSION = 2