From 48f0a8eec830d59cd226bb1c427df8631f8b06bd Mon Sep 17 00:00:00 2001 From: Gaetano Guerriero Date: Tue, 12 May 2026 08:41:13 +0200 Subject: [PATCH 1/5] use native namespace package for cr --- setup.py | 4 +--- src/cr/__init__.py | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 src/cr/__init__.py diff --git a/setup.py b/setup.py index a5b6697d3..3529a6749 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import os import re -from setuptools import find_packages, setup +from setuptools import setup def ascii_bytes_from(path, *paths): @@ -51,10 +51,8 @@ def ascii_bytes_from(path, *paths): install_requires=install_requires, tests_require=test_requires, extras_require={"testing": test_requires}, - packages=find_packages("src", exclude=["tests"]), package_dir={"": "src"}, include_package_data=True, entry_points={}, - namespace_packages=["cr"], zip_safe=True, ) diff --git a/src/cr/__init__.py b/src/cr/__init__.py deleted file mode 100644 index c1905719e..000000000 --- a/src/cr/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: # pragma: no cover - __import__("pkg_resources").declare_namespace(__name__) -except ImportError: # pragma: no cover - __import__("pkgutil").extend_path(__path__, __name__) # type: ignore From ded48b277011a2359cf75aa3adaaaa7ab2a26cd8 Mon Sep 17 00:00:00 2001 From: Gaetano Guerriero Date: Tue, 12 May 2026 08:46:32 +0200 Subject: [PATCH 2/5] remove obsolete tests_require from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 3529a6749..b208c70f4 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,6 @@ def ascii_bytes_from(path, *paths): author_email="dev@crunch.io", license="MIT License", install_requires=install_requires, - tests_require=test_requires, extras_require={"testing": test_requires}, package_dir={"": "src"}, include_package_data=True, From 77462f1d0124d7d1d1f73c5e1cd714a500321a97 Mon Sep 17 00:00:00 2001 From: Gaetano Guerriero Date: Tue, 12 May 2026 08:43:39 +0200 Subject: [PATCH 3/5] Bump version 3.3.8 --- README.md | 4 ++++ src/cr/cube/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7be716924..ec0dd5b38 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ Please visit for the API referenc ## Changes +### 3.3.8 + +- Use native namespace packages for cr instead of deprecated pkg_resources / pkgutil + ### 3.3.7 - Rebuild diff --git a/src/cr/cube/__init__.py b/src/cr/cube/__init__.py index 667d51c76..02b9dce71 100644 --- a/src/cr/cube/__init__.py +++ b/src/cr/cube/__init__.py @@ -2,4 +2,4 @@ """Initialization module for crunch-cube package.""" -__version__ = "3.3.7" +__version__ = "3.3.8" From 3d56c6e725a9e46b41df7dd49258bdd1cc48ef1d Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Tue, 19 May 2026 17:07:22 +0200 Subject: [PATCH 4/5] Add columns and rows margin percentage --- src/cr/cube/cubepart.py | 29 ++++ .../cat-hs-x-mr-columns-margin-percentages.py | 7 + .../cat-hs-x-mr-row-margin-percentages.py | 46 +++++++ .../mr-x-cat-hs-columns-margin-percentages.py | 52 +++++++ .../mr-x-cat-hs-row-margin-percentages.py | 7 + .../mr-x-mr-columns-margin-percentages.py | 6 + .../mr-x-mr-row-margin-percentages.py | 6 + tests/integration/test_cubepart.py | 34 +++++ .../integration/test_headers_and_subtotals.py | 2 + tests/unit/matrix/test_assembler.py | 128 ++++++++++++++++++ 10 files changed, 317 insertions(+) create mode 100644 tests/expectations/cat-hs-x-mr-columns-margin-percentages.py create mode 100644 tests/expectations/cat-hs-x-mr-row-margin-percentages.py create mode 100644 tests/expectations/mr-x-cat-hs-columns-margin-percentages.py create mode 100644 tests/expectations/mr-x-cat-hs-row-margin-percentages.py create mode 100644 tests/expectations/mr-x-mr-columns-margin-percentages.py create mode 100644 tests/expectations/mr-x-mr-row-margin-percentages.py diff --git a/src/cr/cube/cubepart.py b/src/cr/cube/cubepart.py index c5e094f52..91bc94b7e 100644 --- a/src/cr/cube/cubepart.py +++ b/src/cr/cube/cubepart.py @@ -558,6 +558,18 @@ def columns_margin_proportion(self): # --- otherwise columns-margin-proportion is a maginal --- return self._assemble_marginal(self._measures.columns_table_proportion) + @lazyproperty + def columns_margin_percentage(self): + """1D or 2D np.float64 ndarray of weighted-percentage for each column of slice + + This array is 2D (a distinct margin value for each cell) when the rows + dimension is MR, because each MR-subvariable has its own weighted N. This is + because not every possible response is necessarily offered to every respondent. + + In all other cases, the array is 1D, containing one value for each column. + """ + return self.columns_margin_proportion * 100 + @lazyproperty def columns_scale_mean(self): """Optional 1D np.float64 ndarray of scale mean for each column. @@ -1431,6 +1443,18 @@ def rows_margin_proportion(self): # --- otherwise rows-margin is a vector --- return self._assemble_marginal(self._measures.rows_table_proportion) + @lazyproperty + def rows_margin_percentage(self): + """1D or 2D np.float64 ndarray of weighted-percentage for each column of slice + + This array is 2D (a distinct margin value for each cell) when the columns + dimension is MR, because each MR-subvariable has its own weighted N. This is + because not every possible response is necessarily offered to every respondent. + + In all other cases, the array is 1D, containing one value for each column. + """ + return self.rows_margin_proportion * 100 + @lazyproperty def rows_missing(self): """1D/2D np.float64 ndarray of unweighted counts of missing for each cell/row @@ -1754,6 +1778,11 @@ def table_code(self): @lazyproperty def table_percentages(self): + """2D np.float64 ndarray of table-percentage. + + Table-percentage is the fraction of the table weighted-N contributed by each + cell, expressed as a percentage (float between 0.0 and 100.0 inclusive). + """ return self.table_proportions * 100 @lazyproperty diff --git a/tests/expectations/cat-hs-x-mr-columns-margin-percentages.py b/tests/expectations/cat-hs-x-mr-columns-margin-percentages.py new file mode 100644 index 000000000..ebb5eb9e4 --- /dev/null +++ b/tests/expectations/cat-hs-x-mr-columns-margin-percentages.py @@ -0,0 +1,7 @@ +[ + 17.935203679537608, + 33.454988861780095, + 50.762387573092646, + 80.27297327014429, + 79.83476971629214, +] diff --git a/tests/expectations/cat-hs-x-mr-row-margin-percentages.py b/tests/expectations/cat-hs-x-mr-row-margin-percentages.py new file mode 100644 index 000000000..c1373ee6f --- /dev/null +++ b/tests/expectations/cat-hs-x-mr-row-margin-percentages.py @@ -0,0 +1,46 @@ +[ + [ + 12.354283136548759, + 7.444231360707625, + 4.93318248692712, + 4.584390457713685, + 6.55671553927186, + ], + [ + 18.60667239281892, + 19.291282000683193, + 16.542087034551418, + 13.813767505048451, + 18.69747651879045, + ], + [ + 30.960955529367677, + 26.735513361390815, + 21.475269521478538, + 18.398157962762134, + 25.25419205806231, + ], + [ + 30.960955529367677, + 26.735513361390815, + 21.475269521478538, + 18.398157962762138, + 25.25419205806231, + ], + [0.0, 0.0, 0.0, 0.0, 0.0], + [ + 33.37741042168464, + 36.41410555192866, + 37.11714542482715, + 36.248045110476504, + 35.13675072713398, + ], + [ + 35.66163404894768, + 36.850381086680535, + 41.40758505369432, + 45.35379692676135, + 39.60905721480371, + ], + [100.0, 100.0, 100.0, 99.99999999999999, 100.0], +] diff --git a/tests/expectations/mr-x-cat-hs-columns-margin-percentages.py b/tests/expectations/mr-x-cat-hs-columns-margin-percentages.py new file mode 100644 index 000000000..a9d0418dd --- /dev/null +++ b/tests/expectations/mr-x-cat-hs-columns-margin-percentages.py @@ -0,0 +1,52 @@ +[ + [ + 12.354283158588755, + 18.60667238084944, + 30.960955539438196, + 30.960955539438196, + 0.0, + 33.37741042334933, + 35.66163403721248, + 100.0, + ], + [ + 7.444231365436714, + 19.291281991786978, + 26.735513357223688, + 26.735513357223688, + 0.0, + 36.41410557725717, + 36.85038106551913, + 99.99999999999997, + ], + [ + 4.93318248486953, + 16.54208703286872, + 21.47526951773825, + 21.475269517738248, + 0.0, + 37.11714541914551, + 41.40758506311624, + 100.0, + ], + [ + 4.5843904451220485, + 13.813767515853032, + 18.39815796097508, + 18.39815796097508, + 0.0, + 36.248045116670035, + 45.35379692235489, + 100.0, + ], + [ + 6.556715546598428, + 18.697476512833678, + 25.254192059432107, + 25.2541920594321, + 0.0, + 35.1367507213182, + 39.60905721924969, + 100.0, + ], +] diff --git a/tests/expectations/mr-x-cat-hs-row-margin-percentages.py b/tests/expectations/mr-x-cat-hs-row-margin-percentages.py new file mode 100644 index 000000000..b05ffbeb3 --- /dev/null +++ b/tests/expectations/mr-x-cat-hs-row-margin-percentages.py @@ -0,0 +1,7 @@ +[ + 17.935203742867287, + 33.454988896294154, + 50.76238756298953, + 80.27297326388751, + 79.83476971842138, +] diff --git a/tests/expectations/mr-x-mr-columns-margin-percentages.py b/tests/expectations/mr-x-mr-columns-margin-percentages.py new file mode 100644 index 000000000..59ec2a137 --- /dev/null +++ b/tests/expectations/mr-x-mr-columns-margin-percentages.py @@ -0,0 +1,6 @@ +[ + [13.835522515396878, 26.54735977789324, 55.81110549484439, 60.67041874287763], + [12.36197264278271, 32.26874248151863, 53.05910795058967, 67.57591837745224], + [15.913981148498676, 35.664811199716155, 48.05298239913407, 65.97161566200931], + [13.835522515396914, 32.26874248151863, 48.05298239913407, 55.24609836123409], +] diff --git a/tests/expectations/mr-x-mr-row-margin-percentages.py b/tests/expectations/mr-x-mr-row-margin-percentages.py new file mode 100644 index 000000000..958f70b10 --- /dev/null +++ b/tests/expectations/mr-x-mr-row-margin-percentages.py @@ -0,0 +1,6 @@ +[ + [13.835522515396878, 12.36197264278271, 15.913981148498676, 13.835522515396912], + [26.54735977789324, 32.26874248151863, 35.664811199716155, 32.26874248151863], + [55.81110549484439, 53.05910795058967, 48.05298239913407, 48.05298239913407], + [60.670418742877644, 67.57591837745224, 65.97161566200931, 55.24609836123409], +] diff --git a/tests/integration/test_cubepart.py b/tests/integration/test_cubepart.py index c7bb2e4fa..5a7e5356d 100644 --- a/tests/integration/test_cubepart.py +++ b/tests/integration/test_cubepart.py @@ -819,6 +819,23 @@ def test_it_knows_the_rows_margin_proportion(self, fixture, expectation): assert rows_margin_proportions.tolist() == load_python_expression(expectation) + @pytest.mark.parametrize( + "fixture, expectation", + ( + (CR.CAT_HS_X_MR, "cat-hs-x-mr-row-margin-percentages"), + (CR.MR_X_CAT_HS_MT, "mr-x-cat-hs-row-margin-percentages"), + (CR.MR_X_MR, "mr-x-mr-row-margin-percentages"), + ), + ) + def test_it_knows_the_rows_margin_percentage(self, fixture, expectation): + slice_ = _Slice( + Cube(fixture), slice_idx=0, transforms={}, population=None, mask_size=0 + ) + + rows_margin_percentages = slice_.rows_margin_percentage + + assert rows_margin_percentages.tolist() == load_python_expression(expectation) + @pytest.mark.parametrize( "fixture, expectation", ( @@ -855,6 +872,23 @@ def test_it_knows_the_column_margin_proportions(self, fixture, expectation): assert columns_margin_proportion.tolist() == load_python_expression(expectation) + @pytest.mark.parametrize( + "fixture, expectation", + ( + (CR.CAT_HS_X_MR, "cat-hs-x-mr-columns-margin-percentages"), + (CR.MR_X_CAT_HS_MT, "mr-x-cat-hs-columns-margin-percentages"), + (CR.MR_X_MR, "mr-x-mr-columns-margin-percentages"), + ), + ) + def test_it_knows_the_column_margin_percentages(self, fixture, expectation): + slice_ = _Slice( + Cube(fixture), slice_idx=0, transforms={}, population=None, mask_size=0 + ) + + columns_margin_percentage = slice_.columns_margin_percentage + + assert columns_margin_percentage.tolist() == load_python_expression(expectation) + def test_it_knows_the_margins_of_mr_x_mr(self): slice_ = _Slice( Cube(CR.MR_X_MR), slice_idx=0, transforms={}, population=None, mask_size=0 diff --git a/tests/integration/test_headers_and_subtotals.py b/tests/integration/test_headers_and_subtotals.py index 47a75a1c3..d974f810f 100644 --- a/tests/integration/test_headers_and_subtotals.py +++ b/tests/integration/test_headers_and_subtotals.py @@ -3212,7 +3212,9 @@ def test_it_computes_measures_for_cat_x_cat_with_subdiffs_on_both(self): assert slice_.columns_margin[0] == pytest.approx(np.nan, nan_ok=True) assert slice_.rows_margin[0] == pytest.approx(np.nan, nan_ok=True) assert slice_.columns_margin_proportion[0] == pytest.approx(11 / 266) + assert slice_.columns_margin_percentage[0] == pytest.approx(11 / 266 * 100) assert slice_.rows_margin_proportion[0] == pytest.approx(-17 / 266) + assert slice_.rows_margin_percentage[0] == pytest.approx(-17 / 266 * 100) assert slice_.columns_base[0] == pytest.approx(np.nan, nan_ok=True) assert slice_.rows_base[0] == pytest.approx(np.nan, nan_ok=True) assert slice_.column_weighted_bases[:, 0] == pytest.approx( diff --git a/tests/unit/matrix/test_assembler.py b/tests/unit/matrix/test_assembler.py index f248ceca8..fd75fc283 100644 --- a/tests/unit/matrix/test_assembler.py +++ b/tests/unit/matrix/test_assembler.py @@ -348,6 +348,72 @@ def test_but_it_provides_a_2D_columns_margin_proportion_for_an_MR_X_cube_result( _assemble_matrix_.assert_called_once_with(slice_, [[[1], [2]], [[3], [4]]]) assert columns_margin_proportion == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + def test_it_provides_a_1D_columns_margin_percentage_for_a_CAT_X_cube_result( + self, + _rows_dimension_prop_, + dimension_, + _assemble_marginal_, + _measures_prop_, + margin_table_proportion_, + second_order_measures_, + ): + margin_table_proportion_.is_defined = True + _rows_dimension_prop_.return_value = dimension_ + dimension_.dimension_type = DT.CAT + _assemble_marginal_.return_value = np.array([[1, 2, 3], [4, 5, 6]]) + _measures_prop_.return_value = second_order_measures_ + second_order_measures_.columns_table_proportion = margin_table_proportion_ + slice_ = _Slice(None, None, None, None, None) + + columns_margin_percentage = slice_.columns_margin_percentage + + _assemble_marginal_.assert_called_once_with(slice_, margin_table_proportion_) + assert np.array_equal( + columns_margin_percentage, [[100, 200, 300], [400, 500, 600]] + ) + + def test_but_it_provides_a_2D_columns_margin_percentage_for_an_MR_X_cube_result( + self, + request, + dimensions_, + _measures_prop_, + second_order_measures_, + margin_table_proportion_, + SumSubtotals_, + _assemble_matrix_, + ): + margin_table_proportion_.is_defined = False + _measures_prop_.return_value = second_order_measures_ + second_order_measures_.columns_table_proportion = margin_table_proportion_ + property_mock( + request, + _Slice, + "columns_margin", + return_value=np.array([[1, 2], [3, 4]]), + ) + property_mock( + request, + _Slice, + "table_weighted_bases", + return_value=np.array([4.0, 6.0]), + ) + SumSubtotals_.blocks.return_value = np.array([[[1], [2]], [[3], [4]]]) + _assemble_matrix_.return_value = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + with mock.patch("cr.cube.cubepart._Slice._dimensions", new=dimensions_): + slice_ = _Slice(None, None, None, None, None) + + columns_margin_percentage = slice_.columns_margin_percentage + + SumSubtotals_.blocks.assert_called_once_with(ANY, dimensions_) + assert SumSubtotals_.blocks.call_args.args[0].tolist() == [ + pytest.approx([1 / 4.0, 2 / 6.0]), + pytest.approx([3 / 4.0, 4 / 6.0]), + ] + assert np.array_equal( + columns_margin_percentage, + [[100, 200, 300], [400, 500, 600], [700, 800, 900]], + ) + def test_it_knows_the_inserted_column_idxs(self, _column_order_prop_): _column_order_prop_.return_value = [2, -1, 0, -2] assert _Slice(None, None, None, None, None).inserted_column_idxs == (1, 3) @@ -567,6 +633,68 @@ def test_but_it_provides_a_2D_rows_margin_proportion_for_an_X_MR_cube_result( _assemble_matrix_.assert_called_once_with(slice_, [[[1], [2]], [[3], [4]]]) assert rows_margin_proportion == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + def test_it_provides_a_1D_rows_margin_percentage_for_an_X_CAT_cube_result( + self, + request, + dimension_, + _assemble_marginal_, + _measures_prop_, + second_order_measures_, + ): + dimension_.dimension_type = DT.CAT + _assemble_marginal_.return_value = np.array([[1, 2, 3], [4, 5, 6]]) + _measures_prop_.return_value = second_order_measures_ + measure_ = instance_mock(request, _MarginTableProportion) + second_order_measures_.rows_table_proportion = measure_ + with mock.patch( + "cr.cube.cubepart._Slice._dimensions", new=(dimension_, dimension_) + ): + slice_ = _Slice(None, None, None, None, None) + rows_margin_percentage = slice_.rows_margin_percentage + _assemble_marginal_.assert_called_once_with(slice_, measure_) + assert np.array_equal( + rows_margin_percentage, [[100, 200, 300], [400, 500, 600]] + ) + + def test_but_it_provides_a_2D_rows_margin_percentage_for_an_X_MR_cube_result( + self, + request, + dimensions_, + _measures_prop_, + second_order_measures_, + margin_table_proportion_, + SumSubtotals_, + _assemble_matrix_, + ): + margin_table_proportion_.is_defined = False + _measures_prop_.return_value = second_order_measures_ + second_order_measures_.rows_table_proportion = margin_table_proportion_ + property_mock( + request, _Slice, "rows_margin", return_value=np.array([[1, 2], [3, 4]]) + ) + property_mock( + request, + _Slice, + "table_weighted_bases", + return_value=np.array([4.0, 6.0]), + ) + SumSubtotals_.blocks.return_value = np.array([[[1], [2]], [[3], [4]]]) + _assemble_matrix_.return_value = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + with mock.patch("cr.cube.cubepart._Slice._dimensions", new=dimensions_): + slice_ = _Slice(None, None, None, None, None) + + rows_margin_percentage = slice_.rows_margin_percentage + + SumSubtotals_.blocks.assert_called_once_with(ANY, dimensions_) + assert SumSubtotals_.blocks.call_args.args[0].tolist() == [ + pytest.approx([1 / 4.0, 2 / 6.0]), + pytest.approx([3 / 4.0, 4 / 6.0]), + ] + assert np.array_equal( + rows_margin_percentage, + [[100, 200, 300], [400, 500, 600], [700, 800, 900]], + ) + def test_it_knows_the_2D_table_base_of_an_ARRAY_X_ARRAY_matrix( self, request, From 12ad0db403eb7ad4aa7ebb351e405b30956bc2ed Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Tue, 19 May 2026 17:17:18 +0200 Subject: [PATCH 5/5] Release notes --- README.md | 4 ++++ src/cr/cube/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec0dd5b38..8f206b7b3 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ Please visit for the API referenc ## Changes +### 3.4.0 + +- Add rows_margin_percentage and columns_margin_percentage to _Slice + ### 3.3.8 - Use native namespace packages for cr instead of deprecated pkg_resources / pkgutil diff --git a/src/cr/cube/__init__.py b/src/cr/cube/__init__.py index 02b9dce71..a58c2fd5d 100644 --- a/src/cr/cube/__init__.py +++ b/src/cr/cube/__init__.py @@ -2,4 +2,4 @@ """Initialization module for crunch-cube package.""" -__version__ = "3.3.8" +__version__ = "3.4.0"