Skip to content

Refactor profiler column metrics into an extensible registry - #1384

Merged
mwojtyczka merged 31 commits into
databrickslabs:mainfrom
IvannKurchenko:feature/profiler_additional_metrics
Sep 11, 2026
Merged

Refactor profiler column metrics into an extensible registry#1384
mwojtyczka merged 31 commits into
databrickslabs:mainfrom
IvannKurchenko:feature/profiler_additional_metrics

Conversation

@IvannKurchenko

@IvannKurchenko IvannKurchenko commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Changes

Sets up an extension point for the profiler by moving inline column-metric aggregation in DQProfiler._profile into a registry-based system. Users can now register their own column metric functions via the @register_profile_column_metric() decorator, mirroring the existing register_rule / register_profile_builder extension patterns.

The three existing metrics (count_non_null, count_distinct, empty_count) are moved into this registry with no behavioural change. _build_column_metrics builds the aggregation from whatever is registered.

Rationale for landing the refactoring without new metrics

The profiling pipeline is: column metrics → profile builder → check. Adding a new metric only adds value once a builder consumes it and generates a check. Two paths were considered for #1067:

  1. Add percentile metrics (p10/p90) with a matching profile builder that emits is_aggr_not_less_than / is_aggr_not_greater_than checks. But these checks are mostly useful for measurement data (revenue, sales amount, latency, temperature) and not meaningful for keys or categorical columns. Applying them indiscriminately would generate false positives. Selective, purpose-aware application is tracked in [FEATURE]: Profile classification support #1343.
  2. Land the refactoring only, expose the extension point, and let users register the metrics they need. This keeps the profiler flexible without shipping metrics that don't yet have a purpose-fit builder.

This PR takes option 2. New built-in metrics can be added later, together with the specific builder that consumes them, once the classification work in #1343 makes selective application safe.

What changed

  • New PROFILE_COLUMN_METRIC_REGISTRY and register_profile_column_metric decorator in profiler/profiler_column_metrics.py
  • Existing metrics (count_non_null, count_distinct, empty_count) moved into the registry
  • DQProfiler._profile refactored: inline aggregation extracted into _build_column_metrics, which iterates the registry
  • is_text helper moved from profile_builder.py to profiler/common.py (now used across modules)

Linked issues

Relates to #1067, related to #1343

Tests

  • added unit tests
  • manually tested
  • added integration tests
  • added end-to-end tests
  • added performance tests

Unit tests cover: registry (register, overwrite, function-name key); built-in metric functions across column types; is_text helper; _build_column_metrics (alias correctness, count_null derivation, summary merge, empty DataFrame, None-returning metrics). Existing integration tests already cover the refactored aggregation path end-to-end.

Documentation and Demos

  • added/updated docs
  • added/updated agent skills
  • added/updated demos

New sections in data_profiling.mdx guide and profiler.mdx reference showing how to register custom metrics; dqx-profile-and-generate/SKILL.md updated with the extension point.

🤖 Generated with Claude Code

@IvannKurchenko
IvannKurchenko marked this pull request as ready for review July 29, 2026 19:35
@IvannKurchenko
IvannKurchenko requested a review from a team as a code owner July 29, 2026 19:35
@IvannKurchenko
IvannKurchenko requested review from pratikk-databricks and removed request for a team July 29, 2026 19:35

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the profiler column-metrics registry refactor. The extension-point design is reasonable, but the wiring has a blocking crash plus a few correctness regressions in _build_column_metrics — details inline.

Comment thread src/databricks/labs/dqx/profiler/profiler.py
Comment thread src/databricks/labs/dqx/profiler/profiler.py Outdated
Comment thread src/databricks/labs/dqx/profiler/profiler.py Outdated
Comment thread src/databricks/labs/dqx/profiler/profiler.py Outdated
Comment thread src/databricks/labs/dqx/profiler/profiler_column_metrics.py
Comment thread tests/integration/test_profiler.py
@mwojtyczka mwojtyczka added the under-review This PR is currently being reviewed by one of DQX maintainers. label Jul 31, 2026

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review of the head commit. The six earlier threads are all addressed — resolved them. A few new points on the extension point and conventions:

Comment thread src/databricks/labs/dqx/profiler/profiler_column_metrics.py Outdated
Comment thread src/databricks/labs/dqx/profiler/common.py
Comment thread src/databricks/labs/dqx/profiler/profiler_column_metrics.py
Comment thread src/databricks/labs/dqx/profiler/profiler_column_metrics.py Outdated
@mwojtyczka mwojtyczka added the needs-changes Changes required after review label Aug 10, 2026
@IvannKurchenko

Copy link
Copy Markdown
Contributor Author

Hello, @mwojtyczka! Thanks for a review. The previous feedback has been addressed. Would it be possible to have another round?

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few findings from a follow-up review pass (test-coverage and design-depth). None are correctness blockers.

Comment thread tests/unit/test_profiler_column_metrics.py Outdated
Comment thread src/databricks/labs/dqx/profiler/profiler.py Outdated
Comment thread tests/integration/test_profiler.py

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes. The previous issues are resolved now. Left some additional small comments - mainly missing test coverage

Comment thread src/databricks/labs/dqx/profiler/profiler.py
Comment thread docs/dqx/docs/reference/profiler.mdx Outdated
Comment thread docs/dqx/docs/guide/data_profiling.mdx

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 comments are still not fixed. Left a few additional small comments

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at head 863ecf27 — all previously-flagged issues verified resolved. One doc-correctness finding inline. (The other item from the pass — empty_count returning F.lit(0) for non-text columns — is the intentional backward-compat trade-off already discussed here, so no action needed.) Resolving the review threads that are now fixed.

Comment thread docs/dqx/docs/reference/profiler.mdx Outdated
mwojtyczka and others added 2 commits September 11, 2026 14:46
DataFrame.summary() emits percentile rows labelled 25%/50%/75%, and the
profiler stores them verbatim (_summarize: metric = row_dict["summary"]).
The reference table listed them as 25/50/75, so a custom builder doing
profiler_metrics.get("50") always got None. Document the literal keys.

Co-authored-by: Isaac <no-reply@databricks.com>
@mwojtyczka

Copy link
Copy Markdown
Contributor

Docs feedback — "Extending the profiler with custom column metrics" (docs/dqx/docs/guide/data_profiling.mdx)

A few wording/completeness issues in the new section:

1. Say "decorator" and drop the misleading "no extra cost"

Use register_profile_column_metric to add your own per-column metrics.
Metrics are computed once per column before any profile builder runs, so they are available to every builder at no extra cost.

register_profile_column_metric is a decorator (the example uses it as @register_profile_column_metric(...)), so it should read "the register_profile_column_metric decorator". More importantly, "at no extra cost" is misleading — it directly contradicts the warning admonition a few lines below, which says "Leaving unused metrics registered adds work to every profiling run and can slow it down." Each additional registered metric is an extra aggregation on every run. The "once per column, shared across builders" point is worth keeping (builders don't recompute it), but not framed as free. Suggested:

Use the register_profile_column_metric decorator to add your own per-column metrics. Each metric is computed once per column and shared across every profile builder (so builders never recompute it) — but each additional registered metric adds an aggregation to every profiling run (see the warning below).

2. profile_column_metric_type is used as an undefined term

The profile_column_metric_type passed to the decorator becomes the key under which the value is available inside profile builders.

profile_column_metric_type is the decorator's argument name in code, but a reader only sees a bare string ("percentile_10") in the example — the term appears out of nowhere and is never tied to it. Suggest connecting the two:

The string passed to the decorator's profile_column_metric_type argument (e.g. "percentile_10" in the example above) becomes the key under which the value is exposed to profile builders.

3. Document the function interfaces, not just examples

The section shows examples but never states the contract each function must satisfy, so a reader can't write one without reverse-engineering the sample. Please add the input/output signatures (both are code-verified below):

Custom column metric@register_profile_column_metric(profile_column_metric_type)

  • Receives: field (StructField — the column's schema field) and column_label (str — the column name in the DataFrame).
  • Returns: a PySpark aggregation Column, or None to skip the metric for that column type.

Custom profile builder@register_profile_builder(profile_type)

  • Receives: df (DataFrame), column_name (str), column_type (DataType), profiler_metrics (dict[str, Any] — the column-level stats computed by the profiler, keyed by metric type, including any custom metrics), and profiler_options (dict[str, Any] — profiler configuration).
  • Returns: a DQProfile (a candidate rule), or None to emit nothing for that column.

The reference page (docs/dqx/docs/reference/profiler.mdx) already describes the metric-function interface but not the builder's; the guide describes neither. Both interfaces belong in the guide, and points 1–2 above apply to the reference page's parallel wording too.

@mwojtyczka

Copy link
Copy Markdown
Contributor

One more wording nit in the same section:

Combine this with register_profile_builder to generate rules based on the metric:

The profiler produces candidate rules for review, not final rules (the sampling admonition earlier says "Manually review and tune rules generated from profiles"). Suggest:

Combine this with register_profile_builder to generate data quality rule suggestions based on the metric:

@mwojtyczka

Copy link
Copy Markdown
Contributor

…and as with the custom-metric example, this builder example needs a brief explanation of the function interface right next to it — a reader shouldn't have to reverse-engineer it from the sample. The code-verified contract (also listed in point 3 of my earlier comment):

Custom profile builder@register_profile_builder(profile_type)

  • Receives: df (DataFrame), column_name (str), column_type (DataType), profiler_metrics (dict[str, Any] — the column-level stats the profiler computed, keyed by metric type, including any custom metrics you registered), and profiler_options (dict[str, Any] — profiler configuration).
  • Returns: a DQProfile (a data quality rule suggestion), or None to emit nothing for that column.

mwojtyczka and others added 2 commits September 11, 2026 15:37
…ferences

- Call register_profile_column_metric / register_profile_builder decorators
- Drop misleading 'at no extra cost'; note each metric adds work per run
- Tie profile_column_metric_type to the concrete decorator argument
- Document the metric and builder function interfaces (inputs/outputs)
- Frame generated profiles as data quality rule suggestions
- Cross-reference the reference page's Custom Column Metrics section

Co-authored-by: Isaac <no-reply@databricks.com>
…overwrite

- Add integration test for the full extension path: a custom column metric
  consumed by a custom register_profile_builder builder, producing a DQProfile
  via the public profile() API (also asserts the built-in count_distinct metric)
- Add a builder-registry snapshot fixture for test isolation
- Add a unit test for the column-metric overwrite-and-warn path

Co-authored-by: Isaac <no-reply@databricks.com>
@mwojtyczka

Copy link
Copy Markdown
Contributor

Addressed all the docs and testing feedback above directly on this branch (commits 3103199 and a88902e):

Docs (guide/data_profiling.mdx, reference/profiler.mdx):

  • Call register_profile_column_metric / register_profile_builder decorators.
  • Dropped the misleading "at no extra cost"; now notes each registered metric adds an aggregation to every profiling run.
  • Tied profile_column_metric_type to the concrete decorator argument (e.g. "percentile_10").
  • Documented the metric- and builder-function interfaces (inputs/outputs) inline next to each example.
  • Framed generated profiles as data quality rule suggestions.
  • Cross-referenced the reference page's Custom Column Metrics section from the guide (bidirectional).

Tests:

  • New integration test exercising the full extension path (custom column metric → custom register_profile_builder → generated DQProfile via profile()), which previously had no coverage; also asserts the built-in count_distinct metric.
  • Builder-registry snapshot fixture for isolation.
  • Unit test for the column-metric overwrite-and-warn path.

@mwojtyczka mwojtyczka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - made some small doc changes and increased code coverage

@mwojtyczka mwojtyczka added Approved to Merge When PR is reviewed and approved. To be merged once all tests pass and removed under-review This PR is currently being reviewed by one of DQX maintainers. needs-changes Changes required after review labels Sep 11, 2026
@mwojtyczka
mwojtyczka merged commit 8b6539c into databrickslabs:main Sep 11, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved to Merge When PR is reviewed and approved. To be merged once all tests pass

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants