Skip to content
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

Sort by value column labels #409

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
73 changes: 73 additions & 0 deletions src/cr/cube/matrix/assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def column_display_order(cls, dimensions, second_order_measures):
# --- _ColumnOrderHelper, so there's not much to this yet, just keeping
# --- form consistent with `.row_display_order()` and we'll elaborate this when
# --- we add sort-by-value to columns.
ernestoarbitrio marked this conversation as resolved.
Show resolved Hide resolved
collation_method = dimensions[1].order_spec.collation_method
if collation_method == CM.LABEL:
return _SortColumnsByLabelHelper(
dimensions, second_order_measures, format
)._display_order

return _ColumnOrderHelper(dimensions, second_order_measures)._display_order

@classmethod
Expand Down Expand Up @@ -349,6 +355,73 @@ def _subtotal_values(self):
)


class _BaseSortColumnsByValueHelper(_ColumnOrderHelper):
"""A base class that orders elements by a set of values.
This class is intended only to serve as a base for the other sort-by-value classes
which must all provide their own implentations of `_element_values` and
`_subtotal_values`.
If `_order` encouters a ValueError, it falls back to the payload order.
"""

@lazyproperty
def _element_values(self):
"""Sequence of body values that form the basis for sort order.
Must be implemented by child classes.
"""
raise NotImplementedError( # pragma: no cover
f"{type(self).__name__} must implement `._element_values`"
)

@lazyproperty
def _order(self):
"""tuple of int element-idx specifying ordering of dimension elements."""
try:
return SortByValueCollator.display_order(
self._columns_dimension,
self._element_values,
self._subtotal_values,
self._empty_column_idxs,
self._format,
)
except ValueError:
return PayloadOrderCollator.display_order(
self._columns_dimension, self._empty_column_idxs, self._format
)

@lazyproperty
def _subtotal_values(self):
"""Sequence of subtotal values that form the basis for sort order.
Must be implemented by child classes.
"""
raise NotImplementedError( # pragma: no cover
f"{type(self).__name__} must implement `._subtotal_values`"
)


class _SortColumnsByLabelHelper(_BaseSortColumnsByValueHelper):
@lazyproperty
def _element_values(self):
"""Sequence of body labels that form the basis for sort order.
There is one value per column and values appear in payload (dimension) element
order. These are only the "base" values and do not include insertions.
"""
return np.array(self._dimensions[1].element_labels)

@lazyproperty
def _subtotal_values(self):
"""Sequence of col-subtotal labels that contribute to the sort basis.
There is one value per column subtotal and labels appear in payload (dimension)
insertion order.
"""
return np.array(self._dimensions[1].subtotal_labels)


class _SortRowsByBaseColumnHelper(_BaseSortRowsByValueHelper):
"""Orders elements by the values of an opposing base (not a subtotal) vector.
Expand Down
72 changes: 66 additions & 6 deletions tests/integration/test_cubepart.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

"""Integration-test suite for `cr.cube.cubepart` module."""

from textwrap import dedent
import numpy as np
import pytest

Expand Down Expand Up @@ -921,6 +922,47 @@ def test_and_it_respects_explicit_order_transform_for_measures(
expected = load_python_expression(expectation)
np.testing.assert_almost_equal(actual, expected)

def test_it_can_sort_by_column_by_labels(self):
transforms = {
"columns_dimension": {"order": {"type": "label", "direction": "ascending"}}
}
slice_ = _Slice(Cube(CR.CAT_4_X_CAT_5), 0, transforms, None, 0)
expected_slice_repr = """
_Slice(name='Support', dimension_types='CAT x CAT')
Showing: COUNT
As married Divorced Married Never Widowed
---------- ------------ ---------- --------- ------- ---------
Plenty 21 3 46 7 0
Enough 55 13 127 29 1
Not enough 17 41 253 47 1
N/A 80 19 247 26 4
Available measures: [<CUBE_MEASURE.COUNT: 'count'>]
"""
assert slice_.__repr__() == dedent(expected_slice_repr).strip()

transforms = {
"columns_dimension": {
"order": {
"type": "label",
"direction": "ascending",
"fixed": {"bottom": [1]},
}
}
}
slice_ = _Slice(Cube(CR.CAT_4_X_CAT_5), 0, transforms, None, 0)
expected_slice_repr = """
_Slice(name='Support', dimension_types='CAT x CAT')
Showing: COUNT
As married Divorced Never Widowed Married
---------- ------------ ---------- ------- --------- ---------
Plenty 21 3 7 0 46
Enough 55 13 29 1 127
Not enough 17 41 47 1 253
N/A 80 19 26 4 247
Available measures: [<CUBE_MEASURE.COUNT: 'count'>]
"""
assert slice_.__repr__() == dedent(expected_slice_repr).strip()

def test_it_can_sort_by_column_index(self):
"""Responds to order:opposing_element sort-by-column-index."""
transforms = {
Expand Down Expand Up @@ -1016,9 +1058,28 @@ def test_it_can_sort_by_marginal_with_nan_in_body(self):
[2.45356177, 2.11838791, 2.0, 1.97, 1.74213625, np.nan], nan_ok=True
)

def test_it_can_fix_order_of_subvars_identified_by_bogus_id(self):
@pytest.mark.parametrize(
"dimension, expceted_labels",
(
("rows_dimension", ["Finland", "Sweden", "Norway", "Denmark", "Iceland"]),
(
"columns_dimension",
[
"Chitarra",
"Quadrefiore",
"Orecchiette",
"Fileja",
"Bucatini",
"Boccoli",
],
),
),
)
def test_it_can_fix_order_of_subvars_identified_by_bogus_id(
ernestoarbitrio marked this conversation as resolved.
Show resolved Hide resolved
self, dimension, expceted_labels
):
transforms = {
"rows_dimension": {
dimension: {
"order": {
"type": "label",
"direction": "descending",
Expand All @@ -1028,10 +1089,9 @@ def test_it_can_fix_order_of_subvars_identified_by_bogus_id(self):
}
}
slice_ = _Slice(Cube(CR.MR_X_CAT), 0, transforms, None, 0)

expected = ["Finland", "Sweden", "Norway", "Denmark", "Iceland"]
actual = slice_.row_labels.tolist()
assert expected == actual, "\n%s\n\n%s" % (expected, actual)
prop = "row_labels" if dimension == "rows_dimension" else "column_labels"
actual = getattr(slice_, prop).tolist()
assert expceted_labels == actual, "\n%s\n\n%s" % (expceted_labels, actual)

@pytest.mark.parametrize(
"id",
Expand Down
112 changes: 112 additions & 0 deletions tests/unit/matrix/test_assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
)
from cr.cube.matrix.assembler import (
_BaseOrderHelper,
_BaseSortColumnsByValueHelper,
_BaseSortRowsByValueHelper,
_ColumnOrderHelper,
_RowOrderHelper,
_SortColumnsByLabelHelper,
_SortRowsByBaseColumnHelper,
_SortRowsByDerivedColumnHelper,
_SortRowsByInsertedColumnHelper,
Expand Down Expand Up @@ -1393,6 +1395,84 @@ def _subtotal_values_prop_(self, request):
return property_mock(request, _BaseSortRowsByValueHelper, "_subtotal_values")


class Test_BaseSortColumnsByValueHelper:
"""Unit test suite for `cr.cube.matrix.assembler._BaseSortColumnsByValueHelper`."""

def test_it_provides_the_order(
self,
SortByValueCollator_,
_columns_dimension_prop_,
_element_values_prop_,
_subtotal_values_prop_,
_empty_column_idxs_prop_,
):
_BaseSortColumnsByValueHelper(None, None)._order

SortByValueCollator_.display_order.assert_called_once_with(
_columns_dimension_prop_(),
_element_values_prop_(),
_subtotal_values_prop_(),
_empty_column_idxs_prop_(),
ORDER_FORMAT.SIGNED_INDEXES,
)

def test_but_it_falls_back_to_payload_order_on_value_error(
self,
request,
dimensions_,
_element_values_prop_,
_subtotal_values_prop_,
_empty_column_idxs_prop_,
SortByValueCollator_,
):
_element_values_prop_.return_value = None
_subtotal_values_prop_.return_value = None
_empty_column_idxs_prop_.return_value = (4, 2)
SortByValueCollator_.display_order.side_effect = ValueError
PayloadOrderCollator_ = class_mock(
request, "cr.cube.matrix.assembler.PayloadOrderCollator"
)
PayloadOrderCollator_.display_order.return_value = (1, 2, 3, 4)
order_helper = _BaseSortColumnsByValueHelper(dimensions_, None)

order = order_helper._order

PayloadOrderCollator_.display_order.assert_called_once_with(
dimensions_[1], (4, 2), ORDER_FORMAT.SIGNED_INDEXES
)
assert order == (1, 2, 3, 4)

# fixture components ---------------------------------------------

@pytest.fixture
def dimensions_(self, request):
return (instance_mock(request, Dimension), instance_mock(request, Dimension))

@pytest.fixture
def _element_values_prop_(self, request):
return property_mock(request, _BaseSortColumnsByValueHelper, "_element_values")

@pytest.fixture
def _empty_column_idxs_prop_(self, request):
return property_mock(
request, _BaseSortColumnsByValueHelper, "_empty_column_idxs"
)

@pytest.fixture
def _columns_dimension_prop_(self, request):
return property_mock(
request, _BaseSortColumnsByValueHelper, "_columns_dimension"
)

@pytest.fixture
def SortByValueCollator_(self, request):
return class_mock(request, "cr.cube.matrix.assembler.SortByValueCollator")

@pytest.fixture
def _subtotal_values_prop_(self, request):
return property_mock(request, _BaseSortColumnsByValueHelper, "_subtotal_values")


class Test_SortRowsByBaseColumnHelper:
"""Unit test suite for `cr.cube.matrix.assembler._SortRowsByBaseColumnHelper`."""

Expand Down Expand Up @@ -1606,6 +1686,38 @@ def dimensions_(self, request):
return (instance_mock(request, Dimension), instance_mock(request, Dimension))


class Test_SortColumnsByLabelHelper:
"""Unit test suite for `cr.cube.matrix.assembler._SortColumnsByLabelHelper`."""

def test_it_provides_the_element_values_to_help(self, dimensions_):
dimensions_[1].element_labels = ("c", "a", "b")

assert _SortColumnsByLabelHelper(
dimensions_, None
)._element_values.tolist() == [
"c",
"a",
"b",
]

def test_it_provides_the_subtotal_values_to_help(self, dimensions_):
dimensions_[1].subtotal_labels = ("c", "a", "b")

assert _SortColumnsByLabelHelper(
dimensions_, None
)._subtotal_values.tolist() == [
"c",
"a",
"b",
]

# fixture components ---------------------------------------------

@pytest.fixture
def dimensions_(self, request):
return (instance_mock(request, Dimension), instance_mock(request, Dimension))


class Test_SortRowsByMarginalHelper:
"""Unit test suite for `cr.cube.matrix.assembler._SortRowsByMarginalHelper`."""

Expand Down