Commit 679351bb by Daniel Friedman

Surface engagement metric ranges according to spec

AN-6899

Better handles edge cases when there's sparse data.
parent 802bcdf5
...@@ -464,9 +464,9 @@ class ModuleEngagement(models.Model): ...@@ -464,9 +464,9 @@ class ModuleEngagement(models.Model):
class ModuleEngagementMetricRanges(models.Model): class ModuleEngagementMetricRanges(models.Model):
""" """
Represents the low and high values for a module engagement entity and event pair, Represents the low and high values for a module engagement entity and event
known as the metric. The range_type will either be high or low, bounded by pair, known as the metric. The range_type will either be low, normal, or
low_value and high_value. high, bounded by low_value and high_value.
""" """
course_id = models.CharField(db_index=True, max_length=255) course_id = models.CharField(db_index=True, max_length=255)
......
...@@ -412,28 +412,26 @@ class DateRangeSerializer(serializers.Serializer): ...@@ -412,28 +412,26 @@ class DateRangeSerializer(serializers.Serializer):
class EnagementRangeMetricSerializer(serializers.Serializer): class EnagementRangeMetricSerializer(serializers.Serializer):
""" """
Serializes ModuleEngagementMetricRanges (low_range and high_range) into Serializes ModuleEngagementMetricRanges ('low', 'normal', and 'high') into
the below_average, average, above_average ranges represented as arrays. 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') below_average = serializers.SerializerMethodField('get_below_average_range')
average = serializers.SerializerMethodField('get_average_range') average = serializers.SerializerMethodField('get_average_range')
above_average = serializers.SerializerMethodField('get_above_average_range') above_average = serializers.SerializerMethodField('get_above_average_range')
def get_average_range(self, obj): def get_average_range(self, obj):
metric_range = [ return self._transform_range(obj['normal_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
def get_below_average_range(self, obj): 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): 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): def _transform_range(self, metric_range):
return [metric_range.low_value, metric_range.high_value] if metric_range else [None, None] return [metric_range.low_value, metric_range.high_value] if metric_range else None
class CourseLearnerMetadataSerializer(serializers.Serializer): class CourseLearnerMetadataSerializer(serializers.Serializer):
...@@ -452,11 +450,17 @@ class CourseLearnerMetadataSerializer(serializers.Serializer): ...@@ -452,11 +450,17 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
for entity_type in engagement_entity_types.AGGREGATE_TYPES: for entity_type in engagement_entity_types.AGGREGATE_TYPES:
for event in engagement_events.EVENTS[entity_type]: for event in engagement_events.EVENTS[entity_type]:
metric = '{0}_{1}'.format(entity_type, event) 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') 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') high_range_queryset = query_set.filter(metric=metric, range_type='high')
engagement_ranges.update({ engagement_ranges.update({
metric: EnagementRangeMetricSerializer({ metric: EnagementRangeMetricSerializer({
'low_range': low_range_queryset[0] if len(low_range_queryset) else None, '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, 'high_range': high_range_queryset[0] if len(high_range_queryset) else None,
}).data }).data
}) })
......
...@@ -569,7 +569,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -569,7 +569,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
} }
} }
empty_range = { 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: for metric in self.engagement_metrics:
empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range) empty_engagement_ranges['engagement_ranges'][metric] = copy.deepcopy(empty_range)
...@@ -594,7 +594,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -594,7 +594,7 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
start_date = datetime.datetime(2015, 7, 1, tzinfo=pytz.utc) start_date = datetime.datetime(2015, 7, 1, tzinfo=pytz.utc)
end_date = datetime.datetime(2015, 7, 21, 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, 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 = self.empty_engagement_ranges
expected_ranges['engagement_ranges'].update({ expected_ranges['engagement_ranges'].update({
'date_range': { 'date_range': {
...@@ -602,9 +602,9 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -602,9 +602,9 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
'end': '2015-07-21' 'end': '2015-07-21'
}, },
metric_type: { metric_type: {
'below_average': [None, None], 'below_average': None,
'average': [None, 90.0], 'average': [90.0, 6120.0],
'above_average': [90.0, 6120.0] 'above_average': None
} }
}) })
...@@ -631,13 +631,13 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin, ...@@ -631,13 +631,13 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
low_ceil = 100.5 low_ceil = 100.5
G(ModuleEngagementMetricRanges, course_id=self.course_id, start_date=start_date, end_date=end_date, 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) 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, 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] = { expected['engagement_ranges'][metric_type] = {
'below_average': [0.0, low_ceil], 'below_average': [0.0, low_ceil],
'average': [low_ceil, high_floor], 'average': [normal_floor, max_value],
'above_average': [high_floor, max_value] 'above_average': None
} }
return expected return expected
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment