Skip to content

Commit 28b4fad

Browse files
Merge pull request #416 from Crunch-io/ZC-2839-numeric-range
Add numeric range
2 parents 9115395 + 5e27aa6 commit 28b4fad

File tree

7 files changed

+253
-1
lines changed

7 files changed

+253
-1
lines changed

src/cr/cube/cubepart.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,13 @@ def column_labels(self):
317317
self._column_order_signed_indexes
318318
]
319319

320+
@lazyproperty
321+
def column_numeric_ranges(self):
322+
"""List of ranges or none for each column."""
323+
dim = self._dimensions[1]
324+
numeric_ranges = dim.numeric_ranges + len(dim.subtotals) * (None,)
325+
return [numeric_ranges[i] for i in self._row_order_signed_indexes]
326+
320327
def column_order(self, format=ORDER_FORMAT.SIGNED_INDEXES):
321328
"""1D np.int64 ndarray of idx for each assembled column of matrix.
322329
@@ -1144,6 +1151,13 @@ def row_labels(self):
11441151
self._row_order_signed_indexes
11451152
]
11461153

1154+
@lazyproperty
1155+
def row_numeric_ranges(self):
1156+
"""List of numeric ranges or None for each row."""
1157+
dim = self._dimensions[0]
1158+
numeric_ranges = dim.numeric_ranges + len(dim.subtotals) * (None,)
1159+
return [numeric_ranges[i] for i in self._row_order_signed_indexes]
1160+
11471161
def row_order(self, format=ORDER_FORMAT.SIGNED_INDEXES):
11481162
"""1D np.int64 ndarray of idx for each assembled row of matrix.
11491163
@@ -2208,6 +2222,14 @@ def row_labels(self):
22082222
self._rows_dimension.element_labels + self._rows_dimension.subtotal_labels
22092223
)[self._row_order_signed_indexes]
22102224

2225+
@lazyproperty
2226+
def row_numeric_ranges(self):
2227+
"""List of numeric ranges or None for each row."""
2228+
numeric_ranges = self._rows_dimension.numeric_ranges + len(
2229+
self._rows_dimension.subtotals
2230+
) * (None,)
2231+
return [numeric_ranges[i] for i in self._row_order_signed_indexes]
2232+
22112233
def row_order(self, format=ORDER_FORMAT.SIGNED_INDEXES):
22122234
"""1D np.int64 ndarray of idx for each assembled row of stripe.
22132235

src/cr/cube/dimension.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,18 @@ def numeric_values(self) -> Tuple[Union[int, float], ...]:
334334
"""
335335
return tuple(element.numeric_value for element in self.valid_elements)
336336

337+
@lazyproperty
338+
def numeric_ranges(
339+
self,
340+
) -> Tuple[Optional[Tuple[Union[int, float], Union[int, float]]], ...]:
341+
"""tuple of numeric ranges for valid elements of this dimension.
342+
343+
The numeric ranges appear in the same order as the elements of this
344+
dimension. Each element is represented a tuple of two numbers, but an
345+
element with no numeric range appears as `None` in the returned list.
346+
"""
347+
return tuple(element.numeric_range for element in self.valid_elements)
348+
337349
@lazyproperty
338350
def order_spec(self) -> "_OrderSpec":
339351
"""_OrderSpec proxy object for dimension.transforms.order dict from payload."""
@@ -1010,6 +1022,12 @@ def numeric_value(self) -> Union[int, float]:
10101022
numeric_value = self._element_dict.get("numeric_value")
10111023
return np.nan if numeric_value is None else numeric_value
10121024

1025+
@lazyproperty
1026+
def numeric_range(self) -> Optional[Tuple[Union[int, float], Union[int, float]]]:
1027+
"""Numeric range assigned to element or None if absent."""
1028+
numeric_range = self._element_dict.get("numeric_range")
1029+
return None if numeric_range is None else tuple(numeric_range)
1030+
10131031
def _str_representation_for(self, key: str) -> str:
10141032
"""return str representation for this element of a given key (`alias` or `name`)
10151033

tests/fixtures/cat-x-cat.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"id": 2,
5555
"missing": false,
5656
"name": "C",
57-
"numeric_value": 2
57+
"numeric_value": 2,
58+
"numeric_range": [1.5, 2.5]
5859
},
5960
{
6061
"id": -1,

tests/fixtures/num-ranges.json

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"query": {
3+
"dimensions": [
4+
{
5+
"args": [
6+
{
7+
"var": "Age"
8+
}
9+
],
10+
"function": "bin"
11+
}
12+
],
13+
"measures": {
14+
"count": {
15+
"args": [],
16+
"function": "cube_count"
17+
}
18+
}
19+
},
20+
"result": {
21+
"counts": [
22+
10,
23+
0,
24+
0,
25+
0,
26+
7,
27+
3,
28+
0
29+
],
30+
"dimensions": [
31+
{
32+
"derived": true,
33+
"references": {
34+
"alias": "Age",
35+
"name": "Age"
36+
},
37+
"type": {
38+
"categories": [
39+
{
40+
"id": 1,
41+
"missing": false,
42+
"name": "10-20",
43+
"numeric_range": [
44+
10.0,
45+
20.0
46+
],
47+
"numeric_value": null
48+
},
49+
{
50+
"id": 2,
51+
"missing": false,
52+
"name": "20-30",
53+
"numeric_value": null
54+
},
55+
{
56+
"id": 3,
57+
"missing": false,
58+
"name": "30-40",
59+
"numeric_range": [
60+
30.0,
61+
40.0
62+
],
63+
"numeric_value": null
64+
},
65+
{
66+
"id": 4,
67+
"missing": false,
68+
"name": "40-50",
69+
"numeric_range": [
70+
40.0,
71+
50.0
72+
],
73+
"numeric_value": null
74+
},
75+
{
76+
"id": 5,
77+
"missing": false,
78+
"name": "50-60",
79+
"numeric_range": [
80+
50.0,
81+
60.0
82+
],
83+
"numeric_value": null
84+
},
85+
{
86+
"id": 6,
87+
"missing": false,
88+
"name": "60-70",
89+
"numeric_range": [
90+
60.0,
91+
70.0
92+
],
93+
"numeric_value": null
94+
},
95+
{
96+
"id": -1,
97+
"missing": true,
98+
"name": "No Data",
99+
"numeric_value": null
100+
}
101+
],
102+
"class": "categorical",
103+
"ordinal": false
104+
}
105+
}
106+
],
107+
"filter_stats": {
108+
"filtered": {
109+
"unweighted": {
110+
"missing": 0,
111+
"other": 0,
112+
"selected": 20
113+
},
114+
"weighted": {
115+
"missing": 0,
116+
"other": 0,
117+
"selected": 20
118+
}
119+
},
120+
"filtered_complete": {
121+
"unweighted": {
122+
"missing": 0,
123+
"other": 0,
124+
"selected": 20
125+
},
126+
"weighted": {
127+
"missing": 0,
128+
"other": 0,
129+
"selected": 20
130+
}
131+
},
132+
"is_cat_date": false
133+
},
134+
"filtered": {
135+
"unweighted_n": 20,
136+
"weighted_n": 20
137+
},
138+
"measures": {
139+
"count": {
140+
"data": [
141+
10,
142+
0,
143+
0,
144+
0,
145+
7,
146+
3,
147+
0
148+
],
149+
"metadata": {
150+
"derived": true,
151+
"references": {},
152+
"type": {
153+
"class": "numeric",
154+
"integer": true,
155+
"missing_reasons": {
156+
"No Data": -1
157+
},
158+
"missing_rules": {}
159+
}
160+
},
161+
"n_missing": 0
162+
}
163+
},
164+
"missing": 0,
165+
"n": 20,
166+
"unfiltered": {
167+
"unweighted_n": 20,
168+
"weighted_n": 20
169+
}
170+
}
171+
}

tests/integration/test_cubepart.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,11 @@ def test_it_provides_audience_ratio_values_for_mr_x_compare_groups(self):
22102210
nan_ok=True,
22112211
)
22122212

2213+
def test_it_provides_numeric_ranges(self):
2214+
slice_ = Cube(CR.CAT_X_CAT).partitions[0]
2215+
assert slice_.row_numeric_ranges == [None, (1.5, 2.5)]
2216+
assert slice_.column_numeric_ranges == [None, None]
2217+
22132218

22142219
class Test_Strand:
22152220
"""Integration-test suite for `cr.cube.cubepart._Strand` object."""
@@ -2692,6 +2697,15 @@ def test_it_provides_derived_indexes_for_univariate_mr_with_transforms(self):
26922697
# ---There can be no insertions on MR dimensions
26932698
assert slice_.inserted_row_idxs == ()
26942699

2700+
def test_it_provides_no_numeric_ranges(self):
2701+
strand = Cube(CR.MR_MEAN_WEIGHTED).partitions[0]
2702+
assert strand.row_numeric_ranges == 3 * [None]
2703+
2704+
def test_it_provides_numeric_ranges(self):
2705+
strand = Cube(CR.NUM_RANGES).partitions[0]
2706+
expected = [(10, 20), None, (30, 40), (40, 50), (50, 60), (60, 70)]
2707+
assert strand.row_numeric_ranges == pytest.approx(expected)
2708+
26952709

26962710
class Test_Nub:
26972711
"""Integration-test suite for `cr.cube.cubepart._Nub` object."""

tests/integration/test_dimension.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,20 @@ def test_numeric_values(self):
429429
numeric_values = dimension.numeric_values
430430

431431
assert numeric_values == (1, 2, np.nan, np.nan)
432+
433+
def test_numeric_ranges(self):
434+
dimension_dict = {
435+
"type": {
436+
"categories": [
437+
{"id": 42, "missing": False, "numeric_range": [0, 10]},
438+
{"id": 43, "missing": False, "numeric_range": [10, 20]},
439+
{"id": 44, "missing": True, "numeric_range": [20, 30]},
440+
{"id": 46, "missing": False},
441+
],
442+
"class": "categorical",
443+
}
444+
}
445+
dimension = Dimension(dimension_dict, DT.CAT)
446+
447+
numeric_ranges = dimension.numeric_ranges
448+
assert numeric_ranges == ((0, 10), (10, 20), None)

tests/unit/test_dimension.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ def test_it_knows_the_numeric_values_of_its_elements(
330330
)
331331
assert Dimension(None, None).numeric_values == (1, 2.2, np.nan)
332332

333+
def test_it_knows_the_numeric_ranges_of_its_elements(
334+
self, request, valid_elements_prop_
335+
):
336+
valid_elements_prop_.return_value = tuple(
337+
instance_mock(request, Element, numeric_range=numeric_range)
338+
for numeric_range in ((1, 7), (7, 11), None)
339+
)
340+
assert Dimension(None, None).numeric_ranges == ((1, 7), (7, 11), None)
341+
333342
def test_it_provides_access_to_the_order_spec(self, request):
334343
order_spec_ = instance_mock(request, _OrderSpec)
335344
_OrderSpec_ = class_mock(

0 commit comments

Comments
 (0)