universal-data-cleaner 2.3.0__tar.gz → 2.4.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.3.0 → universal_data_cleaner-2.4.0}/PKG-INFO +125 -2
  2. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/README.md +124 -1
  3. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/pyproject.toml +1 -1
  4. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/__init__.py +40 -1
  5. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/_version.py +1 -1
  6. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/contract.py +19 -0
  7. universal_data_cleaner-2.4.0/src/data_cleaner/integration.py +613 -0
  8. universal_data_cleaner-2.4.0/src/data_cleaner/validation.py +581 -0
  9. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/universal_data_cleaner.egg-info/PKG-INFO +125 -2
  10. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/universal_data_cleaner.egg-info/SOURCES.txt +4 -1
  11. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/tests/test_data_cleaner.py +18 -0
  12. universal_data_cleaner-2.4.0/tests/test_validation.py +399 -0
  13. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/LICENSE +0 -0
  14. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/setup.cfg +0 -0
  15. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/_roles.py +0 -0
  16. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/artifacts.py +0 -0
  17. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/cleaner.py +0 -0
  18. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/config.py +0 -0
  19. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/decisions.py +0 -0
  20. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/profiling.py +0 -0
  21. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/data_cleaner/py.typed +0 -0
  22. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/universal_data_cleaner.egg-info/dependency_links.txt +0 -0
  23. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.0}/src/universal_data_cleaner.egg-info/requires.txt +0 -0
  24. {universal_data_cleaner-2.3.0 → universal_data_cleaner-2.4.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.0
3
+ Version: 2.4.0
4
4
  Summary: Production-ready, scikit-learn-compatible data cleaning transformer for pandas.
5
5
  Author: Haseeb Khan
6
6
  License-Expression: MIT
@@ -241,6 +241,110 @@ restored = DataContract.from_json(payload)
241
241
  # restored = DataContract.from_yaml(contract.to_yaml())
242
242
  ```
243
243
 
244
+ ## Expectations, joins, and referential integrity
245
+
246
+ ### Expectations and validation reports
247
+
248
+ Run lightweight expectations against any batch and get a shareable report:
249
+
250
+ ```python
251
+ from data_cleaner import (
252
+ between,
253
+ has_dtype,
254
+ in_set,
255
+ matches_pattern,
256
+ not_null,
257
+ row_count_between,
258
+ unique,
259
+ validate,
260
+ )
261
+
262
+ report = validate(
263
+ orders_new,
264
+ [
265
+ not_null("order_id"),
266
+ unique("order_id"),
267
+ in_set("status", ["new", "paid", "shipped"]),
268
+ between("amount", 0, 100_000),
269
+ matches_pattern("email", r"[^@]+@[^@]+\.[^@]+"),
270
+ has_dtype("created_at", "datetime"),
271
+ row_count_between(min_rows=1),
272
+ ],
273
+ )
274
+ print(report.to_markdown())
275
+ report.raise_if_failed()
276
+ ```
277
+
278
+ `report.success` is true when no error-severity expectation failed.
279
+ `report.failures` includes warnings too, so use `severity="warning"` for soft
280
+ checks. `has_dtype` accepts broad kinds (`numeric`, `categorical`, `boolean`,
281
+ `datetime`) or an exact pandas dtype such as `float64`.
282
+
283
+ Contracts can validate a batch directly and return the same report type:
284
+
285
+ ```python
286
+ from data_cleaner import DataContract
287
+
288
+ contract = DataContract.from_frame(orders_train)
289
+ result = contract.validate(orders_new)
290
+ result.failures.to_markdown()
291
+ ```
292
+
293
+ `Contract` is a shorthand alias for `DataContract`.
294
+
295
+ ### Join diagnostics
296
+
297
+ ```python
298
+ from data_cleaner import diagnose_join
299
+
300
+ report = diagnose_join(orders, customers, on="customer_id", how="left")
301
+
302
+ report.safe # False on cardinality mismatch, left/right row multiplication, or null keys
303
+ report.cardinality # e.g. "1:m"
304
+ report.row_multiplier # output rows / left rows
305
+ report.unmatched_left
306
+ report.unmatched_right
307
+ report.warnings
308
+ report.raise_if_unsafe()
309
+ ```
310
+
311
+ `diagnose_join` defaults to `how="left"` and `validate="1:1"`. It reports
312
+ duplicate keys, null keys, unmatched rows, and the actual output row count
313
+ without materializing the full joined frame.
314
+
315
+ ### Referential integrity
316
+
317
+ ```python
318
+ from data_cleaner import check_referential_integrity
319
+
320
+ report = check_referential_integrity(
321
+ orders, customers, child_key="customer_id", parent_key="id"
322
+ )
323
+ report.orphan_rows
324
+ report.orphan_keys
325
+ report.raise_if_invalid()
326
+ ```
327
+
328
+ Null child keys are reported but allowed by default (`allow_null_keys=True`).
329
+ Duplicate parent keys make the relationship invalid because the parent key is
330
+ not a unique reference target.
331
+
332
+ ### Business-key deduplication
333
+
334
+ ```python
335
+ from data_cleaner import deduplicate_by_business_key
336
+
337
+ deduped, report = deduplicate_by_business_key(
338
+ orders, key="order_id", keep="last", tie_breaker="updated_at"
339
+ )
340
+ report.removed_rows
341
+ report.removed # the actual rows that were dropped
342
+ ```
343
+
344
+ The returned frame preserves the input row order and index. `tie_breaker`
345
+ prefers non-null values, and null business keys are reported explicitly.
346
+ The input frame is never mutated.
347
+
244
348
  ## Audit trail, manifests, and fingerprints
245
349
 
246
350
  Every learned or configured decision is recorded during `fit()` and exposed as
@@ -337,7 +441,21 @@ predictions = pipeline.predict(X_test)
337
441
  | `CleaningDecision` | One auditable learned or configured decision |
338
442
  | `ColumnSpec` | Declarative expectations and semantics for one column |
339
443
  | `DataContract` | A set of column specs, with inference and (de)serialization |
444
+ | `Contract` | Shorthand alias for `DataContract` |
445
+ | `DataContract.validate()` | Check a batch and return a `ValidationReport` |
340
446
  | `DriftReport` | Findings from a contract check, with a Markdown summary |
447
+ | `Expectation` | One reusable expectation produced by a factory |
448
+ | `validate` | Evaluate expectations against a frame |
449
+ | `not_null`, `unique`, `in_set`, `between` | Expectation factories |
450
+ | `matches_pattern`, `has_dtype` | Expectation factories |
451
+ | `row_count_between` | Frame-level row-count expectation |
452
+ | `ValidationReport` | Pass/fail results, failures table, Markdown, and raise helper |
453
+ | `diagnose_join` | Diagnose key duplication, cardinality, and row multiplication |
454
+ | `JoinDiagnosis` | Join report with multiplier, unmatched counts, and warnings |
455
+ | `check_referential_integrity` | Find orphan child keys and non-unique parent keys |
456
+ | `ReferentialIntegrityReport` | Orphan counts/keys, null keys, and validity |
457
+ | `deduplicate_by_business_key` | Deduplicate by key and report removed rows |
458
+ | `DeduplicationReport` | Removed rows, duplicate-key counts, and tie-breaker details |
341
459
  | `DataCleaner.drift_report()` | Drift findings from the last `transform` |
342
460
  | `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
343
461
  | `DataCleaner.decision_log()` | Tidy DataFrame of cleaning decisions |
@@ -381,13 +499,18 @@ predictions = pipeline.predict(X_test)
381
499
  │ ├── __init__.py # public API re-exports
382
500
  │ ├── _version.py # version metadata
383
501
  │ ├── _roles.py # semantic roles and dtype helpers
502
+ │ ├── artifacts.py # fingerprints and runtime metadata
384
503
  │ ├── contract.py # ColumnSpec / DataContract / DriftReport
385
504
  │ ├── config.py # CleanerConfig
386
505
  │ ├── cleaner.py # DataCleaner
506
+ │ ├── decisions.py # CleaningDecision and decision log
507
+ │ ├── integration.py # joins, referential integrity, dedup
387
508
  │ ├── profiling.py # profiling and validation helpers
509
+ │ ├── validation.py # expectations and ValidationReport
388
510
  │ └── py.typed # PEP 561 marker
389
511
  ├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
390
- ├── tests/test_data_cleaner.py # unit and integration tests
512
+ ├── tests/test_data_cleaner.py # core unit and integration tests
513
+ ├── tests/test_validation.py # expectations and integration tests
391
514
  ├── pyproject.toml # package metadata and dev tooling
392
515
  ├── uv.lock # reproducible dependency lock
393
516
  ├── .pre-commit-config.yaml # local lint/format/type hooks
@@ -208,6 +208,110 @@ restored = DataContract.from_json(payload)
208
208
  # restored = DataContract.from_yaml(contract.to_yaml())
209
209
  ```
210
210
 
211
+ ## Expectations, joins, and referential integrity
212
+
213
+ ### Expectations and validation reports
214
+
215
+ Run lightweight expectations against any batch and get a shareable report:
216
+
217
+ ```python
218
+ from data_cleaner import (
219
+ between,
220
+ has_dtype,
221
+ in_set,
222
+ matches_pattern,
223
+ not_null,
224
+ row_count_between,
225
+ unique,
226
+ validate,
227
+ )
228
+
229
+ report = validate(
230
+ orders_new,
231
+ [
232
+ not_null("order_id"),
233
+ unique("order_id"),
234
+ in_set("status", ["new", "paid", "shipped"]),
235
+ between("amount", 0, 100_000),
236
+ matches_pattern("email", r"[^@]+@[^@]+\.[^@]+"),
237
+ has_dtype("created_at", "datetime"),
238
+ row_count_between(min_rows=1),
239
+ ],
240
+ )
241
+ print(report.to_markdown())
242
+ report.raise_if_failed()
243
+ ```
244
+
245
+ `report.success` is true when no error-severity expectation failed.
246
+ `report.failures` includes warnings too, so use `severity="warning"` for soft
247
+ checks. `has_dtype` accepts broad kinds (`numeric`, `categorical`, `boolean`,
248
+ `datetime`) or an exact pandas dtype such as `float64`.
249
+
250
+ Contracts can validate a batch directly and return the same report type:
251
+
252
+ ```python
253
+ from data_cleaner import DataContract
254
+
255
+ contract = DataContract.from_frame(orders_train)
256
+ result = contract.validate(orders_new)
257
+ result.failures.to_markdown()
258
+ ```
259
+
260
+ `Contract` is a shorthand alias for `DataContract`.
261
+
262
+ ### Join diagnostics
263
+
264
+ ```python
265
+ from data_cleaner import diagnose_join
266
+
267
+ report = diagnose_join(orders, customers, on="customer_id", how="left")
268
+
269
+ report.safe # False on cardinality mismatch, left/right row multiplication, or null keys
270
+ report.cardinality # e.g. "1:m"
271
+ report.row_multiplier # output rows / left rows
272
+ report.unmatched_left
273
+ report.unmatched_right
274
+ report.warnings
275
+ report.raise_if_unsafe()
276
+ ```
277
+
278
+ `diagnose_join` defaults to `how="left"` and `validate="1:1"`. It reports
279
+ duplicate keys, null keys, unmatched rows, and the actual output row count
280
+ without materializing the full joined frame.
281
+
282
+ ### Referential integrity
283
+
284
+ ```python
285
+ from data_cleaner import check_referential_integrity
286
+
287
+ report = check_referential_integrity(
288
+ orders, customers, child_key="customer_id", parent_key="id"
289
+ )
290
+ report.orphan_rows
291
+ report.orphan_keys
292
+ report.raise_if_invalid()
293
+ ```
294
+
295
+ Null child keys are reported but allowed by default (`allow_null_keys=True`).
296
+ Duplicate parent keys make the relationship invalid because the parent key is
297
+ not a unique reference target.
298
+
299
+ ### Business-key deduplication
300
+
301
+ ```python
302
+ from data_cleaner import deduplicate_by_business_key
303
+
304
+ deduped, report = deduplicate_by_business_key(
305
+ orders, key="order_id", keep="last", tie_breaker="updated_at"
306
+ )
307
+ report.removed_rows
308
+ report.removed # the actual rows that were dropped
309
+ ```
310
+
311
+ The returned frame preserves the input row order and index. `tie_breaker`
312
+ prefers non-null values, and null business keys are reported explicitly.
313
+ The input frame is never mutated.
314
+
211
315
  ## Audit trail, manifests, and fingerprints
212
316
 
213
317
  Every learned or configured decision is recorded during `fit()` and exposed as
@@ -304,7 +408,21 @@ predictions = pipeline.predict(X_test)
304
408
  | `CleaningDecision` | One auditable learned or configured decision |
305
409
  | `ColumnSpec` | Declarative expectations and semantics for one column |
306
410
  | `DataContract` | A set of column specs, with inference and (de)serialization |
411
+ | `Contract` | Shorthand alias for `DataContract` |
412
+ | `DataContract.validate()` | Check a batch and return a `ValidationReport` |
307
413
  | `DriftReport` | Findings from a contract check, with a Markdown summary |
414
+ | `Expectation` | One reusable expectation produced by a factory |
415
+ | `validate` | Evaluate expectations against a frame |
416
+ | `not_null`, `unique`, `in_set`, `between` | Expectation factories |
417
+ | `matches_pattern`, `has_dtype` | Expectation factories |
418
+ | `row_count_between` | Frame-level row-count expectation |
419
+ | `ValidationReport` | Pass/fail results, failures table, Markdown, and raise helper |
420
+ | `diagnose_join` | Diagnose key duplication, cardinality, and row multiplication |
421
+ | `JoinDiagnosis` | Join report with multiplier, unmatched counts, and warnings |
422
+ | `check_referential_integrity` | Find orphan child keys and non-unique parent keys |
423
+ | `ReferentialIntegrityReport` | Orphan counts/keys, null keys, and validity |
424
+ | `deduplicate_by_business_key` | Deduplicate by key and report removed rows |
425
+ | `DeduplicationReport` | Removed rows, duplicate-key counts, and tie-breaker details |
308
426
  | `DataCleaner.drift_report()` | Drift findings from the last `transform` |
309
427
  | `DataCleaner.fitted_contract()` | The contract used by the fitted cleaner |
310
428
  | `DataCleaner.decision_log()` | Tidy DataFrame of cleaning decisions |
@@ -348,13 +466,18 @@ predictions = pipeline.predict(X_test)
348
466
  │ ├── __init__.py # public API re-exports
349
467
  │ ├── _version.py # version metadata
350
468
  │ ├── _roles.py # semantic roles and dtype helpers
469
+ │ ├── artifacts.py # fingerprints and runtime metadata
351
470
  │ ├── contract.py # ColumnSpec / DataContract / DriftReport
352
471
  │ ├── config.py # CleanerConfig
353
472
  │ ├── cleaner.py # DataCleaner
473
+ │ ├── decisions.py # CleaningDecision and decision log
474
+ │ ├── integration.py # joins, referential integrity, dedup
354
475
  │ ├── profiling.py # profiling and validation helpers
476
+ │ ├── validation.py # expectations and ValidationReport
355
477
  │ └── py.typed # PEP 561 marker
356
478
  ├── Universal_Data_Cleaner.ipynb # end-to-end reference workflow
357
- ├── tests/test_data_cleaner.py # unit and integration tests
479
+ ├── tests/test_data_cleaner.py # core unit and integration tests
480
+ ├── tests/test_validation.py # expectations and integration tests
358
481
  ├── pyproject.toml # package metadata and dev tooling
359
482
  ├── uv.lock # reproducible dependency lock
360
483
  ├── .pre-commit-config.yaml # local lint/format/type hooks
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "universal-data-cleaner"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "Production-ready, scikit-learn-compatible data cleaning transformer for pandas."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -20,28 +20,67 @@ from .artifacts import (
20
20
  )
21
21
  from .cleaner import DataCleaner
22
22
  from .config import CleanerConfig
23
- from .contract import ColumnSpec, DataContract, DriftReport
23
+ from .contract import ColumnSpec, Contract, DataContract, DriftReport
24
24
  from .decisions import CleaningDecision
25
+ from .integration import (
26
+ DeduplicationReport,
27
+ JoinDiagnosis,
28
+ ReferentialIntegrityReport,
29
+ check_referential_integrity,
30
+ deduplicate_by_business_key,
31
+ diagnose_join,
32
+ )
25
33
  from .profiling import (
26
34
  assert_frame_clean,
27
35
  dataset_summary,
28
36
  drop_duplicate_rows,
29
37
  profile_dataframe,
30
38
  )
39
+ from .validation import (
40
+ Expectation,
41
+ ValidationReport,
42
+ ValidationResult,
43
+ between,
44
+ has_dtype,
45
+ in_set,
46
+ matches_pattern,
47
+ not_null,
48
+ row_count_between,
49
+ unique,
50
+ validate,
51
+ )
31
52
 
32
53
  __all__ = [
33
54
  "CleanerConfig",
34
55
  "CleaningDecision",
35
56
  "ColumnSpec",
57
+ "Contract",
36
58
  "DataCleaner",
37
59
  "DataContract",
60
+ "DeduplicationReport",
38
61
  "DriftReport",
62
+ "Expectation",
63
+ "JoinDiagnosis",
64
+ "ReferentialIntegrityReport",
65
+ "ValidationReport",
66
+ "ValidationResult",
39
67
  "__version__",
40
68
  "assert_frame_clean",
41
69
  "assert_schema_stable",
70
+ "between",
71
+ "check_referential_integrity",
42
72
  "compare_fingerprints",
43
73
  "dataset_summary",
74
+ "deduplicate_by_business_key",
75
+ "diagnose_join",
44
76
  "drop_duplicate_rows",
45
77
  "fingerprint_dataframe",
78
+ "has_dtype",
79
+ "in_set",
80
+ "matches_pattern",
81
+ "not_null",
46
82
  "profile_dataframe",
83
+ "row_count_between",
84
+ "unique",
85
+ "validate",
47
86
  ]
@@ -1,4 +1,4 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.4.0"
4
4
  _ARTIFACT_VERSION = 2
@@ -16,6 +16,7 @@ from ._roles import (
16
16
  _normalize_role,
17
17
  _series_kind,
18
18
  )
19
+ from .validation import ValidationReport
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from .config import CleanerConfig
@@ -217,6 +218,20 @@ class DataContract:
217
218
  )
218
219
  return cls(columns=tuple(specs), strict=strict)
219
220
 
221
+ @classmethod
222
+ def from_frame(
223
+ cls,
224
+ frame: pd.DataFrame,
225
+ config: CleanerConfig | None = None,
226
+ strict: bool = True,
227
+ ) -> DataContract:
228
+ """Infer a contract from ``frame`` (alias for :meth:`infer_from`)."""
229
+ return cls.infer_from(frame, config=config, strict=strict)
230
+
231
+ def validate(self, frame: pd.DataFrame) -> ValidationReport:
232
+ """Check ``frame`` against this contract and return a validation report."""
233
+ return ValidationReport.from_findings(self.check(frame).findings)
234
+
220
235
  @staticmethod
221
236
  def _finding(
222
237
  spec: ColumnSpec,
@@ -427,3 +442,7 @@ class DataContract:
427
442
  "`pip install pyyaml` or use to_json()/from_json()."
428
443
  ) from exc
429
444
  return cls.from_dict(yaml.safe_load(text))
445
+
446
+
447
+ #: Shorthand alias for :class:`DataContract`.
448
+ Contract = DataContract