diff --git a/tests/pipeline/test_factor.py b/tests/pipeline/test_factor.py index 2e27eb2f..e7dab7cc 100644 --- a/tests/pipeline/test_factor.py +++ b/tests/pipeline/test_factor.py @@ -2,6 +2,7 @@ Tests for Factor terms. """ from itertools import product +from six import iteritems from nose_parameterized import parameterized from numpy import ( @@ -32,6 +33,7 @@ from zipline.testing import ( check_arrays, parameter_space, ) +from zipline.utils.functional import dzip_exact from zipline.utils.numpy_utils import ( datetime64ns_dtype, float64_dtype, @@ -423,6 +425,91 @@ class FactorTestCase(BasePipelineTestCase): check_arrays(float_result, datetime_result) + def test_normalizations_hand_computed(self): + """ + Test the hand-computed example in factor.demean. + """ + f = self.f + m = Mask() + c = C() + + factor_data = array( + [[1.0, 2.0, 3.0, 4.0], + [1.5, 2.5, 3.5, 1.0], + [2.0, 3.0, 4.0, 1.5], + [2.5, 3.5, 1.0, 2.0]], + ) + filter_data = array( + [[False, True, True, True], + [True, False, True, True], + [True, True, False, True], + [True, True, True, False]], + dtype=bool, + ) + classifier_data = array( + [[1, 1, 2, 2], + [1, 1, 2, 2], + [1, 1, 2, 2], + [1, 1, 2, 2]], + dtype=int, + ) + + terms = { + 'vanilla': f.demean(), + 'masked': f.demean(mask=m), + 'grouped': f.demean(groupby=c), + 'grouped_masked': f.demean(mask=m, groupby=c), + } + expected = { + 'vanilla': array( + [[-1.500, -0.500, 0.500, 1.500], + [-0.625, 0.375, 1.375, -1.125], + [-0.625, 0.375, 1.375, -1.125], + [0.250, 1.250, -1.250, -0.250]], + ), + 'masked': array( + [[nan, -1.000, 0.000, 1.000], + [-0.500, nan, 1.500, -1.000], + [-0.166, 0.833, nan, -0.666], + [0.166, 1.166, -1.333, nan]], + ), + 'grouped': array( + [[-0.500, 0.500, -0.500, 0.500], + [-0.500, 0.500, 1.250, -1.250], + [-0.500, 0.500, 1.250, -1.250], + [-0.500, 0.500, -0.500, 0.500]], + ), + 'grouped_masked': array( + [[nan, 0.000, -0.500, 0.500], + [0.000, nan, 1.250, -1.250], + [-0.500, 0.500, nan, 0.000], + [-0.500, 0.500, 0.000, nan]] + ) + } + + graph = TermGraph(terms) + results = self.run_graph( + graph, + initial_workspace={ + f: factor_data, + c: classifier_data, + m: filter_data, + }, + mask=self.build_mask(self.ones_mask(shape=factor_data.shape)), + ) + + for key, (res, exp) in iteritems(dzip_exact(results, expected)): + check_allclose( + res, + exp, + # The hand-computed values aren't very precise (in particular, + # we truncate repeating decimals at 3 places) This is just + # asserting that the example isn't misleading by being totally + # wrong. + atol=0.001, + err_msg="Mismatch for %r" % key + ) + @parameter_space( seed_value=range(1, 2), normalizer_name_and_func=[ @@ -431,10 +518,10 @@ class FactorTestCase(BasePipelineTestCase): ], add_nulls_to_factor=(False, True,) ) - def test_normalizations(self, - seed_value, - normalizer_name_and_func, - add_nulls_to_factor): + def test_normalizations_randomized(self, + seed_value, + normalizer_name_and_func, + add_nulls_to_factor): name, func = normalizer_name_and_func diff --git a/zipline/testing/core.py b/zipline/testing/core.py index 15d1a703..8bc05bc0 100644 --- a/zipline/testing/core.py +++ b/zipline/testing/core.py @@ -568,8 +568,14 @@ def check_allclose(actual, """ if type(actual) != type(desired): raise AssertionError("%s != %s" % (type(actual), type(desired))) - return assert_allclose(actual, desired, rtol=rtol, atol=atol, - err_msg=err_msg, verbose=verbose) + return assert_allclose( + actual, + desired, + atol=atol, + rtol=rtol, + err_msg=err_msg, + verbose=verbose, + ) def check_arrays(x, y, err_msg='', verbose=True): diff --git a/zipline/utils/functional.py b/zipline/utils/functional.py index 420cd604..80ac1878 100644 --- a/zipline/utils/functional.py +++ b/zipline/utils/functional.py @@ -1,3 +1,8 @@ +from operator import methodcaller +from six.moves import map +from pprint import pformat + + def mapall(funcs, seq): """ Parameters @@ -20,3 +25,59 @@ def mapall(funcs, seq): for func in funcs: for elem in seq: yield func(elem) + + +def same(*values): + """ + Check if all values in a sequence are equal. + + Returns True on empty sequences. + + Example + ------- + >>> same(1, 1, 1, 1) + True + >>> same(1, 2, 1) + False + >>> same() + True + """ + if not values: + return True + first, rest = values[0], values[1:] + return all(value == first for value in rest) + + +def _format_unequal_keys(dicts): + return pformat([sorted(d.keys()) for d in dicts]) + + +def dzip_exact(*dicts): + """ + Parameters + ---------- + *dicts : iterable[dict] + A sequence of dicts all sharing the same keys. + + Returns + ------- + zipped : dict + A dict whose keys are the union of all keys in *dicts, and whose values + are tuples of length len(dicts) containing the result of looking up + each key in each dict. + + Raises + ------ + ValueError + If dicts don't all have the same keys. + + Example + ------- + >>> dzip_exact({'a': 1, 'b': 2}, {'a': 3, 'b': 4}) + {'a': (1, 3), 'b': (2, 4)} + """ + if not same(*map(methodcaller('viewkeys'), dicts)): + raise ValueError( + "dict keys not all equal:\n\n%s" % _format_unequal_keys(dicts) + ) + return {k: tuple(d[k] for d in dicts) for k in dicts[0]} diff --git a/zipline/utils/input_validation.py b/zipline/utils/input_validation.py index d04869fc..692cfaf7 100644 --- a/zipline/utils/input_validation.py +++ b/zipline/utils/input_validation.py @@ -321,6 +321,19 @@ def optional(type_): return (type_, type(None)) +def _expect_element(collection): + template = ( + "%(funcname)s() expected a value in {collection} " + "for argument '%(argname)s', but got %(actual)s instead." + ).format(collection=collection) + return make_check( + ValueError, + template, + complement(op.contains(collection)), + repr, + ) + + def expect_element(*_pos, **named): """ Preprocessing decorator that verifies inputs are elements of some @@ -391,16 +404,3 @@ def coerce(from_, to, **to_kwargs): coerce_string = partial(coerce, string_types) - - -def _expect_element(collection): - template = ( - "%(funcname)s() expected a value in {collection} " - "for argument '%(argname)s', but got %(actual)s instead." - ).format(collection=collection) - return make_check( - ValueError, - template, - complement(op.contains(collection)), - repr, - )