diff --git a/src/cr/cube/cubepart.py b/src/cr/cube/cubepart.py index f054f7b0c..f1632d7d6 100644 --- a/src/cr/cube/cubepart.py +++ b/src/cr/cube/cubepart.py @@ -182,6 +182,10 @@ def inserted_column_idxs(self): def inserted_row_idxs(self): return tuple(i for i, row in enumerate(self._matrix.rows) if row.is_insertion) + @lazyproperty + def is_empty(self): + return any(s == 0 for s in self.shape) + @lazyproperty def means(self): return np.array([row.means for row in self._matrix.rows]) @@ -504,6 +508,10 @@ def bases(self): def counts(self): return tuple(row.count for row in self._stripe.rows) + @lazyproperty + def is_empty(self): + return any(s == 0 for s in self._shape) + @lazyproperty def inserted_row_idxs(self): # TODO: add integration-test coverage for this. @@ -717,6 +725,10 @@ class _Nub(CubePartition): def base_count(self): return self._cube.base_counts + @lazyproperty + def is_empty(self): + return False if self.base_count else True + @lazyproperty def means(self): return self._scalar.means diff --git a/tests/fixtures/econ-nodata-no-dims.json b/tests/fixtures/econ-nodata-no-dims.json new file mode 100644 index 000000000..d61ce83b0 --- /dev/null +++ b/tests/fixtures/econ-nodata-no-dims.json @@ -0,0 +1,90 @@ +{ + "element": "shoji:view", + "self": "https://app.crunch.io/api/datasets/43f0d19eb1f241a39b7d5c547a0a6b6e/cube/?filter=%5B%5D&query=%7B%22dimensions%22:%5B%5D,%22measures%22:%7B%22count%22:%7B%22function%22:%22cube_count%22,%22args%22:%5B%5D%7D,%22mean%22:%7B%22function%22:%22cube_mean%22,%22args%22:%5B%7B%22variable%22:%22https:%2F%2Fapp.crunch.io%2Fapi%2Fdatasets%2F43f0d19eb1f241a39b7d5c547a0a6b6e%2Fvariables%2F00000c%2F%22%7D%5D%7D%7D,%22weight%22:null%7D", + "value": { + "query": { + "measures": { + "count": { + "function": "cube_count", + "args": [ + + ] + }, + "mean": { + "function": "cube_mean", + "args": [ + { + "variable": "https://app.crunch.io/api/datasets/43f0d19eb1f241a39b7d5c547a0a6b6e/variables/00000c/" + } + ] + } + }, + "dimensions": [ + + ], + "weight": null + }, + "query_environment": { + "filter": [ + + ] + }, + "result": { + "dimensions": [ + + ], + "missing": 0, + "measures": { + "count": { + "data": [ + null + ], + "n_missing": 0, + "metadata": { + "references": { + + }, + "derived": true, + "type": { + "integer": true, + "missing_rules": { + + }, + "missing_reasons": { + "No Data": -1 + }, + "class": "numeric" + } + } + }, + "mean": { + "data": [ + null + ], + "n_missing": 0, + "metadata": { + "references": { + + }, + "derived": true, + "type": { + "integer": null, + "missing_rules": { + + }, + "missing_reasons": { + "No Data": -1 + }, + "class": "numeric" + } + } + } + }, + "element": "crunch:cube", + "counts": [ + null + ], + "n": null + } + } +} diff --git a/tests/fixtures/om-sgp8334215-vn-2019-sep-19-strand.json b/tests/fixtures/om-sgp8334215-vn-2019-sep-19-strand.json new file mode 100644 index 000000000..922267db5 --- /dev/null +++ b/tests/fixtures/om-sgp8334215-vn-2019-sep-19-strand.json @@ -0,0 +1,105 @@ +{ + "result": { + "dimensions": [ + { + "derived": true, + "references": { + "alias": "VCAO2_open1", + "notes": "Base: Those using other specified streaming platforms", + "name": "Device for consumption of free TV and video content - Other specified", + "description": "Which of the following devices do you use to watch free television and video content? Please select all that apply. : Other Specify" + }, + "type": { + "subtype": { + "missing_rules": { + "No data": { + "function": "==", + "args": [ + { + "variable": "000009" + }, + { + "value": "__NA__" + } + ] + } + }, + "missing_reasons": { + "No Data": -1, + "No data": 1 + }, + "class": "text" + }, + "elements": [ + { + "id": 0, + "value": { + "?": -1 + }, + "missing": true + } + ], + "class": "enum" + } + } + ], + "measures": { + "count": { + "data": [ + 1044.9999999999 + ], + "n_missing": 1045, + "metadata": { + "references": {}, + "derived": true, + "type": { + "integer": false, + "class": "numeric", + "missing_reasons": { + "No Data": -1 + }, + "missing_rules": {} + } + } + } + }, + "n": 1045, + "filter_stats": { + "filtered_complete": { + "unweighted": { + "selected": 1045, + "other": 0, + "missing": 0 + }, + "weighted": { + "selected": 1044.9999999999, + "other": 0, + "missing": 0 + } + }, + "filtered": { + "unweighted": { + "selected": 1045, + "other": 0, + "missing": 0 + }, + "weighted": { + "selected": 1044.9999999999, + "other": 0, + "missing": 0 + } + } + }, + "unfiltered": { + "unweighted_n": 1045, + "weighted_n": 1044.9999999999 + }, + "filtered": { + "unweighted_n": 1045, + "weighted_n": 1044.9999999999 + }, + "counts": [ + 1045 + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/om-sgp8334215-vn-2019-sep-19.json b/tests/fixtures/om-sgp8334215-vn-2019-sep-19.json new file mode 100644 index 000000000..7f84f085b --- /dev/null +++ b/tests/fixtures/om-sgp8334215-vn-2019-sep-19.json @@ -0,0 +1,405 @@ +{ + "result": { + "dimensions": [ + { + "derived": true, + "references": { + "alias": "VCAO2_open1", + "notes": "Base: Those using other specified streaming platforms", + "name": "Device for consumption of free TV and video content - Other specified", + "description": "Which of the following devices do you use to watch free television and video content? Please select all that apply. : Other Specify" + }, + "type": { + "subtype": { + "missing_rules": { + "No data": { + "function": "==", + "args": [ + { + "variable": "000009" + }, + { + "value": "__NA__" + } + ] + } + }, + "missing_reasons": { + "No Data": -1, + "No data": 1 + }, + "class": "text" + }, + "elements": [ + { + "id": 0, + "value": { + "?": -1 + }, + "missing": true + } + ], + "class": "enum" + } + }, + { + "references": { + "uniform_basis": false, + "description": "", + "subreferences": [ + { + "alias": "VCAO1_cb_1", + "name": "IndoXXI Lite" + }, + { + "alias": "VCAO1_cb_2", + "name": "Vua Xem Phim HD" + }, + { + "alias": "VCAO1_cb_3", + "name": "VungTV" + }, + { + "alias": "VCAO1_cb_4", + "name": "Dang Cap HD" + }, + { + "alias": "VCAO1_cb_5", + "name": "VSport TV" + }, + { + "alias": "VCAO1_cb_6", + "name": "Zam TV" + }, + { + "alias": "VCAO1_cb_7", + "name": "Phim HD Xem Phim Viet Online" + }, + { + "alias": "VCAO1_cb_8", + "name": "Bong Da TV" + }, + { + "alias": "VCAO1_cb_9", + "name": "Other online video platforms or other services on set top boxes" + } + ], + "notes": "", + "alias": "VCAO1_cb", + "name": "Apps and services usage for streaming content" + }, + "derived": true, + "type": { + "subtype": { + "class": "variable" + }, + "elements": [ + { + "id": 1, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_1", + "name": "IndoXXI Lite" + }, + "id": "0001" + }, + "missing": false + }, + { + "id": 2, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_2", + "name": "Vua Xem Phim HD" + }, + "id": "0002" + }, + "missing": false + }, + { + "id": 3, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_3", + "name": "VungTV" + }, + "id": "0003" + }, + "missing": false + }, + { + "id": 4, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_4", + "name": "Dang Cap HD" + }, + "id": "0004" + }, + "missing": false + }, + { + "id": 5, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_5", + "name": "VSport TV" + }, + "id": "0005" + }, + "missing": false + }, + { + "id": 6, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_6", + "name": "Zam TV" + }, + "id": "0006" + }, + "missing": false + }, + { + "id": 7, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_7", + "name": "Phim HD Xem Phim Viet Online" + }, + "id": "0007" + }, + "missing": false + }, + { + "id": 8, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_8", + "name": "Bong Da TV" + }, + "id": "0008" + }, + "missing": false + }, + { + "id": 9, + "value": { + "derived": false, + "references": { + "alias": "VCAO1_cb_9", + "name": "Other online video platforms or other services on set top boxes" + }, + "id": "0009" + }, + "missing": false + } + ], + "class": "enum" + } + }, + { + "derived": true, + "references": { + "uniform_basis": false, + "description": "", + "subreferences": [ + { + "alias": "VCAO1_cb_1", + "name": "IndoXXI Lite" + }, + { + "alias": "VCAO1_cb_2", + "name": "Vua Xem Phim HD" + }, + { + "alias": "VCAO1_cb_3", + "name": "VungTV" + }, + { + "alias": "VCAO1_cb_4", + "name": "Dang Cap HD" + }, + { + "alias": "VCAO1_cb_5", + "name": "VSport TV" + }, + { + "alias": "VCAO1_cb_6", + "name": "Zam TV" + }, + { + "alias": "VCAO1_cb_7", + "name": "Phim HD Xem Phim Viet Online" + }, + { + "alias": "VCAO1_cb_8", + "name": "Bong Da TV" + }, + { + "alias": "VCAO1_cb_9", + "name": "Other online video platforms or other services on set top boxes" + } + ], + "notes": "", + "alias": "VCAO1_cb", + "name": "Apps and services usage for streaming content" + }, + "type": { + "ordinal": false, + "subvariables": [ + "0001", + "0002", + "0003", + "0004", + "0005", + "0006", + "0007", + "0008", + "0009" + ], + "class": "categorical", + "categories": [ + { + "numeric_value": 1, + "selected": true, + "id": 1, + "missing": false, + "name": "Selected" + }, + { + "numeric_value": 0, + "id": 0, + "name": "Other", + "missing": false + }, + { + "numeric_value": null, + "id": -1, + "name": "No Data", + "missing": true + } + ] + } + } + ], + "measures": { + "count": { + "data": [ + 58.0879956743, + 986.9120043257, + 0.0, + 224.0156021741, + 820.9843978259, + 0.0, + 212.7206408334, + 832.2793591665, + 0.0, + 125.5352062187, + 919.4647937812, + 0.0, + 119.1137688741, + 925.8862311259, + 0.0, + 95.937314861, + 949.0626851389, + 0.0, + 381.6430087752, + 663.3569912247, + 0.0, + 331.5300133092, + 713.4699866907, + 0.0, + 49.7427167023, + 995.2572832976, + 0.0 + ], + "n_missing": 1045, + "metadata": { + "references": {}, + "derived": true, + "type": { + "integer": false, + "class": "numeric", + "missing_reasons": { + "No Data": -1 + }, + "missing_rules": {} + } + } + } + }, + "n": 1045, + "filter_stats": { + "filtered_complete": { + "unweighted": { + "selected": 1045, + "other": 0, + "missing": 0 + }, + "weighted": { + "selected": 1044.9999999999, + "other": 0, + "missing": 0 + } + }, + "filtered": { + "unweighted": { + "selected": 1045, + "other": 0, + "missing": 0 + }, + "weighted": { + "selected": 1044.9999999999, + "other": 0, + "missing": 0 + } + } + }, + "unfiltered": { + "unweighted_n": 1045, + "weighted_n": 1044.9999999999 + }, + "filtered": { + "unweighted_n": 1045, + "weighted_n": 1044.9999999999 + }, + "counts": [ + 60, + 985, + 0, + 226, + 819, + 0, + 210, + 835, + 0, + 129, + 916, + 0, + 121, + 924, + 0, + 99, + 946, + 0, + 383, + 662, + 0, + 339, + 706, + 0, + 50, + 995, + 0 + ] + } +} \ No newline at end of file diff --git a/tests/integration/test_cubepart.py b/tests/integration/test_cubepart.py index 9e1f0196e..e7d33e1c6 100644 --- a/tests/integration/test_cubepart.py +++ b/tests/integration/test_cubepart.py @@ -12,6 +12,14 @@ class Describe_Slice(object): """Integration-test suite for _Slice object.""" + def it_is_not_empty(self): + slice_ = Cube(CR.CAT_X_CAT_PRUNING_HS).partitions[0] + assert slice_.is_empty is False + + def it_is_empty(self): + slice_ = Cube(CR.OM_SGP8334215_VN_2019_SEP_19).partitions[0] + assert slice_.is_empty is True + def it_loads_from_cat_x_cat_cube(self): cube = Cube(CR.CAT_X_CAT) slice_ = _Slice(cube, 0, None, None, 0) @@ -468,3 +476,29 @@ def it_provides_nans_for_means_insertions(self): np.testing.assert_almost_equal( strand.means, [19.85555556, 13.85416667, 52.78947368, np.nan, np.nan] ) + + def it_is_not_empty(self): + strand = CubePartition.factory( + Cube(CR.CAT_WITH_MEANS_AND_INSERTIONS), 0, None, None, None, 0 + ) + assert strand.is_empty is False + + def it_is_empty(self): + strand = CubePartition.factory( + Cube(CR.OM_SGP8334215_VN_2019_SEP_19_STRAND), 0, None, None, None, 0 + ) + assert strand.is_empty is True + + +class Describe_Nub(object): + """Integration-test suite for `cr.cube.cubepart._Nub` object.""" + + def it_is_not_empty(self): + cube = Cube(CR.ECON_MEAN_NO_DIMS) + nub = cube.partitions[0] + assert nub.is_empty is False + + def it_is_empty(self): + cube = Cube(CR.ECON_NODATA_NO_DIMS) + nub = cube.partitions[0] + assert nub.is_empty is True diff --git a/tests/unit/test_cubepart.py b/tests/unit/test_cubepart.py index 48bfe9dd5..5bc49d609 100644 --- a/tests/unit/test_cubepart.py +++ b/tests/unit/test_cubepart.py @@ -7,7 +7,7 @@ import numpy as np import pytest -from cr.cube.cubepart import _Slice +from cr.cube.cubepart import _Slice, _Strand, _Nub from cr.cube.matrix import TransformedMatrix, _VectorAfterHiding from ..unitutil import instance_mock, property_mock @@ -15,6 +15,15 @@ class Describe_Slice(object): """Unit test suite for `cr.cube.cubepart._Slice` object.""" + def it_knows_its_data_status(self, _slice_prop_, slice_is_empty_fixture): + slice_shape, expected_value = slice_is_empty_fixture + _slice_prop_.return_value = slice_shape + slice_ = _Slice(None, None, None, None, None) + + is_empty = slice_.is_empty + + assert is_empty is expected_value + def it_knows_the_row_proportions(self, request, _matrix_prop_, matrix_): _matrix_prop_.return_value = matrix_ matrix_.rows = ( @@ -50,3 +59,70 @@ def _matrix_prop_(self, request): @pytest.fixture def matrix_(self, request): return instance_mock(request, TransformedMatrix) + + @pytest.fixture + def _slice_prop_(self, request): + return property_mock(request, _Slice, "shape") + + # fixtures --------------------------------------------- + + @pytest.fixture( + params=[((1,), False), ((0,), True), ((7, 6), False), ((0, 0), True)] + ) + def slice_is_empty_fixture(self, request): + slice_shape, expected_value = request.param + return slice_shape, expected_value + + +class Describe_Strand(object): + """Unit test suite for `cr.cube.cubepart._Strand` object.""" + + def it_knows_its_data_status(self, _strand_prop_, strand_is_empty_fixture): + strand_shape, expected_value = strand_is_empty_fixture + _strand_prop_.return_value = strand_shape + strand_ = _Strand(None, None, None, None, None, None) + + is_empty = strand_.is_empty + + assert is_empty is expected_value + + # fixture components --------------------------------------------- + + @pytest.fixture + def _strand_prop_(self, request): + return property_mock(request, _Strand, "_shape") + + # fixtures --------------------------------------------- + + @pytest.fixture( + params=[((1,), False), ((0,), True), ((7, 6), False), ((0, 0), True)] + ) + def strand_is_empty_fixture(self, request): + slice_shape, expected_value = request.param + return slice_shape, expected_value + + +class Describe_Nub(object): + """Unit test suite for `cr.cube.cubepart._Nub` object.""" + + def it_knows_its_data_status(self, _nub_prop_, nub_is_empty_fixture): + base_count, expected_value = nub_is_empty_fixture + _nub_prop_.return_value = base_count + nub_ = _Nub(None) + + is_empty = nub_.is_empty + + assert is_empty == expected_value + + # fixture components --------------------------------------------- + + @pytest.fixture + def _nub_prop_(self, request): + return property_mock(request, _Nub, "base_count") + + # fixtures --------------------------------------------- + + @pytest.fixture(params=[(None, True), (45.4, False)]) + def nub_is_empty_fixture(self, request): + base_count, expected_value = request.param + return base_count, expected_value