Skip to content
This repository has been archived by the owner on Aug 28, 2023. It is now read-only.

Degree Days Indicators #154

Merged
merged 7 commits into from
Dec 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions django/climate_change_api/climate_data/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ def climate_indicator(request, *args, **kwargs):
required: false
type: integer
paramType: query
- name: basetemp
description: (Appropriate indicators only) The base temperature used to calculate the daily
difference for degree days sumations. Can be a number with a unit suffix (Such
as 20C) or a bare number (Such as 65) measured in the request output units.
required: false
type: integer
paramType: query

"""
try:
Expand Down
97 changes: 79 additions & 18 deletions django/climate_change_api/indicators/abstract_indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from climate_data.models import ClimateData, ClimateDataSource
from climate_data.filters import ClimateDataFilterSet
from .serializers import IndicatorSerializer
from .unit_converters import DaysUnitsMixin
from .unit_converters import DaysUnitsMixin, TemperatureConverter


class Indicator(object):
Expand Down Expand Up @@ -174,24 +174,44 @@ def row_group_key(self, row):


class YearlyAggregationIndicator(YearlyIndicator):
default = 0
conditions = None

def aggregate(self):
if self.conditions:
agg_function = self.agg_function(
Case(When(then=self.expression, **self.conditions),
default=self.default,
output_field=IntegerField()))
else:
agg_function = self.agg_function(self.expression)

return (self.queryset.values('data_source__year', 'data_source__model')
.annotate(value=self.agg_function(self.variables[0])))
.annotate(value=agg_function))

@property
def expression(self):
""" Lookup function to get the actual value used for aggregation

Defaults to the first variable mentioned in the variables variable, but can be overloaded
for complex queries.

This is necessary because the variables value is serialized for the /indicators/ endpoint,
but complex queries may require non-serializable components. Using the expression property
allows us to subsitute the variable specifically for the serialization instead.
"""
return self.variables[0]


class YearlyCountIndicator(YearlyIndicator):
class YearlyCountIndicator(YearlyAggregationIndicator):
""" Class to count days on which a condition is met.

Since using a filter would result in ignoring year/model combinations where the count is zero
and Count doesn't discriminate between values, uses a Case/When clause to return 1 for hits
and 0 for misses then Sum to count them up.
Essentially a specialized version of the YearlyAggregationIndicator where all values count as 1
if they match the conditions and 0 in all other cases.
"""
def aggregate(self):
agg_function = Sum(Case(When(then=1, **self.conditions),
default=0,
output_field=IntegerField()))
return (self.queryset.values('data_source__year', 'data_source__model')
.annotate(value=agg_function))
agg_function = Sum
default = 0
expression = 1


class YearlySequenceIndicator(YearlyCountIndicator):
Expand Down Expand Up @@ -349,13 +369,54 @@ def key_result(self, result):


class MonthlyAggregationIndicator(MonthlyIndicator):
default = 0
conditions = None

def aggregate(self):
return self.monthly_queryset.annotate(value=self.agg_function(self.variables[0]))
if self.conditions:
agg_function = self.agg_function(
Case(When(then=self.expression, **self.conditions),
default=self.default,
output_field=IntegerField()))
else:
agg_function = self.agg_function(self.expression)

return (self.monthly_queryset.annotate(value=agg_function))

@property
def expression(self):
""" Lookup function to get the actual value used for aggregation

Defaults to the first variable mentioned in the variables variable, but can be overloaded
for complex queries.

This is necessary because the variables value is serialized for the /indicators/ endpoint,
but complex queries may require non-serializable components. Using the expression property
allows us to subsitute the variable specifically for the serialization instead.
"""
return self.variables[0]


class MonthlyCountIndicator(MonthlyAggregationIndicator):
def aggregate(self):
agg_function = Sum(Case(When(then=1, **self.conditions),
default=0,
output_field=IntegerField()))
return self.monthly_queryset.annotate(value=agg_function)
agg_function = Sum
default = 0
expression = 1


class BasetempIndicatorMixin(object):
""" Framework for pre-processing the basetemp parameter to a native unit
"""
def calculate(self):
m = re.match(r'(?P<value>\d+(\.\d+)?)(?P<unit>[FKC])?', self.parameters['basetemp'])
assert m, "Parameter basetemp must be numeric"

value = float(m.group('value'))
unit = m.group('unit')
if unit is None:
unit = self.parameters.get('units', self.default_units)

converter = TemperatureConverter.get(unit, self.storage_units)
self.parameters['basetemp'] = converter(value)
self.parameters['units'] = self.storage_units

return super(BasetempIndicatorMixin, self).calculate()
133 changes: 113 additions & 20 deletions django/climate_change_api/indicators/indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import sys
from itertools import groupby

from django.db.models import F, Avg, Max, Min
from django.db.models import F, Sum, Avg, Max, Min

from .abstract_indicators import (YearlyAggregationIndicator, YearlyCountIndicator,
YearlyMaxConsecutiveDaysIndicator, YearlySequenceIndicator,
MonthlyAggregationIndicator, MonthlyCountIndicator,
DailyRawIndicator)
from .unit_converters import (TemperatureUnitsMixin, PrecipUnitsMixin,
DaysUnitsMixin, CountUnitsMixin)
DailyRawIndicator, BasetempIndicatorMixin)
from .unit_converters import (TemperatureUnitsMixin, PrecipUnitsMixin, DaysUnitsMixin,
CountUnitsMixin, TemperatureDeltaUnitsMixin)


##########################
Expand Down Expand Up @@ -96,10 +96,11 @@ class YearlyExtremePrecipitationEvents(CountUnitsMixin, YearlyCountIndicator):
variables = ('pr',)
parameters = {'percentile': 99}

conditions = {'pr__gt': F('map_cell__baseline__pr')}

@property
def conditions(self):
return {'pr__gt': F('map_cell__baseline__pr'),
'map_cell__baseline__percentile': self.parameters['percentile']}
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class YearlyExtremeHeatEvents(CountUnitsMixin, YearlyCountIndicator):
Expand All @@ -109,10 +110,11 @@ class YearlyExtremeHeatEvents(CountUnitsMixin, YearlyCountIndicator):
variables = ('tasmax',)
parameters = {'percentile': 99}

conditions = {'tasmax__gt': F('map_cell__baseline__tasmax')}

@property
def conditions(self):
return {'tasmax__gt': F('map_cell__baseline__tasmax'),
'map_cell__baseline__percentile': self.parameters['percentile']}
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class YearlyExtremeColdEvents(CountUnitsMixin, YearlyCountIndicator):
Expand All @@ -122,10 +124,55 @@ class YearlyExtremeColdEvents(CountUnitsMixin, YearlyCountIndicator):
variables = ('tasmin',)
parameters = {'percentile': 1}

conditions = {'tasmin__lt': F('map_cell__baseline__tasmin')}

@property
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class YearlyHeatingDegreeDays(TemperatureDeltaUnitsMixin, BasetempIndicatorMixin,
YearlyAggregationIndicator):
label = 'Yearly Heating Degree Days'
description = ('Total difference of daily low temperature to a reference base temperature '
'(Default 65F)')
variables = ('tasmin',)
agg_function = Sum

# List units as a parameter so it gets updated by the query params if it is overriden.
# This way we can fall back to the units param if we need to handle bare numbers for basetemp
parameters = {'basetemp': '65F',
'units': 'F'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including units here seems a little weird within the broader context, but it is, after all, a paremeter, so it might be the broader context that's weird. In any case, we have PR #158 to address such questions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here is to allow the fallback when basetemp is given as a pure number... for instance, in ?basetemp=65 we'd interpret it as 65F, and ?basetemp=20&units=C would interpret it as 20C and also present the result in Centigrade as well.

We can go the route that basetemp must always have a unit, but this felt more user friendly.


@property
def conditions(self):
return {'tasmin__lt': F('map_cell__baseline__tasmin'),
'map_cell__baseline__percentile': self.parameters['percentile']}
return {'tasmin__lte': self.parameters['basetemp']}

@property
def expression(self):
return self.parameters['basetemp'] - F('tasmin')


class YearlyCoolingDegreeDays(TemperatureDeltaUnitsMixin, BasetempIndicatorMixin,
YearlyAggregationIndicator):
label = 'Yearly Cooling Degree Days'
description = ('Total difference of daily high temperature to a reference base temperature '
'(Default 65F)')
variables = ('tasmax',)
agg_function = Sum

# List units as a parameter so it gets updated by the query params if it is overriden.
# This way we can fall back to the units param if we need to handle bare numbers for basetemp
parameters = {'basetemp': '65F',
'units': 'F'}

@property
def conditions(self):
return {'tasmax__gte': self.parameters['basetemp']}

@property
def expression(self):
return F('tasmax') - self.parameters['basetemp']


class HeatWaveDurationIndex(YearlyMaxConsecutiveDaysIndicator):
Expand Down Expand Up @@ -198,10 +245,11 @@ class MonthlyExtremePrecipitationEvents(CountUnitsMixin, MonthlyCountIndicator):
variables = ('pr',)
parameters = {'percentile': 99}

conditions = {'pr__gt': F('map_cell__baseline__pr')}

@property
def conditions(self):
return {'pr__gt': F('map_cell__baseline__pr'),
'map_cell__baseline__percentile': self.parameters['percentile']}
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class MonthlyExtremeHeatEvents(CountUnitsMixin, MonthlyCountIndicator):
Expand All @@ -211,10 +259,11 @@ class MonthlyExtremeHeatEvents(CountUnitsMixin, MonthlyCountIndicator):
variables = ('tasmax',)
parameters = {'percentile': 99}

conditions = {'tasmax__gt': F('map_cell__baseline__tasmax')}

@property
def conditions(self):
return {'tasmax__gt': F('map_cell__baseline__tasmax'),
'map_cell__baseline__percentile': self.parameters['percentile']}
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class MonthlyExtremeColdEvents(CountUnitsMixin, MonthlyCountIndicator):
Expand All @@ -224,11 +273,55 @@ class MonthlyExtremeColdEvents(CountUnitsMixin, MonthlyCountIndicator):
variables = ('tasmin',)
parameters = {'percentile': 1}

conditions = {'tasmin__lt': F('map_cell__baseline__tasmin')}

@property
def filters(self):
return {'map_cell__baseline__percentile': self.parameters['percentile']}


class MonthlyHeatingDegreeDays(TemperatureDeltaUnitsMixin, BasetempIndicatorMixin,
MonthlyAggregationIndicator):
label = 'Monthly Heating Degree Days'
description = ('Total difference of daily low temperature to a reference base temperature '
'(Default 65F)')
variables = ('tasmin',)
agg_function = Sum

# List units as a parameter so it gets updated by the query params if it is overriden.
# This way we can fall back to the units param if we need to handle bare numbers for basetemp
parameters = {'basetemp': '65F',
'units': 'F'}

@property
def conditions(self):
return {'tasmin__lt': F('map_cell__baseline__tasmin'),
'map_cell__baseline__percentile': self.parameters['percentile']}
return {'tasmin__lte': self.parameters['basetemp']}

@property
def expression(self):
return self.parameters['basetemp'] - F('tasmin')


class MonthlyCoolingDegreeDays(TemperatureDeltaUnitsMixin, BasetempIndicatorMixin,
MonthlyAggregationIndicator):
label = 'Monthly Cooling Degree Days'
description = ('Total difference of daily high temperature to a reference base temperature '
'(Default 65F)')
variables = ('tasmax',)
agg_function = Sum

# List units as a parameter so it gets updated by the query params if it is overriden.
# This way we can fall back to the units param if we need to handle bare numbers for basetemp
parameters = {'basetemp': '65F',
'units': 'F'}

@property
def conditions(self):
return {'tasmax__gte': self.parameters['basetemp']}

@property
def expression(self):
return F('tasmax') - self.parameters['basetemp']


##########################
Expand Down
11 changes: 11 additions & 0 deletions django/climate_change_api/indicators/tests/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class ConverterTestMixin():
def test_conversions(self):
for units, tests in self.cases.iteritems():
converter = self.converter_class.get(*units)

for start, expected in tests:
value = converter(start)
self.assertAlmostEqual(value, expected,
places=3,
msg='Failed assertion that %f %s = %f %s, got %f %s' %
(start, units[0], expected, units[1], value, units[1]))
8 changes: 0 additions & 8 deletions django/climate_change_api/indicators/tests/test_indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ def test_models_filter(self):
data = indicator.calculate()
self.assertEqual(data, self.test_models_filter_equals)

def test_unit_conversion_definitions(self):
""" Some sanity checks for unit conversion class attributes """
self.assertIn(self.indicator_class.default_units, self.indicator_class.available_units)
storage_units = self.indicator_class.storage_units
for units in self.indicator_class.available_units:
self.assertTrue(units == storage_units or callable(
self.indicator_class.conversions[self.indicator_class.storage_units][units]))


class TemperatureIndicatorTests(IndicatorTests):
def test_unit_conversion(self):
Expand Down
Loading