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

Commit

Permalink
Merge pull request #154 from azavea/feature/degree_day_indicator
Browse files Browse the repository at this point in the history
Degree Days Indicators
  • Loading branch information
rmartz authored Dec 15, 2016
2 parents 0ea2d42 + 675a2cd commit 5bc1fed
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 79 deletions.
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'}

@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

0 comments on commit 5bc1fed

Please sign in to comment.