Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions diffly/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
quantile,
std,
)
from .data import _make_data_metric

DEFAULT_METRICS: dict[str, MetricFn | Metric] = {
**change.DEFAULT_CHANGE_METRICS,
Expand All @@ -47,4 +48,5 @@
"quantile",
"std",
"_make_numeric_metric",
"_make_data_metric",
]
9 changes: 8 additions & 1 deletion diffly/metrics/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@

from collections.abc import Callable
from dataclasses import dataclass
from typing import Literal

import polars as pl
import polars.selectors as cs


@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]
Expand Down
7 changes: 6 additions & 1 deletion diffly/metrics/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<old> -> <new> (<delta>)``.

Expand All @@ -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()),
"Null%": _make_data_metric(null_fraction_change),
}
"""Preset metrics describing the left and right datasets individually."""

Expand Down
72 changes: 65 additions & 7 deletions diffly/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))
Comment thread
MoritzPotthoffQC marked this conversation as resolved.
matches.add_column(
"Column",
Expand Down Expand Up @@ -632,6 +635,36 @@ def _section_columns(self) -> RenderableType:

return Group(*display_items)

# ------------------------------- DATA INSPECTION -------------------------------- #

def _print_data_inspection(self, console: Console) -> None:
if not self._data.columns or not self._data._data_metric_labels:
return
_print_section(
console,
"Data Inspection",
self._section_data_inspection(),
)

def _section_data_inspection(self) -> RenderableType:
assert self._data.columns is not None

table = Table()
table.add_column(
"Column",
max_width=COLUMN_SECTION_COLUMN_WIDTH,
overflow=OVERFLOW,
)
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 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)
return table

# ------------------------------ ROWS ONLY ONE SIDE ------------------------------ #

def _print_sample_rows_only_one_side(self, console: Console, side: Side) -> None:
Expand Down Expand Up @@ -716,6 +749,7 @@ class SummaryDataColumn:
n_total_changes: int
changes: list[SummaryDataColumnChange] | None
metrics: dict[str, Any] | None
data_metrics: dict[str, Any] | None


@dataclass
Expand All @@ -733,7 +767,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:
Expand Down Expand Up @@ -836,12 +871,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)
Expand All @@ -852,6 +893,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
Expand All @@ -871,7 +914,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,
)


Expand Down Expand Up @@ -977,6 +1021,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.
Expand Down Expand Up @@ -1017,13 +1063,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
└────────┴───────────────────────┘
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
└────────┴───────────────────────┘
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
└────────┴───────────────────────┘
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
└────────┴───────────────────────┘
Loading