Text-cleaning pipeline + context-aware per-cell field validation#133
Conversation
fd.clean declined to convert a column that was e.g. 83% numeric (below numeric_threshold) and emitted nothing, so one 'apple' inside an amount column passed through invisibly. fix_dtypes now adds a report warning naming the unparseable values and their rows whenever a clearly-dominant numeric column (>=60% parse share) is blocked by only a few odd cells (<= max(3, 10%)); mixed alphanumeric ID columns stay silent. Values are never coerced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ext) New freshdata/textclean.py: configurable ordered pipeline (Unicode NFC, control/zero-width/bidi stripping, punctuation normalization, optional HTML/URL removal, casing, repeat capping, truncation, custom ops) that never mutates the input, returns original+cleaned+transform log for every changed cell, and automatically withholds lossy operations from structural field types (amounts, identifiers, emails, URLs, dates, tickers) and entity names. Stdlib + pandas only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New freshdata/fieldcheck.py: judges each value in the context of its field via declared FieldSpecs (semantic type, ranges, vocabulary, pattern, injected reference/suggestion sources, per-field null markers) or, without a schema, via column consensus (>=80% dominant shape). Failure classes stay distinct (parse_failure / semantic_mismatch / domain_mismatch / schema_violation / statistical_outlier / categorical_rare / cross_field_inconsistency) and map to configurable non-destructive actions; apply_field_policy splits accepted/quarantined/ rejected/needs_review copies with a full audit trail. Vectorized per-column pre-screen keeps clean feeds at bulk speed; issues export as QualityFinding (step 'fieldcheck'). Tests cover the six contextual 'apple' financial cases, a nine-domain dirty-data matrix, adversarial inputs, boundaries, and a sqlite ingestion simulation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/field-validation.md (verified-executable examples, apple scenario, policy table, limitations) wired into mkdocs nav; benchmarks/ bench_fieldcheck.py reproducibly scores validate_fields vs regex and pandas-coercion baselines on a seeded corrupted financial feed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strix Security Review1 open security finding on this PR:
Review summaryThe PR adds new text-cleaning and per-cell field-validation paths, plus a warning path in dtype inference. I confirmed one issue in Unsafe regular expression execution in Updated for Reviewed by Strix |
|
Warning Review limit reached
Next review available in: 33 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds field-aware text cleaning, schema-based validation, remediation policies, audit/reporting outputs, numeric contamination warnings, public exports, documentation, tests, and a reproducible validation benchmark. ChangesData quality pipeline
Sequence Diagram(s)sequenceDiagram
participant IngestionPipeline
participant validate_fields
participant FieldValidationReport
participant apply_field_policy
participant PolicyResult
IngestionPipeline->>validate_fields: validate DataFrame with schema and policy
validate_fields-->>FieldValidationReport: classified cell issues and audits
IngestionPipeline->>apply_field_policy: apply report to original DataFrame
apply_field_policy->>FieldValidationReport: aggregate row actions
apply_field_policy-->>PolicyResult: partition rows and retain audit records
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
FreshData benchmark report —
|
| fixture | n_rows | n_cols | p50 s | p95 s | peak MB | repair % | false-repair % | preserve % | trust | monotonic | export % |
|---|---|---|---|---|---|---|---|---|---|---|---|
| crm | 10,200 | 40 | 1.480 | 1.482 | 13.8 | 100.0 | 0.0 | 100.0 | 93.84 | ✅ | 100.0 |
| event_log | 10,000 | 25 | 0.616 | 0.637 | 5.1 | 100.0 | 0.0 | 100.0 | 99.677 | ✅ | 100.0 |
| finance | 10,200 | 60 | 1.930 | 1.938 | 18.3 | 100.0 | 0.0 | 100.0 | 99.499 | ✅ | 100.0 |
| gold | 10,200 | 7 | 0.224 | 0.225 | 3.7 | 100.0 | 0.0 | 100.0 | 98.3 | ✅ | 100.0 |
| provenance | 10,000 | 18 | 0.492 | 0.492 | 7.2 | 100.0 | 0.0 | 100.0 | 99.765 | ✅ | 100.0 |
| wide_schema | 10,000 | 100 | 3.663 | 3.688 | 13.7 | 100.0 | 0.0 | 100.0 | 96.042 | ✅ | 100.0 |
Authored-code reduction (Metric 6)
- FreshData: 3 lines
- pandas baseline: 25 lines (88.0% reduction)
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/test_fieldcheck.py (1)
305-314: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused
gooddict intest_clean_frames_produce_zero_issuesThe
goodvariable is created on line 309 and deleted on line 314 without ever being read. This appears to be leftover scaffolding.♻️ Proposed cleanup
def test_clean_frames_produce_zero_issues(): for domain, (df, schema, _) in DOMAIN_CASES.items(): clean_rows = df.drop(index=df.index[-1]) - good = { - "finance": None, "healthcare": None, # earlier rows fully clean - } report = validate_fields(clean_rows.iloc[:1], schema) errors = [i for i in report.issues if i.severity == "error"] assert errors == [], f"{domain}: clean data flagged: {errors}" - del good🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_fieldcheck.py` around lines 305 - 314, Remove the unused good dictionary and its corresponding del statement from test_clean_frames_produce_zero_issues, leaving the clean_rows, validate_fields, and error assertions unchanged.src/freshdata/textclean.py (1)
210-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
normalize_punctuationstep also normalizes irregular whitespaceThe transform
_IRREGULAR_WS_RE.sub(" ", out.translate(_PUNCT_MAP))does two things: Unicode→ASCII punctuation mapping and irregular whitespace replacement. When only whitespace changes (e.g., NBSP→space), the audit trail records"normalize_punctuation", which is misleading. Test line 33 confirms this:"a b"(with U+00A0) gets transform("normalize_punctuation",)with no punctuation involved.Consider splitting into separate steps or renaming to reflect both operations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/freshdata/textclean.py` around lines 210 - 211, Update the normalize_punctuation flow to separate punctuation mapping from irregular-whitespace replacement, so whitespace-only changes are recorded under an accurate transform name rather than normalize_punctuation. Preserve the existing punctuation normalization behavior and audit both operations independently using the surrounding step mechanism.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/freshdata/textclean.py`:
- Around line 210-211: Update the normalize_punctuation flow to separate
punctuation mapping from irregular-whitespace replacement, so whitespace-only
changes are recorded under an accurate transform name rather than
normalize_punctuation. Preserve the existing punctuation normalization behavior
and audit both operations independently using the surrounding step mechanism.
In `@tests/test_fieldcheck.py`:
- Around line 305-314: Remove the unused good dictionary and its corresponding
del statement from test_clean_frames_produce_zero_issues, leaving the
clean_rows, validate_fields, and error assertions unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b75b7306-0dce-428f-ba59-aa8a9d8ff60e
📒 Files selected for processing (9)
benchmarks/bench_fieldcheck.pydocs/field-validation.mdmkdocs.ymlsrc/freshdata/__init__.pysrc/freshdata/fieldcheck.pysrc/freshdata/steps/dtypes.pysrc/freshdata/textclean.pytests/test_fieldcheck.pytests/test_textclean.py
Co-authored-by: strix-security[bot] <257889806+strix-security[bot]@users.noreply.github.com>
Co-authored-by: strix-security[bot] <257889806+strix-security[bot]@users.noreply.github.com>
The two calls to _safe_fullmatch introduced upstream referenced a function that didn't exist, breaking CI (NameError at runtime whenever a FieldSpec.pattern check ran). Added the helper: an invalid regex now degrades to a normal domain_mismatch instead of crashing validation mid-run, with a regression test covering it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Adds a production-grade text data cleaning layer and context-aware per-cell field validation, plus a root-cause fix in the core engine that stops a contaminated numeric column from passing through silently.
The driving scenario: a scraped feed inserts
"apple"into different columns of a financial pipeline. The verdict must depend on the field —transaction_amount"apple"semantic_mismatch→ quarantine (never coerced)company_name"Apple"/"apple"stock_ticker"AAPL"stock_ticker"apple"domain_mismatch; suggestsAAPLonly from a caller-supplied trusted mappingtransaction_description"Payment to Apple""apple"What's in here (4 commits)
fix_dtypessilence fix (steps/dtypes.py): a column that is ≥60% numeric but blocked from conversion by a few unparseable cells (≤ max(3, 10%)) now emits a report warning naming the values and rows. Mixed alphanumeric ID columns (e.g. titanicticket) stay silent. Nothing is coerced.fd.clean_text/fd.clean_text_value/TextCleanConfig(textclean.py): deterministic ordered pipeline — Unicode NFC, control/zero-width/bidi stripping, punctuation normalization, optional HTML/URL removal, casing, repeat capping, truncation, custom ops. Input never mutated; every changed cell logged with original + cleaned + ordered transforms. Field-aware: lossy ops are automatically withheld from structural types (amounts, identifiers, emails, URLs, dates, tickers) and entity names. Stdlib + pandas only.fd.validate_fields/FieldSpec/RemediationPolicy/fd.apply_field_policy(fieldcheck.py): per-cell validation combining declared specs (semantic type, range, vocabulary, pattern, per-field null markers, injectedreference/suggest) with value shape and column consensus. Seven distinct failure classes (no generic "outlier" bucket); rare/extreme values are warned, never auto-rejected; configurable non-destructive actions (accept…manual_review);apply_field_policysplits accepted/quarantined/rejected/needs-review copies with a full audit trail. Vectorized per-column pre-screen keeps clean feeds at bulk speed; the per-cell explanation path runs only on suspects. Issues export asQualityFinding(stepfieldcheck) into the existing quality-ops exporters.docs/field-validation.md(examples executed and verified against the API) andbenchmarks/bench_fieldcheck.py, a seeded labeled-corruption benchmark.Benchmark (seeded, reproducible:
python benchmarks/bench_fieldcheck.py 100000)fd.validate_fieldsBaselines miss out-of-range rates, unknown currencies and impossible-but-well-shaped dates;
validate_fieldscatches all labeled corruptions at ~4× the cost of bare regex (after the vectorized pre-screen; it was ~130× before). Peak extra RSS at 100k rows: ~20 MB on a ~33 MB frame.Tests
93 new tests: the six
"apple"cases, a nine-domain dirty-data matrix (finance, insurance, healthcare, e-commerce, support, HR, telecom, logistics, general business), adversarial inputs (homoglyphs, SQL/script/prompt payloads, bidi overrides, 100k-char strings), boundary cases (leap days, empty/one-row/all-null/constant/mixed columns), policy semantics (null-replacement audited, reject/quarantine/review splits), a sqlite ingestion simulation (batch, duplicates, schema change, new category, 5k-row batch), and a regression test proving the old silent-contamination behavior is fixed.ruffclean;mypyclean on the new/changed modules; wheel builds; full suite green locally (coverage gate short of 93% locally only because optional-dep tests skip, as usual).Compatibility
Purely additive public API; no existing behavior changes except the new (report-only) warning in
fix_dtypes. One golden snapshot was intentionally not regenerated — the warning guard was tightened until titanic'sticketcolumn stayed silent.Summary by CodeRabbit