Skip to content

Commit

Permalink
Surface engagement metric ranges according to spec
Browse files Browse the repository at this point in the history
AN-6899

Better handles edge cases when there's sparse data.
  • Loading branch information
dan-f committed May 23, 2016
1 parent 802bcdf commit 679351b
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 23 deletions.
6 changes: 3 additions & 3 deletions analytics_data_api/v0/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,9 +464,9 @@ class Meta(object):

class ModuleEngagementMetricRanges(models.Model):
"""
Represents the low and high values for a module engagement entity and event pair,
known as the metric. The range_type will either be high or low, bounded by
low_value and high_value.
Represents the low and high values for a module engagement entity and event
pair, known as the metric. The range_type will either be low, normal, or
high, bounded by low_value and high_value.
"""

course_id = models.CharField(db_index=True, max_length=255)
Expand Down
26 changes: 15 additions & 11 deletions analytics_data_api/v0/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,28 +412,26 @@ class DateRangeSerializer(serializers.Serializer):

class EnagementRangeMetricSerializer(serializers.Serializer):
"""
Serializes ModuleEngagementMetricRanges (low_range and high_range) into
the below_average, average, above_average ranges represented as arrays.
Serializes ModuleEngagementMetricRanges ('low', 'normal', and 'high') into
the below_average, average, and above_average ranges represented as arrays.
If any one of the ranges is not defined, it is not included in the
serialized output.
"""
below_average = serializers.SerializerMethodField('get_below_average_range')
average = serializers.SerializerMethodField('get_average_range')
above_average = serializers.SerializerMethodField('get_above_average_range')

def get_average_range(self, obj):
metric_range = [
obj['low_range'].high_value if obj['low_range'] else None,
obj['high_range'].low_value if obj['high_range'] else None,
]
return metric_range
return self._transform_range(obj['normal_range'])

def get_below_average_range(self, obj):
return self._get_range(obj['low_range'])
return self._transform_range(obj['low_range'])

def get_above_average_range(self, obj):
return self._get_range(obj['high_range'])
return self._transform_range(obj['high_range'])

def _get_range(self, metric_range):
return [metric_range.low_value, metric_range.high_value] if metric_range else [None, None]
def _transform_range(self, metric_range):
return [metric_range.low_value, metric_range.high_value] if metric_range else None


class CourseLearnerMetadataSerializer(serializers.Serializer):
Expand All @@ -452,11 +450,17 @@ def get_engagement_ranges(self, obj):
for entity_type in engagement_entity_types.AGGREGATE_TYPES:
for event in engagement_events.EVENTS[entity_type]:
metric = '{0}_{1}'.format(entity_type, event)
# It's assumed that there may be any combination of low, normal,
# and high ranges in the database for the given course. Some
# edge cases result from a lack of available data; in such
# cases, only some ranges may be returned.
low_range_queryset = query_set.filter(metric=metric, range_type='low')
normal_range_queryset = query_set.filter(metric=metric, range_type='normal')
high_range_queryset = query_set.filter(metric=metric, range_type='high')
engagement_ranges.update({
metric: EnagementRangeMetricSerializer({
'low_range': low_range_queryset[0] if len(low_range_queryset) else None,
'normal_range': normal_range_queryset[0] if len(normal_range_queryset) else None,
'high_range': high_range_queryset[0] if len(high_range_queryset) else None,
}).data
})
Expand Down
18 changes: 9 additions & 9 deletions analytics_data_api/v0/tests/views/test_learners.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ def empty_engagement_ranges(self):
}
}
empty_range = {
range_type: [None, None] for range_type in ['below_average', 'average', 'above_average']
range_type: None for range_type in ['below_average', 'average', 'above_average']
}
for metric in self.engagement_metrics:
empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range)
Expand All @@ -594,17 +594,17 @@ def test_one_engagement_range(self):
start_date = datetime.datetime(2015, 7, 1, tzinfo=pytz.utc)
end_date = datetime.datetime(2015, 7, 21, tzinfo=pytz.utc)
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='high', low_value=90, high_value=6120)
metric=metric_type, range_type='normal', low_value=90, high_value=6120)
expected_ranges = self.empty_engagement_ranges
expected_ranges['engagement_ranges'].update({
'date_range': {
'start': '2015-07-01',
'end': '2015-07-21'
},
metric_type: {
'below_average': [None, None],
'average': [None, 90.0],
'above_average': [90.0, 6120.0]
'below_average': None,
'average': [90.0, 6120.0],
'above_average': None
}
})

Expand All @@ -631,13 +631,13 @@ def _get_full_engagement_ranges(self):
low_ceil = 100.5
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='low', low_value=0, high_value=low_ceil)
high_floor = 800.8
normal_floor = 800.8
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date,
metric=metric_type, range_type='high', low_value=high_floor, high_value=max_value)
metric=metric_type, range_type='normal', low_value=normal_floor, high_value=max_value)
expected['engagement_ranges'][metric_type] = {
'below_average': [0.0, low_ceil],
'average': [low_ceil, high_floor],
'above_average': [high_floor, max_value]
'average': [normal_floor, max_value],
'above_average': None
}

return expected
Expand Down

0 comments on commit 679351b

Please sign in to comment.