-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Introduce new summary section for data metrics #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bff2594
51fbda2
cb086ef
526c944
5732c37
5867021
6b705e7
6c868d6
6802592
d827967
2a49d96
eef3d3b
e918bbf
65d80d6
ffeb05a
c116fdf
a57dc0f
1adcf78
4000181
a52e24c
2d3f152
a0955c0
0d9be70
7ecd908
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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,9 @@ | |||||
| lazy_len, | ||||||
| make_and_validate_mapping, | ||||||
| ) | ||||||
| from .metrics import Metric, MetricFn, _make_numeric_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 | ||||||
|
|
@@ -920,7 +923,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 +954,14 @@ 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.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.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. | ||||||
|
|
||||||
| Returns: | ||||||
|
|
@@ -978,10 +979,7 @@ def summary( | |||||
| from .summary import Summary | ||||||
|
|
||||||
| resolved_metrics = ( | ||||||
| { | ||||||
| label: v if isinstance(v, Metric) else _make_numeric_metric(v) | ||||||
| for label, v in metrics.items() | ||||||
| } | ||||||
| {label: _resolve_metric(v) for label, v in metrics.items()} | ||||||
| if metrics is not None | ||||||
| else None | ||||||
| ) | ||||||
|
|
@@ -1239,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)): | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ?
Suggested change
|
||||||
| 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: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not:
Suggested change
Also, how about being a bit defensive here and do: elif isinstance(v, DataMetricFn):
...
else:
raiseso that the signature issue is easier to understand for the user? |
||||||
| return ChangeMetric(fn=cast(ChangeMetricFn, v)) | ||||||
| return DataMetric(fn=cast(DataMetricFn, v)) | ||||||
|
MoritzPotthoffQC marked this conversation as resolved.
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,50 +1,18 @@ | ||
| # Copyright (c) QuantCo 2025-2026 | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| """Metrics computed per column when generating a summary. | ||
|
|
||
| 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:`~diffly.metrics.change.ChangeMetric`s in :mod:`~diffly.metrics.change` describe the change between | ||
| numeric columns itself by aggregating over ``right - left``. | ||
| - :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 __future__ import annotations | ||
|
|
||
| from . import change, data | ||
| from ._common import Metric, MetricFn | ||
| from .change import ( | ||
| _make_numeric_metric, | ||
| max, | ||
| mean, | ||
| mean_absolute_deviation, | ||
| mean_relative_deviation, | ||
| median, | ||
| min, | ||
| quantile, | ||
| std, | ||
| ) | ||
|
|
||
| DEFAULT_METRICS: dict[str, MetricFn | Metric] = { | ||
| **change.DEFAULT_CHANGE_METRICS, | ||
| } | ||
| """The default preset metrics, consisting of the change default set.""" | ||
| from ._common import ALL_METRICS | ||
|
|
||
| __all__ = [ | ||
| "DEFAULT_METRICS", | ||
| "Metric", | ||
| "MetricFn", | ||
| "change", | ||
| "data", | ||
| "max", | ||
| "mean", | ||
| "mean_absolute_deviation", | ||
| "mean_relative_deviation", | ||
| "median", | ||
| "min", | ||
| "quantile", | ||
| "std", | ||
| "_make_numeric_metric", | ||
| ] | ||
| __all__ = ["ALL_METRICS", "change", "data"] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,27 +1,14 @@ | ||||||
| # Copyright (c) QuantCo 2025-2026 | ||||||
| # SPDX-License-Identifier: BSD-3-Clause | ||||||
|
|
||||||
| from __future__ import annotations | ||||||
| from .change import DEFAULT_CHANGE_METRICS, ChangeMetric | ||||||
| from .data import DEFAULT_DATA_METRICS, DataMetric | ||||||
|
|
||||||
| from collections.abc import Callable | ||||||
| from dataclasses import dataclass | ||||||
| Metric = ChangeMetric | DataMetric | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use MetricFn = ChangeMetricFn | DataMetricFn? |
||||||
| """A change or data metric paired with a column-applicability selector.""" | ||||||
|
|
||||||
| import polars as pl | ||||||
| import polars.selectors as cs | ||||||
|
|
||||||
|
|
||||||
| @dataclass(frozen=True) | ||||||
| class Metric: | ||||||
| """A metric function paired with a column-applicability selector.""" | ||||||
|
|
||||||
| fn: MetricFn | ||||||
| selector: cs.Selector | ||||||
|
|
||||||
|
|
||||||
| MetricFn = Callable[[pl.Expr, pl.Expr], pl.Expr] | ||||||
| """A metric function 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. | ||||||
| """ | ||||||
| ALL_METRICS: dict[str, Metric] = { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ?
Suggested change
|
||||||
| **DEFAULT_CHANGE_METRICS, | ||||||
| **DEFAULT_DATA_METRICS, | ||||||
| } | ||||||
| """All preset metrics, combining the change and data default sets.""" | ||||||
Uh oh!
There was an error while loading. Please reload this page.