MAINT: Clean up mixin usage.

- Use RestrictedDTypeMixin for dtype validation in
  Filter/Factor/Classifier.
- Use new LatestMixin for Latest{Filter,Factor,Classifier} instead of
  duplicating logic across all three.
- Always ignore return values in _validate.
- Consistently call super() first in validation mixins.
This commit is contained in:
Scott Sanderson
2016-03-19 19:09:16 -04:00
parent bae78ae522
commit 381a231725
12 changed files with 106 additions and 127 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
from .classifier import Classifier, CustomClassifier, Everything
from .latest import Latest
from .classifier import Classifier, CustomClassifier, Everything, Latest
__all__ = [
'Classifier',
+20 -15
View File
@@ -3,26 +3,19 @@ classifier.py
"""
from numpy import zeros, where
from zipline.errors import UnsupportedDataType
from zipline.pipeline.term import ComputableTerm
from zipline.utils.numpy_utils import int64_dtype
from ..mixins import CustomTermMixin, PositiveWindowLengthMixin
from ..mixins import (
CustomTermMixin,
LatestMixin,
PositiveWindowLengthMixin,
RestrictedDTypeMixin
)
class Classifier(ComputableTerm):
def _validate(self):
# Run superclass validation first so that we handle `dtype not passed`
# before this.
retval = super(Classifier, self)._validate()
# TODO: Support strings here.
if self.dtype != int64_dtype:
raise UnsupportedDataType(
typename=type(self).__name__,
dtype=self.dtype
)
return retval
class Classifier(RestrictedDTypeMixin, ComputableTerm):
ALLOWED_DTYPES = (int64_dtype,) # Used by RestrictedDTypeMixin
class Everything(Classifier):
@@ -52,3 +45,15 @@ class CustomClassifier(PositiveWindowLengthMixin, CustomTermMixin, Classifier):
zipline.pipeline.CustomFilter
"""
pass
class Latest(LatestMixin, CustomClassifier):
"""
A classifier producing the latest value of an input.
See Also
--------
zipline.pipeline.data.dataset.BoundColumn.latest
zipline.pipeline.factors.factor.Latest
zipline.pipeline.filters.filter.Latest
"""
-29
View File
@@ -1,29 +0,0 @@
"""
Classifier that produces the most most recently-known value of a
integer-valued column.
"""
from zipline.utils.numpy_utils import int64_dtype
from .classifier import CustomClassifier
from ..mixins import SingleInputMixin
class Latest(SingleInputMixin, CustomClassifier):
"""
Filter producing the most recently-known value of `inputs[0]` on each day.
"""
window_length = 1
def compute(self, today, assets, out, data):
out[:] = data[-1]
def _validate(self):
if self.inputs[0].dtype != int64_dtype:
raise TypeError(
"{name} expected an input of dtype int64, "
"but got {not_bool} instead.".format(
name=type(self).__name__,
not_bool=self.inputs[0].dtype,
)
)
super(Latest, self)._validate()
+1 -1
View File
@@ -223,7 +223,7 @@ class NumericalExpression(ComputableTerm):
expected_indices, expr_indices,
)
)
return super(NumericalExpression, self)._validate()
super(NumericalExpression, self)._validate()
def _compute(self, arrays, dates, assets, mask):
"""
+2 -2
View File
@@ -1,8 +1,8 @@
from .factor import (
Factor,
CustomFactor,
Factor,
Latest
)
from .latest import Latest
from .events import (
BusinessDaysSinceCashBuybackAuth,
BusinessDaysUntilNextEarnings,
+19 -16
View File
@@ -8,16 +8,15 @@ from numbers import Number
from numpy import inf, where, nanstd
from toolz import curry
from zipline.errors import (
UnknownRankMethod,
UnsupportedDataType,
)
from zipline.errors import UnknownRankMethod
from zipline.lib.normalize import naive_grouped_rowwise_apply
from zipline.lib.rank import masked_rankdata_2d
from zipline.pipeline.classifiers import Classifier, Everything
from zipline.pipeline.mixins import (
CustomTermMixin,
LatestMixin,
PositiveWindowLengthMixin,
RestrictedDTypeMixin,
SingleInputMixin,
)
from zipline.pipeline.term import (
@@ -396,10 +395,12 @@ float64_only = restrict_to_dtype(
FACTOR_DTYPES = frozenset([datetime64ns_dtype, float64_dtype, int64_dtype])
class Factor(ComputableTerm):
class Factor(RestrictedDTypeMixin, ComputableTerm):
"""
Pipeline API expression producing numerically-valued outputs.
"""
ALLOWED_DTYPES = FACTOR_DTYPES # Used by RestrictedDTypeMixin
# Dynamically add functions for creating NumExprFactor/NumExprFilter
# instances.
clsdict = locals()
@@ -436,17 +437,6 @@ class Factor(ComputableTerm):
eq = binary_operator('==')
def _validate(self):
# Do superclass validation first so that `NotSpecified` dtypes get
# handled.
retval = super(Factor, self)._validate()
if self.dtype not in FACTOR_DTYPES:
raise UnsupportedDataType(
typename=type(self).__name__,
dtype=self.dtype
)
return retval
@expect_types(
mask=(Filter, NotSpecifiedType),
groupby=(Classifier, NotSpecifiedType),
@@ -1097,3 +1087,16 @@ class CustomFactor(PositiveWindowLengthMixin, CustomTermMixin, Factor):
median_low15 = MedianValue([USEquityPricing.low], window_length=15)
'''
dtype = float64_dtype
class Latest(LatestMixin, CustomFactor):
"""
Factor producing the most recently-known value of `inputs[0]` on each day.
The `.latest` attribute of DataSet columns returns an instance of this
Factor.
"""
window_length = 1
def compute(self, today, assets, out, data):
out[:] = data[-1]
-18
View File
@@ -1,18 +0,0 @@
"""
Factor that produces the most most recently-known value of Column.
"""
from .factor import CustomFactor
from ..mixins import SingleInputMixin
class Latest(SingleInputMixin, CustomFactor):
"""
Factor producing the most recently-known value of `inputs[0]` on each day.
The `.latest` attribute of DataSet columns returns an instance of this
Factor.
"""
window_length = 1
def compute(self, today, assets, out, data):
out[:] = data[-1]
+3 -3
View File
@@ -1,17 +1,17 @@
from .filter import (
CustomFilter,
Filter,
NumExprFilter,
Latest,
NullFilter,
NumExprFilter,
PercentileFilter,
)
from .latest import Latest
__all__ = [
'CustomFilter',
'Filter',
'Latest',
'NumExprFilter',
'NullFilter',
'NumExprFilter',
'PercentileFilter',
]
+11 -3
View File
@@ -16,7 +16,9 @@ from zipline.errors import (
from zipline.lib.rank import ismissing
from zipline.pipeline.mixins import (
CustomTermMixin,
LatestMixin,
PositiveWindowLengthMixin,
RestrictedDTypeMixin,
SingleInputMixin,
)
from zipline.pipeline.term import ComputableTerm, Term
@@ -26,7 +28,6 @@ from zipline.pipeline.expression import (
method_name_for_op,
NumericalExpression,
)
from zipline.utils.control_flow import nullctx
from zipline.utils.numpy_utils import bool_dtype
@@ -114,10 +115,11 @@ def unary_operator(op):
return unary_operator
class Filter(ComputableTerm):
class Filter(RestrictedDTypeMixin, ComputableTerm):
"""
Pipeline API expression producing boolean-valued outputs.
"""
ALLOWED_DTYPES = (bool_dtype,) # Used by RestrictedDTypeMixin
dtype = bool_dtype
clsdict = locals()
@@ -324,4 +326,10 @@ class CustomFilter(PositiveWindowLengthMixin, CustomTermMixin, Filter):
--------
zipline.pipeline.factors.factor.CustomFactor
"""
ctx = nullctx()
class Latest(LatestMixin, CustomFilter):
"""
Filter producing the most recently-known value of `inputs[0]` on each day.
"""
pass
-29
View File
@@ -1,29 +0,0 @@
"""
Filter that produces the most most recently-known value of a boolean-valued
Column.
"""
from zipline.utils.numpy_utils import bool_dtype
from .filter import CustomFilter
from ..mixins import SingleInputMixin
class Latest(SingleInputMixin, CustomFilter):
"""
Filter producing the most recently-known value of `inputs[0]` on each day.
"""
window_length = 1
def compute(self, today, assets, out, data):
out[:] = data[-1]
def _validate(self):
if self.inputs[0].dtype != bool_dtype:
raise TypeError(
"{name} expected an input of dtype bool, "
"but got {not_bool} instead.".format(
name=type(self).__name__,
not_bool=self.inputs[0].dtype,
)
)
super(Latest, self)._validate()
+45 -3
View File
@@ -4,7 +4,7 @@ Mixins classes for use with Filters and Factors.
from numpy import full_like
from zipline.utils.control_flow import nullctx
from zipline.errors import WindowLengthNotPositive
from zipline.errors import WindowLengthNotPositive, UnsupportedDataType
from .term import NotSpecified
@@ -14,9 +14,9 @@ class PositiveWindowLengthMixin(object):
Validation mixin enforcing that a Term gets a positive WindowLength
"""
def _validate(self):
super(PositiveWindowLengthMixin, self)._validate()
if not self.windowed:
raise WindowLengthNotPositive(window_length=self.window_length)
return super(PositiveWindowLengthMixin, self)._validate()
class SingleInputMixin(object):
@@ -24,6 +24,7 @@ class SingleInputMixin(object):
Validation mixin enforcing that a Term gets a length-1 inputs list.
"""
def _validate(self):
super(SingleInputMixin, self)._validate()
num_inputs = len(self.inputs)
if num_inputs != 1:
raise ValueError(
@@ -33,7 +34,26 @@ class SingleInputMixin(object):
num_inputs=num_inputs
)
)
return super(SingleInputMixin, self)._validate()
class RestrictedDTypeMixin(object):
"""
Validation mixin enforcing that a term has a specific dtype.
"""
ALLOWED_DTYPES = NotSpecified
def _validate(self):
super(RestrictedDTypeMixin, self)._validate()
assert self.ALLOWED_DTYPES is not NotSpecified, (
"ALLOWED_DTYPES not supplied on subclass "
"of RestrictedDTypeMixin: %s." % type(self).__name__
)
if self.dtype not in self.ALLOWED_DTYPES:
raise UnsupportedDataType(
typename=type(self.__name__),
dtype=self.dtype,
)
class CustomTermMixin(object):
@@ -105,3 +125,25 @@ class CustomTermMixin(object):
def short_repr(self):
return type(self).__name__ + '(%d)' % self.window_length
class LatestMixin(SingleInputMixin):
"""
Mixin for behavior shared by Custom{Factor,Filter,Classifier}.
"""
window_length = 1
def compute(self, today, assets, out, data):
out[:] = data[-1]
def _validate(self):
super(LatestMixin, self)._validate()
if self.inputs[0].dtype != self.dtype:
raise TypeError(
"{name} expected an input of dtype {expected}, "
"but got {actual} instead.".format(
name=type(self).__name__,
expected=self.dtype,
actual=self.inputs[0].dtype,
)
)
+4 -6
View File
@@ -398,14 +398,14 @@ class ComputableTerm(Term):
)
def _validate(self):
"""
Assert that this term is well-formed. This should be called exactly
once, at the end of Term._init().
"""
super(ComputableTerm, self)._validate()
if self.inputs is NotSpecified:
raise TermInputsNotSpecified(termname=type(self).__name__)
if self.window_length is NotSpecified:
raise WindowLengthNotSpecified(termname=type(self).__name__)
if self.mask is NotSpecified:
# This isn't user error, this is a bug in our code.
raise AssertionError("{term} has no mask".format(term=self))
@@ -415,8 +415,6 @@ class ComputableTerm(Term):
if child.windowed:
raise WindowedInputToWindowedTerm(parent=self, child=child)
return super(ComputableTerm, self)._validate()
def _compute(self, inputs, dates, assets, mask):
"""
Subclasses should implement this to perform actual computation.