Honest limitations¶
freshdata's one rule is that it never claims more than it does. This page is the single source of truth for what each path guarantees, where it materializes, and which edges are still sharp. If something here contradicts a headline number elsewhere, this page wins — please open an issue.
The short version:
- Correctness first, scale second. The accuracy-first decision engine is pandas. Native backends (Polars/DuckDB/Spark) and streaming scale the safe steps out-of-core and transparently fall back to pandas — always recorded — when a step needs the decision engine.
- Nothing is guessed silently. Ambiguous repairs become suggestions or
flags, never quiet edits.
strict=Trueturns ambiguity into a hard error. - No LLM, no cloud, no per-cell inference. The only network call in the
package is the explicit
fd.models.pull(...); cleaning never touches the wire.
Memory: what materializes and what doesn't¶
| Path | Behaviour |
|---|---|
fd.clean(df) (default pandas) |
In-memory; the whole frame is in RAM. |
engine="polars" / "duckdb" / "spark", output_format="pandas" (default) |
Scales out during the pipeline (spill-to-disk), then materializes the result into a pandas frame. |
output_format="duckdb" |
Returns an un-fetched DuckDBPyRelation — not materialized; you call .fetchdf(). |
output_format="polars-lazy" |
Returns an uncollected LazyFrame — the result is not materialized (you call .collect()), but pipeline stages currently collect intermediates eagerly, so peak memory during cleaning is comparable to eager output. DuckDB is the lower-peak-memory native path today (measured: python benchmarks/bench_outofcore.py). |
StreamingCleaner / fd.clean_timeseries(..., stream=...) |
Genuinely out-of-core: bounded micro-batches, running statistics, never concatenated. |
report.materialized is False whenever a native handle is returned, and
report.summary() says so. If a strategy needs the pandas decision engine
(balanced / aggressive imputation, dtype heuristics), the native backends
transparently fall back to pandas — recorded in report.fallback_events — and
the result is materialized. To keep the native handle use
strategy="conservative" and fix_dtypes=False — dtype fixing uses
sampled pandas heuristics and forces the fallback even under conservative
(measure it yourself: python benchmarks/bench_outofcore.py).
Streaming holds memory flat by design: bounded reservoirs and counters,
a recent-window (not global) dedup, and no cross-batch concatenation.
Which steps run natively vs. on pandas¶
- Streaming-safe (Polars/DuckDB native): column rename, whitespace/sentinel normalization, empty column/row drops, full-row dedup.
- Under streaming native dedup, exact full-row dedup does not preserve
original row order (disclosed in
report.backend_differences). SetEngineConfig(streaming_dedup=False)to preserve order — which materializes. - The accuracy-first decision engine, heuristic dtype repair, and opt-in impute/outliers run on pandas (materialized).
- pandas-only features:
contract=gates,fd.validate(suite=...)validation suites (non-pandas inputs are materialized — recorded onValidationResult.execution),memory=replay,compare_to_baseline(key=...)key-level diffs,fd.lint_text_encoding,fd.evaluate_quality_debt, and the compliance-report generators all operate on in-memory pandas frames. - Native-engine users can make an unrequested pandas materialization
impossible with
fallback_policy="error"— see the fallback matrix.
Native-engine semantic cleaning¶
The semantic stage reasons only about a column's distinct values, so on a
Polars/DuckDB engine it runs over a natively extracted distinct table (a
GROUP BY, bounded by semantic_max_distinct_values) rather than pulling the
whole frame into pandas. The distinct table is scored through the same gate as
the pandas reference path, and accepted repairs are mapped back natively with
replace/SQL CASE. This keeps the scale path out-of-core and honest — with
edges:
- Representation, not correctness. Native engines have no
objectdtype. A partially-mapped boolean column that pandas returns as a mixed[True, False, "unknown"]object column is kept as a canonical string column (["true", "false", "unknown"]) on native engines. The same cells are repaired; only the storage dtype differs. Fully-coercible columns (all values mapped) tighten toBoolean/Int64/Float64and match pandas exactly. - Lazy frames + non-string targets. On an un-collected
LazyFrame, a repair that would target a non-string column is left unchanged and disclosed inreport.fallback_events(experts target string columns, so this is rare). - Non-default backends. The native distinct path serves the default
deterministic backend. When
semantic_backendsalso includesmemory/profile/embedding, or a learnedprofile=is supplied, the whole clean routes through pandas with a recorded semantic fallback so results stay byte-identical to the reference path.
Context policies and protected columns¶
The context compiler, semantic experts (email / phone / reference lists / numbers / dates), repair plans, and the protected-column guard are all deterministic, offline code paths — no LLM, no ONNX runtime, no learned component, no network.
- Only the tier-0 context language.
context=understands the documented sentence patterns (uniqueness, protection, formats, allowed values, imputation confidence, ranges, dedup keys). Arbitrary natural language is surfaced as unparsed, never guessed at;strict=Truemakes that a hard error. - Ambiguous repairs are never auto-applied.
bob[at]gmail.com, a phone number with the wrong digit count, a typo close to two allowed values, an ambiguous01/02/2026date — these become suggestions or flags in the report/plan, not silent changes. - Phone normalization ships for
region="IN"only today; other regions compile into the policy but produce no value repairs yet. - Hard byte-identity for context-protected columns (a
protectedrule ormutable=False). Legacypreserve_columnskeeps its historical meaning — never dropped, but representation repair (whitespace, dtypes) still applies — unless the column is also context-protected.fd.apply_planadditionally guardspreserve_columns,target_column, and id columns. - Streaming compiles the policy once. A stream has no single frame, so
StreamingCleaner(context=...)compiles the policy against the first batch and applies the same protected columns and constraints to every batch — it is never recompiled, so a batch missing a column can't make the policy drift. Protected columns are excluded from the streaming imputer too, so their missing cells are preserved rather than filled. Inspectcleaner.policy_. - Undo is cell-scoped. Row drops, aggregations, and column drops are not reversible from the undo log and are never marked as such.
Models and calibration honesty¶
- The default install is model-free. Everything in this section applies only
after
pip install "freshdata-cleaner[semantic]"and an explicitfd.models.pull(...)(or air-gapped file placement). Nothing is ever downloaded during cleaning. - Official model artifacts are not hosted yet.
fd.models.pullraises a clearModelNotPublishedErroruntil they are; the air-gapped path and theFRESHDATA_MODEL_URL_BASEmirror override work today. Checksums are pinned as artifacts publish; unpinned manual placements load as unverified. - Embedding proposals are evidence, not authority. They pass the same gate as deterministic proposals, are calibrated conservatively (pure similarity clustering is capped below the auto threshold — suggest-only by default), and ambiguous matches produce no proposal at all.
- Calibration is honest, not magical. Out of the box, freshdata ships a
deterministic isotonic table that is identity for deterministic/memory
proposals and conservative for embedding ones. On CleanBench this reaches an
expected calibration error (ECE) of ~0.038, which clears the ≤ 0.05 target
but not the stricter ≤ 0.03 tier. The stricter tier requires the trained
calib-v1artifact, which is not published yet — so the strict-ECE gate is a known, disclosed gap, not a silent failure (see Benchmarks). The ">95% confidence" imputation clause remains mostly a polite refusal: honestly calibrated confidence rarely clears 0.95 outside near-deterministic cases, and freshdata preserves rather than guesses. - No LLM, no cloud, no per-cell inference — structurally: backends see distinct values only.
Benchmarks: what the numbers mean¶
CleanBench headline results are committed to the repo and reproducible with
python -m benchmarks.cleanbench ... --reproduce-headline /
--verify-results (see Benchmarks). Read them with these
caveats:
- Release evals run on synthetic and template-derived corpora reviewed by maintainers, plus a set of curated real public datasets. Strong scores there are necessary, not sufficient, evidence for arbitrary real-world data.
- The headline run uses the deterministic calibration, not
calib-v1(trained calib-v1 used: no). The strict-ECE row is expected to fail until that artifact ships; every other headline gate passes. - Performance gates compare against a same-machine pinned baseline. On a fresh machine the first run bootstraps the baseline and the perf gate is informational until then.
Requires an optional extra¶
| Feature | Extra |
|---|---|
| Polars / DuckDB / Spark backends | polars / duckdb / spark |
| Interactive HTML upgrades (tables, charts) | viz / notebook |
| NER-based PII + format-preserving encryption | privacy |
| dbt / Great Expectations / orchestration exporters | integrations (or dbt, dagster, airflow) |
| YAML domain packs & CLI | domains / cli |
| Optional learned models | semantic (+ ml for the tensor-framework build) |
The base renderers, regex PII detection, entity resolution (pandas fallback), context policies, and cleaning memory need none of these.
Experimental / evolving¶
output_format="duckdb"/"polars-lazy"native handles are new; the exact handle type follows the installed DuckDB/Polars version.- Quality-debt escalation heuristics and the dirty-join confidence scoring are tuned conservatively and may change between minor versions.
fd.lint_text_encodingis heuristic; treat "auto-repair-safe" as advisory and review before bulk-applying.- The Spark backend requires a JVM and
pyspark; it is exercised in native parity tests but is not part of the default CI matrix.
Development-time training pipeline¶
These apply only to the dev-time training/ package, never to import freshdata:
- The optional encoder contrastive-distillation stage requires a tensor
framework; without it the stage records
skippedand the Phase-3 baseline encoder is retained (its safety gates still run). - Dev artifact builds without the
onnxpackage export portable weight JSON instead of.onnxgraphs; release builds requireonnxand fail without it.