From bff2594c6d7c6bae31af40992e6341194334638a Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Fri, 17 Jul 2026 14:01:24 +0200 Subject: [PATCH 01/26] init --- diffly/metrics/_common.py | 9 ++- diffly/metrics/data.py | 2 +- diffly/summary.py | 76 +++++++++++++++++-- ...alse_sample_rows_False_sample_pk_False.txt | 21 +++-- ...False_sample_rows_True_sample_pk_False.txt | 21 +++-- ...True_sample_rows_False_sample_pk_False.txt | 21 +++-- ..._True_sample_rows_True_sample_pk_False.txt | 21 +++-- ...alse_sample_rows_False_sample_pk_False.txt | 34 +++++---- ..._False_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...True_sample_rows_False_sample_pk_False.txt | 34 +++++---- ...m_True_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...alse_sample_rows_False_sample_pk_False.txt | 21 +++-- ...False_sample_rows_True_sample_pk_False.txt | 21 +++-- ...True_sample_rows_False_sample_pk_False.txt | 21 +++-- ..._True_sample_rows_True_sample_pk_False.txt | 21 +++-- ...alse_sample_rows_False_sample_pk_False.txt | 34 +++++---- ..._False_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...True_sample_rows_False_sample_pk_False.txt | 34 +++++---- ...m_True_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...alse_sample_rows_False_sample_pk_False.txt | 21 +++-- ...False_sample_rows_True_sample_pk_False.txt | 21 +++-- ...True_sample_rows_False_sample_pk_False.txt | 21 +++-- ..._True_sample_rows_True_sample_pk_False.txt | 21 +++-- ...alse_sample_rows_False_sample_pk_False.txt | 34 +++++---- ..._False_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...True_sample_rows_False_sample_pk_False.txt | 34 +++++---- ...m_True_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...alse_sample_rows_False_sample_pk_False.txt | 21 +++-- ...False_sample_rows_True_sample_pk_False.txt | 21 +++-- ...True_sample_rows_False_sample_pk_False.txt | 21 +++-- ..._True_sample_rows_True_sample_pk_False.txt | 21 +++-- ...alse_sample_rows_False_sample_pk_False.txt | 34 +++++---- ..._False_sample_rows_True_sample_pk_True.txt | 37 ++++----- ...True_sample_rows_False_sample_pk_False.txt | 34 +++++---- ...m_True_sample_rows_True_sample_pk_True.txt | 37 ++++----- tests/summary/test_summary.py | 2 + 36 files changed, 640 insertions(+), 353 deletions(-) diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index 5ba7c38..73996f3 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import Literal import polars as pl import polars.selectors as cs @@ -12,10 +13,16 @@ @dataclass(frozen=True) class Metric: - """A metric function paired with a column-applicability selector.""" + """A metric function paired with a column-applicability selector. + + ``kind`` selects the summary section the metric is rendered in: ``"change"`` metrics + appear as columns in the "Columns" table, while ``"data"`` metrics get their own + "Data Inspection" section. + """ fn: MetricFn selector: cs.Selector + kind: Literal["change", "data"] = "change" MetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index 066a06b..bea08d3 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -35,7 +35,7 @@ def null_fraction_change(left: pl.Expr, right: pl.Expr) -> pl.Expr: DEFAULT_DATA_METRICS: dict[str, MetricFn | Metric] = { - "Null%": Metric(fn=null_fraction_change, selector=cs.all()), + "Null%": Metric(fn=null_fraction_change, selector=cs.all(), kind="data"), } """Preset metrics describing the left and right datasets individually.""" diff --git a/diffly/summary.py b/diffly/summary.py index ac3dda7..c811127 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -131,7 +131,9 @@ def to_json(self, **kwargs: Any) -> str: "name": "value", "match_rate": 0.667, "n_total_changes": 1, - "changes": [{"old": 1.0, "new": 2.0, "count": 1, "sample_pk": [1]}] + "changes": [{"old": 1.0, "new": 2.0, "count": 1, "sample_pk": [1]}], + "metrics": null, + "data_metrics": null } ], "sample_rows_left_only": [], @@ -179,6 +181,7 @@ def _print_diff(self, console: Console) -> None: self._print_schemas(console) self._print_rows(console) self._print_columns(console) + self._print_data_inspection(console) self._print_sample_rows_only_one_side(console, side=Side.LEFT) self._print_sample_rows_only_one_side(console, side=Side.RIGHT) @@ -571,7 +574,7 @@ def _section_columns(self) -> RenderableType: elif not columns: display_items.append(Text("All columns match perfectly.", style="italic")) else: - metric_labels = self._data._metric_labels + metric_labels = self._data._change_metric_labels matches = Table(show_header=bool(metric_labels)) matches.add_column( "Column", @@ -632,6 +635,40 @@ def _section_columns(self) -> RenderableType: return Group(*display_items) + # ------------------------------- DATA INSPECTION -------------------------------- # + + def _print_data_inspection(self, console: Console) -> None: + columns = self._data.columns + data_metric_labels = self._data._data_metric_labels + if not columns or not data_metric_labels: + return + _print_section( + console, + "Data Inspection", + self._section_data_inspection(), + ) + + def _section_data_inspection(self) -> RenderableType: + columns = self._data.columns + assert columns is not None + data_metric_labels = self._data._data_metric_labels + + table = Table() + table.add_column( + "Column", + max_width=COLUMN_SECTION_COLUMN_WIDTH, + overflow=OVERFLOW, + ) + for label in data_metric_labels: + table.add_column(label, justify="right") + for col in columns: + row_items: list[RenderableType] = [Text(col.name, style="cyan")] + for label in data_metric_labels: + value = col.data_metrics.get(label) if col.data_metrics else None + row_items.append(_format_metric_value(value)) + table.add_row(*row_items) + return table + # ------------------------------ ROWS ONLY ONE SIDE ------------------------------ # def _print_sample_rows_only_one_side(self, console: Console, side: Side) -> None: @@ -716,6 +753,7 @@ class SummaryDataColumn: n_total_changes: int changes: list[SummaryDataColumnChange] | None metrics: dict[str, Any] | None + data_metrics: dict[str, Any] | None @dataclass @@ -733,7 +771,8 @@ class SummaryData: _other_common_columns: list[str] _truncated_left_name: str _truncated_right_name: str - _metric_labels: list[str] + _change_metric_labels: list[str] + _data_metric_labels: list[str] def to_dict(self) -> dict[str, Any]: def _convert(obj: Any) -> Any: @@ -836,12 +875,18 @@ def _validate_primary_key_hidden_columns() -> None: _other_common_columns=comp._other_common_columns, _truncated_left_name=truncated_left, _truncated_right_name=truncated_right, - _metric_labels=[], + _change_metric_labels=[], + _data_metric_labels=[], ) metrics_resolved: dict[str, Metric] = dict(metrics or {}) metrics_by_column = _compute_column_metrics(comp, metrics_resolved) - metric_labels = list(metrics_resolved.keys()) + change_metric_labels = [ + label for label, m in metrics_resolved.items() if m.kind == "change" + ] + data_metric_labels = [ + label for label, m in metrics_resolved.items() if m.kind == "data" + ] schemas = _compute_schemas(comp, slim) rows = _compute_rows(comp, slim) @@ -852,6 +897,8 @@ def _validate_primary_key_hidden_columns() -> None: top_k_changes_by_column, show_sample_primary_key_per_change, metrics_by_column, + change_metric_labels, + data_metric_labels, ) sample_rows_left_only, sample_rows_right_only = _compute_sample_rows( comp, sample_k_rows_only @@ -871,7 +918,8 @@ def _validate_primary_key_hidden_columns() -> None: _other_common_columns=comp._other_common_columns, _truncated_left_name=truncated_left, _truncated_right_name=truncated_right, - _metric_labels=metric_labels, + _change_metric_labels=change_metric_labels, + _data_metric_labels=data_metric_labels, ) @@ -977,6 +1025,8 @@ def _compute_columns( top_k_changes_by_column: dict[str, int], show_sample_primary_key_per_change: bool, metrics_by_column: dict[str, dict[str, Any]], + change_metric_labels: list[str], + data_metric_labels: list[str], ) -> list[SummaryDataColumn] | None: # NOTE: We can only compute column matches if there are primary key columns and at # least one joined row. @@ -1017,13 +1067,25 @@ def _compute_columns( sample_pk=sample_pk, ) ) + col_metrics = metrics_by_column.get(col_name) or {} + change_metrics = { + label: col_metrics[label] + for label in change_metric_labels + if label in col_metrics + } + data_metrics = { + label: col_metrics[label] + for label in data_metric_labels + if label in col_metrics + } columns.append( SummaryDataColumn( name=col_name, match_rate=rate, n_total_changes=n_total_changes, changes=changes, - metrics=metrics_by_column.get(col_name), + metrics=change_metrics or None, + data_metrics=data_metrics or None, ) ) return columns diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt index f3d3d5f..93abb0f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt index f3d3d5f..93abb0f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt index 2f849f9..e076120 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt index 2f849f9..e076120 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt index 18568f4..b194b1f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,17 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt index f2a498b..bc7dd6b 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -20,20 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 06d07fe..6b6fb7e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,17 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 01c654b..f44867a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -8,20 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt index f3d3d5f..93abb0f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt index f3d3d5f..93abb0f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt index 2f849f9..e076120 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt index 2f849f9..e076120 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt index 18568f4..b194b1f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,17 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt index f2a498b..bc7dd6b 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -20,20 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 06d07fe..6b6fb7e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,17 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 01c654b..f44867a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -8,20 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃ Null% ┃ str_len_delta ┃ Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 3a2a43b..05e8662 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 3a2a43b..05e8662 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt index 1316f6b..a9a2422 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt index 1316f6b..a9a2422 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt index bdca125..b070abb 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,17 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt index cdd7114..5b88e39 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -20,20 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 370a2d3..f588b90 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,17 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 4a10c0c..21be51f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -8,20 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 3a2a43b..05e8662 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 3a2a43b..05e8662 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -20,9 +20,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt index 1316f6b..a9a2422 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt index 1316f6b..a9a2422 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -8,9 +8,18 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% (-20.0) │ │ - │ status │ 40.00% │ │ 0.0% -> 40.0% (+40.0) │ 0 │ - └────────┴────────────┴──────┴───────────────────────┴───────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ + │ status │ 40.00% │ │ 0 │ + └────────┴────────────┴──────┴───────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt index bdca125..b070abb 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -20,17 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt index cdd7114..5b88e39 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -20,20 +20,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 370a2d3..f588b90 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -8,17 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x) │ - │ │ │ │ (+40.0) │ │ "c" -> "x" (1x) │ - │ │ │ │ │ │ "b" -> None (1x) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x) │ + │ │ │ │ │ 40.0 -> 42.0 (1x) │ + │ │ │ │ │ 20.0 -> 21.0 (1x) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x) │ + │ │ │ │ │ "c" -> "x" (1x) │ + │ │ │ │ │ "b" -> None (1x) │ + └────────┴────────────┴──────┴───────────────┴───────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 4a10c0c..21be51f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -8,20 +8,23 @@ Columns ▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Match Rate ┃ Mean ┃  Null% ┃ str_len_delta ┃  Top Changes ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ - │ price  │ 40.00% │ 0.75 │ 20.0% -> 0.0% │ │ None -> 30.0 │ - │ │ │ │ (-20.0) │ │ (1x, e.g. 3) │ - │ │ │ │ │ │ 40.0 -> 42.0 │ - │ │ │ │ │ │ (1x, e.g. 4) │ - │ │ │ │ │ │ 20.0 -> 21.0 │ - │ │ │ │ │ │ (1x, e.g. 2) │ - ├────────┼────────────┼──────┼───────────────────┼───────────────┼──────────────────┤ - │ status │ 40.00% │ │ 0.0% -> 40.0% │ 0 │ "d" -> None (1x, │ - │ │ │ │ (+40.0) │ │ e.g. 4) │ - │ │ │ │ │ │ "c" -> "x" (1x, │ - │ │ │ │ │ │ e.g. 3) │ - │ │ │ │ │ │ "b" -> None (1x, │ - │ │ │ │ │ │ e.g. 2) │ - └────────┴────────────┴──────┴───────────────────┴───────────────┴──────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃ Match Rate ┃ Mean ┃ str_len_delta ┃  Top Changes ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 40.00% │ 0.75 │ │ None -> 30.0 (1x, e.g. 3) │ + │ │ │ │ │ 40.0 -> 42.0 (1x, e.g. 4) │ + │ │ │ │ │ 20.0 -> 21.0 (1x, e.g. 2) │ + ├────────┼────────────┼──────┼───────────────┼───────────────────────────┤ + │ status │ 40.00% │ │ 0 │ "d" -> None (1x, e.g. 4) │ + │ │ │ │ │ "c" -> "x" (1x, e.g. 3) │ + │ │ │ │ │ "b" -> None (1x, e.g. 2) │ + └────────┴────────────┴──────┴───────────────┴───────────────────────────┘ + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ Column ┃  Null% ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ + │ status │ 0.0% -> 40.0% (+40.0) │ + └────────┴───────────────────────┘ diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index 6e43832..ef6c70b 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -229,6 +229,7 @@ def test_summary_data_parametrized( ), # Joined rows (id=1,2,3): value deltas = [0, 5, 0]. "metrics": {"Mean": pytest.approx(5 / 3), "Max": 5.0} if with_metrics else None, + "data_metrics": None, } expected_columns = [] if show_perfect_column_matches: @@ -239,6 +240,7 @@ def test_summary_data_parametrized( "n_total_changes": 0, "changes": None, "metrics": None, + "data_metrics": None, } ) expected_columns.append(value_col) From 51fbda298ea9e9e24e10045d81f6191982d8daa7 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Fri, 17 Jul 2026 14:08:36 +0200 Subject: [PATCH 02/26] add test --- tests/summary/test_summary.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index ef6c70b..d5757c1 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -131,6 +131,27 @@ def test_zero_top_k_column_changes_with_show_sample_primary_key() -> None: ) +def test_change_and_data_metrics_routed_to_separate_fields() -> None: + from diffly.metrics.data import DEFAULT_DATA_METRICS + + # Joined rows id=1,2,3. value deltas (right - left) = [0, 5, null] → Mean = 2.5. + # value nulls: left 0/3 = 0%, right 1/3 = 33.33% → Null% = "0.0% -> 33.33% (+33.33)". + left = pl.DataFrame({"id": [1, 2, 3], "value": [10.0, 20.0, 30.0]}) + right = pl.DataFrame({"id": [1, 2, 3], "value": [10.0, 25.0, None]}) + comp = compare_frames(left, right, primary_key="id") + + summary = comp.summary( + metrics={"Mean": metrics.mean, "Null%": DEFAULT_DATA_METRICS["Null%"]}, + ) + result = json.loads(summary.to_json()) + + (value_col,) = result["columns"] + assert value_col["name"] == "value" + # Change metric lands in `metrics`, data metric in `data_metrics`. + assert value_col["metrics"] == {"Mean": pytest.approx(2.5)} + assert value_col["data_metrics"] == {"Null%": "0.0% -> 33.33% (+33.33)"} + + def _make_comparison() -> DataFrameComparison: # Designed so every parametrized flag affects the expected JSON output: # - Same columns in both frames → schemas equal → slim suppresses schemas section From cb086ef782fd5cca81bdf43a252d9daf984f68cc Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Fri, 17 Jul 2026 14:39:13 +0200 Subject: [PATCH 03/26] refactor --- diffly/metrics/__init__.py | 2 ++ diffly/metrics/data.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index a3a165b..1f39c10 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -26,6 +26,7 @@ quantile, std, ) +from .data import _make_data_metric DEFAULT_METRICS: dict[str, MetricFn | Metric] = { **change.DEFAULT_CHANGE_METRICS, @@ -47,4 +48,5 @@ "quantile", "std", "_make_numeric_metric", + "_make_data_metric", ] diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index bea08d3..4516a9e 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -17,6 +17,11 @@ from ._common import Metric, MetricFn +def _make_data_metric(fn: MetricFn, selector: cs.Selector = cs.all()) -> Metric: + """Wrap a metric function as a data metric, applicable to all columns by default.""" + return Metric(fn=fn, selector=selector, kind="data") + + def null_fraction_change(left: pl.Expr, right: pl.Expr) -> pl.Expr: """Change in the fraction of null entries, rendered as `` -> ()``. @@ -35,7 +40,7 @@ def null_fraction_change(left: pl.Expr, right: pl.Expr) -> pl.Expr: DEFAULT_DATA_METRICS: dict[str, MetricFn | Metric] = { - "Null%": Metric(fn=null_fraction_change, selector=cs.all(), kind="data"), + "Null%": _make_data_metric(null_fraction_change), } """Preset metrics describing the left and right datasets individually.""" From 526c94436a00496b93e8470e8cd1b600f378a384 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Fri, 17 Jul 2026 14:44:09 +0200 Subject: [PATCH 04/26] simplify --- diffly/summary.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/diffly/summary.py b/diffly/summary.py index c811127..74af592 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -638,9 +638,7 @@ def _section_columns(self) -> RenderableType: # ------------------------------- DATA INSPECTION -------------------------------- # def _print_data_inspection(self, console: Console) -> None: - columns = self._data.columns - data_metric_labels = self._data._data_metric_labels - if not columns or not data_metric_labels: + if not self._data.columns or not self._data._data_metric_labels: return _print_section( console, @@ -649,9 +647,7 @@ def _print_data_inspection(self, console: Console) -> None: ) def _section_data_inspection(self) -> RenderableType: - columns = self._data.columns - assert columns is not None - data_metric_labels = self._data._data_metric_labels + assert self._data.columns is not None table = Table() table.add_column( @@ -659,11 +655,11 @@ def _section_data_inspection(self) -> RenderableType: max_width=COLUMN_SECTION_COLUMN_WIDTH, overflow=OVERFLOW, ) - for label in data_metric_labels: - table.add_column(label, justify="right") - for col in columns: + for label in self._data._data_metric_labels: + table.add_column(label, justify="right", overflow=OVERFLOW) + for col in self._data.columns: row_items: list[RenderableType] = [Text(col.name, style="cyan")] - for label in data_metric_labels: + for label in self._data._data_metric_labels: value = col.data_metrics.get(label) if col.data_metrics else None row_items.append(_format_metric_value(value)) table.add_row(*row_items) From 5732c371230022e20b19c30b6dd0763a45bd9de0 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Fri, 17 Jul 2026 15:54:01 +0200 Subject: [PATCH 05/26] fix --- tests/summary/test_summary.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index d5757c1..e5c2d0c 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -13,6 +13,7 @@ from diffly import compare_frames, metrics from diffly.comparison import DataFrameComparison +from diffly.metrics.data import DEFAULT_DATA_METRICS from diffly.summary import _format_fraction_as_percentage, to_json_safe @@ -132,8 +133,6 @@ def test_zero_top_k_column_changes_with_show_sample_primary_key() -> None: def test_change_and_data_metrics_routed_to_separate_fields() -> None: - from diffly.metrics.data import DEFAULT_DATA_METRICS - # Joined rows id=1,2,3. value deltas (right - left) = [0, 5, null] → Mean = 2.5. # value nulls: left 0/3 = 0%, right 1/3 = 33.33% → Null% = "0.0% -> 33.33% (+33.33)". left = pl.DataFrame({"id": [1, 2, 3], "value": [10.0, 20.0, 30.0]}) From 5867021aa8c0a053f30885642d6c36a600d4ae02 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Wed, 22 Jul 2026 19:07:22 +0200 Subject: [PATCH 06/26] review --- diffly/summary.py | 10 ++++++---- tests/summary/test_summary.py | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/diffly/summary.py b/diffly/summary.py index 74af592..8d2bae5 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -132,7 +132,7 @@ def to_json(self, **kwargs: Any) -> str: "match_rate": 0.667, "n_total_changes": 1, "changes": [{"old": 1.0, "new": 2.0, "count": 1, "sample_pk": [1]}], - "metrics": null, + "change_metrics": null, "data_metrics": null } ], @@ -596,7 +596,9 @@ def _section_columns(self) -> RenderableType: f"{_format_fraction_as_percentage(col.match_rate)}", ] for label in metric_labels: - value = col.metrics.get(label) if col.metrics else None + value = ( + col.change_metrics.get(label) if col.change_metrics else None + ) row_items.append(_format_metric_value(value)) if col.changes is not None: change_lines = [] @@ -748,7 +750,7 @@ class SummaryDataColumn: match_rate: float n_total_changes: int changes: list[SummaryDataColumnChange] | None - metrics: dict[str, Any] | None + change_metrics: dict[str, Any] | None data_metrics: dict[str, Any] | None @@ -1080,7 +1082,7 @@ def _compute_columns( match_rate=rate, n_total_changes=n_total_changes, changes=changes, - metrics=change_metrics or None, + change_metrics=change_metrics or None, data_metrics=data_metrics or None, ) ) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index e5c2d0c..cf9e6c1 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -146,8 +146,8 @@ def test_change_and_data_metrics_routed_to_separate_fields() -> None: (value_col,) = result["columns"] assert value_col["name"] == "value" - # Change metric lands in `metrics`, data metric in `data_metrics`. - assert value_col["metrics"] == {"Mean": pytest.approx(2.5)} + # Change metric lands in `change_metrics`, data metric in `data_metrics`. + assert value_col["change_metrics"] == {"Mean": pytest.approx(2.5)} assert value_col["data_metrics"] == {"Null%": "0.0% -> 33.33% (+33.33)"} @@ -248,7 +248,9 @@ def test_summary_data_parametrized( else None ), # Joined rows (id=1,2,3): value deltas = [0, 5, 0]. - "metrics": {"Mean": pytest.approx(5 / 3), "Max": 5.0} if with_metrics else None, + "change_metrics": {"Mean": pytest.approx(5 / 3), "Max": 5.0} + if with_metrics + else None, "data_metrics": None, } expected_columns = [] @@ -259,7 +261,7 @@ def test_summary_data_parametrized( "match_rate": 1.0, "n_total_changes": 0, "changes": None, - "metrics": None, + "change_metrics": None, "data_metrics": None, } ) From 6b705e7338f73bde2e0d9b340ef96a170586c810 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Wed, 22 Jul 2026 19:09:23 +0200 Subject: [PATCH 07/26] review --- diffly/metrics/_common.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index 73996f3..aefa92c 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -15,9 +15,14 @@ class Metric: """A metric function paired with a column-applicability selector. - ``kind`` selects the summary section the metric is rendered in: ``"change"`` metrics - appear as columns in the "Columns" table, while ``"data"`` metrics get their own - "Data Inspection" section. + ``kind`` distinguishes two semantically different metric families: + + - ``"change"`` metrics aggregate over ``right - left`` to quantify the *change* + between the two sides (e.g. the mean delta). They describe the change itself and + are rendered as extra columns in the "Columns" table, alongside the match rate. + - ``"data"`` metrics describe each dataset *individually* (e.g. the fraction of null + entries on each side), characterizing the data rather than the change between the + sides. They are rendered in a dedicated "Data Inspection" section. """ fn: MetricFn From 6c868d68b3a1fc1bea5ae09173c016cee04d0e12 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Wed, 22 Jul 2026 19:48:59 +0200 Subject: [PATCH 08/26] review --- diffly/cli.py | 4 +- diffly/comparison.py | 47 ++++++---- diffly/metrics/__init__.py | 15 +++- diffly/metrics/_common.py | 70 ++++++++++----- diffly/metrics/change.py | 10 +-- diffly/metrics/data.py | 71 +++------------ diffly/summary.py | 87 +++++++++++++++---- diffly/testing.py | 32 +++---- docs/api/metrics.rst | 36 +++++--- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ...False_sample_rows_True_sample_pk_False.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ..._True_sample_rows_True_sample_pk_False.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ..._False_sample_rows_True_sample_pk_True.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ...m_True_sample_rows_True_sample_pk_True.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ...False_sample_rows_True_sample_pk_False.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ..._True_sample_rows_True_sample_pk_False.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ..._False_sample_rows_True_sample_pk_True.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ...m_True_sample_rows_True_sample_pk_True.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ...False_sample_rows_True_sample_pk_False.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ..._True_sample_rows_True_sample_pk_False.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ..._False_sample_rows_True_sample_pk_True.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ...m_True_sample_rows_True_sample_pk_True.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ...False_sample_rows_True_sample_pk_False.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ..._True_sample_rows_True_sample_pk_False.txt | 12 +-- ...alse_sample_rows_False_sample_pk_False.txt | 12 +-- ..._False_sample_rows_True_sample_pk_True.txt | 12 +-- ...True_sample_rows_False_sample_pk_False.txt | 12 +-- ...m_True_sample_rows_True_sample_pk_True.txt | 12 +-- .../test_metrics_null_fraction.py | 10 ++- tests/summary/test_summary.py | 4 +- tests/test_metrics.py | 22 ++--- 44 files changed, 434 insertions(+), 358 deletions(-) diff --git a/diffly/cli.py b/diffly/cli.py index 7177e58..9eb736a 100644 --- a/diffly/cli.py +++ b/diffly/cli.py @@ -12,14 +12,14 @@ from ._compat import typer from ._utils import ABS_TOL_DEFAULT, ABS_TOL_TEMPORAL_DEFAULT, REL_TOL_DEFAULT -from .metrics import Metric, MetricFn +from .metrics import ChangeMetricFn, Metric from .metrics.change import DEFAULT_CHANGE_METRICS from .metrics.data import DEFAULT_DATA_METRICS app = typer.Typer() #: All metric presets selectable via ``--metric``, combining the change and data sets. -AVAILABLE_METRICS: dict[str, MetricFn | Metric] = { +AVAILABLE_METRICS: dict[str, ChangeMetricFn | Metric] = { **DEFAULT_CHANGE_METRICS, **DEFAULT_DATA_METRICS, } diff --git a/diffly/comparison.py b/diffly/comparison.py index 6976b9c..622d8ee 100644 --- a/diffly/comparison.py +++ b/diffly/comparison.py @@ -4,10 +4,11 @@ from __future__ import annotations import datetime as dt +import inspect import warnings from collections.abc import Iterable, Mapping, Sequence from functools import cached_property -from typing import TYPE_CHECKING, Literal, Self, overload +from typing import TYPE_CHECKING, Literal, Self, cast, overload import polars as pl from polars.schema import Schema as PolarsSchema @@ -25,7 +26,13 @@ lazy_len, make_and_validate_mapping, ) -from .metrics import Metric, MetricFn, _make_numeric_metric +from .metrics import ( + ChangeMetric, + ChangeMetricFn, + DataMetric, + DataMetricFn, + Metric, +) if TYPE_CHECKING: # pragma: no cover # NOTE: We cannot import at runtime as we're otherwise running into circular @@ -920,7 +927,7 @@ def summary( right_name: str = Side.RIGHT, slim: bool = False, hidden_columns: list[str] | None = None, - metrics: Mapping[str, MetricFn | Metric] | None = None, + metrics: Mapping[str, ChangeMetricFn | DataMetricFn | Metric] | None = None, ) -> Summary: """Generate a summary of all aspects of the comparison. @@ -951,16 +958,16 @@ def summary( hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. metrics: Optional mapping from display label to a metric. A value may be a - callable ``(left_expr, right_expr) -> pl.Expr`` or a - :class:`~diffly.metrics.Metric`. Each callable receives two - :class:`polars.Expr` referring to the left and right values of a single - column across all joined rows, and must return a scalar aggregation - expression. Bare callables are only computed for numerical columns; wrap - one in a :class:`~diffly.metrics.Metric` with a column selector to target - other column types (e.g. ``Metric(fn, selector=cs.all())``). - See :doc:`/api/metrics` for the full list of presets and the - :data:`~diffly.metrics.MetricFn` type. When ``None`` (default), no metrics - are computed; presets are not applied automatically. Prefer short labels — + :class:`~diffly.metrics.ChangeMetric` (callable + ``(left_expr, right_expr) -> pl.Expr`` aggregating over the change), a + :class:`~diffly.metrics.DataMetric` (callable ``(col_expr) -> pl.Expr`` + evaluated on each side to describe the data), or a bare callable resolved + by its arity (two arguments → change metric on numerical columns, one + argument → data metric on all columns). To target other column types, + construct the metric explicitly with a column selector + (e.g. ``ChangeMetric(fn, selector=cs.all())``). See :doc:`/api/metrics` + for the full list of presets. When ``None`` (default), no metrics are + computed; presets are not applied automatically. Prefer short labels — the summary has a fixed width and many or long labels degrade rendering. Returns: @@ -977,11 +984,17 @@ def summary( # NOTE: We're importing here to prevent circular imports from .summary import Summary + def _resolve(v: ChangeMetricFn | DataMetricFn | Metric) -> Metric: + if isinstance(v, (ChangeMetric, DataMetric)): + return v + # Infer the metric family from the callable's arity: a single-argument + # callable describes one side (data), two arguments describe a change. + if len(inspect.signature(v).parameters) >= 2: + return ChangeMetric(fn=cast(ChangeMetricFn, v)) + return DataMetric(fn=cast(DataMetricFn, v)) + resolved_metrics = ( - { - label: v if isinstance(v, Metric) else _make_numeric_metric(v) - for label, v in metrics.items() - } + {label: _resolve(v) for label, v in metrics.items()} if metrics is not None else None ) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index 1f39c10..d338253 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -14,7 +14,14 @@ from __future__ import annotations from . import change, data -from ._common import Metric, MetricFn +from ._common import ( + ChangeMetric, + ChangeMetricFn, + DataMetric, + DataMetricFn, + Metric, + MetricFn, +) from .change import ( _make_numeric_metric, max, @@ -26,7 +33,6 @@ quantile, std, ) -from .data import _make_data_metric DEFAULT_METRICS: dict[str, MetricFn | Metric] = { **change.DEFAULT_CHANGE_METRICS, @@ -35,6 +41,10 @@ __all__ = [ "DEFAULT_METRICS", + "ChangeMetric", + "ChangeMetricFn", + "DataMetric", + "DataMetricFn", "Metric", "MetricFn", "change", @@ -48,5 +58,4 @@ "quantile", "std", "_make_numeric_metric", - "_make_data_metric", ] diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index aefa92c..b7e9038 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -4,36 +4,64 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass -from typing import Literal +from dataclasses import dataclass, field +from typing import Any import polars as pl import polars.selectors as cs +ChangeMetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] +"""A change metric maps ``(left_expr, right_expr)`` to a scalar aggregation expression. -@dataclass(frozen=True) -class Metric: - """A metric function paired with a column-applicability selector. +The expressions refer to the left-side and right-side values of a single column across +all joined rows. +""" + +DataMetricFn = Callable[[pl.Expr], pl.Expr] +"""A data metric maps a single column expression to a scalar aggregation expression. - ``kind`` distinguishes two semantically different metric families: +It is evaluated on the left and right side separately to describe each dataset on its +own, rather than the change between them. +""" + +# Retained for backwards compatibility; a plain metric callable is a change metric. +MetricFn = ChangeMetricFn + + +@dataclass(frozen=True) +class ChangeMetric: + """A metric quantifying the *change* between the two sides of a comparison. - - ``"change"`` metrics aggregate over ``right - left`` to quantify the *change* - between the two sides (e.g. the mean delta). They describe the change itself and - are rendered as extra columns in the "Columns" table, alongside the match rate. - - ``"data"`` metrics describe each dataset *individually* (e.g. the fraction of null - entries on each side), characterizing the data rather than the change between the - sides. They are rendered in a dedicated "Data Inspection" section. + ``fn`` aggregates over ``right - left`` (e.g. the mean delta) to describe the change + itself. Change metrics are rendered as extra columns in the "Columns" table, + alongside the match rate. """ - fn: MetricFn - selector: cs.Selector - kind: Literal["change", "data"] = "change" + fn: ChangeMetricFn + selector: cs.Selector = field(default_factory=cs.numeric) -MetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] -"""A metric function maps ``(left_expr, right_expr)`` to a scalar aggregation -expression. +@dataclass(frozen=True) +class DataMetric: + """A metric describing each dataset *individually*. -The expressions refer to the left-side and right-side values of a single column across -all joined rows. -""" + ``fn`` is applied to the left and right side separately (e.g. the fraction of null + entries on each side), characterizing the data rather than the change between the + sides. Data metrics are rendered in a dedicated "Data Inspection" section, showing + the left and right value side by side, followed by their signed delta for numeric + values. + + ``formatter`` formats a single left/right value for display. ``delta_formatter`` + formats the (always non-negative) magnitude of the delta, which is rendered with an + explicit sign; when ``None``, it falls back to ``formatter``. Both fall back to the + default numeric precision when unset. + """ + + fn: DataMetricFn + selector: cs.Selector = field(default_factory=cs.all) + formatter: Callable[[Any], str] | None = None + delta_formatter: Callable[[Any], str] | None = None + + +Metric = ChangeMetric | DataMetric +"""A change or data metric paired with a column-applicability selector.""" diff --git a/diffly/metrics/change.py b/diffly/metrics/change.py index f4965f1..cadf17a 100644 --- a/diffly/metrics/change.py +++ b/diffly/metrics/change.py @@ -11,11 +11,11 @@ import polars as pl import polars.selectors as cs -from ._common import Metric, MetricFn +from ._common import ChangeMetric, ChangeMetricFn -def _make_numeric_metric(fn: MetricFn) -> Metric: - return Metric(fn=fn, selector=cs.numeric()) +def _make_numeric_metric(fn: ChangeMetricFn) -> ChangeMetric: + return ChangeMetric(fn=fn, selector=cs.numeric()) def mean(left: pl.Expr, right: pl.Expr) -> pl.Expr: @@ -54,7 +54,7 @@ def mean_relative_deviation(left: pl.Expr, right: pl.Expr) -> pl.Expr: return ((right - left) / left).abs().mean() -def quantile(q: float) -> MetricFn: +def quantile(q: float) -> ChangeMetricFn: """Factory returning a metric that computes the ``q``-quantile of ``right - left``.""" if not 0 <= q <= 1: @@ -66,7 +66,7 @@ def _quantile(left: pl.Expr, right: pl.Expr) -> pl.Expr: return _quantile -DEFAULT_CHANGE_METRICS: dict[str, MetricFn] = { +DEFAULT_CHANGE_METRICS: dict[str, ChangeMetricFn] = { "Mean": mean, "Median": median, "Min": min, diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index 4516a9e..12544f2 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -9,72 +9,27 @@ from __future__ import annotations -from collections.abc import Callable - import polars as pl import polars.selectors as cs -from ._common import Metric, MetricFn - - -def _make_data_metric(fn: MetricFn, selector: cs.Selector = cs.all()) -> Metric: - """Wrap a metric function as a data metric, applicable to all columns by default.""" - return Metric(fn=fn, selector=selector, kind="data") +from ._common import DataMetric -def null_fraction_change(left: pl.Expr, right: pl.Expr) -> pl.Expr: - """Change in the fraction of null entries, rendered as `` -> ()``. +def null_fraction(col: pl.Expr) -> pl.Expr: + """Fraction of null entries in a column. - ``old`` and ``new`` are the null percentages of the left and right side, and - ``delta`` is their signed difference (``+`` when the right side has proportionally - more nulls, ``-`` when it has fewer). This metric function can be applied to columns - of any type. + Evaluated on each side separately and rendered as a percentage. This metric can be + applied to columns of any type. """ - return _render_change( - left.is_null().mean(), - right.is_null().mean(), - lambda value, signed: _percentage_string( - value, signed=signed, percent_sign=not signed - ), - ) + return col.is_null().mean() -DEFAULT_DATA_METRICS: dict[str, MetricFn | Metric] = { - "Null%": _make_data_metric(null_fraction_change), +DEFAULT_DATA_METRICS: dict[str, DataMetric] = { + "Null%": DataMetric( + fn=null_fraction, + selector=cs.all(), + formatter=lambda value: f"{round(value * 100, 2)}%", + delta_formatter=lambda value: f"{round(value * 100, 2)}", + ), } """Preset metrics describing the left and right datasets individually.""" - - -# ------------------------------------------------------------------------------------ # -# UTILITY METHODS # -# ------------------------------------------------------------------------------------ # - - -def _percentage_string( - fraction: pl.Expr, *, signed: bool = False, percent_sign: bool = True -) -> pl.Expr: - """Format a fraction as a percentage string, optionally with an explicit sign.""" - pct = (fraction * 100).round(2) - body = pl.format("{}%", pct) if percent_sign else pl.format("{}", pct) - if signed: - return pl.when(pct >= 0).then(pl.format("+{}", body)).otherwise(body) - return body - - -def _render_change( - old: pl.Expr, - new: pl.Expr, - format_value: Callable[[pl.Expr, bool], pl.Expr], -) -> pl.Expr: - """Render a change as `` -> ()``. - - ``format_value(value, signed)`` formats a value for display; ``old`` and ``new`` are - rendered unsigned and the delta ``new - old`` is rendered signed (with an explicit - ``+`` prefix for positive values). - """ - return pl.format( - "{} -> {} ({})", - format_value(old, False), - format_value(new, False), - format_value(new - old, True), - ) diff --git a/diffly/summary.py b/diffly/summary.py index 8d2bae5..e157581 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -6,7 +6,7 @@ import dataclasses import io import json -from collections.abc import Mapping +from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal @@ -23,7 +23,7 @@ from rich.text import Text from ._utils import Side, capitalize_first -from .metrics import Metric +from .metrics import ChangeMetric, DataMetric, Metric if TYPE_CHECKING: # pragma: no cover from .comparison import DataFrameComparison @@ -663,7 +663,12 @@ def _section_data_inspection(self) -> RenderableType: row_items: list[RenderableType] = [Text(col.name, style="cyan")] for label in self._data._data_metric_labels: value = col.data_metrics.get(label) if col.data_metrics else None - row_items.append(_format_metric_value(value)) + metric = self._data._data_metrics[label] + row_items.append( + _format_data_metric_value( + value, metric.formatter, metric.delta_formatter + ) + ) table.add_row(*row_items) return table @@ -771,6 +776,7 @@ class SummaryData: _truncated_right_name: str _change_metric_labels: list[str] _data_metric_labels: list[str] + _data_metrics: dict[str, DataMetric] def to_dict(self) -> dict[str, Any]: def _convert(obj: Any) -> Any: @@ -875,16 +881,20 @@ def _validate_primary_key_hidden_columns() -> None: _truncated_right_name=truncated_right, _change_metric_labels=[], _data_metric_labels=[], + _data_metrics={}, ) metrics_resolved: dict[str, Metric] = dict(metrics or {}) metrics_by_column = _compute_column_metrics(comp, metrics_resolved) change_metric_labels = [ - label for label, m in metrics_resolved.items() if m.kind == "change" + label for label, m in metrics_resolved.items() if isinstance(m, ChangeMetric) ] data_metric_labels = [ - label for label, m in metrics_resolved.items() if m.kind == "data" + label for label, m in metrics_resolved.items() if isinstance(m, DataMetric) ] + data_metrics = { + label: m for label, m in metrics_resolved.items() if isinstance(m, DataMetric) + } schemas = _compute_schemas(comp, slim) rows = _compute_rows(comp, slim) @@ -918,6 +928,7 @@ def _validate_primary_key_hidden_columns() -> None: _truncated_right_name=truncated_right, _change_metric_labels=change_metric_labels, _data_metric_labels=data_metric_labels, + _data_metrics=data_metrics, ) @@ -1001,18 +1012,27 @@ def select_columns(selector: cs.Selector) -> set[str]: out: dict[str, dict[str, Any]] = {c: {} for c in all_columns} joined = comp.joined(lazy=True) - agg_exprs = [ - metric.fn( - pl.col(f"{column}_{Side.LEFT}"), - pl.col(f"{column}_{Side.RIGHT}"), - ).alias(f"{label}__{column}") - for label, metric in metrics.items() - for column in sorted(metric_to_columns[label]) - ] + agg_exprs: list[pl.Expr] = [] + for label, metric in metrics.items(): + for column in sorted(metric_to_columns[label]): + left = pl.col(f"{column}_{Side.LEFT}") + right = pl.col(f"{column}_{Side.RIGHT}") + if isinstance(metric, ChangeMetric): + agg_exprs.append(metric.fn(left, right).alias(f"{label}__{column}")) + else: + agg_exprs.append(metric.fn(left).alias(f"{label}__{column}__left")) + agg_exprs.append(metric.fn(right).alias(f"{label}__{column}__right")) + row = joined.select(agg_exprs).collect().row(0, named=True) - for label, columns in metric_to_columns.items(): - for column in columns: - out[column][label] = row[f"{label}__{column}"] + for label, metric in metrics.items(): + for column in metric_to_columns[label]: + if isinstance(metric, ChangeMetric): + out[column][label] = row[f"{label}__{column}"] + else: + out[column][label] = { + "left": row[f"{label}__{column}__left"], + "right": row[f"{label}__{column}__right"], + } return out @@ -1201,5 +1221,40 @@ def _format_metric_value(value: Any) -> str: return _format_value(value, float_format=".4g") +def _format_data_metric_value( + value: Any, + formatter: Callable[[Any], str] | None, + delta_formatter: Callable[[Any], str] | None, +) -> str: + """Format a data metric's ``{"left", "right"}`` pair as `` -> ``. + + Numeric values additionally show the signed delta ``()``; non-numeric + values omit it. ``formatter`` formats a left/right value and ``delta_formatter`` the + delta magnitude (rendered with an explicit sign); either falling back to ``.4g`` + precision for floats when unset. + """ + if value is None: + return "" + + def _fmt(v: Any, fn: Callable[[Any], str] | None) -> str: + if fn is not None: + return fn(v) + if isinstance(v, float): + return format(v, ".4g") + return str(v) + + left, right = value["left"], value["right"] + text = f"{_fmt(left, formatter)} -> {_fmt(right, formatter)}" + if _is_numeric(left) and _is_numeric(right): + delta = right - left + sign = "+" if delta >= 0 else "-" + text += f" ({sign}{_fmt(abs(delta), delta_formatter or formatter)})" + return _yellow(text) + + +def _is_numeric(value: Any) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) + + def _trim_whitespaces(s: str) -> str: return "\n".join(line.rstrip() for line in s.splitlines()) diff --git a/diffly/testing.py b/diffly/testing.py index 9a36458..31ff6cb 100644 --- a/diffly/testing.py +++ b/diffly/testing.py @@ -19,7 +19,7 @@ from ._compat import dy from .comparison import DataFrameComparison, compare_frames -from .metrics import Metric, MetricFn +from .metrics import ChangeMetricFn, DataMetricFn, Metric def assert_collection_equal( @@ -40,7 +40,7 @@ def assert_collection_equal( right_name: str = Side.RIGHT, slim: bool = False, hidden_columns: list[str] | None = None, - metrics: Mapping[str, MetricFn | Metric] | None = None, + metrics: Mapping[str, ChangeMetricFn | DataMetricFn | Metric] | None = None, ) -> None: """Assert that two :mod:`dataframely` collections are equal. @@ -84,12 +84,13 @@ def assert_collection_equal( advanced users who are familiar with the summary format. hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. - metrics: Optional mapping from display label to a metric callable - ``(left_expr, right_expr) -> pl.Expr`` or a :class:`~diffly.metrics.Metric`. - Bare callables are only computed for numerical columns; wrap one in a - :class:`~diffly.metrics.Metric` with a column selector to target other column - types. See :mod:`diffly.metrics` for presets. When ``None`` (default), no - metrics are computed; presets are not applied automatically. + metrics: Optional mapping from display label to a + :class:`~diffly.metrics.ChangeMetric`, :class:`~diffly.metrics.DataMetric`, + or a bare callable resolved by its arity (two arguments → change metric on + numerical columns, one argument → data metric on all columns). To target + other column types, construct the metric explicitly with a column selector. + See :mod:`diffly.metrics` for presets. When ``None`` (default), no metrics + are computed; presets are not applied automatically. Raises: AssertionError: If the collections are not equal. @@ -176,7 +177,7 @@ def assert_frame_equal( right_name: str = Side.RIGHT, slim: bool = False, hidden_columns: list[str] | None = None, - metrics: Mapping[str, MetricFn | Metric] | None = None, + metrics: Mapping[str, ChangeMetricFn | DataMetricFn | Metric] | None = None, ) -> None: """Assert that two :mod:`polars` data frames are equal. @@ -227,12 +228,13 @@ def assert_frame_equal( advanced users who are familiar with the summary format. hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. - metrics: Optional mapping from display label to a metric callable - ``(left_expr, right_expr) -> pl.Expr`` or a :class:`~diffly.metrics.Metric`. - Bare callables are only computed for numerical columns; wrap one in a - :class:`~diffly.metrics.Metric` with a column selector to target other column - types. See :mod:`diffly.metrics` for presets. When ``None`` (default), no - metrics are computed; presets are not applied automatically. + metrics: Optional mapping from display label to a + :class:`~diffly.metrics.ChangeMetric`, :class:`~diffly.metrics.DataMetric`, + or a bare callable resolved by its arity (two arguments → change metric on + numerical columns, one argument → data metric on all columns). To target + other column types, construct the metric explicitly with a column selector. + See :mod:`diffly.metrics` for presets. When ``None`` (default), no metrics + are computed; presets are not applied automatically. Raises: AssertionError: If the data frames are not equal. diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 84c8f82..0ddb6d5 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -6,14 +6,23 @@ Metrics Metrics are scalar aggregations computed per column when generating a :meth:`~diffly.comparison.DataFrameComparison.summary`. Pass them via the -``metrics`` argument as a mapping from display label to a :data:`MetricFn` -callable. :mod:`diffly.metrics` ships a set of presets; you can also supply -your own callable ``(left_expr, right_expr) -> pl.Expr``. - -A bare callable is only computed for numerical columns. To target a different -set of columns, wrap it in a :class:`Metric` with a column selector, e.g. -``Metric(fn, selector=cs.all())``, ``Metric(fn, selector=cs.boolean())``, or -``Metric(fn, selector=cs.by_name("my_column_name"))``. +``metrics`` argument as a mapping from display label to a metric. There are two +families: + +- A :class:`ChangeMetric` describes the *change* between the two sides. Its + callable takes ``(left_expr, right_expr)`` and aggregates over the difference + (e.g. the mean delta). It is rendered as a column in the "Columns" table. +- A :class:`DataMetric` describes each dataset *individually*. Its callable takes + a single column expression and is evaluated on the left and right side + separately (e.g. the fraction of null entries). It is rendered in the + "Data Inspection" section, showing the left and right value side by side. + +A bare callable is resolved by its arity: a two-argument callable becomes a +:class:`ChangeMetric` (computed for numerical columns only), a one-argument +callable becomes a :class:`DataMetric` (computed for all columns). To target a +different set of columns, construct the metric explicitly with a column selector, +e.g. ``ChangeMetric(fn, selector=cs.all())`` or +``DataMetric(fn, selector=cs.boolean())``. Presets come in two families, each with its own module and default set: @@ -24,10 +33,15 @@ Presets come in two families, each with its own module and default set: The change default set is exposed as :data:`DEFAULT_METRICS`. -.. autodata:: MetricFn +.. autodata:: ChangeMetricFn + :no-value: + +.. autodata:: DataMetricFn :no-value: -.. autoclass:: Metric +.. autoclass:: ChangeMetric + +.. autoclass:: DataMetric .. autodata:: DEFAULT_METRICS :no-value: @@ -66,7 +80,7 @@ understand how a change affects the data. .. autosummary:: :toctree: _gen/ - null_fraction_change + null_fraction .. autodata:: DEFAULT_DATA_METRICS :no-value: diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 93abb0f..8df6788 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 93abb0f..8df6788 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt index e076120..b202aac 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt index e076120..b202aac 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt index b194b1f..ff0578d 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt index bc7dd6b..5c5d96f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 6b6fb7e..a15e074 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt index f44867a..7b844e8 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 93abb0f..8df6788 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 93abb0f..8df6788 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt index e076120..b202aac 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt index e076120..b202aac 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt index b194b1f..ff0578d 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt index bc7dd6b..5c5d96f 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt index 6b6fb7e..a15e074 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt index f44867a..7b844e8 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃ Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 05e8662..eb2719e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 05e8662..eb2719e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt index a9a2422..573f85a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt index a9a2422..573f85a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt index b070abb..0aa07e7 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt index 5b88e39..a21fa2c 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt index f588b90..e74afd8 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 21be51f..c5446f4 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt index 05e8662..eb2719e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt index 05e8662..eb2719e 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -29,9 +29,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt index a9a2422..573f85a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt index a9a2422..573f85a 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -17,9 +17,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt index b070abb..0aa07e7 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt index 5b88e39..a21fa2c 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -34,9 +34,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt index f588b90..e74afd8 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt index 21be51f..c5446f4 100644 --- a/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt +++ b/tests/summary/fixtures/metrics_null_fraction/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -22,9 +22,9 @@ Data Inspection ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ Column ┃  Null% ┃ - ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩ - │ price  │ 20.0% -> 0.0% (-20.0) │ - │ status │ 0.0% -> 40.0% (+40.0) │ - └────────┴───────────────────────┘ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py b/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py index 9e26bd4..376d2d7 100644 --- a/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py +++ b/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py @@ -6,7 +6,7 @@ import pytest from diffly import compare_frames, metrics -from diffly.metrics import Metric +from diffly.metrics import ChangeMetric, DataMetric from diffly.metrics.data import DEFAULT_DATA_METRICS from tests.utils import generate_summaries @@ -34,8 +34,12 @@ def test_generate() -> None: # Numeric-only preset alongside a metric applied to all columns. "Mean": metrics.mean, "Null%": DEFAULT_DATA_METRICS["Null%"], - # A user-supplied metric with a custom (string-only) selector. - "str_len_delta": Metric( + # A second, numeric data metric to render more than one data column. + "Distinct": DataMetric(fn=lambda col: col.n_unique()), + # A non-numeric data metric: rendered as ``left -> right`` without a delta. + "Max": DataMetric(fn=lambda col: col.max(), selector=cs.string()), + # A user-supplied change metric with a custom (string-only) selector. + "str_len_delta": ChangeMetric( fn=lambda left, right: ( right.str.len_chars() - left.str.len_chars() ).mean(), diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index cf9e6c1..d10f4cb 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -148,7 +148,9 @@ def test_change_and_data_metrics_routed_to_separate_fields() -> None: assert value_col["name"] == "value" # Change metric lands in `change_metrics`, data metric in `data_metrics`. assert value_col["change_metrics"] == {"Mean": pytest.approx(2.5)} - assert value_col["data_metrics"] == {"Null%": "0.0% -> 33.33% (+33.33)"} + assert value_col["data_metrics"] == { + "Null%": {"left": pytest.approx(0.0), "right": pytest.approx(1 / 3)} + } def _make_comparison() -> DataFrameComparison: diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 27f5825..8d88bc8 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -65,22 +65,16 @@ def test_mean_relative_deviation_div_by_zero() -> None: assert math.isinf(_apply(metrics.mean_relative_deviation, frame)) -def test_null_fraction_change() -> None: - # left nulls: 1/4 = 25%; right nulls: 3/4 = 75%; delta = +50% - frame = pl.DataFrame({"l": [1, None, 3, 4], "r": [None, None, 3, None]}) - assert _apply(data.null_fraction_change, frame) == "25.0% -> 75.0% (+50.0)" +def test_null_fraction() -> None: + # A data metric describes a single side: 1 null out of 4 rows. + frame = pl.DataFrame({"l": [1, None, 3, 4]}) + assert frame.select(data.null_fraction(pl.col("l"))).item() == pytest.approx(0.25) -def test_null_fraction_change_negative_delta() -> None: - # left nulls: 1/2 = 50%; right nulls: 0%; delta = -50% - frame = pl.DataFrame({"l": [1, None], "r": [1, 2]}) - assert _apply(data.null_fraction_change, frame) == "50.0% -> 0.0% (-50.0)" - - -def test_null_fraction_change_non_numeric() -> None: - # Applies to any column type; here strings. left nulls: 0%; right nulls: 50% - frame = pl.DataFrame({"l": ["a", "b"], "r": ["a", None]}) - assert _apply(data.null_fraction_change, frame) == "0.0% -> 50.0% (+50.0)" +def test_null_fraction_non_numeric() -> None: + # Applies to any column type; here strings. 1 null out of 2 rows. + frame = pl.DataFrame({"l": ["a", None]}) + assert frame.select(data.null_fraction(pl.col("l"))).item() == pytest.approx(0.5) def test_quantile(frame: pl.DataFrame) -> None: From 680259270c3328190cf9ed3e6c86f8e6fd8bfa0a Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 13:15:03 +0200 Subject: [PATCH 09/26] review --- diffly/metrics/_common.py | 48 +++++++++++++++++++++++---------------- diffly/metrics/data.py | 7 +----- pixi.lock | 40 ++++++++++++++++++++++---------- pixi.toml | 2 +- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index b7e9038..ef52746 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -11,17 +11,14 @@ import polars.selectors as cs ChangeMetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] -"""A change metric maps ``(left_expr, right_expr)`` to a scalar aggregation expression. - -The expressions refer to the left-side and right-side values of a single column across -all joined rows. -""" +"""A change metric maps the difference between two joined columns from the compared data +frames to a scalar aggregation expression.""" DataMetricFn = Callable[[pl.Expr], pl.Expr] """A data metric maps a single column expression to a scalar aggregation expression. -It is evaluated on the left and right side separately to describe each dataset on its -own, rather than the change between them. +It is evaluated on both data frames individually, and the change between them is +rendered. """ # Retained for backwards compatibility; a plain metric callable is a change metric. @@ -32,35 +29,46 @@ class ChangeMetric: """A metric quantifying the *change* between the two sides of a comparison. - ``fn`` aggregates over ``right - left`` (e.g. the mean delta) to describe the change - itself. Change metrics are rendered as extra columns in the "Columns" table, - alongside the match rate. + Change metrics are rendered as extra columns in the "Columns" table, alongside the + match rate. """ fn: ChangeMetricFn + """Aggregates over ``right - left`` (e.g. the mean delta) to describe the change + itself.""" + selector: cs.Selector = field(default_factory=cs.numeric) + """Selects the columns the metric applies to; defaults to numeric columns.""" @dataclass(frozen=True) class DataMetric: """A metric describing each dataset *individually*. - ``fn`` is applied to the left and right side separately (e.g. the fraction of null - entries on each side), characterizing the data rather than the change between the - sides. Data metrics are rendered in a dedicated "Data Inspection" section, showing - the left and right value side by side, followed by their signed delta for numeric - values. - - ``formatter`` formats a single left/right value for display. ``delta_formatter`` - formats the (always non-negative) magnitude of the delta, which is rendered with an - explicit sign; when ``None``, it falls back to ``formatter``. Both fall back to the - default numeric precision when unset. + Data metrics are rendered in a dedicated "Data Inspection" section, showing the left + and right value side by side, followed by their signed delta for numeric values. """ fn: DataMetricFn + """Applied to the left and right side separately, characterizing the data rather + than the change between the sides.""" + selector: cs.Selector = field(default_factory=cs.all) + """Selects the columns the metric applies to; defaults to all columns.""" + formatter: Callable[[Any], str] | None = None + """Formats a single left/right value for display. + + Falls back to the default numeric precision when unset. + """ + delta_formatter: Callable[[Any], str] | None = None + """Formats the (always non-negative) magnitude of the delta, which is rendered with + an explicit sign. + + Falls back to ``formatter`` when ``None``, which in turn falls back to the default + numeric precision when unset. + """ Metric = ChangeMetric | DataMetric diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index 12544f2..12b6644 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -1,6 +1,5 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause - """Metrics describing the left and right datasets individually. These characterize each side of a change so you can understand how the change affects @@ -16,11 +15,7 @@ def null_fraction(col: pl.Expr) -> pl.Expr: - """Fraction of null entries in a column. - - Evaluated on each side separately and rendered as a percentage. This metric can be - applied to columns of any type. - """ + """Fraction of null entries in a column.""" return col.is_null().mean() diff --git a/pixi.lock b/pixi.lock index 184fd51..0a6bc50 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2068,7 +2068,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda @@ -2085,6 +2085,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.5.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda linux-aarch64: @@ -2140,7 +2141,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda @@ -2157,6 +2158,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.5.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda osx-64: @@ -2164,7 +2166,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda @@ -2181,6 +2183,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.5.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2232,7 +2235,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda @@ -2249,6 +2252,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.5.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda @@ -2301,7 +2305,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.29.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda @@ -2318,6 +2322,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.5.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda @@ -7366,18 +7371,18 @@ packages: run_exports: {} size: 303705 timestamp: 1781320269259 -- conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.8-pyhc364b38_0.conda - sha256: 3e57149faca76593a869d91242dd166a9cc816cb7f01e3b39bda90e44407818e - md5: 2207be19c7b2f72e46e9d4049cb4cabc +- conda: https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.7-pyhd8ed1ab_0.conda + sha256: 290a1d9935947c72d90781efad59d7be36fc3e6c9ce4bf5d5478804ce9070006 + md5: b1e2509c25d36f7b19ceb42193120071 depends: - charset-normalizer >=3.0.0 - - python >=3.10 - - python + - python >=3.9 + - untokenize >=0.1.1 license: MIT license_family: MIT run_exports: {} - size: 41556 - timestamp: 1777489364203 + size: 30130 + timestamp: 1746994987190 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda sha256: 0d605569a77350fb681f9ed8d357cc71649b59a304099dc9d09fbeec5e84a65e md5: d6bd3cd217e62bbd7efe67ff224cd667 @@ -9526,6 +9531,17 @@ packages: run_exports: {} size: 119135 timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-pyhcf101f3_3.conda + sha256: 557f4cef95e3f239d9b7c2e75b32840f0fe1b2c62a9832da76bb51ae3d080687 + md5: 5ab2494adac58ab85da2e8e4ed0fa057 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 11029 + timestamp: 1767727736121 - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 md5: e7cb0f5745e4c5035a460248334af7eb diff --git a/pixi.toml b/pixi.toml index aa10b3d..b359b8c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -49,7 +49,7 @@ build-wheel = "python -m build --no-isolation ." check-wheel = "twine check dist/*" [feature.lint.dependencies] -docformatter = "*" +docformatter = "<1.7.8" insert-license-header = "*" pre-commit = "*" pre-commit-hooks = "*" From d827967da55fe2b31983c29838b6179119feef84 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 13:41:26 +0200 Subject: [PATCH 10/26] refactor --- diffly/metrics/__init__.py | 20 ++--------- diffly/metrics/_common.py | 71 ++------------------------------------ diffly/metrics/change.py | 24 ++++++++++--- diffly/metrics/data.py | 41 +++++++++++++++++++++- docs/api/index.rst | 2 +- docs/api/metrics.rst | 6 ++-- tests/test_metrics.py | 8 ++--- 7 files changed, 71 insertions(+), 101 deletions(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index d338253..a349e28 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -1,6 +1,5 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause - """Metrics computed per column when generating a summary. Two families are provided: @@ -14,16 +13,10 @@ from __future__ import annotations from . import change, data -from ._common import ( +from ._common import Metric +from .change import ( ChangeMetric, ChangeMetricFn, - DataMetric, - DataMetricFn, - Metric, - MetricFn, -) -from .change import ( - _make_numeric_metric, max, mean, mean_absolute_deviation, @@ -33,20 +26,14 @@ quantile, std, ) - -DEFAULT_METRICS: dict[str, MetricFn | Metric] = { - **change.DEFAULT_CHANGE_METRICS, -} -"""The default preset metrics, consisting of the change default set.""" +from .data import DataMetric, DataMetricFn __all__ = [ - "DEFAULT_METRICS", "ChangeMetric", "ChangeMetricFn", "DataMetric", "DataMetricFn", "Metric", - "MetricFn", "change", "data", "max", @@ -57,5 +44,4 @@ "min", "quantile", "std", - "_make_numeric_metric", ] diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index ef52746..5958864 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -1,75 +1,8 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass, field -from typing import Any - -import polars as pl -import polars.selectors as cs - -ChangeMetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] -"""A change metric maps the difference between two joined columns from the compared data -frames to a scalar aggregation expression.""" - -DataMetricFn = Callable[[pl.Expr], pl.Expr] -"""A data metric maps a single column expression to a scalar aggregation expression. - -It is evaluated on both data frames individually, and the change between them is -rendered. -""" - -# Retained for backwards compatibility; a plain metric callable is a change metric. -MetricFn = ChangeMetricFn - - -@dataclass(frozen=True) -class ChangeMetric: - """A metric quantifying the *change* between the two sides of a comparison. - - Change metrics are rendered as extra columns in the "Columns" table, alongside the - match rate. - """ - - fn: ChangeMetricFn - """Aggregates over ``right - left`` (e.g. the mean delta) to describe the change - itself.""" - - selector: cs.Selector = field(default_factory=cs.numeric) - """Selects the columns the metric applies to; defaults to numeric columns.""" - - -@dataclass(frozen=True) -class DataMetric: - """A metric describing each dataset *individually*. - - Data metrics are rendered in a dedicated "Data Inspection" section, showing the left - and right value side by side, followed by their signed delta for numeric values. - """ - - fn: DataMetricFn - """Applied to the left and right side separately, characterizing the data rather - than the change between the sides.""" - - selector: cs.Selector = field(default_factory=cs.all) - """Selects the columns the metric applies to; defaults to all columns.""" - - formatter: Callable[[Any], str] | None = None - """Formats a single left/right value for display. - - Falls back to the default numeric precision when unset. - """ - - delta_formatter: Callable[[Any], str] | None = None - """Formats the (always non-negative) magnitude of the delta, which is rendered with - an explicit sign. - - Falls back to ``formatter`` when ``None``, which in turn falls back to the default - numeric precision when unset. - """ - +from .change import ChangeMetric +from .data import DataMetric Metric = ChangeMetric | DataMetric """A change or data metric paired with a column-applicability selector.""" diff --git a/diffly/metrics/change.py b/diffly/metrics/change.py index cadf17a..63478a3 100644 --- a/diffly/metrics/change.py +++ b/diffly/metrics/change.py @@ -1,6 +1,5 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause - """Metrics describing the change between numeric columns. These aggregate over ``right - left`` to characterize the change itself. @@ -8,14 +7,31 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass, field + import polars as pl import polars.selectors as cs -from ._common import ChangeMetric, ChangeMetricFn +ChangeMetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] +"""A change metric maps the difference between two joined columns from the compared data +frames to a scalar aggregation expression.""" + + +@dataclass(frozen=True) +class ChangeMetric: + """A metric quantifying the *change* between the two sides of a comparison. + + Change metrics are rendered as extra columns in the "Columns" table, alongside the + match rate. + """ + fn: ChangeMetricFn + """Aggregates over ``right - left`` (e.g. the mean delta) to describe the change + itself.""" -def _make_numeric_metric(fn: ChangeMetricFn) -> ChangeMetric: - return ChangeMetric(fn=fn, selector=cs.numeric()) + selector: cs.Selector = field(default_factory=cs.numeric) + """Selects the columns the metric applies to; defaults to numeric columns.""" def mean(left: pl.Expr, right: pl.Expr) -> pl.Expr: diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index 12b6644..c21e434 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -8,10 +8,49 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + import polars as pl import polars.selectors as cs -from ._common import DataMetric +DataMetricFn = Callable[[pl.Expr], pl.Expr] +"""A data metric maps a single column expression to a scalar aggregation expression. + +It is evaluated on both data frames individually, and the change between them is +rendered. +""" + + +@dataclass(frozen=True) +class DataMetric: + """A metric describing each dataset *individually*. + + Data metrics are rendered in a dedicated "Data Inspection" section, showing the left + and right value side by side, followed by their signed delta for numeric values. + """ + + fn: DataMetricFn + """Applied to the left and right side separately, characterizing the data rather + than the change between the sides.""" + + selector: cs.Selector = field(default_factory=cs.all) + """Selects the columns the metric applies to; defaults to all columns.""" + + formatter: Callable[[Any], str] | None = None + """Formats a single left/right value for display. + + Falls back to the default numeric precision when unset. + """ + + delta_formatter: Callable[[Any], str] | None = None + """Formats the (always non-negative) magnitude of the delta, which is rendered with + an explicit sign. + + Falls back to ``formatter`` when ``None``, which in turn falls back to the default + numeric precision when unset. + """ def null_fraction(col: pl.Expr) -> pl.Expr: diff --git a/docs/api/index.rst b/docs/api/index.rst index e0a44a9..741b9e0 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -27,7 +27,7 @@ API Reference :link: metrics :link-type: doc - Built-in metric presets and the ``MetricFn`` callable for ``summary(metrics=...)``. + Built-in metric presets and the metric types for ``summary(metrics=...)``. .. grid-item-card:: Testing :link: testing diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 0ddb6d5..6c81f9b 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -31,7 +31,8 @@ Presets come in two families, each with its own module and default set: - :mod:`diffly.metrics.data` describes the left and right datasets *individually*, so you can see how a change affects the data. -The change default set is exposed as :data:`DEFAULT_METRICS`. +The preset default sets are :data:`~diffly.metrics.change.DEFAULT_CHANGE_METRICS` +and :data:`~diffly.metrics.data.DEFAULT_DATA_METRICS`. .. autodata:: ChangeMetricFn :no-value: @@ -43,9 +44,6 @@ The change default set is exposed as :data:`DEFAULT_METRICS`. .. autoclass:: DataMetric -.. autodata:: DEFAULT_METRICS - :no-value: - Change metrics ============== diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 8d88bc8..d2db47f 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -8,7 +8,7 @@ import pytest from diffly import metrics -from diffly.metrics import MetricFn, data +from diffly.metrics import ChangeMetricFn, data @pytest.fixture @@ -17,7 +17,7 @@ def frame() -> pl.DataFrame: return pl.DataFrame({"l": [1, 2, 3, None], "r": [1, 2, 5, 4]}) -def _apply(metric: MetricFn, frame: pl.DataFrame) -> Any: +def _apply(metric: ChangeMetricFn, frame: pl.DataFrame) -> Any: return frame.select(metric(pl.col("l"), pl.col("r"))).item() @@ -91,7 +91,5 @@ def test_quantile_out_of_range() -> None: def test_default_metrics_partition() -> None: from diffly.metrics import change - # The top-level defaults consist of the change metrics only. - assert metrics.DEFAULT_METRICS == {**change.DEFAULT_CHANGE_METRICS} + # The change and data preset sets are disjoint. assert set(change.DEFAULT_CHANGE_METRICS) & set(data.DEFAULT_DATA_METRICS) == set() - assert list(metrics.DEFAULT_METRICS) == [*change.DEFAULT_CHANGE_METRICS] From 2a49d96fe8133487973cdbec2ef36c213468d75b Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 13:45:07 +0200 Subject: [PATCH 11/26] simplify --- diffly/cli.py | 4 ++-- diffly/metrics/change.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/diffly/cli.py b/diffly/cli.py index 9eb736a..75b9e73 100644 --- a/diffly/cli.py +++ b/diffly/cli.py @@ -12,14 +12,14 @@ from ._compat import typer from ._utils import ABS_TOL_DEFAULT, ABS_TOL_TEMPORAL_DEFAULT, REL_TOL_DEFAULT -from .metrics import ChangeMetricFn, Metric +from .metrics import Metric from .metrics.change import DEFAULT_CHANGE_METRICS from .metrics.data import DEFAULT_DATA_METRICS app = typer.Typer() #: All metric presets selectable via ``--metric``, combining the change and data sets. -AVAILABLE_METRICS: dict[str, ChangeMetricFn | Metric] = { +AVAILABLE_METRICS: dict[str, Metric] = { **DEFAULT_CHANGE_METRICS, **DEFAULT_DATA_METRICS, } diff --git a/diffly/metrics/change.py b/diffly/metrics/change.py index 63478a3..dc0c252 100644 --- a/diffly/metrics/change.py +++ b/diffly/metrics/change.py @@ -82,13 +82,13 @@ def _quantile(left: pl.Expr, right: pl.Expr) -> pl.Expr: return _quantile -DEFAULT_CHANGE_METRICS: dict[str, ChangeMetricFn] = { - "Mean": mean, - "Median": median, - "Min": min, - "Max": max, - "Std": std, - "Mean absolute deviation": mean_absolute_deviation, - "Mean relative deviation": mean_relative_deviation, +DEFAULT_CHANGE_METRICS: dict[str, ChangeMetric] = { + "Mean": ChangeMetric(fn=mean), + "Median": ChangeMetric(fn=median), + "Min": ChangeMetric(fn=min), + "Max": ChangeMetric(fn=max), + "Std": ChangeMetric(fn=std), + "Mean absolute deviation": ChangeMetric(fn=mean_absolute_deviation), + "Mean relative deviation": ChangeMetric(fn=mean_relative_deviation), } """Preset metrics describing the change between numeric columns.""" From eef3d3b0198711e53efa4bb1346f6be8d17615d1 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 13:46:45 +0200 Subject: [PATCH 12/26] simplify --- diffly/cli.py | 18 +++++------------- diffly/metrics/__init__.py | 3 ++- diffly/metrics/_common.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/diffly/cli.py b/diffly/cli.py index 75b9e73..c292e9e 100644 --- a/diffly/cli.py +++ b/diffly/cli.py @@ -12,18 +12,10 @@ from ._compat import typer from ._utils import ABS_TOL_DEFAULT, ABS_TOL_TEMPORAL_DEFAULT, REL_TOL_DEFAULT -from .metrics import Metric -from .metrics.change import DEFAULT_CHANGE_METRICS -from .metrics.data import DEFAULT_DATA_METRICS +from .metrics import ALL_METRICS app = typer.Typer() -#: All metric presets selectable via ``--metric``, combining the change and data sets. -AVAILABLE_METRICS: dict[str, Metric] = { - **DEFAULT_CHANGE_METRICS, - **DEFAULT_DATA_METRICS, -} - @app.command() def main( @@ -148,7 +140,7 @@ def main( typer.Option( help=( "Metric presets to display per column. Repeatable. " - f"Available: {', '.join(AVAILABLE_METRICS)}." + f"Available: {', '.join(ALL_METRICS)}." ) ), ] = [], @@ -162,11 +154,11 @@ def main( hidden_column = [*hidden_column, *hidden_columns] for name in metric: - if name not in AVAILABLE_METRICS: + if name not in ALL_METRICS: raise typer.BadParameter( - f"Unknown metric: {name!r}. Available: {', '.join(AVAILABLE_METRICS)}." + f"Unknown metric: {name!r}. Available: {', '.join(ALL_METRICS)}." ) - metrics = {name: AVAILABLE_METRICS[name] for name in metric} + metrics = {name: ALL_METRICS[name] for name in metric} comparison = compare_frames( pl.scan_parquet(left), diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index a349e28..c6c2688 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -13,7 +13,7 @@ from __future__ import annotations from . import change, data -from ._common import Metric +from ._common import ALL_METRICS, Metric from .change import ( ChangeMetric, ChangeMetricFn, @@ -29,6 +29,7 @@ from .data import DataMetric, DataMetricFn __all__ = [ + "ALL_METRICS", "ChangeMetric", "ChangeMetricFn", "DataMetric", diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index 5958864..dc3172c 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -1,8 +1,14 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause -from .change import ChangeMetric -from .data import DataMetric +from .change import DEFAULT_CHANGE_METRICS, ChangeMetric +from .data import DEFAULT_DATA_METRICS, DataMetric Metric = ChangeMetric | DataMetric """A change or data metric paired with a column-applicability selector.""" + +ALL_METRICS: dict[str, Metric] = { + **DEFAULT_CHANGE_METRICS, + **DEFAULT_DATA_METRICS, +} +"""All preset metrics, combining the change and data default sets.""" From e918bbf320ed0e8684d5d930334b84ddbefcfd73 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 14:04:55 +0200 Subject: [PATCH 13/26] simplify --- diffly/comparison.py | 14 +++--- diffly/metrics/__init__.py | 32 +------------- diffly/summary.py | 4 +- diffly/testing.py | 8 ++-- docs/api/metrics.rst | 43 ++++++++++--------- docs/guides/features/summary.ipynb | 15 +------ .../metrics_custom/test_metrics_custom.py | 4 +- .../test_metrics_long_labels.py | 14 +++--- .../test_metrics_null_fraction.py | 6 +-- .../test_metrics_presets_few.py | 2 +- .../test_metrics_presets_many.py | 14 +++--- tests/summary/test_summary.py | 8 +++- tests/test_metrics.py | 25 +++++------ 13 files changed, 76 insertions(+), 113 deletions(-) diff --git a/diffly/comparison.py b/diffly/comparison.py index 622d8ee..e4025fe 100644 --- a/diffly/comparison.py +++ b/diffly/comparison.py @@ -26,13 +26,9 @@ lazy_len, make_and_validate_mapping, ) -from .metrics import ( - ChangeMetric, - ChangeMetricFn, - DataMetric, - DataMetricFn, - Metric, -) +from .metrics._common import Metric +from .metrics.change import ChangeMetric, ChangeMetricFn +from .metrics.data import DataMetric, DataMetricFn if TYPE_CHECKING: # pragma: no cover # NOTE: We cannot import at runtime as we're otherwise running into circular @@ -958,9 +954,9 @@ def summary( hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. metrics: Optional mapping from display label to a metric. A value may be a - :class:`~diffly.metrics.ChangeMetric` (callable + :class:`~diffly.metrics.change.ChangeMetric` (callable ``(left_expr, right_expr) -> pl.Expr`` aggregating over the change), a - :class:`~diffly.metrics.DataMetric` (callable ``(col_expr) -> pl.Expr`` + :class:`~diffly.metrics.data.DataMetric` (callable ``(col_expr) -> pl.Expr`` evaluated on each side to describe the data), or a bare callable resolved by its arity (two arguments → change metric on numerical columns, one argument → data metric on all columns). To target other column types, diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index c6c2688..7374be6 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -14,35 +14,5 @@ from . import change, data from ._common import ALL_METRICS, Metric -from .change import ( - ChangeMetric, - ChangeMetricFn, - max, - mean, - mean_absolute_deviation, - mean_relative_deviation, - median, - min, - quantile, - std, -) -from .data import DataMetric, DataMetricFn -__all__ = [ - "ALL_METRICS", - "ChangeMetric", - "ChangeMetricFn", - "DataMetric", - "DataMetricFn", - "Metric", - "change", - "data", - "max", - "mean", - "mean_absolute_deviation", - "mean_relative_deviation", - "median", - "min", - "quantile", - "std", -] +__all__ = ["ALL_METRICS", "Metric", "change", "data"] diff --git a/diffly/summary.py b/diffly/summary.py index e157581..9f3a58e 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -23,7 +23,9 @@ from rich.text import Text from ._utils import Side, capitalize_first -from .metrics import ChangeMetric, DataMetric, Metric +from .metrics._common import Metric +from .metrics.change import ChangeMetric +from .metrics.data import DataMetric if TYPE_CHECKING: # pragma: no cover from .comparison import DataFrameComparison diff --git a/diffly/testing.py b/diffly/testing.py index 31ff6cb..b89200c 100644 --- a/diffly/testing.py +++ b/diffly/testing.py @@ -19,7 +19,9 @@ from ._compat import dy from .comparison import DataFrameComparison, compare_frames -from .metrics import ChangeMetricFn, DataMetricFn, Metric +from .metrics._common import Metric +from .metrics.change import ChangeMetricFn +from .metrics.data import DataMetricFn def assert_collection_equal( @@ -85,7 +87,7 @@ def assert_collection_equal( hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. metrics: Optional mapping from display label to a - :class:`~diffly.metrics.ChangeMetric`, :class:`~diffly.metrics.DataMetric`, + :class:`~diffly.metrics.change.ChangeMetric`, :class:`~diffly.metrics.data.DataMetric`, or a bare callable resolved by its arity (two arguments → change metric on numerical columns, one argument → data metric on all columns). To target other column types, construct the metric explicitly with a column selector. @@ -229,7 +231,7 @@ def assert_frame_equal( hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. metrics: Optional mapping from display label to a - :class:`~diffly.metrics.ChangeMetric`, :class:`~diffly.metrics.DataMetric`, + :class:`~diffly.metrics.change.ChangeMetric`, :class:`~diffly.metrics.data.DataMetric`, or a bare callable resolved by its arity (two arguments → change metric on numerical columns, one argument → data metric on all columns). To target other column types, construct the metric explicitly with a column selector. diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 6c81f9b..5163b55 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -9,20 +9,21 @@ Metrics are scalar aggregations computed per column when generating a ``metrics`` argument as a mapping from display label to a metric. There are two families: -- A :class:`ChangeMetric` describes the *change* between the two sides. Its - callable takes ``(left_expr, right_expr)`` and aggregates over the difference - (e.g. the mean delta). It is rendered as a column in the "Columns" table. -- A :class:`DataMetric` describes each dataset *individually*. Its callable takes - a single column expression and is evaluated on the left and right side - separately (e.g. the fraction of null entries). It is rendered in the +- A :class:`~diffly.metrics.change.ChangeMetric` describes the *change* between + the two sides. Its callable takes ``(left_expr, right_expr)`` and aggregates over + the difference (e.g. the mean delta). It is rendered as a column in the "Columns" + table. +- A :class:`~diffly.metrics.data.DataMetric` describes each dataset *individually*. + Its callable takes a single column expression and is evaluated on the left and + right side separately (e.g. the fraction of null entries). It is rendered in the "Data Inspection" section, showing the left and right value side by side. A bare callable is resolved by its arity: a two-argument callable becomes a -:class:`ChangeMetric` (computed for numerical columns only), a one-argument -callable becomes a :class:`DataMetric` (computed for all columns). To target a -different set of columns, construct the metric explicitly with a column selector, -e.g. ``ChangeMetric(fn, selector=cs.all())`` or -``DataMetric(fn, selector=cs.boolean())``. +:class:`~diffly.metrics.change.ChangeMetric` (computed for numerical columns only), +a one-argument callable becomes a :class:`~diffly.metrics.data.DataMetric` +(computed for all columns). To target a different set of columns, construct the +metric explicitly with a column selector, e.g. ``ChangeMetric(fn, selector=cs.all())`` +or ``DataMetric(fn, selector=cs.boolean())``. Presets come in two families, each with its own module and default set: @@ -34,16 +35,6 @@ Presets come in two families, each with its own module and default set: The preset default sets are :data:`~diffly.metrics.change.DEFAULT_CHANGE_METRICS` and :data:`~diffly.metrics.data.DEFAULT_DATA_METRICS`. -.. autodata:: ChangeMetricFn - :no-value: - -.. autodata:: DataMetricFn - :no-value: - -.. autoclass:: ChangeMetric - -.. autoclass:: DataMetric - Change metrics ============== @@ -52,6 +43,11 @@ Change metrics Metrics that describe the change between numeric columns by aggregating over ``right - left``. +.. autodata:: ChangeMetricFn + :no-value: + +.. autoclass:: ChangeMetric + .. autosummary:: :toctree: _gen/ @@ -75,6 +71,11 @@ Data metrics Metrics that describe the left and right datasets individually, so you can understand how a change affects the data. +.. autodata:: DataMetricFn + :no-value: + +.. autoclass:: DataMetric + .. autosummary:: :toctree: _gen/ diff --git a/docs/guides/features/summary.ipynb b/docs/guides/features/summary.ipynb index bec4877..9c43e9d 100644 --- a/docs/guides/features/summary.ipynb +++ b/docs/guides/features/summary.ipynb @@ -544,20 +544,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from diffly import metrics\n", - "\n", - "print(\n", - " comparison.summary(\n", - " top_k_column_changes=3,\n", - " metrics={\n", - " \"Mean\": metrics.mean,\n", - " \"Mean relative deviation\": metrics.mean_relative_deviation,\n", - " \"Max absolute deviation\": lambda l, r: (r - l).abs().max(),\n", - " },\n", - " )\n", - ")" - ] + "source": "from diffly import metrics\n\nprint(\n comparison.summary(\n top_k_column_changes=3,\n metrics={\n \"Mean\": metrics.change.mean,\n \"Mean relative deviation\": metrics.change.mean_relative_deviation,\n \"Max absolute deviation\": lambda left, right: (right - left).abs().max(),\n },\n )\n)" }, { "cell_type": "markdown", diff --git a/tests/summary/fixtures/metrics_custom/test_metrics_custom.py b/tests/summary/fixtures/metrics_custom/test_metrics_custom.py index 61d257d..2bb1f75 100644 --- a/tests/summary/fixtures/metrics_custom/test_metrics_custom.py +++ b/tests/summary/fixtures/metrics_custom/test_metrics_custom.py @@ -30,8 +30,8 @@ def test_generate() -> None: generate_summaries( comp, metrics={ - "mean_delta": metrics.mean, - "p95_delta": metrics.quantile(0.95), + "mean_delta": metrics.change.mean, + "p95_delta": metrics.change.quantile(0.95), "max_abs_delta": lambda left, right: (right - left).abs().max(), }, ) diff --git a/tests/summary/fixtures/metrics_long_labels/test_metrics_long_labels.py b/tests/summary/fixtures/metrics_long_labels/test_metrics_long_labels.py index 1972e22..4ee1b38 100644 --- a/tests/summary/fixtures/metrics_long_labels/test_metrics_long_labels.py +++ b/tests/summary/fixtures/metrics_long_labels/test_metrics_long_labels.py @@ -31,12 +31,12 @@ def test_generate() -> None: generate_summaries( comp, metrics={ - "Mean Delta": metrics.mean, - "Median Delta": metrics.median, - "Minimum Delta": metrics.min, - "Maximum Delta": metrics.max, - "Standard Deviation": metrics.std, - "Mean Absolute Deviation": metrics.mean_absolute_deviation, - "Mean Relative Deviation": metrics.mean_relative_deviation, + "Mean Delta": metrics.change.mean, + "Median Delta": metrics.change.median, + "Minimum Delta": metrics.change.min, + "Maximum Delta": metrics.change.max, + "Standard Deviation": metrics.change.std, + "Mean Absolute Deviation": metrics.change.mean_absolute_deviation, + "Mean Relative Deviation": metrics.change.mean_relative_deviation, }, ) diff --git a/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py b/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py index 376d2d7..9fbd539 100644 --- a/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py +++ b/tests/summary/fixtures/metrics_null_fraction/test_metrics_null_fraction.py @@ -6,8 +6,8 @@ import pytest from diffly import compare_frames, metrics -from diffly.metrics import ChangeMetric, DataMetric -from diffly.metrics.data import DEFAULT_DATA_METRICS +from diffly.metrics.change import ChangeMetric +from diffly.metrics.data import DEFAULT_DATA_METRICS, DataMetric from tests.utils import generate_summaries @@ -32,7 +32,7 @@ def test_generate() -> None: comp, metrics={ # Numeric-only preset alongside a metric applied to all columns. - "Mean": metrics.mean, + "Mean": metrics.change.mean, "Null%": DEFAULT_DATA_METRICS["Null%"], # A second, numeric data metric to render more than one data column. "Distinct": DataMetric(fn=lambda col: col.n_unique()), diff --git a/tests/summary/fixtures/metrics_presets_few/test_metrics_presets_few.py b/tests/summary/fixtures/metrics_presets_few/test_metrics_presets_few.py index 4fcd6d1..54bb42d 100644 --- a/tests/summary/fixtures/metrics_presets_few/test_metrics_presets_few.py +++ b/tests/summary/fixtures/metrics_presets_few/test_metrics_presets_few.py @@ -29,5 +29,5 @@ def test_generate() -> None: comp = compare_frames(left, right, primary_key=["id"]) generate_summaries( comp, - metrics={"Mean": metrics.mean, "Max": metrics.max}, + metrics={"Mean": metrics.change.mean, "Max": metrics.change.max}, ) diff --git a/tests/summary/fixtures/metrics_presets_many/test_metrics_presets_many.py b/tests/summary/fixtures/metrics_presets_many/test_metrics_presets_many.py index ede52cc..4b1f080 100644 --- a/tests/summary/fixtures/metrics_presets_many/test_metrics_presets_many.py +++ b/tests/summary/fixtures/metrics_presets_many/test_metrics_presets_many.py @@ -30,12 +30,12 @@ def test_generate() -> None: generate_summaries( comp, metrics={ - "Mean": metrics.mean, - "Median": metrics.median, - "Min": metrics.min, - "Max": metrics.max, - "Std": metrics.std, - "Mean absolute deviation": metrics.mean_absolute_deviation, - "Mean relative deviation": metrics.mean_relative_deviation, + "Mean": metrics.change.mean, + "Median": metrics.change.median, + "Min": metrics.change.min, + "Max": metrics.change.max, + "Std": metrics.change.std, + "Mean absolute deviation": metrics.change.mean_absolute_deviation, + "Mean relative deviation": metrics.change.mean_relative_deviation, }, ) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index d10f4cb..1830993 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -140,7 +140,7 @@ def test_change_and_data_metrics_routed_to_separate_fields() -> None: comp = compare_frames(left, right, primary_key="id") summary = comp.summary( - metrics={"Mean": metrics.mean, "Null%": DEFAULT_DATA_METRICS["Null%"]}, + metrics={"Mean": metrics.change.mean, "Null%": DEFAULT_DATA_METRICS["Null%"]}, ) result = json.loads(summary.to_json()) @@ -203,7 +203,11 @@ def test_summary_data_parametrized( comp = _make_comparison() top_k = 3 if show_top_column_changes else 0 hidden_columns = ["value"] if hide_value else None - metrics_arg = {"Mean": metrics.mean, "Max": metrics.max} if with_metrics else None + metrics_arg = ( + {"Mean": metrics.change.mean, "Max": metrics.change.max} + if with_metrics + else None + ) summary = comp.summary( show_perfect_column_matches=show_perfect_column_matches, top_k_column_changes=top_k, diff --git a/tests/test_metrics.py b/tests/test_metrics.py index d2db47f..609d10c 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -8,7 +8,8 @@ import pytest from diffly import metrics -from diffly.metrics import ChangeMetricFn, data +from diffly.metrics import data +from diffly.metrics.change import ChangeMetricFn @pytest.fixture @@ -22,24 +23,24 @@ def _apply(metric: ChangeMetricFn, frame: pl.DataFrame) -> Any: def test_mean(frame: pl.DataFrame) -> None: - assert _apply(metrics.mean, frame) == pytest.approx(2 / 3) + assert _apply(metrics.change.mean, frame) == pytest.approx(2 / 3) def test_median(frame: pl.DataFrame) -> None: - assert _apply(metrics.median, frame) == 0 + assert _apply(metrics.change.median, frame) == 0 def test_min(frame: pl.DataFrame) -> None: - assert _apply(metrics.min, frame) == 0 + assert _apply(metrics.change.min, frame) == 0 def test_max(frame: pl.DataFrame) -> None: - assert _apply(metrics.max, frame) == 2 + assert _apply(metrics.change.max, frame) == 2 def test_std(frame: pl.DataFrame) -> None: sample_mean = 2 / 3 - assert _apply(metrics.std, frame) == pytest.approx( + assert _apply(metrics.change.std, frame) == pytest.approx( math.sqrt( ((0 - sample_mean) ** 2 + (0 - sample_mean) ** 2 + (2 - sample_mean) ** 2) / 2 @@ -50,19 +51,19 @@ def test_std(frame: pl.DataFrame) -> None: def test_mean_absolute_deviation() -> None: # deltas: [-1, 0, 2, null]; |deltas|: [1, 0, 2, null]; mean = 1.0 frame = pl.DataFrame({"l": [2, 2, 3, None], "r": [1, 2, 5, 4]}) - assert _apply(metrics.mean_absolute_deviation, frame) == pytest.approx(1.0) + assert _apply(metrics.change.mean_absolute_deviation, frame) == pytest.approx(1.0) def test_mean_relative_deviation() -> None: # left: [1, 2, 4, None]; delta: [0, 0, 2, null]; rel: [0, 0, 0.5, null]; mean = 1/6 frame = pl.DataFrame({"l": [1, 2, 4, None], "r": [1, 2, 6, 4]}) - assert _apply(metrics.mean_relative_deviation, frame) == pytest.approx(1 / 6) + assert _apply(metrics.change.mean_relative_deviation, frame) == pytest.approx(1 / 6) def test_mean_relative_deviation_div_by_zero() -> None: # Matches numpy: x/0 -> inf, so .abs().mean() -> inf frame = pl.DataFrame({"l": [0.0, 1.0], "r": [1.0, 1.0]}) - assert math.isinf(_apply(metrics.mean_relative_deviation, frame)) + assert math.isinf(_apply(metrics.change.mean_relative_deviation, frame)) def test_null_fraction() -> None: @@ -79,13 +80,13 @@ def test_null_fraction_non_numeric() -> None: def test_quantile(frame: pl.DataFrame) -> None: # deltas [0, 0, 2]: p50 = 0, p100 = 2 - assert _apply(metrics.quantile(0.5), frame) == 0 - assert _apply(metrics.quantile(1.0), frame) == 2 + assert _apply(metrics.change.quantile(0.5), frame) == 0 + assert _apply(metrics.change.quantile(1.0), frame) == 2 def test_quantile_out_of_range() -> None: with pytest.raises(ValueError, match="q must be in"): - metrics.quantile(1.5) + metrics.change.quantile(1.5) def test_default_metrics_partition() -> None: From 65d80d63af26207551e4082598e0d87f0a94bf44 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 14:33:03 +0200 Subject: [PATCH 14/26] fix --- diffly/comparison.py | 29 ++++++++++++++--------------- diffly/summary.py | 39 +++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/diffly/comparison.py b/diffly/comparison.py index e4025fe..9d58e39 100644 --- a/diffly/comparison.py +++ b/diffly/comparison.py @@ -954,14 +954,12 @@ def summary( hidden_columns: Columns for which no values are printed, e.g. because they contain sensitive information. metrics: Optional mapping from display label to a metric. A value may be a - :class:`~diffly.metrics.change.ChangeMetric` (callable - ``(left_expr, right_expr) -> pl.Expr`` aggregating over the change), a - :class:`~diffly.metrics.data.DataMetric` (callable ``(col_expr) -> pl.Expr`` - evaluated on each side to describe the data), or a bare callable resolved + :class:`~diffly.metrics.change.ChangeMetric`, a + :class:`~diffly.metrics.data.DataMetric`, or a bare callable resolved by its arity (two arguments → change metric on numerical columns, one argument → data metric on all columns). To target other column types, construct the metric explicitly with a column selector - (e.g. ``ChangeMetric(fn, selector=cs.all())``). See :doc:`/api/metrics` + (e.g. ``ChangeMetric(fn, selector=cs.numeric())``). See :doc:`/api/metrics` for the full list of presets. When ``None`` (default), no metrics are computed; presets are not applied automatically. Prefer short labels — the summary has a fixed width and many or long labels degrade rendering. @@ -980,17 +978,8 @@ def summary( # NOTE: We're importing here to prevent circular imports from .summary import Summary - def _resolve(v: ChangeMetricFn | DataMetricFn | Metric) -> Metric: - if isinstance(v, (ChangeMetric, DataMetric)): - return v - # Infer the metric family from the callable's arity: a single-argument - # callable describes one side (data), two arguments describe a change. - if len(inspect.signature(v).parameters) >= 2: - return ChangeMetric(fn=cast(ChangeMetricFn, v)) - return DataMetric(fn=cast(DataMetricFn, v)) - resolved_metrics = ( - {label: _resolve(v) for label, v in metrics.items()} + {label: _resolve_metric(v) for label, v in metrics.items()} if metrics is not None else None ) @@ -1248,3 +1237,13 @@ def _list_length_exprs( for e in _list_length_exprs(expr.struct[field.name], field.dtype) ] return [] + + +def _resolve_metric(v: ChangeMetricFn | DataMetricFn | Metric) -> Metric: + if isinstance(v, (ChangeMetric, DataMetric)): + return v + # Infer the metric family from the callable's arity: a single-argument + # callable describes one side (data), two arguments describe a change. + if len(inspect.signature(v).parameters) >= 2: + return ChangeMetric(fn=cast(ChangeMetricFn, v)) + return DataMetric(fn=cast(DataMetricFn, v)) diff --git a/diffly/summary.py b/diffly/summary.py index 9f3a58e..068a9b6 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -664,11 +664,11 @@ def _section_data_inspection(self) -> RenderableType: for col in self._data.columns: row_items: list[RenderableType] = [Text(col.name, style="cyan")] for label in self._data._data_metric_labels: - value = col.data_metrics.get(label) if col.data_metrics else None + result = col.data_metrics.get(label) if col.data_metrics else None metric = self._data._data_metrics[label] row_items.append( - _format_data_metric_value( - value, metric.formatter, metric.delta_formatter + _format_data_metric_result( + result, metric.formatter, metric.delta_formatter ) ) table.add_row(*row_items) @@ -751,6 +751,14 @@ class SummaryDataColumnChange: sample_pk: tuple[Any, ...] | None +@dataclass +class DataMetricResult: + """A data metric evaluated on each side of the comparison.""" + + left: Any + right: Any + + @dataclass class SummaryDataColumn: name: str @@ -758,7 +766,7 @@ class SummaryDataColumn: n_total_changes: int changes: list[SummaryDataColumnChange] | None change_metrics: dict[str, Any] | None - data_metrics: dict[str, Any] | None + data_metrics: dict[str, DataMetricResult] | None @dataclass @@ -1031,10 +1039,10 @@ def select_columns(selector: cs.Selector) -> set[str]: if isinstance(metric, ChangeMetric): out[column][label] = row[f"{label}__{column}"] else: - out[column][label] = { - "left": row[f"{label}__{column}__left"], - "right": row[f"{label}__{column}__right"], - } + out[column][label] = DataMetricResult( + left=row[f"{label}__{column}__left"], + right=row[f"{label}__{column}__right"], + ) return out @@ -1223,19 +1231,19 @@ def _format_metric_value(value: Any) -> str: return _format_value(value, float_format=".4g") -def _format_data_metric_value( - value: Any, +def _format_data_metric_result( + result: DataMetricResult | None, formatter: Callable[[Any], str] | None, delta_formatter: Callable[[Any], str] | None, ) -> str: - """Format a data metric's ``{"left", "right"}`` pair as `` -> ``. + """Format a data metric's left/right pair as `` -> ``. Numeric values additionally show the signed delta ``()``; non-numeric values omit it. ``formatter`` formats a left/right value and ``delta_formatter`` the delta magnitude (rendered with an explicit sign); either falling back to ``.4g`` precision for floats when unset. """ - if value is None: + if result is None: return "" def _fmt(v: Any, fn: Callable[[Any], str] | None) -> str: @@ -1245,10 +1253,9 @@ def _fmt(v: Any, fn: Callable[[Any], str] | None) -> str: return format(v, ".4g") return str(v) - left, right = value["left"], value["right"] - text = f"{_fmt(left, formatter)} -> {_fmt(right, formatter)}" - if _is_numeric(left) and _is_numeric(right): - delta = right - left + text = f"{_fmt(result.left, formatter)} -> {_fmt(result.right, formatter)}" + if _is_numeric(result.left) and _is_numeric(result.right): + delta = result.right - result.left sign = "+" if delta >= 0 else "-" text += f" ({sign}{_fmt(abs(delta), delta_formatter or formatter)})" return _yellow(text) From ffeb05a193d4d9c75b32c9a82f90e5c4dcf64ce7 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 14:52:22 +0200 Subject: [PATCH 15/26] cleanup --- diffly/metrics/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index 7374be6..f93394e 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -13,6 +13,6 @@ from __future__ import annotations from . import change, data -from ._common import ALL_METRICS, Metric +from ._common import ALL_METRICS -__all__ = ["ALL_METRICS", "Metric", "change", "data"] +__all__ = ["ALL_METRICS", "change", "data"] From c116fdfa16759b33020708ceb7ab37a9c8cf037e Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 14:58:08 +0200 Subject: [PATCH 16/26] improve --- diffly/summary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/diffly/summary.py b/diffly/summary.py index 068a9b6..02642ae 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -1247,6 +1247,8 @@ def _format_data_metric_result( return "" def _fmt(v: Any, fn: Callable[[Any], str] | None) -> str: + if v is None: + return "None" if fn is not None: return fn(v) if isinstance(v, float): From a57dc0ffdb5180e391f4a901786fddb2ae375a80 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 15:41:27 +0200 Subject: [PATCH 17/26] docs --- diffly/metrics/__init__.py | 8 ++++---- diffly/metrics/change.py | 14 +++++++------- diffly/metrics/data.py | 20 ++++++-------------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index f93394e..f2c34ce 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -4,10 +4,10 @@ Two families are provided: -- Metrics in :mod:`~diffly.metrics.change` describe the change between numeric - columns itself by aggregating over ``right - left``. -- Metrics in :mod:`~diffly.metrics.data` describe the left and right datasets - individually, explaining how a change affects the data. +- :class:`ChangeMetrics` in :mod:`~diffly.metrics.change` describe the change between + numeric columns itself by aggregating over ``right - left``. +- :class:`DataMetrics` in :mod:`~diffly.metrics.data` describe the left and right + datasets individually, explaining how a change affects the data. """ from __future__ import annotations diff --git a/diffly/metrics/change.py b/diffly/metrics/change.py index dc0c252..e3f7541 100644 --- a/diffly/metrics/change.py +++ b/diffly/metrics/change.py @@ -1,9 +1,5 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause -"""Metrics describing the change between numeric columns. - -These aggregate over ``right - left`` to characterize the change itself. -""" from __future__ import annotations @@ -14,13 +10,14 @@ import polars.selectors as cs ChangeMetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] -"""A change metric maps the difference between two joined columns from the compared data -frames to a scalar aggregation expression.""" +"""A `ChangeMetricFn` maps a pair of column expressions to a scalar aggregation +expression.""" @dataclass(frozen=True) class ChangeMetric: - """A metric quantifying the *change* between the two sides of a comparison. + """A metric quantifying the *change* in a column between the two sides of a + comparison. Change metrics are rendered as extra columns in the "Columns" table, alongside the match rate. @@ -34,6 +31,9 @@ class ChangeMetric: """Selects the columns the metric applies to; defaults to numeric columns.""" +# ---------------------------------- CHANGE METRICS ---------------------------------- # + + def mean(left: pl.Expr, right: pl.Expr) -> pl.Expr: """Mean of ``right - left``.""" return (right - left).mean() diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index c21e434..9ade440 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -1,10 +1,5 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause -"""Metrics describing the left and right datasets individually. - -These characterize each side of a change so you can understand how the change affects -the data, rather than describing the change itself. -""" from __future__ import annotations @@ -16,11 +11,7 @@ import polars.selectors as cs DataMetricFn = Callable[[pl.Expr], pl.Expr] -"""A data metric maps a single column expression to a scalar aggregation expression. - -It is evaluated on both data frames individually, and the change between them is -rendered. -""" +"""A data metric maps a single column expression to a scalar aggregation expression.""" @dataclass(frozen=True) @@ -45,14 +36,15 @@ class DataMetric: """ delta_formatter: Callable[[Any], str] | None = None - """Formats the (always non-negative) magnitude of the delta, which is rendered with - an explicit sign. + """Formats the magnitude of the delta, which is rendered with an explicit sign. - Falls back to ``formatter`` when ``None``, which in turn falls back to the default - numeric precision when unset. + Falls back to ``formatter`` when ``None``. """ +# ----------------------------------- DATA METRICS ----------------------------------- # + + def null_fraction(col: pl.Expr) -> pl.Expr: """Fraction of null entries in a column.""" return col.is_null().mean() From 1adcf784c421434f373ce635c98f988118bc2634 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 15:49:38 +0200 Subject: [PATCH 18/26] review --- diffly/metrics/__init__.py | 4 ++-- diffly/summary.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index f2c34ce..820c546 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -4,9 +4,9 @@ Two families are provided: -- :class:`ChangeMetrics` in :mod:`~diffly.metrics.change` describe the change between +- :class:`ChangeMetric`s in :mod:`~diffly.metrics.change` describe the change between numeric columns itself by aggregating over ``right - left``. -- :class:`DataMetrics` in :mod:`~diffly.metrics.data` describe the left and right +- :class:`DataMetric`s in :mod:`~diffly.metrics.data` describe the left and right datasets individually, explaining how a change affects the data. """ diff --git a/diffly/summary.py b/diffly/summary.py index 02642ae..20dce23 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -576,15 +576,14 @@ def _section_columns(self) -> RenderableType: elif not columns: display_items.append(Text("All columns match perfectly.", style="italic")) else: - metric_labels = self._data._change_metric_labels - matches = Table(show_header=bool(metric_labels)) + matches = Table(show_header=bool(self._data._change_metric_labels)) matches.add_column( "Column", max_width=COLUMN_SECTION_COLUMN_WIDTH, overflow=OVERFLOW, ) matches.add_column("Match Rate", justify="right") - for label in metric_labels: + for label in self._data._change_metric_labels: matches.add_column(label, justify="right") has_top_changes_column = any( c.changes is not None for c in columns if c.match_rate < 1 @@ -597,7 +596,7 @@ def _section_columns(self) -> RenderableType: Text(col.name, style="cyan"), f"{_format_fraction_as_percentage(col.match_rate)}", ] - for label in metric_labels: + for label in self._data._change_metric_labels: value = ( col.change_metrics.get(label) if col.change_metrics else None ) From 4000181036cbbeb2a69e0c98923eba6b7a3cd050 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 16:29:21 +0200 Subject: [PATCH 19/26] fix --- diffly/summary.py | 163 +++++++++++------- ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ...False_sample_rows_True_sample_pk_False.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ..._True_sample_rows_True_sample_pk_False.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ..._False_sample_rows_True_sample_pk_True.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ...m_True_sample_rows_True_sample_pk_True.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ...False_sample_rows_True_sample_pk_False.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ..._True_sample_rows_True_sample_pk_False.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ..._False_sample_rows_True_sample_pk_True.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ...m_True_sample_rows_True_sample_pk_True.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ...False_sample_rows_True_sample_pk_False.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ..._True_sample_rows_True_sample_pk_False.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ..._False_sample_rows_True_sample_pk_True.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ...m_True_sample_rows_True_sample_pk_True.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ...False_sample_rows_True_sample_pk_False.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ..._True_sample_rows_True_sample_pk_False.txt | 12 ++ ...alse_sample_rows_False_sample_pk_False.txt | 23 +++ ..._False_sample_rows_True_sample_pk_True.txt | 23 +++ ...True_sample_rows_False_sample_pk_False.txt | 12 ++ ...m_True_sample_rows_True_sample_pk_True.txt | 12 ++ .../test_metrics_data_no_pk.py | 39 +++++ tests/summary/test_summary.py | 12 +- 35 files changed, 707 insertions(+), 67 deletions(-) create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt create mode 100644 tests/summary/fixtures/metrics_data_no_pk/test_metrics_data_no_pk.py diff --git a/diffly/summary.py b/diffly/summary.py index 20dce23..f6461bd 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -134,10 +134,10 @@ def to_json(self, **kwargs: Any) -> str: "match_rate": 0.667, "n_total_changes": 1, "changes": [{"old": 1.0, "new": 2.0, "count": 1, "sample_pk": [1]}], - "change_metrics": null, - "data_metrics": null + "change_metrics": null } ], + "data_inspection": null, "sample_rows_left_only": [], "sample_rows_right_only": [] } @@ -641,7 +641,7 @@ def _section_columns(self) -> RenderableType: # ------------------------------- DATA INSPECTION -------------------------------- # def _print_data_inspection(self, console: Console) -> None: - if not self._data.columns or not self._data._data_metric_labels: + if not self._data.data_inspection or not self._data._data_metric_labels: return _print_section( console, @@ -650,7 +650,7 @@ def _print_data_inspection(self, console: Console) -> None: ) def _section_data_inspection(self) -> RenderableType: - assert self._data.columns is not None + assert self._data.data_inspection is not None table = Table() table.add_column( @@ -660,10 +660,10 @@ def _section_data_inspection(self) -> RenderableType: ) for label in self._data._data_metric_labels: table.add_column(label, justify="right", overflow=OVERFLOW) - for col in self._data.columns: + for col in self._data.data_inspection: row_items: list[RenderableType] = [Text(col.name, style="cyan")] for label in self._data._data_metric_labels: - result = col.data_metrics.get(label) if col.data_metrics else None + result = col.data_metrics.get(label) metric = self._data._data_metrics[label] row_items.append( _format_data_metric_result( @@ -765,7 +765,12 @@ class SummaryDataColumn: n_total_changes: int changes: list[SummaryDataColumnChange] | None change_metrics: dict[str, Any] | None - data_metrics: dict[str, DataMetricResult] | None + + +@dataclass +class SummaryDataInspectionColumn: + name: str + data_metrics: dict[str, DataMetricResult] @dataclass @@ -777,6 +782,7 @@ class SummaryData: schemas: SummaryDataSchemas | None rows: SummaryDataRows | None columns: list[SummaryDataColumn] | None + data_inspection: list[SummaryDataInspectionColumn] | None sample_rows_left_only: list[tuple[Any, ...]] | None sample_rows_right_only: list[tuple[Any, ...]] | None _is_empty: bool @@ -882,6 +888,7 @@ def _validate_primary_key_hidden_columns() -> None: schemas=None, rows=None, columns=None, + data_inspection=None, sample_rows_left_only=None, sample_rows_right_only=None, _is_empty=is_empty, @@ -894,16 +901,21 @@ def _validate_primary_key_hidden_columns() -> None: ) metrics_resolved: dict[str, Metric] = dict(metrics or {}) - metrics_by_column = _compute_column_metrics(comp, metrics_resolved) - change_metric_labels = [ - label for label, m in metrics_resolved.items() if isinstance(m, ChangeMetric) - ] - data_metric_labels = [ - label for label, m in metrics_resolved.items() if isinstance(m, DataMetric) - ] + change_metrics = { + label: m for label, m in metrics_resolved.items() if isinstance(m, ChangeMetric) + } data_metrics = { label: m for label, m in metrics_resolved.items() if isinstance(m, DataMetric) } + change_metrics_by_column: dict[str, dict[str, Any]] = {} + if comp.primary_key is not None and comp.num_rows_joined() > 0: + change_columns = _metric_target_columns(comp, change_metrics) + if any(change_columns.values()): + change_metrics_by_column = _compute_change_metrics( + comp, change_metrics, change_columns + ) + change_metric_labels = list(change_metrics) + data_metric_labels = list(data_metrics) schemas = _compute_schemas(comp, slim) rows = _compute_rows(comp, slim) @@ -913,10 +925,9 @@ def _validate_primary_key_hidden_columns() -> None: show_perfect_column_matches, top_k_changes_by_column, show_sample_primary_key_per_change, - metrics_by_column, - change_metric_labels, - data_metric_labels, + change_metrics_by_column, ) + data_inspection = _compute_data_inspection(comp, data_metrics) sample_rows_left_only, sample_rows_right_only = _compute_sample_rows( comp, sample_k_rows_only ) @@ -929,6 +940,7 @@ def _validate_primary_key_hidden_columns() -> None: schemas=schemas, rows=rows, columns=columns, + data_inspection=data_inspection, sample_rows_left_only=sample_rows_left_only, sample_rows_right_only=sample_rows_right_only, _is_empty=is_empty, @@ -999,49 +1011,69 @@ def _compute_rows(comp: DataFrameComparison, slim: bool) -> SummaryDataRows | No ) -def _compute_column_metrics( - comp: DataFrameComparison, - metrics: Mapping[str, Metric], -) -> dict[str, dict[str, Any]]: - if comp.primary_key is None or comp.num_rows_joined() == 0: - return {} +def _select_metric_columns( + comp: DataFrameComparison, selector: cs.Selector +) -> set[str]: + left = set(cs.expand_selector(comp.left_schema, selector)) + right = set(cs.expand_selector(comp.right_schema, selector)) + return (left & right) & set(comp._other_common_columns) - def select_columns(selector: cs.Selector) -> set[str]: - left = set(cs.expand_selector(comp.left_schema, selector)) - right = set(cs.expand_selector(comp.right_schema, selector)) - return (left & right) & set(comp._other_common_columns) - metric_to_columns = { - label: select_columns(m.selector) for label, m in metrics.items() +def _metric_target_columns( + comp: DataFrameComparison, metrics: Mapping[str, Metric] +) -> dict[str, set[str]]: + """Map each metric label to the columns it applies to.""" + return { + label: _select_metric_columns(comp, m.selector) for label, m in metrics.items() } - all_columns = sorted(set().union(*metric_to_columns.values())) - if not all_columns: - return {} - out: dict[str, dict[str, Any]] = {c: {} for c in all_columns} - joined = comp.joined(lazy=True) +def _compute_change_metrics( + comp: DataFrameComparison, + metrics: Mapping[str, ChangeMetric], + metric_to_columns: dict[str, set[str]], +) -> dict[str, dict[str, Any]]: + """Compute change metrics over the joined (matched) rows only.""" agg_exprs: list[pl.Expr] = [] for label, metric in metrics.items(): for column in sorted(metric_to_columns[label]): left = pl.col(f"{column}_{Side.LEFT}") right = pl.col(f"{column}_{Side.RIGHT}") - if isinstance(metric, ChangeMetric): - agg_exprs.append(metric.fn(left, right).alias(f"{label}__{column}")) - else: - agg_exprs.append(metric.fn(left).alias(f"{label}__{column}__left")) - agg_exprs.append(metric.fn(right).alias(f"{label}__{column}__right")) + agg_exprs.append(metric.fn(left, right).alias(f"{label}__{column}")) + + row = comp.joined(lazy=True).select(agg_exprs).collect().row(0, named=True) + out: dict[str, dict[str, Any]] = { + c: {} for c in set().union(*metric_to_columns.values()) + } + for label in metrics: + for column in metric_to_columns[label]: + out[column][label] = row[f"{label}__{column}"] + return out + - row = joined.select(agg_exprs).collect().row(0, named=True) +def _compute_data_metrics( + comp: DataFrameComparison, + metrics: Mapping[str, DataMetric], + metric_to_columns: dict[str, set[str]], +) -> dict[str, dict[str, Any]]: + """Compute data metrics over the entire left and right columns, including rows that + are not part of the join.""" + agg_exprs: list[pl.Expr] = [] for label, metric in metrics.items(): + for column in sorted(metric_to_columns[label]): + agg_exprs.append(metric.fn(pl.col(column)).alias(f"{label}__{column}")) + + left_row = comp.left.select(agg_exprs).collect().row(0, named=True) + right_row = comp.right.select(agg_exprs).collect().row(0, named=True) + out: dict[str, dict[str, Any]] = { + c: {} for c in set().union(*metric_to_columns.values()) + } + for label in metrics: for column in metric_to_columns[label]: - if isinstance(metric, ChangeMetric): - out[column][label] = row[f"{label}__{column}"] - else: - out[column][label] = DataMetricResult( - left=row[f"{label}__{column}__left"], - right=row[f"{label}__{column}__right"], - ) + out[column][label] = DataMetricResult( + left=left_row[f"{label}__{column}"], + right=right_row[f"{label}__{column}"], + ) return out @@ -1051,9 +1083,7 @@ def _compute_columns( show_perfect_column_matches: bool, top_k_changes_by_column: dict[str, int], show_sample_primary_key_per_change: bool, - metrics_by_column: dict[str, dict[str, Any]], - change_metric_labels: list[str], - data_metric_labels: list[str], + change_metrics_by_column: dict[str, dict[str, Any]], ) -> list[SummaryDataColumn] | None: # NOTE: We can only compute column matches if there are primary key columns and at # least one joined row. @@ -1094,30 +1124,37 @@ def _compute_columns( sample_pk=sample_pk, ) ) - col_metrics = metrics_by_column.get(col_name) or {} - change_metrics = { - label: col_metrics[label] - for label in change_metric_labels - if label in col_metrics - } - data_metrics = { - label: col_metrics[label] - for label in data_metric_labels - if label in col_metrics - } columns.append( SummaryDataColumn( name=col_name, match_rate=rate, n_total_changes=n_total_changes, changes=changes, - change_metrics=change_metrics or None, - data_metrics=data_metrics or None, + change_metrics=change_metrics_by_column.get(col_name) or None, ) ) return columns +def _compute_data_inspection( + comp: DataFrameComparison, + metrics: Mapping[str, DataMetric], +) -> list[SummaryDataInspectionColumn] | None: + # NOTE: Data metrics describe each side individually and do not need a join, but we + # still require the columns to be present on both sides so left/right values are + # comparable. + metric_to_columns = _metric_target_columns(comp, metrics) + if not any(metric_to_columns.values()): + return None + data_metrics_by_column = _compute_data_metrics(comp, metrics, metric_to_columns) + return [ + SummaryDataInspectionColumn( + name=col_name, data_metrics=data_metrics_by_column[col_name] + ) + for col_name in sorted(data_metrics_by_column) + ] + + def _compute_sample_rows( comp: DataFrameComparison, sample_k_rows_only: int ) -> tuple[list[tuple[Any, ...]] | None, list[tuple[Any, ...]] | None]: diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..b6a38d4 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Diffly Summary ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..288a3cf --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_False_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃ Null% ┃ Distinct ┃ Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_False_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_False_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_False_slim_True_sample_rows_True_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..1016d10 --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_False_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,23 @@ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃  Diffly Summary  ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Schemas + ▔▔▔▔▔▔▔ + Schemas match exactly (column count: 3). + + Rows + ▔▔▔▔ + The number of rows matches exactly (row count: 5). + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_False_sample_pk_False.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt new file mode 100644 index 0000000..036091a --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/gen/pretty_True_perfect_True_top_True_slim_True_sample_rows_True_sample_pk_True.txt @@ -0,0 +1,12 @@ + Attention: the data frames do not match exactly, but as no primary key columns are  + provided, the row and column matches cannot be computed. + + Data Inspection + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┓ + ┃ Column ┃  Null% ┃  Distinct ┃  Max ┃ + ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━┩ + │ id  │ 0.0% -> 0.0% (+0.0) │ 5 -> 5 (+0) │ │ + │ price  │ 20.0% -> 0.0% (-20.0) │ 5 -> 5 (+0) │ │ + │ status │ 0.0% -> 40.0% (+40.0) │ 5 -> 4 (-1) │ e -> x │ + └────────┴───────────────────────┴─────────────┴────────┘ diff --git a/tests/summary/fixtures/metrics_data_no_pk/test_metrics_data_no_pk.py b/tests/summary/fixtures/metrics_data_no_pk/test_metrics_data_no_pk.py new file mode 100644 index 0000000..acf053f --- /dev/null +++ b/tests/summary/fixtures/metrics_data_no_pk/test_metrics_data_no_pk.py @@ -0,0 +1,39 @@ +# Copyright (c) QuantCo 2025-2026 +# SPDX-License-Identifier: BSD-3-Clause + +import polars as pl +import polars.selectors as cs +import pytest + +from diffly import compare_frames +from diffly.metrics.data import DEFAULT_DATA_METRICS, DataMetric +from tests.utils import generate_summaries + + +@pytest.mark.generate +def test_generate() -> None: + # No primary key: row and column matches cannot be computed, but data metrics still + # characterize each side individually and are shown in the Data Inspection section. + left = pl.DataFrame( + { + "id": [1, 2, 3, 4, 5], + "price": [10.0, 20.0, None, 40.0, 50.0], + "status": ["a", "b", "c", "d", "e"], + } + ) + right = pl.DataFrame( + { + "id": [1, 2, 3, 4, 6], + "price": [10.0, 21.0, 30.0, 42.0, 50.0], + "status": ["a", None, "x", None, "e"], + } + ) + comp = compare_frames(left, right) + generate_summaries( + comp, + metrics={ + "Null%": DEFAULT_DATA_METRICS["Null%"], + "Distinct": DataMetric(fn=lambda col: col.n_unique()), + "Max": DataMetric(fn=lambda col: col.max(), selector=cs.string()), + }, + ) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index 1830993..47be5c4 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -146,9 +146,13 @@ def test_change_and_data_metrics_routed_to_separate_fields() -> None: (value_col,) = result["columns"] assert value_col["name"] == "value" - # Change metric lands in `change_metrics`, data metric in `data_metrics`. + # Change metric lands in the column's `change_metrics`, data metric in the separate + # `data_inspection` section. assert value_col["change_metrics"] == {"Mean": pytest.approx(2.5)} - assert value_col["data_metrics"] == { + assert "data_metrics" not in value_col + (value_inspection,) = result["data_inspection"] + assert value_inspection["name"] == "value" + assert value_inspection["data_metrics"] == { "Null%": {"left": pytest.approx(0.0), "right": pytest.approx(1 / 3)} } @@ -257,7 +261,6 @@ def test_summary_data_parametrized( "change_metrics": {"Mean": pytest.approx(5 / 3), "Max": 5.0} if with_metrics else None, - "data_metrics": None, } expected_columns = [] if show_perfect_column_matches: @@ -268,7 +271,6 @@ def test_summary_data_parametrized( "n_total_changes": 0, "changes": None, "change_metrics": None, - "data_metrics": None, } ) expected_columns.append(value_col) @@ -288,6 +290,8 @@ def test_summary_data_parametrized( "n_right_only": 1, }, "columns": expected_columns, + # Only change metrics are supplied, so the data inspection section is absent. + "data_inspection": None, "sample_rows_left_only": [[4]] if sample_rows else None, "sample_rows_right_only": [[5]] if sample_rows else None, } From a52e24c850450cf7c5e4cca0113993c8ad338f5b Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 16:33:35 +0200 Subject: [PATCH 20/26] simplify --- diffly/summary.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/diffly/summary.py b/diffly/summary.py index f6461bd..5e56985 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -641,7 +641,7 @@ def _section_columns(self) -> RenderableType: # ------------------------------- DATA INSPECTION -------------------------------- # def _print_data_inspection(self, console: Console) -> None: - if not self._data.data_inspection or not self._data._data_metric_labels: + if not self._data.data_inspection or not self._data._data_metrics: return _print_section( console, @@ -658,11 +658,11 @@ def _section_data_inspection(self) -> RenderableType: max_width=COLUMN_SECTION_COLUMN_WIDTH, overflow=OVERFLOW, ) - for label in self._data._data_metric_labels: + for label in self._data._data_metrics: table.add_column(label, justify="right", overflow=OVERFLOW) for col in self._data.data_inspection: row_items: list[RenderableType] = [Text(col.name, style="cyan")] - for label in self._data._data_metric_labels: + for label in self._data._data_metrics: result = col.data_metrics.get(label) metric = self._data._data_metrics[label] row_items.append( @@ -790,7 +790,6 @@ class SummaryData: _truncated_left_name: str _truncated_right_name: str _change_metric_labels: list[str] - _data_metric_labels: list[str] _data_metrics: dict[str, DataMetric] def to_dict(self) -> dict[str, Any]: @@ -896,7 +895,6 @@ def _validate_primary_key_hidden_columns() -> None: _truncated_left_name=truncated_left, _truncated_right_name=truncated_right, _change_metric_labels=[], - _data_metric_labels=[], _data_metrics={}, ) @@ -915,7 +913,6 @@ def _validate_primary_key_hidden_columns() -> None: comp, change_metrics, change_columns ) change_metric_labels = list(change_metrics) - data_metric_labels = list(data_metrics) schemas = _compute_schemas(comp, slim) rows = _compute_rows(comp, slim) @@ -948,7 +945,6 @@ def _validate_primary_key_hidden_columns() -> None: _truncated_left_name=truncated_left, _truncated_right_name=truncated_right, _change_metric_labels=change_metric_labels, - _data_metric_labels=data_metric_labels, _data_metrics=data_metrics, ) From 2d3f152bd3671302ced26dcc01413e77af48f0a8 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 16:41:47 +0200 Subject: [PATCH 21/26] fix --- diffly/summary.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/diffly/summary.py b/diffly/summary.py index 5e56985..ec58b85 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -905,13 +905,7 @@ def _validate_primary_key_hidden_columns() -> None: data_metrics = { label: m for label, m in metrics_resolved.items() if isinstance(m, DataMetric) } - change_metrics_by_column: dict[str, dict[str, Any]] = {} - if comp.primary_key is not None and comp.num_rows_joined() > 0: - change_columns = _metric_target_columns(comp, change_metrics) - if any(change_columns.values()): - change_metrics_by_column = _compute_change_metrics( - comp, change_metrics, change_columns - ) + change_metrics_by_column = _compute_change_metrics(comp, change_metrics) change_metric_labels = list(change_metrics) schemas = _compute_schemas(comp, slim) @@ -1027,9 +1021,18 @@ def _metric_target_columns( def _compute_change_metrics( comp: DataFrameComparison, metrics: Mapping[str, ChangeMetric], - metric_to_columns: dict[str, set[str]], ) -> dict[str, dict[str, Any]]: - """Compute change metrics over the joined (matched) rows only.""" + """Compute change metrics over the joined (matched) rows only. + + Change metrics compare matched rows and therefore require a primary key and at least + one joined row. + """ + if comp.primary_key is None or comp.num_rows_joined() == 0: + return {} + metric_to_columns = _metric_target_columns(comp, metrics) + if not any(metric_to_columns.values()): + return {} + agg_exprs: list[pl.Expr] = [] for label, metric in metrics.items(): for column in sorted(metric_to_columns[label]): From a0955c04555065927e365f5f1f86e35eaaefd011 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 16:48:16 +0200 Subject: [PATCH 22/26] fix --- diffly/summary.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/diffly/summary.py b/diffly/summary.py index ec58b85..b3cd57d 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -1033,12 +1033,14 @@ def _compute_change_metrics( if not any(metric_to_columns.values()): return {} - agg_exprs: list[pl.Expr] = [] - for label, metric in metrics.items(): - for column in sorted(metric_to_columns[label]): - left = pl.col(f"{column}_{Side.LEFT}") - right = pl.col(f"{column}_{Side.RIGHT}") - agg_exprs.append(metric.fn(left, right).alias(f"{label}__{column}")) + agg_exprs = [ + metric.fn( + pl.col(f"{column}_{Side.LEFT}"), + pl.col(f"{column}_{Side.RIGHT}"), + ).alias(f"{label}__{column}") + for label, metric in metrics.items() + for column in sorted(metric_to_columns[label]) + ] row = comp.joined(lazy=True).select(agg_exprs).collect().row(0, named=True) out: dict[str, dict[str, Any]] = { From 0d9be700c5d015de39c18a158255975d29855c3f Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Thu, 23 Jul 2026 16:54:00 +0200 Subject: [PATCH 23/26] add test --- tests/summary/test_summary.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/summary/test_summary.py b/tests/summary/test_summary.py index 47be5c4..14f75dd 100644 --- a/tests/summary/test_summary.py +++ b/tests/summary/test_summary.py @@ -13,7 +13,7 @@ from diffly import compare_frames, metrics from diffly.comparison import DataFrameComparison -from diffly.metrics.data import DEFAULT_DATA_METRICS +from diffly.metrics.data import DEFAULT_DATA_METRICS, DataMetric from diffly.summary import _format_fraction_as_percentage, to_json_safe @@ -157,6 +157,23 @@ def test_change_and_data_metrics_routed_to_separate_fields() -> None: } +def test_data_metrics_consider_unjoined_rows() -> None: + # Joined rows are id=1,2,3. The extreme `value`s live in unjoined rows: id=4 is + # left-only (999.0) and id=5 is right-only (888.0). A data metric that only looked at + # the joined rows would report max 30.0 on both sides, so the metric picking these up + # proves it considers values from unjoined rows. + left = pl.DataFrame({"id": [1, 2, 3, 4], "value": [10.0, 20.0, 30.0, 999.0]}) + right = pl.DataFrame({"id": [1, 2, 3, 5], "value": [10.0, 25.0, 30.0, 888.0]}) + comp = compare_frames(left, right, primary_key="id") + + summary = comp.summary(metrics={"Max": DataMetric(fn=lambda col: col.max())}) + result = json.loads(summary.to_json()) + + (value_inspection,) = result["data_inspection"] + assert value_inspection["name"] == "value" + assert value_inspection["data_metrics"] == {"Max": {"left": 999.0, "right": 888.0}} + + def _make_comparison() -> DataFrameComparison: # Designed so every parametrized flag affects the expected JSON output: # - Same columns in both frames → schemas equal → slim suppresses schemas section From 7ecd908192063e67db0a74c27f39768d200b8d03 Mon Sep 17 00:00:00 2001 From: Moritz Potthoff <160181542+MoritzPotthoffQC@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:04:06 +0200 Subject: [PATCH 24/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- diffly/metrics/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index 820c546..bcfa5ef 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -4,9 +4,9 @@ Two families are provided: -- :class:`ChangeMetric`s in :mod:`~diffly.metrics.change` describe the change between +- :class:`~diffly.metrics.change.ChangeMetric`s in :mod:`~diffly.metrics.change` describe the change between numeric columns itself by aggregating over ``right - left``. -- :class:`DataMetric`s in :mod:`~diffly.metrics.data` describe the left and right +- :class:`~diffly.metrics.data.DataMetric`s in :mod:`~diffly.metrics.data` describe the left and right datasets individually, explaining how a change affects the data. """ From f44fc4ff0f081433c23015d43c3c32a208f91acd Mon Sep 17 00:00:00 2001 From: Moritz Potthoff Date: Sat, 25 Jul 2026 15:13:28 +0200 Subject: [PATCH 25/26] review --- diffly/cli.py | 13 ++++++------ diffly/comparison.py | 41 ++++++++++++++++++++++++++---------- diffly/metrics/__init__.py | 4 ++-- diffly/metrics/_common.py | 9 +++++--- diffly/metrics/data.py | 1 - diffly/summary.py | 2 +- tests/test_metrics.py | 43 +++++++++++++++++++++++++++++++++++++- 7 files changed, 88 insertions(+), 25 deletions(-) diff --git a/diffly/cli.py b/diffly/cli.py index c292e9e..5a34406 100644 --- a/diffly/cli.py +++ b/diffly/cli.py @@ -12,7 +12,7 @@ from ._compat import typer from ._utils import ABS_TOL_DEFAULT, ABS_TOL_TEMPORAL_DEFAULT, REL_TOL_DEFAULT -from .metrics import ALL_METRICS +from .metrics import DEFAULT_METRICS app = typer.Typer() @@ -139,8 +139,9 @@ def main( list[str], typer.Option( help=( - "Metric presets to display per column. Repeatable. " - f"Available: {', '.join(ALL_METRICS)}." + "Metric presets to display. Repeatable. Change metrics appear as " + "extra columns in the Columns table; data metrics appear in the Data " + f"Inspection section. Available: {', '.join(DEFAULT_METRICS)}." ) ), ] = [], @@ -154,11 +155,11 @@ def main( hidden_column = [*hidden_column, *hidden_columns] for name in metric: - if name not in ALL_METRICS: + if name not in DEFAULT_METRICS: raise typer.BadParameter( - f"Unknown metric: {name!r}. Available: {', '.join(ALL_METRICS)}." + f"Unknown metric: {name!r}. Available: {', '.join(DEFAULT_METRICS)}." ) - metrics = {name: ALL_METRICS[name] for name in metric} + metrics = {name: DEFAULT_METRICS[name] for name in metric} comparison = compare_frames( pl.scan_parquet(left), diff --git a/diffly/comparison.py b/diffly/comparison.py index 9d58e39..7649751 100644 --- a/diffly/comparison.py +++ b/diffly/comparison.py @@ -26,14 +26,16 @@ lazy_len, make_and_validate_mapping, ) -from .metrics._common import Metric -from .metrics.change import ChangeMetric, ChangeMetricFn -from .metrics.data import DataMetric, DataMetricFn +from .metrics._common import Metric, MetricFn +from .metrics.change import ChangeMetric +from .metrics.data import DataMetric if TYPE_CHECKING: # pragma: no cover # NOTE: We cannot import at runtime as we're otherwise running into circular # imports. We're importing again below where we need `Summary` for more than # type annotations. + from .metrics.change import ChangeMetricFn + from .metrics.data import DataMetricFn from .summary import Summary @@ -923,7 +925,7 @@ def summary( right_name: str = Side.RIGHT, slim: bool = False, hidden_columns: list[str] | None = None, - metrics: Mapping[str, ChangeMetricFn | DataMetricFn | Metric] | None = None, + metrics: Mapping[str, MetricFn | Metric] | None = None, ) -> Summary: """Generate a summary of all aspects of the comparison. @@ -1239,11 +1241,28 @@ def _list_length_exprs( return [] -def _resolve_metric(v: ChangeMetricFn | DataMetricFn | Metric) -> Metric: - if isinstance(v, (ChangeMetric, DataMetric)): +def _resolve_metric(v: MetricFn | Metric) -> Metric: + if isinstance(v, Metric): return v - # Infer the metric family from the callable's arity: a single-argument - # callable describes one side (data), two arguments describe a change. - if len(inspect.signature(v).parameters) >= 2: - return ChangeMetric(fn=cast(ChangeMetricFn, v)) - return DataMetric(fn=cast(DataMetricFn, v)) + # Infer the metric family from the number of required positional parameters: a + # single-argument callable describes one side (data), two arguments describe a + # change. Ambiguous signatures (variadic or a different arity) are rejected so the + # user wraps them explicitly in `DataMetric`/`ChangeMetric`. + params = inspect.signature(v).parameters.values() + required_positional = [ + p + for p in params + if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) + and p.default is p.empty + ] + has_variadic = any(p.kind is p.VAR_POSITIONAL for p in params) + if has_variadic or len(required_positional) not in (1, 2): + raise ValueError( + "Cannot infer the metric family from the callable's signature: expected " + "exactly one required positional argument (data metric) or two (change " + "metric), but got an ambiguous signature. Wrap it explicitly in " + "`DataMetric` or `ChangeMetric`." + ) + if len(required_positional) == 2: + return ChangeMetric(fn=cast("ChangeMetricFn", v)) + return DataMetric(fn=cast("DataMetricFn", v)) diff --git a/diffly/metrics/__init__.py b/diffly/metrics/__init__.py index bcfa5ef..b8f8d49 100644 --- a/diffly/metrics/__init__.py +++ b/diffly/metrics/__init__.py @@ -13,6 +13,6 @@ from __future__ import annotations from . import change, data -from ._common import ALL_METRICS +from ._common import DEFAULT_METRICS -__all__ = ["ALL_METRICS", "change", "data"] +__all__ = ["DEFAULT_METRICS", "change", "data"] diff --git a/diffly/metrics/_common.py b/diffly/metrics/_common.py index dc3172c..2d8f334 100644 --- a/diffly/metrics/_common.py +++ b/diffly/metrics/_common.py @@ -1,13 +1,16 @@ # Copyright (c) QuantCo 2025-2026 # SPDX-License-Identifier: BSD-3-Clause -from .change import DEFAULT_CHANGE_METRICS, ChangeMetric -from .data import DEFAULT_DATA_METRICS, DataMetric +from .change import DEFAULT_CHANGE_METRICS, ChangeMetric, ChangeMetricFn +from .data import DEFAULT_DATA_METRICS, DataMetric, DataMetricFn Metric = ChangeMetric | DataMetric """A change or data metric paired with a column-applicability selector.""" -ALL_METRICS: dict[str, Metric] = { +MetricFn = ChangeMetricFn | DataMetricFn +"""A bare change or data metric callable, resolved to a :data:`Metric` by arity.""" + +DEFAULT_METRICS: dict[str, Metric] = { **DEFAULT_CHANGE_METRICS, **DEFAULT_DATA_METRICS, } diff --git a/diffly/metrics/data.py b/diffly/metrics/data.py index 9ade440..c3babd5 100644 --- a/diffly/metrics/data.py +++ b/diffly/metrics/data.py @@ -53,7 +53,6 @@ def null_fraction(col: pl.Expr) -> pl.Expr: DEFAULT_DATA_METRICS: dict[str, DataMetric] = { "Null%": DataMetric( fn=null_fraction, - selector=cs.all(), formatter=lambda value: f"{round(value * 100, 2)}%", delta_formatter=lambda value: f"{round(value * 100, 2)}", ), diff --git a/diffly/summary.py b/diffly/summary.py index b3cd57d..0086af7 100644 --- a/diffly/summary.py +++ b/diffly/summary.py @@ -1131,7 +1131,7 @@ def _compute_columns( match_rate=rate, n_total_changes=n_total_changes, changes=changes, - change_metrics=change_metrics_by_column.get(col_name) or None, + change_metrics=change_metrics_by_column.get(col_name), ) ) return columns diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 609d10c..3410fac 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -8,8 +8,10 @@ import pytest from diffly import metrics +from diffly.comparison import _resolve_metric from diffly.metrics import data -from diffly.metrics.change import ChangeMetricFn +from diffly.metrics.change import ChangeMetric, ChangeMetricFn +from diffly.metrics.data import DataMetric @pytest.fixture @@ -94,3 +96,42 @@ def test_default_metrics_partition() -> None: # The change and data preset sets are disjoint. assert set(change.DEFAULT_CHANGE_METRICS) & set(data.DEFAULT_DATA_METRICS) == set() + + +@pytest.mark.parametrize( + "metric", + [ChangeMetric(fn=metrics.change.mean), DataMetric(fn=data.null_fraction)], +) +def test_resolve_metric_passthrough(metric: Any) -> None: + assert _resolve_metric(metric) is metric + + +@pytest.mark.parametrize( + "fn, expected", + [ + # One required positional argument → data metric, two → change metric. Optional + # parameters do not count towards the arity. + (lambda col: col.max(), DataMetric), + (lambda left, right: right - left, ChangeMetric), + # A closure over a tuning parameter binds it away, so only the columns remain as + # required positional arguments: a single-column data metric vs. the + # `metrics.change.quantile` factory whose closure takes `(left, right)`. + (lambda col, q=0.95: col.quantile(q), DataMetric), + (metrics.change.quantile(0.95), ChangeMetric), + ], +) +def test_resolve_metric_infers_family_from_arity(fn: Any, expected: type) -> None: + assert isinstance(_resolve_metric(fn), expected) + + +@pytest.mark.parametrize( + "fn", + [ + lambda: pl.lit(0), # no positional arguments + lambda a, b, c: a, # too many required positional arguments + lambda *cols: cols[0], # variadic + ], +) +def test_resolve_metric_rejects_ambiguous_signature(fn: Any) -> None: + with pytest.raises(ValueError, match="Cannot infer the metric family"): + _resolve_metric(fn) From 8c7c45aefb4754561ccc124e95dbc9be6fed588f Mon Sep 17 00:00:00 2001 From: Moritz Potthoff <160181542+MoritzPotthoffQC@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:20:18 +0200 Subject: [PATCH 26/26] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/api/metrics.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 5163b55..3680690 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -33,7 +33,8 @@ Presets come in two families, each with its own module and default set: so you can see how a change affects the data. The preset default sets are :data:`~diffly.metrics.change.DEFAULT_CHANGE_METRICS` -and :data:`~diffly.metrics.data.DEFAULT_DATA_METRICS`. +and :data:`~diffly.metrics.data.DEFAULT_DATA_METRICS`. The union of both families is +also available as :data:`~diffly.metrics.DEFAULT_METRICS`. Change metrics ==============