Ejemplo n.º 1
0
    def __init__(self, column_names, term_slices=None, term_name_slices=None, builder=None):
        self.column_name_indexes = OrderedDict(zip(column_names, range(len(column_names))))
        if term_slices is not None:
            #: An OrderedDict mapping :class:`Term` objects to Python
            #: func:`slice` objects. May be None, for design matrices which
            #: were constructed directly rather than by using the patsy
            #: machinery. If it is not None, then it
            #: is guaranteed to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps.
            self.term_slices = OrderedDict(term_slices)
            if term_name_slices is not None:
                raise ValueError("specify only one of term_slices and " "term_name_slices")
            term_names = [term.name() for term in self.term_slices]
            #: And OrderedDict mapping term names (as strings) to Python
            #: :func:`slice` objects. Guaranteed never to be None. Guaranteed
            #: to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps. Name overlap is allowed between term names and column
            #: names, but it is guaranteed that if it occurs, then they refer
            #: to exactly the same column.
            self.term_name_slices = OrderedDict(zip(term_names, self.term_slices.values()))
        else:  # term_slices is None
            self.term_slices = None
            if term_name_slices is None:
                # Make up one term per column
                term_names = column_names
                slices = [slice(i, i + 1) for i in xrange(len(column_names))]
                term_name_slices = zip(term_names, slices)
            self.term_name_slices = OrderedDict(term_name_slices)

        self.builder = builder

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert self.term_slices.values() == self.term_name_slices.values()
        covered = 0
        for slice_ in self.term_name_slices.itervalues():
            start, stop, step = slice_.indices(len(column_names))
            if start != covered:
                raise ValueError, "bad term slices"
            if step != 1:
                raise ValueError, "bad term slices"
            covered = stop
        if covered != len(column_names):
            raise ValueError, "bad term indices"
        for column_name, index in self.column_name_indexes.iteritems():
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError, "term/column name collision"
Ejemplo n.º 2
0
def _make_term_column_builders(terms, num_column_counts, cat_levels_contrasts):
    # Sort each term into a bucket based on the set of numeric factors it
    # contains:
    term_buckets = OrderedDict()
    bucket_ordering = []
    for term in terms:
        num_factors = []
        for factor in term.factors:
            if factor in num_column_counts:
                num_factors.append(factor)
        bucket = frozenset(num_factors)
        if bucket not in term_buckets:
            bucket_ordering.append(bucket)
        term_buckets.setdefault(bucket, []).append(term)
    # Special rule: if there is a no-numerics bucket, then it always comes
    # first:
    if frozenset() in term_buckets:
        bucket_ordering.remove(frozenset())
        bucket_ordering.insert(0, frozenset())
    term_to_column_builders = {}
    new_term_order = []
    # Then within each bucket, work out which sort of contrasts we want to use
    # for each term to avoid redundancy
    for bucket in bucket_ordering:
        bucket_terms = term_buckets[bucket]
        # Sort by degree of interaction
        bucket_terms.sort(key=lambda t: len(t.factors))
        new_term_order += bucket_terms
        used_subterms = set()
        for term in bucket_terms:
            column_builders = []
            factor_codings = pick_contrasts_for_term(term, num_column_counts, used_subterms)
            # Construct one _ColumnBuilder for each subterm
            for factor_coding in factor_codings:
                builder_factors = []
                num_columns = {}
                cat_contrasts = {}
                # In order to preserve factor ordering information, the
                # coding_for_term just returns dicts, and we refer to
                # the original factors to figure out which are included in
                # each subterm, and in what order
                for factor in term.factors:
                    # Numeric factors are included in every subterm
                    if factor in num_column_counts:
                        builder_factors.append(factor)
                        num_columns[factor] = num_column_counts[factor]
                    elif factor in factor_coding:
                        builder_factors.append(factor)
                        levels, contrast = cat_levels_contrasts[factor]
                        # This is where the default coding is set to
                        # Treatment:
                        coded = code_contrast_matrix(factor_coding[factor], levels, contrast, default=Treatment)
                        cat_contrasts[factor] = coded
                column_builder = _ColumnBuilder(builder_factors, num_columns, cat_contrasts)
                column_builders.append(column_builder)
            term_to_column_builders[term] = column_builders
    return new_term_order, term_to_column_builders
Ejemplo n.º 3
0
    def __init__(self,
                 column_names,
                 term_slices=None,
                 term_name_slices=None,
                 builder=None):
        self.column_name_indexes = OrderedDict(
            zip(column_names, range(len(column_names))))
        if term_slices is not None:
            #: An OrderedDict mapping :class:`Term` objects to Python
            #: func:`slice` objects. May be None, for design matrices which
            #: were constructed directly rather than by using the patsy
            #: machinery. If it is not None, then it
            #: is guaranteed to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps.
            self.term_slices = OrderedDict(term_slices)
            if term_name_slices is not None:
                raise ValueError("specify only one of term_slices and "
                                 "term_name_slices")
            term_names = [term.name() for term in self.term_slices]
            #: And OrderedDict mapping term names (as strings) to Python
            #: :func:`slice` objects. Guaranteed never to be None. Guaranteed
            #: to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps. Name overlap is allowed between term names and column
            #: names, but it is guaranteed that if it occurs, then they refer
            #: to exactly the same column.
            self.term_name_slices = OrderedDict(
                zip(term_names, self.term_slices.values()))
        else:  # term_slices is None
            self.term_slices = None
            if term_name_slices is None:
                # Make up one term per column
                term_names = column_names
                slices = [slice(i, i + 1) for i in range(len(column_names))]
                term_name_slices = zip(term_names, slices)
            self.term_name_slices = OrderedDict(term_name_slices)

        self.builder = builder

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values()) == list(
                self.term_name_slices.values()))
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            if start != covered:
                raise ValueError("bad term slices")
            if step != 1:
                raise ValueError("bad term slices")
            covered = stop
        if covered != len(column_names):
            raise ValueError("bad term indices")
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")
Ejemplo n.º 4
0
class DesignInfo(object):
    """A DesignInfo object holds metadata about a design matrix.

    This is the main object that Patsy uses to pass information to
    statistical libraries. Usually encountered as the `.design_info` attribute
    on design matrices.
    """
    def __init__(self,
                 column_names,
                 term_slices=None,
                 term_name_slices=None,
                 builder=None):
        self.column_name_indexes = OrderedDict(
            zip(column_names, range(len(column_names))))
        if term_slices is not None:
            #: An OrderedDict mapping :class:`Term` objects to Python
            #: func:`slice` objects. May be None, for design matrices which
            #: were constructed directly rather than by using the patsy
            #: machinery. If it is not None, then it
            #: is guaranteed to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps.
            self.term_slices = OrderedDict(term_slices)
            if term_name_slices is not None:
                raise ValueError("specify only one of term_slices and "
                                 "term_name_slices")
            term_names = [term.name() for term in self.term_slices]
            #: And OrderedDict mapping term names (as strings) to Python
            #: :func:`slice` objects. Guaranteed never to be None. Guaranteed
            #: to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps. Name overlap is allowed between term names and column
            #: names, but it is guaranteed that if it occurs, then they refer
            #: to exactly the same column.
            self.term_name_slices = OrderedDict(
                zip(term_names, self.term_slices.values()))
        else:  # term_slices is None
            self.term_slices = None
            if term_name_slices is None:
                # Make up one term per column
                term_names = column_names
                slices = [slice(i, i + 1) for i in range(len(column_names))]
                term_name_slices = zip(term_names, slices)
            self.term_name_slices = OrderedDict(term_name_slices)

        self.builder = builder

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values()) == list(
                self.term_name_slices.values()))
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            if start != covered:
                raise ValueError("bad term slices")
            if step != 1:
                raise ValueError("bad term slices")
            covered = stop
        if covered != len(column_names):
            raise ValueError("bad term indices")
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")

    __repr__ = repr_pretty_delegate

    def _repr_pretty_(self, p, cycle):
        assert not cycle
        if self.term_slices is None:
            kwargs = [("term_name_slices", self.term_name_slices)]
        else:
            kwargs = [("term_slices", self.term_slices)]
        if self.builder is not None:
            kwargs.append(("builder", self.builder))
        repr_pretty_impl(p, self, [self.column_names], kwargs)

    @property
    def column_names(self):
        "A list of the column names, in order."
        return list(self.column_name_indexes)

    @property
    def terms(self):
        "A list of :class:`Terms`, in order, or else None."
        if self.term_slices is None:
            return None
        return list(self.term_slices)

    @property
    def term_names(self):
        "A list of terms, in order."
        return list(self.term_name_slices)

    def slice(self, columns_specifier):
        """Locate a subset of design matrix columns, specified symbolically.

        A patsy design matrix has two levels of structure: the individual
        columns (which are named), and the :ref:`terms <formulas>` in
        the formula that generated those columns. This is a one-to-many
        relationship: a single term may span several columns. This method
        provides a user-friendly API for locating those columns.

        (While we talk about columns here, this is probably most useful for
        indexing into other arrays that are derived from the design matrix,
        such as regression coefficients or covariance matrices.)

        The `columns_specifier` argument can take a number of forms:

        * A term name
        * A column name
        * A :class:`Term` object
        * An integer giving a raw index
        * A raw slice object

        In all cases, a Python :func:`slice` object is returned, which can be
        used directly for indexing.

        Example::

          y, X = dmatrices("y ~ a", demo_data("y", "a", nlevels=3))
          betas = np.linalg.lstsq(X, y)[0]
          a_betas = betas[X.design_info.slice("a")]

        (If you want to look up a single individual column by name, use
        ``design_info.column_name_indexes[name]``.)
        """
        if isinstance(columns_specifier, slice):
            return columns_specifier
        if np.issubsctype(type(columns_specifier), np.integer):
            return slice(columns_specifier, columns_specifier + 1)
        if (self.term_slices is not None
                and columns_specifier in self.term_slices):
            return self.term_slices[columns_specifier]
        if columns_specifier in self.term_name_slices:
            return self.term_name_slices[columns_specifier]
        if columns_specifier in self.column_name_indexes:
            idx = self.column_name_indexes[columns_specifier]
            return slice(idx, idx + 1)
        raise PatsyError("unknown column specified '%s'" %
                         (columns_specifier, ))

    def linear_constraint(self, constraint_likes):
        """Construct a linear constraint in matrix form from a (possibly
        symbolic) description.

        Possible inputs:

        * A dictionary which is taken as a set of equality constraint. Keys
          can be either string column names, or integer column indexes.
        * A string giving a arithmetic expression referring to the matrix
          columns by name.
        * A list of such strings which are ANDed together.
        * A tuple (A, b) where A and b are array_likes, and the constraint is
          Ax = b. If necessary, these will be coerced to the proper
          dimensionality by appending dimensions with size 1.

        The string-based language has the standard arithmetic operators, / * +
        - and parentheses, plus "=" is used for equality and "," is used to
        AND together multiple constraint equations within a string. You can
        If no = appears in some expression, then that expression is assumed to
        be equal to zero. Division is always float-based, even if
        ``__future__.true_division`` isn't in effect.

        Returns a :class:`LinearConstraint` object.

        Examples::

          di = DesignInfo(["x1", "x2", "x3"])

          # Equivalent ways to write x1 == 0:
          di.linear_constraint({"x1": 0})  # by name
          di.linear_constraint({0: 0})  # by index
          di.linear_constraint("x1 = 0")  # string based
          di.linear_constraint("x1")  # can leave out "= 0"
          di.linear_constraint("2 * x1 = (x1 + 2 * x1) / 3")
          di.linear_constraint(([1, 0, 0], 0))  # constraint matrices

          # Equivalent ways to write x1 == 0 and x3 == 10
          di.linear_constraint({"x1": 0, "x3": 10})
          di.linear_constraint({0: 0, 2: 10})
          di.linear_constraint({0: 0, "x3": 10})
          di.linear_constraint("x1 = 0, x3 = 10")
          di.linear_constraint("x1, x3 = 10")
          di.linear_constraint(["x1", "x3 = 0"])  # list of strings
          di.linear_constraint("x1 = 0, x3 - 10 = x1")
          di.linear_constraint([[1, 0, 0], [0, 0, 1]], [0, 10])

          # You can also chain together equalities, just like Python:
          di.linear_constraint("x1 = x2 = 3")
        """
        return linear_constraint(constraint_likes, self.column_names)

    def describe(self):
        """Returns a human-readable string describing this design info.

        Example:

        .. ipython::

          In [1]: y, X = dmatrices("y ~ x1 + x2", demo_data("y", "x1", "x2"))

          In [2]: y.design_info.describe()
          Out[2]: 'y'

          In [3]: X.design_info.describe()
          Out[3]: '1 + x1 + x2'

        .. warning::

           There is no guarantee that the strings returned by this
           function can be parsed as formulas. They are best-effort descriptions
           intended for human users.
        """

        names = []
        for name in self.term_names:
            if name == "Intercept":
                names.append("1")
            else:
                names.append(name)
        return " + ".join(names)

    @classmethod
    def from_array(cls, array_like, default_column_prefix="column"):
        """Find or construct a DesignInfo appropriate for a given array_like.

        If the input `array_like` already has a ``.design_info``
        attribute, then it will be returned. Otherwise, a new DesignInfo
        object will be constructed, using names either taken from the
        `array_like` (e.g., for a pandas DataFrame with named columns), or
        constructed using `default_column_prefix`.

        This is how :func:`dmatrix` (for example) creates a DesignInfo object
        if an arbitrary matrix is passed in.

        :arg array_like: An ndarray or pandas container.
        :arg default_column_prefix: If it's necessary to invent column names,
          then this will be used to construct them.
        :returns: a DesignInfo object
        """
        if hasattr(array_like, "design_info") and isinstance(
                array_like.design_info, cls):
            return array_like.design_info
        arr = atleast_2d_column_default(array_like, preserve_pandas=True)
        if arr.ndim > 2:
            raise ValueError("design matrix can't have >2 dimensions")
        columns = getattr(arr, "columns", range(arr.shape[1]))
        if (isinstance(columns, np.ndarray)
                and not np.issubdtype(columns.dtype, np.integer)):
            column_names = [str(obj) for obj in columns]
        else:
            column_names = [
                "%s%s" % (default_column_prefix, i) for i in columns
            ]
        return DesignInfo(column_names)
Ejemplo n.º 5
0
def _make_subterm_infos(terms,
                        num_column_counts,
                        cat_levels_contrasts):
    # Sort each term into a bucket based on the set of numeric factors it
    # contains:
    term_buckets = OrderedDict()
    bucket_ordering = []
    for term in terms:
        num_factors = []
        for factor in term.factors:
            if factor in num_column_counts:
                num_factors.append(factor)
        bucket = frozenset(num_factors)
        if bucket not in term_buckets:
            bucket_ordering.append(bucket)
        term_buckets.setdefault(bucket, []).append(term)
    # Special rule: if there is a no-numerics bucket, then it always comes
    # first:
    if frozenset() in term_buckets:
        bucket_ordering.remove(frozenset())
        bucket_ordering.insert(0, frozenset())
    term_to_subterm_infos = OrderedDict()
    new_term_order = []
    # Then within each bucket, work out which sort of contrasts we want to use
    # for each term to avoid redundancy
    for bucket in bucket_ordering:
        bucket_terms = term_buckets[bucket]
        # Sort by degree of interaction
        bucket_terms.sort(key=lambda t: len(t.factors))
        new_term_order += bucket_terms
        used_subterms = set()
        for term in bucket_terms:
            subterm_infos = []
            factor_codings = pick_contrasts_for_term(term,
                                                     num_column_counts,
                                                     used_subterms)
            # Construct one SubtermInfo for each subterm
            for factor_coding in factor_codings:
                subterm_factors = []
                contrast_matrices = {}
                subterm_columns = 1
                # In order to preserve factor ordering information, the
                # coding_for_term just returns dicts, and we refer to
                # the original factors to figure out which are included in
                # each subterm, and in what order
                for factor in term.factors:
                    # Numeric factors are included in every subterm
                    if factor in num_column_counts:
                        subterm_factors.append(factor)
                        subterm_columns *= num_column_counts[factor]
                    elif factor in factor_coding:
                        subterm_factors.append(factor)
                        levels, contrast = cat_levels_contrasts[factor]
                        # This is where the default coding is set to
                        # Treatment:
                        coded = code_contrast_matrix(factor_coding[factor],
                                                     levels, contrast,
                                                     default=Treatment)
                        contrast_matrices[factor] = coded
                        subterm_columns *= coded.matrix.shape[1]
                subterm_infos.append(SubtermInfo(subterm_factors,
                                                       contrast_matrices,
                                                       subterm_columns))
            term_to_subterm_infos[term] = subterm_infos
    assert new_term_order == list(term_to_subterm_infos)
    return term_to_subterm_infos
Ejemplo n.º 6
0
    def __init__(self, column_names,
                 factor_infos=None, term_codings=None):
        self.column_name_indexes = OrderedDict(zip(column_names,
                                                   range(len(column_names))))

        if (factor_infos is None) != (term_codings is None):
            raise ValueError("Must specify either both or neither of "
                             "factor_infos= and term_codings=")

        self.factor_infos = factor_infos
        self.term_codings = term_codings

        # factor_infos is a dict containing one entry for every factor
        #    mentioned in our terms
        #    and mapping each to FactorInfo object
        if self.factor_infos is not None:
            if not isinstance(self.factor_infos, dict):
                raise ValueError("factor_infos should be a dict")

            if not isinstance(self.term_codings, OrderedDict):
                raise ValueError("term_codings must be an OrderedDict")
            for term, subterms in six.iteritems(self.term_codings):
                if not isinstance(term, Term):
                    raise ValueError("expected a Term, not %r" % (term,))
                if not isinstance(subterms, list):
                    raise ValueError("term_codings must contain lists")
                term_factors = set(term.factors)
                for subterm in subterms:
                    if not isinstance(subterm, SubtermInfo):
                        raise ValueError("expected SubtermInfo, "
                                         "not %r" % (subterm,))
                    if not term_factors.issuperset(subterm.factors):
                        raise ValueError("unexpected factors in subterm")

            all_factors = set()
            for term in self.term_codings:
                all_factors.update(term.factors)
            if all_factors != set(self.factor_infos):
                raise ValueError("Provided Term objects and factor_infos "
                                 "do not match")
            for factor, factor_info in six.iteritems(self.factor_infos):
                if not isinstance(factor_info, FactorInfo):
                    raise ValueError("expected FactorInfo object, not %r"
                                     % (factor_info,))
                if factor != factor_info.factor:
                    raise ValueError("mismatched factor_info.factor")

            for term, subterms in six.iteritems(self.term_codings):
                for subterm in subterms:
                    exp_cols = 1
                    cat_factors = set()
                    for factor in subterm.factors:
                        fi = self.factor_infos[factor]
                        if fi.type == "numerical":
                            exp_cols *= fi.num_columns
                        else:
                            assert fi.type == "categorical"
                            cm = subterm.contrast_matrices[factor].matrix
                            if cm.shape[0] != len(fi.categories):
                                raise ValueError("Mismatched contrast matrix "
                                                 "for factor %r" % (factor,))
                            cat_factors.add(factor)
                            exp_cols *= cm.shape[1]
                    if cat_factors != set(subterm.contrast_matrices):
                        raise ValueError("Mismatch between contrast_matrices "
                                         "and categorical factors")
                    if exp_cols != subterm.num_columns:
                        raise ValueError("Unexpected num_columns")

        if term_codings is None:
            # Need to invent term information
            self.term_slices = None
            # We invent one term per column, with the same name as the column
            term_names = column_names
            slices = [slice(i, i + 1) for i in range(len(column_names))]
            self.term_name_slices = OrderedDict(zip(term_names, slices))
        else:
            # Need to derive term information from term_codings
            self.term_slices = OrderedDict()
            idx = 0
            for term, subterm_infos in six.iteritems(self.term_codings):
                term_columns = 0
                for subterm_info in subterm_infos:
                    term_columns += subterm_info.num_columns
                self.term_slices[term] = slice(idx, idx + term_columns)
                idx += term_columns
            if idx != len(self.column_names):
                raise ValueError("mismatch between column_names and columns "
                                 "coded by given terms")
            self.term_name_slices = OrderedDict(
                [(term.name(), slice_)
                 for (term, slice_) in six.iteritems(self.term_slices)])

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values())
                    == list(self.term_name_slices.values()))
        # These checks probably aren't necessary anymore now that we always
        # generate the slices ourselves, but we'll leave them in just to be
        # safe.
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            assert start == covered
            assert step == 1
            covered = stop
        assert covered == len(column_names)
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")
Ejemplo n.º 7
0
class DesignInfo(object):
    """A DesignInfo object holds metadata about a design matrix.

    This is the main object that Patsy uses to pass metadata about a design
    matrix to statistical libraries, in order to allow further downstream
    processing like intelligent tests, prediction on new data, etc. Usually
    encountered as the `.design_info` attribute on design matrices.

    """

    def __init__(self, column_names,
                 factor_infos=None, term_codings=None):
        self.column_name_indexes = OrderedDict(zip(column_names,
                                                   range(len(column_names))))

        if (factor_infos is None) != (term_codings is None):
            raise ValueError("Must specify either both or neither of "
                             "factor_infos= and term_codings=")

        self.factor_infos = factor_infos
        self.term_codings = term_codings

        # factor_infos is a dict containing one entry for every factor
        #    mentioned in our terms
        #    and mapping each to FactorInfo object
        if self.factor_infos is not None:
            if not isinstance(self.factor_infos, dict):
                raise ValueError("factor_infos should be a dict")

            if not isinstance(self.term_codings, OrderedDict):
                raise ValueError("term_codings must be an OrderedDict")
            for term, subterms in six.iteritems(self.term_codings):
                if not isinstance(term, Term):
                    raise ValueError("expected a Term, not %r" % (term,))
                if not isinstance(subterms, list):
                    raise ValueError("term_codings must contain lists")
                term_factors = set(term.factors)
                for subterm in subterms:
                    if not isinstance(subterm, SubtermInfo):
                        raise ValueError("expected SubtermInfo, "
                                         "not %r" % (subterm,))
                    if not term_factors.issuperset(subterm.factors):
                        raise ValueError("unexpected factors in subterm")

            all_factors = set()
            for term in self.term_codings:
                all_factors.update(term.factors)
            if all_factors != set(self.factor_infos):
                raise ValueError("Provided Term objects and factor_infos "
                                 "do not match")
            for factor, factor_info in six.iteritems(self.factor_infos):
                if not isinstance(factor_info, FactorInfo):
                    raise ValueError("expected FactorInfo object, not %r"
                                     % (factor_info,))
                if factor != factor_info.factor:
                    raise ValueError("mismatched factor_info.factor")

            for term, subterms in six.iteritems(self.term_codings):
                for subterm in subterms:
                    exp_cols = 1
                    cat_factors = set()
                    for factor in subterm.factors:
                        fi = self.factor_infos[factor]
                        if fi.type == "numerical":
                            exp_cols *= fi.num_columns
                        else:
                            assert fi.type == "categorical"
                            cm = subterm.contrast_matrices[factor].matrix
                            if cm.shape[0] != len(fi.categories):
                                raise ValueError("Mismatched contrast matrix "
                                                 "for factor %r" % (factor,))
                            cat_factors.add(factor)
                            exp_cols *= cm.shape[1]
                    if cat_factors != set(subterm.contrast_matrices):
                        raise ValueError("Mismatch between contrast_matrices "
                                         "and categorical factors")
                    if exp_cols != subterm.num_columns:
                        raise ValueError("Unexpected num_columns")

        if term_codings is None:
            # Need to invent term information
            self.term_slices = None
            # We invent one term per column, with the same name as the column
            term_names = column_names
            slices = [slice(i, i + 1) for i in range(len(column_names))]
            self.term_name_slices = OrderedDict(zip(term_names, slices))
        else:
            # Need to derive term information from term_codings
            self.term_slices = OrderedDict()
            idx = 0
            for term, subterm_infos in six.iteritems(self.term_codings):
                term_columns = 0
                for subterm_info in subterm_infos:
                    term_columns += subterm_info.num_columns
                self.term_slices[term] = slice(idx, idx + term_columns)
                idx += term_columns
            if idx != len(self.column_names):
                raise ValueError("mismatch between column_names and columns "
                                 "coded by given terms")
            self.term_name_slices = OrderedDict(
                [(term.name(), slice_)
                 for (term, slice_) in six.iteritems(self.term_slices)])

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values())
                    == list(self.term_name_slices.values()))
        # These checks probably aren't necessary anymore now that we always
        # generate the slices ourselves, but we'll leave them in just to be
        # safe.
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            assert start == covered
            assert step == 1
            covered = stop
        assert covered == len(column_names)
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")

    __repr__ = repr_pretty_delegate
    def _repr_pretty_(self, p, cycle):
        assert not cycle
        repr_pretty_impl(p, self,
                         [self.column_names],
                         [("factor_infos", self.factor_infos),
                          ("term_codings", self.term_codings)])

    @property
    def column_names(self):
        "A list of the column names, in order."
        return list(self.column_name_indexes)

    @property
    def terms(self):
        "A list of :class:`Terms`, in order, or else None."
        if self.term_slices is None:
            return None
        return list(self.term_slices)

    @property
    def term_names(self):
        "A list of terms, in order."
        return list(self.term_name_slices)

    @property
    def builder(self):
        ".. deprecated:: 0.4.0"
        warnings.warn(DeprecationWarning(
            "The DesignInfo.builder attribute is deprecated starting in "
            "patsy v0.4.0; distinct builder objects have been eliminated "
            "and design_info.builder is now just a long-winded way of "
            "writing 'design_info' (i.e. the .builder attribute just "
            "returns self)"), stacklevel=2)
        return self

    @property
    def design_info(self):
        ".. deprecated:: 0.4.0"
        warnings.warn(DeprecationWarning(
            "Starting in patsy v0.4.0, the DesignMatrixBuilder class has "
            "been merged into the DesignInfo class. So there's no need to "
            "use builder.design_info to access the DesignInfo; 'builder' "
            "already *is* a DesignInfo."), stacklevel=2)
        return self

    def slice(self, columns_specifier):
        """Locate a subset of design matrix columns, specified symbolically.

        A patsy design matrix has two levels of structure: the individual
        columns (which are named), and the :ref:`terms <formulas>` in
        the formula that generated those columns. This is a one-to-many
        relationship: a single term may span several columns. This method
        provides a user-friendly API for locating those columns.

        (While we talk about columns here, this is probably most useful for
        indexing into other arrays that are derived from the design matrix,
        such as regression coefficients or covariance matrices.)

        The `columns_specifier` argument can take a number of forms:

        * A term name
        * A column name
        * A :class:`Term` object
        * An integer giving a raw index
        * A raw slice object

        In all cases, a Python :func:`slice` object is returned, which can be
        used directly for indexing.

        Example::

          y, X = dmatrices("y ~ a", demo_data("y", "a", nlevels=3))
          betas = np.linalg.lstsq(X, y)[0]
          a_betas = betas[X.design_info.slice("a")]

        (If you want to look up a single individual column by name, use
        ``design_info.column_name_indexes[name]``.)
        """
        if isinstance(columns_specifier, slice):
            return columns_specifier
        if np.issubsctype(type(columns_specifier), np.integer):
            return slice(columns_specifier, columns_specifier + 1)
        if (self.term_slices is not None
            and columns_specifier in self.term_slices):
            return self.term_slices[columns_specifier]
        if columns_specifier in self.term_name_slices:
            return self.term_name_slices[columns_specifier]
        if columns_specifier in self.column_name_indexes:
            idx = self.column_name_indexes[columns_specifier]
            return slice(idx, idx + 1)
        raise PatsyError("unknown column specified '%s'"
                            % (columns_specifier,))

    def linear_constraint(self, constraint_likes):
        """Construct a linear constraint in matrix form from a (possibly
        symbolic) description.

        Possible inputs:

        * A dictionary which is taken as a set of equality constraint. Keys
          can be either string column names, or integer column indexes.
        * A string giving a arithmetic expression referring to the matrix
          columns by name.
        * A list of such strings which are ANDed together.
        * A tuple (A, b) where A and b are array_likes, and the constraint is
          Ax = b. If necessary, these will be coerced to the proper
          dimensionality by appending dimensions with size 1.

        The string-based language has the standard arithmetic operators, / * +
        - and parentheses, plus "=" is used for equality and "," is used to
        AND together multiple constraint equations within a string. You can
        If no = appears in some expression, then that expression is assumed to
        be equal to zero. Division is always float-based, even if
        ``__future__.true_division`` isn't in effect.

        Returns a :class:`LinearConstraint` object.

        Examples::

          di = DesignInfo(["x1", "x2", "x3"])

          # Equivalent ways to write x1 == 0:
          di.linear_constraint({"x1": 0})  # by name
          di.linear_constraint({0: 0})  # by index
          di.linear_constraint("x1 = 0")  # string based
          di.linear_constraint("x1")  # can leave out "= 0"
          di.linear_constraint("2 * x1 = (x1 + 2 * x1) / 3")
          di.linear_constraint(([1, 0, 0], 0))  # constraint matrices

          # Equivalent ways to write x1 == 0 and x3 == 10
          di.linear_constraint({"x1": 0, "x3": 10})
          di.linear_constraint({0: 0, 2: 10})
          di.linear_constraint({0: 0, "x3": 10})
          di.linear_constraint("x1 = 0, x3 = 10")
          di.linear_constraint("x1, x3 = 10")
          di.linear_constraint(["x1", "x3 = 0"])  # list of strings
          di.linear_constraint("x1 = 0, x3 - 10 = x1")
          di.linear_constraint([[1, 0, 0], [0, 0, 1]], [0, 10])

          # You can also chain together equalities, just like Python:
          di.linear_constraint("x1 = x2 = 3")
        """
        return linear_constraint(constraint_likes, self.column_names)

    def describe(self):
        """Returns a human-readable string describing this design info.

        Example:

        .. ipython::

          In [1]: y, X = dmatrices("y ~ x1 + x2", demo_data("y", "x1", "x2"))

          In [2]: y.design_info.describe()
          Out[2]: 'y'

          In [3]: X.design_info.describe()
          Out[3]: '1 + x1 + x2'

        .. warning::

           There is no guarantee that the strings returned by this function
           can be parsed as formulas, or that if they can be parsed as a
           formula that they will produce a model equivalent to the one you
           started with. This function produces a best-effort description
           intended for humans to read.

        """

        names = []
        for name in self.term_names:
            if name == "Intercept":
                names.append("1")
            else:
                names.append(name)
        return " + ".join(names)

    def subset(self, which_terms):
        """Create a new :class:`DesignInfo` for design matrices that contain a
        subset of the terms that the current :class:`DesignInfo` does.

        For example, if ``design_info`` has terms ``x``, ``y``, and ``z``,
        then::

          design_info2 = design_info.subset(["x", "z"])

        will return a new DesignInfo that can be used to construct design
        matrices with only the columns corresponding to the terms ``x`` and
        ``z``. After we do this, then in general these two expressions will
        return the same thing (here we assume that ``x``, ``y``, and ``z``
        each generate a single column of the output)::

          build_design_matrix([design_info], data)[0][:, [0, 2]]
          build_design_matrix([design_info2], data)[0]

        However, a critical difference is that in the second case, ``data``
        need not contain any values for ``y``. This is very useful when doing
        prediction using a subset of a model, in which situation R usually
        forces you to specify dummy values for ``y``.

        If using a formula to specify the terms to include, remember that like
        any formula, the intercept term will be included by default, so use
        ``0`` or ``-1`` in your formula if you want to avoid this.

        This method can also be used to reorder the terms in your design
        matrix, in case you want to do that for some reason. I can't think of
        any.

        Note that this method will generally *not* produce the same result as
        creating a new model directly. Consider these DesignInfo objects::

            design1 = dmatrix("1 + C(a)", data)
            design2 = design1.subset("0 + C(a)")
            design3 = dmatrix("0 + C(a)", data)

        Here ``design2`` and ``design3`` will both produce design matrices
        that contain an encoding of ``C(a)`` without any intercept term. But
        ``design3`` uses a full-rank encoding for the categorical term
        ``C(a)``, while ``design2`` uses the same reduced-rank encoding as
        ``design1``.

        :arg which_terms: The terms which should be kept in the new
          :class:`DesignMatrixBuilder`. If this is a string, then it is parsed
          as a formula, and then the names of the resulting terms are taken as
          the terms to keep. If it is a list, then it can contain a mixture of
          term names (as strings) and :class:`Term` objects.

        .. versionadded: 0.2.0
           New method on the class DesignMatrixBuilder.

        .. versionchanged: 0.4.0
           Moved from DesignMatrixBuilder to DesignInfo, as part of the
           removal of DesignMatrixBuilder.

        """
        if isinstance(which_terms, str):
            desc = ModelDesc.from_formula(which_terms)
            if desc.lhs_termlist:
                raise PatsyError("right-hand-side-only formula required")
            which_terms = [term.name() for term in desc.rhs_termlist]

        if self.term_codings is None:
            # This is a minimal DesignInfo
            # If the name is unknown we just let the KeyError escape
            new_names = []
            for t in which_terms:
                new_names += self.column_names[self.term_name_slices[t]]
            return DesignInfo(new_names)
        else:
            term_name_to_term = {}
            for term in self.term_codings:
                term_name_to_term[term.name()] = term

            new_column_names = []
            new_factor_infos = {}
            new_term_codings = OrderedDict()
            for name_or_term in which_terms:
                term = term_name_to_term.get(name_or_term, name_or_term)
                # If the name is unknown we just let the KeyError escape
                s = self.term_slices[term]
                new_column_names += self.column_names[s]
                for f in term.factors:
                    new_factor_infos[f] = self.factor_infos[f]
                new_term_codings[term] = self.term_codings[term]
            return DesignInfo(new_column_names,
                              factor_infos=new_factor_infos,
                              term_codings=new_term_codings)

    @classmethod
    def from_array(cls, array_like, default_column_prefix="column"):
        """Find or construct a DesignInfo appropriate for a given array_like.

        If the input `array_like` already has a ``.design_info``
        attribute, then it will be returned. Otherwise, a new DesignInfo
        object will be constructed, using names either taken from the
        `array_like` (e.g., for a pandas DataFrame with named columns), or
        constructed using `default_column_prefix`.

        This is how :func:`dmatrix` (for example) creates a DesignInfo object
        if an arbitrary matrix is passed in.

        :arg array_like: An ndarray or pandas container.
        :arg default_column_prefix: If it's necessary to invent column names,
          then this will be used to construct them.
        :returns: a DesignInfo object
        """
        if hasattr(array_like, "design_info") and isinstance(array_like.design_info, cls):
            return array_like.design_info
        arr = atleast_2d_column_default(array_like, preserve_pandas=True)
        if arr.ndim > 2:
            raise ValueError("design matrix can't have >2 dimensions")
        columns = getattr(arr, "columns", range(arr.shape[1]))
        if (hasattr(columns, "dtype")
            and not safe_issubdtype(columns.dtype, np.integer)):
            column_names = [str(obj) for obj in columns]
        else:
            column_names = ["%s%s" % (default_column_prefix, i)
                            for i in columns]
        return DesignInfo(column_names)

    __getstate__ = no_pickling
Ejemplo n.º 8
0
def test_linear_constraint():
    from nose.tools import assert_raises
    from patsy.compat import OrderedDict
    t = _check_lincon

    t(LinearConstraint(["a", "b"], [2, 3]), ["a", "b"], [[2, 3]], [[0]])
    assert_raises(ValueError, linear_constraint,
                  LinearConstraint(["b", "a"], [2, 3]),
                  ["a", "b"])

    t({"a": 2}, ["a", "b"], [[1, 0]], [[2]])
    t(OrderedDict([("a", 2), ("b", 3)]),
      ["a", "b"], [[1, 0], [0, 1]], [[2], [3]])
    t(OrderedDict([("a", 2), ("b", 3)]),
      ["b", "a"], [[0, 1], [1, 0]], [[2], [3]])

    t({0: 2}, ["a", "b"], [[1, 0]], [[2]])
    t(OrderedDict([(0, 2), (1, 3)]), ["a", "b"], [[1, 0], [0, 1]], [[2], [3]])

    t(OrderedDict([("a", 2), (1, 3)]),
      ["a", "b"], [[1, 0], [0, 1]], [[2], [3]])

    assert_raises(ValueError, linear_constraint, {"q": 1}, ["a", "b"])
    assert_raises(ValueError, linear_constraint, {"a": 1, 0: 2}, ["a", "b"])

    t(np.array([2, 3]), ["a", "b"], [[2, 3]], [[0]])
    t(np.array([[2, 3], [4, 5]]), ["a", "b"], [[2, 3], [4, 5]], [[0], [0]])

    t("a = 2", ["a", "b"], [[1, 0]], [[2]])
    t("a - 2", ["a", "b"], [[1, 0]], [[2]])
    t("a + 1 = 3", ["a", "b"], [[1, 0]], [[2]])
    t("a + b = 3", ["a", "b"], [[1, 1]], [[3]])
    t("a = 2, b = 3", ["a", "b"], [[1, 0], [0, 1]], [[2], [3]])
    t("b = 3, a = 2", ["a", "b"], [[0, 1], [1, 0]], [[3], [2]])

    t(["a = 2", "b = 3"], ["a", "b"], [[1, 0], [0, 1]], [[2], [3]])

    assert_raises(ValueError, linear_constraint, ["a", {"b": 0}], ["a", "b"])

    # Actual evaluator tests
    t("2 * (a + b/3) + b + 2*3/4 = 1 + 2*3", ["a", "b"],
      [[2, 2.0/3 + 1]], [[7 - 6.0/4]])
    t("+2 * -a", ["a", "b"], [[-2, 0]], [[0]])
    t("a - b, a + b = 2", ["a", "b"], [[1, -1], [1, 1]], [[0], [2]])
    t("a = 1, a = 2, a = 3", ["a", "b"],
      [[1, 0], [1, 0], [1, 0]], [[1], [2], [3]])
    t("a * 2", ["a", "b"], [[2, 0]], [[0]])
    t("-a = 1", ["a", "b"], [[-1, 0]], [[1]])
    t("(2 + a - a) * b", ["a", "b"], [[0, 2]], [[0]])

    t("a = 1 = b", ["a", "b"], [[1, 0], [0, -1]], [[1], [-1]])
    t("a = (1 = b)", ["a", "b"], [[0, -1], [1, 0]], [[-1], [1]])
    t("a = 1, a = b = c", ["a", "b", "c"],
      [[1, 0, 0], [1, -1, 0], [0, 1, -1]], [[1], [0], [0]])

    # One should never do this of course, but test that it works anyway...
    t("a + 1 = 2", ["a", "a + 1"], [[0, 1]], [[2]])

    t(([10, 20], [30]), ["a", "b"], [[10, 20]], [[30]])
    t(([[10, 20], [20, 40]], [[30], [35]]), ["a", "b"],
      [[10, 20], [20, 40]], [[30], [35]])
    # wrong-length tuple
    assert_raises(ValueError, linear_constraint,
                  ([1, 0], [0], [0]), ["a", "b"])
    assert_raises(ValueError, linear_constraint, ([1, 0],), ["a", "b"])

    t([10, 20], ["a", "b"], [[10, 20]], [[0]])
    t([[10, 20], [20, 40]], ["a", "b"], [[10, 20], [20, 40]], [[0], [0]])
    t(np.array([10, 20]), ["a", "b"], [[10, 20]], [[0]])
    t(np.array([[10, 20], [20, 40]]), ["a", "b"],
      [[10, 20], [20, 40]], [[0], [0]])

    # unknown object type
    assert_raises(ValueError, linear_constraint, None, ["a", "b"])
Ejemplo n.º 9
0
def test_DesignInfo():
    from nose.tools import assert_raises
    class _MockFactor(object):
        def __init__(self, name):
            self._name = name

        def name(self):
            return self._name
    f_x = _MockFactor("x")
    f_y = _MockFactor("y")
    t_x = Term([f_x])
    t_y = Term([f_y])
    factor_infos = {f_x:
                      FactorInfo(f_x, "numerical", {}, num_columns=3),
                    f_y:
                      FactorInfo(f_y, "numerical", {}, num_columns=1),
                   }
    term_codings = OrderedDict([(t_x, [SubtermInfo([f_x], {}, 3)]),
                                (t_y, [SubtermInfo([f_y], {}, 1)])])
    di = DesignInfo(["x1", "x2", "x3", "y"], factor_infos, term_codings)
    assert di.column_names == ["x1", "x2", "x3", "y"]
    assert di.term_names == ["x", "y"]
    assert di.terms == [t_x, t_y]
    assert di.column_name_indexes == {"x1": 0, "x2": 1, "x3": 2, "y": 3}
    assert di.term_name_slices == {"x": slice(0, 3), "y": slice(3, 4)}
    assert di.term_slices == {t_x: slice(0, 3), t_y: slice(3, 4)}
    assert di.describe() == "x + y"

    assert di.slice(1) == slice(1, 2)
    assert di.slice("x1") == slice(0, 1)
    assert di.slice("x2") == slice(1, 2)
    assert di.slice("x3") == slice(2, 3)
    assert di.slice("x") == slice(0, 3)
    assert di.slice(t_x) == slice(0, 3)
    assert di.slice("y") == slice(3, 4)
    assert di.slice(t_y) == slice(3, 4)
    assert di.slice(slice(2, 4)) == slice(2, 4)
    assert_raises(PatsyError, di.slice, "asdf")

    # smoke test
    repr(di)

    assert_no_pickling(di)

    # One without term objects
    di = DesignInfo(["a1", "a2", "a3", "b"])
    assert di.column_names == ["a1", "a2", "a3", "b"]
    assert di.term_names == ["a1", "a2", "a3", "b"]
    assert di.terms is None
    assert di.column_name_indexes == {"a1": 0, "a2": 1, "a3": 2, "b": 3}
    assert di.term_name_slices == {"a1": slice(0, 1),
                                   "a2": slice(1, 2),
                                   "a3": slice(2, 3),
                                   "b": slice(3, 4)}
    assert di.term_slices is None
    assert di.describe() == "a1 + a2 + a3 + b"

    assert di.slice(1) == slice(1, 2)
    assert di.slice("a1") == slice(0, 1)
    assert di.slice("a2") == slice(1, 2)
    assert di.slice("a3") == slice(2, 3)
    assert di.slice("b") == slice(3, 4)

    # Check intercept handling in describe()
    assert DesignInfo(["Intercept", "a", "b"]).describe() == "1 + a + b"

    # Failure modes
    # must specify either both or neither of factor_infos and term_codings:
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos=factor_infos)
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], term_codings=term_codings)
    # factor_infos must be a dict
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], list(factor_infos), term_codings)
    # wrong number of column names:
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y1", "y2"], factor_infos, term_codings)
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3"], factor_infos, term_codings)
    # name overlap problems
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "y", "y2"], factor_infos, term_codings)
    # duplicate name
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x1", "x1", "y"], factor_infos, term_codings)

    # f_y is in factor_infos, but not mentioned in any term
    term_codings_x_only = OrderedDict(term_codings)
    del term_codings_x_only[t_y]
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3"], factor_infos, term_codings_x_only)

    # f_a is in a term, but not in factor_infos
    f_a = _MockFactor("a")
    t_a = Term([f_a])
    term_codings_with_a = OrderedDict(term_codings)
    term_codings_with_a[t_a] = [SubtermInfo([f_a], {}, 1)]
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y", "a"],
                  factor_infos, term_codings_with_a)

    # bad factor_infos
    not_factor_infos = dict(factor_infos)
    not_factor_infos[f_x] = "what is this I don't even"
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], not_factor_infos, term_codings)

    mismatch_factor_infos = dict(factor_infos)
    mismatch_factor_infos[f_x] = FactorInfo(f_a, "numerical", {}, num_columns=3)
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], mismatch_factor_infos, term_codings)

    # bad term_codings
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos, dict(term_codings))

    not_term_codings = OrderedDict(term_codings)
    not_term_codings["this is a string"] = term_codings[t_x]
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos, not_term_codings)

    non_list_term_codings = OrderedDict(term_codings)
    non_list_term_codings[t_y] = tuple(term_codings[t_y])
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos, non_list_term_codings)

    non_subterm_term_codings = OrderedDict(term_codings)
    non_subterm_term_codings[t_y][0] = "not a SubtermInfo"
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos, non_subterm_term_codings)

    bad_subterm = OrderedDict(term_codings)
    # f_x is a factor in this model, but it is not a factor in t_y
    term_codings[t_y][0] = SubtermInfo([f_x], {}, 1)
    assert_raises(ValueError, DesignInfo,
                  ["x1", "x2", "x3", "y"], factor_infos, bad_subterm)

    # contrast matrix has wrong number of rows
    factor_codings_a = {f_a:
                          FactorInfo(f_a, "categorical", {},
                                     categories=["a1", "a2"])}
    term_codings_a_bad_rows = OrderedDict([
        (t_a,
         [SubtermInfo([f_a],
                      {f_a: ContrastMatrix(np.ones((3, 2)),
                                           ["[1]", "[2]"])},
                      2)])])
    assert_raises(ValueError, DesignInfo,
                  ["a[1]", "a[2]"],
                  factor_codings_a,
                  term_codings_a_bad_rows)

    # have a contrast matrix for a non-categorical factor
    t_ax = Term([f_a, f_x])
    factor_codings_ax = {f_a:
                           FactorInfo(f_a, "categorical", {},
                                      categories=["a1", "a2"]),
                         f_x:
                           FactorInfo(f_x, "numerical", {},
                                      num_columns=2)}
    term_codings_ax_extra_cm = OrderedDict([
        (t_ax,
         [SubtermInfo([f_a, f_x],
                      {f_a: ContrastMatrix(np.ones((2, 2)), ["[1]", "[2]"]),
                       f_x: ContrastMatrix(np.ones((2, 2)), ["[1]", "[2]"])},
                      4)])])
    assert_raises(ValueError, DesignInfo,
                  ["a[1]:x[1]", "a[2]:x[1]", "a[1]:x[2]", "a[2]:x[2]"],
                  factor_codings_ax,
                  term_codings_ax_extra_cm)

    # no contrast matrix for a categorical factor
    term_codings_ax_missing_cm = OrderedDict([
        (t_ax,
         [SubtermInfo([f_a, f_x],
                      {},
                      4)])])
    # This actually fails before it hits the relevant check with a KeyError,
    # but that's okay... the previous test still exercises the check.
    assert_raises((ValueError, KeyError), DesignInfo,
                  ["a[1]:x[1]", "a[2]:x[1]", "a[1]:x[2]", "a[2]:x[2]"],
                  factor_codings_ax,
                  term_codings_ax_missing_cm)

    # subterm num_columns doesn't match the value computed from the individual
    # factors
    term_codings_ax_wrong_subterm_columns = OrderedDict([
        (t_ax,
         [SubtermInfo([f_a, f_x],
                      {f_a: ContrastMatrix(np.ones((2, 3)),
                                           ["[1]", "[2]", "[3]"])},
                      # should be 2 * 3 = 6
                      5)])])
    assert_raises(ValueError, DesignInfo,
                  ["a[1]:x[1]", "a[2]:x[1]", "a[3]:x[1]",
                   "a[1]:x[2]", "a[2]:x[2]", "a[3]:x[2]"],
                  factor_codings_ax,
                  term_codings_ax_wrong_subterm_columns)
Ejemplo n.º 10
0
    def subset(self, which_terms):
        """Create a new :class:`DesignInfo` for design matrices that contain a
        subset of the terms that the current :class:`DesignInfo` does.

        For example, if ``design_info`` has terms ``x``, ``y``, and ``z``,
        then::

          design_info2 = design_info.subset(["x", "z"])

        will return a new DesignInfo that can be used to construct design
        matrices with only the columns corresponding to the terms ``x`` and
        ``z``. After we do this, then in general these two expressions will
        return the same thing (here we assume that ``x``, ``y``, and ``z``
        each generate a single column of the output)::

          build_design_matrix([design_info], data)[0][:, [0, 2]]
          build_design_matrix([design_info2], data)[0]

        However, a critical difference is that in the second case, ``data``
        need not contain any values for ``y``. This is very useful when doing
        prediction using a subset of a model, in which situation R usually
        forces you to specify dummy values for ``y``.

        If using a formula to specify the terms to include, remember that like
        any formula, the intercept term will be included by default, so use
        ``0`` or ``-1`` in your formula if you want to avoid this.

        This method can also be used to reorder the terms in your design
        matrix, in case you want to do that for some reason. I can't think of
        any.

        Note that this method will generally *not* produce the same result as
        creating a new model directly. Consider these DesignInfo objects::

            design1 = dmatrix("1 + C(a)", data)
            design2 = design1.subset("0 + C(a)")
            design3 = dmatrix("0 + C(a)", data)

        Here ``design2`` and ``design3`` will both produce design matrices
        that contain an encoding of ``C(a)`` without any intercept term. But
        ``design3`` uses a full-rank encoding for the categorical term
        ``C(a)``, while ``design2`` uses the same reduced-rank encoding as
        ``design1``.

        :arg which_terms: The terms which should be kept in the new
          :class:`DesignMatrixBuilder`. If this is a string, then it is parsed
          as a formula, and then the names of the resulting terms are taken as
          the terms to keep. If it is a list, then it can contain a mixture of
          term names (as strings) and :class:`Term` objects.

        .. versionadded: 0.2.0
           New method on the class DesignMatrixBuilder.

        .. versionchanged: 0.4.0
           Moved from DesignMatrixBuilder to DesignInfo, as part of the
           removal of DesignMatrixBuilder.

        """
        if isinstance(which_terms, str):
            desc = ModelDesc.from_formula(which_terms)
            if desc.lhs_termlist:
                raise PatsyError("right-hand-side-only formula required")
            which_terms = [term.name() for term in desc.rhs_termlist]

        if self.term_codings is None:
            # This is a minimal DesignInfo
            # If the name is unknown we just let the KeyError escape
            new_names = []
            for t in which_terms:
                new_names += self.column_names[self.term_name_slices[t]]
            return DesignInfo(new_names)
        else:
            term_name_to_term = {}
            for term in self.term_codings:
                term_name_to_term[term.name()] = term

            new_column_names = []
            new_factor_infos = {}
            new_term_codings = OrderedDict()
            for name_or_term in which_terms:
                term = term_name_to_term.get(name_or_term, name_or_term)
                # If the name is unknown we just let the KeyError escape
                s = self.term_slices[term]
                new_column_names += self.column_names[s]
                for f in term.factors:
                    new_factor_infos[f] = self.factor_infos[f]
                new_term_codings[term] = self.term_codings[term]
            return DesignInfo(new_column_names,
                              factor_infos=new_factor_infos,
                              term_codings=new_term_codings)
Ejemplo n.º 11
0
    def __init__(self, column_names,
                 factor_infos=None, term_codings=None):
        self.column_name_indexes = OrderedDict(zip(column_names,
                                                   range(len(column_names))))

        if (factor_infos is None) != (term_codings is None):
            raise ValueError("Must specify either both or neither of "
                             "factor_infos= and term_codings=")

        self.factor_infos = factor_infos
        self.term_codings = term_codings

        # factor_infos is a dict containing one entry for every factor
        #    mentioned in our terms
        #    and mapping each to FactorInfo object
        if self.factor_infos is not None:
            if not isinstance(self.factor_infos, dict):
                raise ValueError("factor_infos should be a dict")

            if not isinstance(self.term_codings, OrderedDict):
                raise ValueError("term_codings must be an OrderedDict")
            for term, subterms in six.iteritems(self.term_codings):
                if not isinstance(term, Term):
                    raise ValueError("expected a Term, not %r" % (term,))
                if not isinstance(subterms, list):
                    raise ValueError("term_codings must contain lists")
                term_factors = set(term.factors)
                for subterm in subterms:
                    if not isinstance(subterm, SubtermInfo):
                        raise ValueError("expected SubtermInfo, "
                                         "not %r" % (subterm,))
                    if not term_factors.issuperset(subterm.factors):
                        raise ValueError("unexpected factors in subterm")

            all_factors = set()
            for term in self.term_codings:
                all_factors.update(term.factors)
            if all_factors != set(self.factor_infos):
                raise ValueError("Provided Term objects and factor_infos "
                                 "do not match")
            for factor, factor_info in six.iteritems(self.factor_infos):
                if not isinstance(factor_info, FactorInfo):
                    raise ValueError("expected FactorInfo object, not %r"
                                     % (factor_info,))
                if factor != factor_info.factor:
                    raise ValueError("mismatched factor_info.factor")

            for term, subterms in six.iteritems(self.term_codings):
                for subterm in subterms:
                    exp_cols = 1
                    cat_factors = set()
                    for factor in subterm.factors:
                        fi = self.factor_infos[factor]
                        if fi.type == "numerical":
                            exp_cols *= fi.num_columns
                        else:
                            assert fi.type == "categorical"
                            cm = subterm.contrast_matrices[factor].matrix
                            if cm.shape[0] != len(fi.categories):
                                raise ValueError("Mismatched contrast matrix "
                                                 "for factor %r" % (factor,))
                            cat_factors.add(factor)
                            exp_cols *= cm.shape[1]
                    if cat_factors != set(subterm.contrast_matrices):
                        raise ValueError("Mismatch between contrast_matrices "
                                         "and categorical factors")
                    if exp_cols != subterm.num_columns:
                        raise ValueError("Unexpected num_columns")

        if term_codings is None:
            # Need to invent term information
            self.term_slices = None
            # We invent one term per column, with the same name as the column
            term_names = column_names
            slices = [slice(i, i + 1) for i in range(len(column_names))]
            self.term_name_slices = OrderedDict(zip(term_names, slices))
        else:
            # Need to derive term information from term_codings
            self.term_slices = OrderedDict()
            idx = 0
            for term, subterm_infos in six.iteritems(self.term_codings):
                term_columns = 0
                for subterm_info in subterm_infos:
                    term_columns += subterm_info.num_columns
                self.term_slices[term] = slice(idx, idx + term_columns)
                idx += term_columns
            if idx != len(self.column_names):
                raise ValueError("mismatch between column_names and columns "
                                 "coded by given terms")
            self.term_name_slices = OrderedDict(
                [(term.name(), slice_)
                 for (term, slice_) in six.iteritems(self.term_slices)])

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values())
                    == list(self.term_name_slices.values()))
        # These checks probably aren't necessary anymore now that we always
        # generate the slices ourselves, but we'll leave them in just to be
        # safe.
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            assert start == covered
            assert step == 1
            covered = stop
        assert covered == len(column_names)
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")
Ejemplo n.º 12
0
class DesignInfo(object):
    """A DesignInfo object holds metadata about a design matrix.

    This is the main object that Patsy uses to pass metadata about a design
    matrix to statistical libraries, in order to allow further downstream
    processing like intelligent tests, prediction on new data, etc. Usually
    encountered as the `.design_info` attribute on design matrices.

    """

    def __init__(self, column_names,
                 factor_infos=None, term_codings=None):
        self.column_name_indexes = OrderedDict(zip(column_names,
                                                   range(len(column_names))))

        if (factor_infos is None) != (term_codings is None):
            raise ValueError("Must specify either both or neither of "
                             "factor_infos= and term_codings=")

        self.factor_infos = factor_infos
        self.term_codings = term_codings

        # factor_infos is a dict containing one entry for every factor
        #    mentioned in our terms
        #    and mapping each to FactorInfo object
        if self.factor_infos is not None:
            if not isinstance(self.factor_infos, dict):
                raise ValueError("factor_infos should be a dict")

            if not isinstance(self.term_codings, OrderedDict):
                raise ValueError("term_codings must be an OrderedDict")
            for term, subterms in six.iteritems(self.term_codings):
                if not isinstance(term, Term):
                    raise ValueError("expected a Term, not %r" % (term,))
                if not isinstance(subterms, list):
                    raise ValueError("term_codings must contain lists")
                term_factors = set(term.factors)
                for subterm in subterms:
                    if not isinstance(subterm, SubtermInfo):
                        raise ValueError("expected SubtermInfo, "
                                         "not %r" % (subterm,))
                    if not term_factors.issuperset(subterm.factors):
                        raise ValueError("unexpected factors in subterm")

            all_factors = set()
            for term in self.term_codings:
                all_factors.update(term.factors)
            if all_factors != set(self.factor_infos):
                raise ValueError("Provided Term objects and factor_infos "
                                 "do not match")
            for factor, factor_info in six.iteritems(self.factor_infos):
                if not isinstance(factor_info, FactorInfo):
                    raise ValueError("expected FactorInfo object, not %r"
                                     % (factor_info,))
                if factor != factor_info.factor:
                    raise ValueError("mismatched factor_info.factor")

            for term, subterms in six.iteritems(self.term_codings):
                for subterm in subterms:
                    exp_cols = 1
                    cat_factors = set()
                    for factor in subterm.factors:
                        fi = self.factor_infos[factor]
                        if fi.type == "numerical":
                            exp_cols *= fi.num_columns
                        else:
                            assert fi.type == "categorical"
                            cm = subterm.contrast_matrices[factor].matrix
                            if cm.shape[0] != len(fi.categories):
                                raise ValueError("Mismatched contrast matrix "
                                                 "for factor %r" % (factor,))
                            cat_factors.add(factor)
                            exp_cols *= cm.shape[1]
                    if cat_factors != set(subterm.contrast_matrices):
                        raise ValueError("Mismatch between contrast_matrices "
                                         "and categorical factors")
                    if exp_cols != subterm.num_columns:
                        raise ValueError("Unexpected num_columns")

        if term_codings is None:
            # Need to invent term information
            self.term_slices = None
            # We invent one term per column, with the same name as the column
            term_names = column_names
            slices = [slice(i, i + 1) for i in range(len(column_names))]
            self.term_name_slices = OrderedDict(zip(term_names, slices))
        else:
            # Need to derive term information from term_codings
            self.term_slices = OrderedDict()
            idx = 0
            for term, subterm_infos in six.iteritems(self.term_codings):
                term_columns = 0
                for subterm_info in subterm_infos:
                    term_columns += subterm_info.num_columns
                self.term_slices[term] = slice(idx, idx + term_columns)
                idx += term_columns
            if idx != len(self.column_names):
                raise ValueError("mismatch between column_names and columns "
                                 "coded by given terms")
            self.term_name_slices = OrderedDict(
                [(term.name(), slice_)
                 for (term, slice_) in six.iteritems(self.term_slices)])

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert (list(self.term_slices.values())
                    == list(self.term_name_slices.values()))
        # These checks probably aren't necessary anymore now that we always
        # generate the slices ourselves, but we'll leave them in just to be
        # safe.
        covered = 0
        for slice_ in six.itervalues(self.term_name_slices):
            start, stop, step = slice_.indices(len(column_names))
            assert start == covered
            assert step == 1
            covered = stop
        assert covered == len(column_names)
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        for column_name, index in six.iteritems(self.column_name_indexes):
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError("term/column name collision")

    __repr__ = repr_pretty_delegate
    def _repr_pretty_(self, p, cycle):
        assert not cycle
        repr_pretty_impl(p, self,
                         [self.column_names],
                         [("factor_infos", self.factor_infos),
                          ("term_codings", self.term_codings)])

    @property
    def column_names(self):
        "A list of the column names, in order."
        return list(self.column_name_indexes)

    @property
    def terms(self):
        "A list of :class:`Terms`, in order, or else None."
        if self.term_slices is None:
            return None
        return list(self.term_slices)

    @property
    def term_names(self):
        "A list of terms, in order."
        return list(self.term_name_slices)

    @property
    def builder(self):
        ".. deprecated:: 0.4.0"
        warnings.warn(DeprecationWarning(
            "The DesignInfo.builder attribute is deprecated starting in "
            "patsy v0.4.0; distinct builder objects have been eliminated "
            "and design_info.builder is now just a long-winded way of "
            "writing 'design_info' (i.e. the .builder attribute just "
            "returns self)"), stacklevel=2)
        return self

    @property
    def design_info(self):
        ".. deprecated:: 0.4.0"
        warnings.warn(DeprecationWarning(
            "Starting in patsy v0.4.0, the DesignMatrixBuilder class has "
            "been merged into the DesignInfo class. So there's no need to "
            "use builder.design_info to access the DesignInfo; 'builder' "
            "already *is* a DesignInfo."), stacklevel=2)
        return self

    def slice(self, columns_specifier):
        """Locate a subset of design matrix columns, specified symbolically.

        A patsy design matrix has two levels of structure: the individual
        columns (which are named), and the :ref:`terms <formulas>` in
        the formula that generated those columns. This is a one-to-many
        relationship: a single term may span several columns. This method
        provides a user-friendly API for locating those columns.

        (While we talk about columns here, this is probably most useful for
        indexing into other arrays that are derived from the design matrix,
        such as regression coefficients or covariance matrices.)

        The `columns_specifier` argument can take a number of forms:

        * A term name
        * A column name
        * A :class:`Term` object
        * An integer giving a raw index
        * A raw slice object

        In all cases, a Python :func:`slice` object is returned, which can be
        used directly for indexing.

        Example::

          y, X = dmatrices("y ~ a", demo_data("y", "a", nlevels=3))
          betas = np.linalg.lstsq(X, y)[0]
          a_betas = betas[X.design_info.slice("a")]

        (If you want to look up a single individual column by name, use
        ``design_info.column_name_indexes[name]``.)
        """
        if isinstance(columns_specifier, slice):
            return columns_specifier
        if np.issubsctype(type(columns_specifier), np.integer):
            return slice(columns_specifier, columns_specifier + 1)
        if (self.term_slices is not None
            and columns_specifier in self.term_slices):
            return self.term_slices[columns_specifier]
        if columns_specifier in self.term_name_slices:
            return self.term_name_slices[columns_specifier]
        if columns_specifier in self.column_name_indexes:
            idx = self.column_name_indexes[columns_specifier]
            return slice(idx, idx + 1)
        raise PatsyError("unknown column specified '%s'"
                            % (columns_specifier,))

    def linear_constraint(self, constraint_likes):
        """Construct a linear constraint in matrix form from a (possibly
        symbolic) description.

        Possible inputs:

        * A dictionary which is taken as a set of equality constraint. Keys
          can be either string column names, or integer column indexes.
        * A string giving a arithmetic expression referring to the matrix
          columns by name.
        * A list of such strings which are ANDed together.
        * A tuple (A, b) where A and b are array_likes, and the constraint is
          Ax = b. If necessary, these will be coerced to the proper
          dimensionality by appending dimensions with size 1.

        The string-based language has the standard arithmetic operators, / * +
        - and parentheses, plus "=" is used for equality and "," is used to
        AND together multiple constraint equations within a string. You can
        If no = appears in some expression, then that expression is assumed to
        be equal to zero. Division is always float-based, even if
        ``__future__.true_division`` isn't in effect.

        Returns a :class:`LinearConstraint` object.

        Examples::

          di = DesignInfo(["x1", "x2", "x3"])

          # Equivalent ways to write x1 == 0:
          di.linear_constraint({"x1": 0})  # by name
          di.linear_constraint({0: 0})  # by index
          di.linear_constraint("x1 = 0")  # string based
          di.linear_constraint("x1")  # can leave out "= 0"
          di.linear_constraint("2 * x1 = (x1 + 2 * x1) / 3")
          di.linear_constraint(([1, 0, 0], 0))  # constraint matrices

          # Equivalent ways to write x1 == 0 and x3 == 10
          di.linear_constraint({"x1": 0, "x3": 10})
          di.linear_constraint({0: 0, 2: 10})
          di.linear_constraint({0: 0, "x3": 10})
          di.linear_constraint("x1 = 0, x3 = 10")
          di.linear_constraint("x1, x3 = 10")
          di.linear_constraint(["x1", "x3 = 0"])  # list of strings
          di.linear_constraint("x1 = 0, x3 - 10 = x1")
          di.linear_constraint([[1, 0, 0], [0, 0, 1]], [0, 10])

          # You can also chain together equalities, just like Python:
          di.linear_constraint("x1 = x2 = 3")
        """
        return linear_constraint(constraint_likes, self.column_names)

    def describe(self):
        """Returns a human-readable string describing this design info.

        Example:

        .. ipython::

          In [1]: y, X = dmatrices("y ~ x1 + x2", demo_data("y", "x1", "x2"))

          In [2]: y.design_info.describe()
          Out[2]: 'y'

          In [3]: X.design_info.describe()
          Out[3]: '1 + x1 + x2'

        .. warning::

           There is no guarantee that the strings returned by this function
           can be parsed as formulas, or that if they can be parsed as a
           formula that they will produce a model equivalent to the one you
           started with. This function produces a best-effort description
           intended for humans to read.

        """

        names = []
        for name in self.term_names:
            if name == "Intercept":
                names.append("1")
            else:
                names.append(name)
        return " + ".join(names)

    def subset(self, which_terms):
        """Create a new :class:`DesignInfo` for design matrices that contain a
        subset of the terms that the current :class:`DesignInfo` does.

        For example, if ``design_info`` has terms ``x``, ``y``, and ``z``,
        then::

          design_info2 = design_info.subset(["x", "z"])

        will return a new DesignInfo that can be used to construct design
        matrices with only the columns corresponding to the terms ``x`` and
        ``z``. After we do this, then in general these two expressions will
        return the same thing (here we assume that ``x``, ``y``, and ``z``
        each generate a single column of the output)::

          build_design_matrix([design_info], data)[0][:, [0, 2]]
          build_design_matrix([design_info2], data)[0]

        However, a critical difference is that in the second case, ``data``
        need not contain any values for ``y``. This is very useful when doing
        prediction using a subset of a model, in which situation R usually
        forces you to specify dummy values for ``y``.

        If using a formula to specify the terms to include, remember that like
        any formula, the intercept term will be included by default, so use
        ``0`` or ``-1`` in your formula if you want to avoid this.

        This method can also be used to reorder the terms in your design
        matrix, in case you want to do that for some reason. I can't think of
        any.

        Note that this method will generally *not* produce the same result as
        creating a new model directly. Consider these DesignInfo objects::

            design1 = dmatrix("1 + C(a)", data)
            design2 = design1.subset("0 + C(a)")
            design3 = dmatrix("0 + C(a)", data)

        Here ``design2`` and ``design3`` will both produce design matrices
        that contain an encoding of ``C(a)`` without any intercept term. But
        ``design3`` uses a full-rank encoding for the categorical term
        ``C(a)``, while ``design2`` uses the same reduced-rank encoding as
        ``design1``.

        :arg which_terms: The terms which should be kept in the new
          :class:`DesignMatrixBuilder`. If this is a string, then it is parsed
          as a formula, and then the names of the resulting terms are taken as
          the terms to keep. If it is a list, then it can contain a mixture of
          term names (as strings) and :class:`Term` objects.

        .. versionadded: 0.2.0
           New method on the class DesignMatrixBuilder.

        .. versionchanged: 0.4.0
           Moved from DesignMatrixBuilder to DesignInfo, as part of the
           removal of DesignMatrixBuilder.

        """
        if isinstance(which_terms, str):
            desc = ModelDesc.from_formula(which_terms)
            if desc.lhs_termlist:
                raise PatsyError("right-hand-side-only formula required")
            which_terms = [term.name() for term in desc.rhs_termlist]

        if self.term_codings is None:
            # This is a minimal DesignInfo
            # If the name is unknown we just let the KeyError escape
            new_names = []
            for t in which_terms:
                new_names += self.column_names[self.term_name_slices[t]]
            return DesignInfo(new_names)
        else:
            term_name_to_term = {}
            for term in self.term_codings:
                term_name_to_term[term.name()] = term

            new_column_names = []
            new_factor_infos = {}
            new_term_codings = OrderedDict()
            for name_or_term in which_terms:
                term = term_name_to_term.get(name_or_term, name_or_term)
                # If the name is unknown we just let the KeyError escape
                s = self.term_slices[term]
                new_column_names += self.column_names[s]
                for f in term.factors:
                    new_factor_infos[f] = self.factor_infos[f]
                new_term_codings[term] = self.term_codings[term]
            return DesignInfo(new_column_names,
                              factor_infos=new_factor_infos,
                              term_codings=new_term_codings)

    @classmethod
    def from_array(cls, array_like, default_column_prefix="column"):
        """Find or construct a DesignInfo appropriate for a given array_like.

        If the input `array_like` already has a ``.design_info``
        attribute, then it will be returned. Otherwise, a new DesignInfo
        object will be constructed, using names either taken from the
        `array_like` (e.g., for a pandas DataFrame with named columns), or
        constructed using `default_column_prefix`.

        This is how :func:`dmatrix` (for example) creates a DesignInfo object
        if an arbitrary matrix is passed in.

        :arg array_like: An ndarray or pandas container.
        :arg default_column_prefix: If it's necessary to invent column names,
          then this will be used to construct them.
        :returns: a DesignInfo object
        """
        if hasattr(array_like, "design_info") and isinstance(array_like.design_info, cls):
            return array_like.design_info
        arr = atleast_2d_column_default(array_like, preserve_pandas=True)
        if arr.ndim > 2:
            raise ValueError("design matrix can't have >2 dimensions")
        columns = getattr(arr, "columns", range(arr.shape[1]))
        if (hasattr(columns, "dtype")
            and not safe_issubdtype(columns.dtype, np.integer)):
            column_names = [str(obj) for obj in columns]
        else:
            column_names = ["%s%s" % (default_column_prefix, i)
                            for i in columns]
        return DesignInfo(column_names)

    __getstate__ = no_pickling
Ejemplo n.º 13
0
class DesignInfo(object):
    """A DesignInfo object holds metadata about a design matrix.

    This is the main object that Patsy uses to pass information to
    statistical libraries. Usually encountered as the `.design_info` attribute
    on design matrices.
    """
    def __init__(self, column_names,
                 term_slices=None, term_name_slices=None,
                 builder=None):
        self.column_name_indexes = OrderedDict(zip(column_names,
                                                   range(len(column_names))))
        if term_slices is not None:
            #: An OrderedDict mapping :class:`Term` objects to Python
            #: func:`slice` objects. May be None, for design matrices which
            #: were constructed directly rather than by using the patsy
            #: machinery. If it is not None, then it
            #: is guaranteed to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps.
            self.term_slices = OrderedDict(term_slices)
            if term_name_slices is not None:
                raise ValueError("specify only one of term_slices and "
                                 "term_name_slices")
            term_names = [term.name() for term in self.term_slices]
            #: And OrderedDict mapping term names (as strings) to Python
            #: :func:`slice` objects. Guaranteed never to be None. Guaranteed
            #: to list the terms in order, and the slices are
            #: guaranteed to exactly cover all columns with no overlap or
            #: gaps. Name overlap is allowed between term names and column
            #: names, but it is guaranteed that if it occurs, then they refer
            #: to exactly the same column.
            self.term_name_slices = OrderedDict(zip(term_names,
                                                    self.term_slices.values()))
        else: # term_slices is None
            self.term_slices = None
            if term_name_slices is None:
                # Make up one term per column
                term_names = column_names
                slices = [slice(i, i + 1) for i in xrange(len(column_names))]
                term_name_slices = zip(term_names, slices)
            self.term_name_slices = OrderedDict(term_name_slices)

        self.builder = builder

        # Guarantees:
        #   term_name_slices is never None
        #   The slices in term_name_slices are in order and exactly cover the
        #     whole range of columns.
        #   term_slices may be None
        #   If term_slices is not None, then its slices match the ones in
        #     term_name_slices.
        #   If there is any name overlap between terms and columns, they refer
        #     to the same columns.
        assert self.term_name_slices is not None
        if self.term_slices is not None:
            assert self.term_slices.values() == self.term_name_slices.values()
        covered = 0
        for slice_ in self.term_name_slices.itervalues():
            start, stop, step = slice_.indices(len(column_names))
            if start != covered:
                raise ValueError, "bad term slices"
            if step != 1:
                raise ValueError, "bad term slices"
            covered = stop
        if covered != len(column_names):
            raise ValueError, "bad term indices"
        for column_name, index in self.column_name_indexes.iteritems():
            if column_name in self.term_name_slices:
                slice_ = self.term_name_slices[column_name]
                if slice_ != slice(index, index + 1):
                    raise ValueError, "term/column name collision"

    __repr__ = repr_pretty_delegate
    def _repr_pretty_(self, p, cycle):
        assert not cycle
        if self.term_slices is None:
            kwargs = [("term_name_slices", self.term_name_slices)]
        else:
            kwargs = [("term_slices", self.term_slices)]
        if self.builder is not None:
            kwargs.append(("builder", self.builder))
        repr_pretty_impl(p, self, [self.column_names], kwargs)

    @property
    def column_names(self):
        "A list of the column names, in order."
        return self.column_name_indexes.keys()

    @property
    def terms(self):
        "A list of :class:`Terms`, in order, or else None."
        if self.term_slices is None:
            return None
        return self.term_slices.keys()

    @property
    def term_names(self):
        "A list of terms, in order."
        return self.term_name_slices.keys()

    def slice(self, columns_specifier):
        """Locate a subset of design matrix columns, specified symbolically.

        A patsy design matrix has two levels of structure: the individual
        columns (which are named), and the :ref:`terms <formulas>` in
        the formula that generated those columns. This is a one-to-many
        relationship: a single term may span several columns. This method
        provides a user-friendly API for locating those columns.

        (While we talk about columns here, this is probably most useful for
        indexing into other arrays that are derived from the design matrix,
        such as regression coefficients or covariance matrices.)

        The `columns_specifier` argument can take a number of forms:

        * A term name
        * A column name
        * A :class:`Term` object
        * An integer giving a raw index
        * A raw slice object

        In all cases, a Python :func:`slice` object is returned, which can be
        used directly for indexing.

        Example::

          y, X = dmatrices("y ~ a", demo_data("y", "a", nlevels=3))
          betas = np.linalg.lstsq(X, y)[0]
          a_betas = betas[X.design_info.slice("a")]

        (If you want to look up a single individual column by name, use
        ``design_info.column_name_indexes[name]``.)
        """
        if isinstance(columns_specifier, slice):
            return columns_specifier
        if np.issubsctype(type(columns_specifier), np.integer):
            return slice(columns_specifier, columns_specifier + 1)
        if (self.term_slices is not None
            and columns_specifier in self.term_slices):
            return self.term_slices[columns_specifier]
        if columns_specifier in self.term_name_slices:
            return self.term_name_slices[columns_specifier]
        if columns_specifier in self.column_name_indexes:
            idx = self.column_name_indexes[columns_specifier]
            return slice(idx, idx + 1)
        raise PatsyError("unknown column specified '%s'"
                            % (columns_specifier,))

    def linear_constraint(self, constraint_likes):
        """Construct a linear constraint in matrix form from a (possibly
        symbolic) description.

        Possible inputs:

        * A dictionary which is taken as a set of equality constraint. Keys
          can be either string column names, or integer column indexes.
        * A string giving a arithmetic expression referring to the matrix
          columns by name.
        * A list of such strings which are ANDed together.
        * A tuple (A, b) where A and b are array_likes, and the constraint is
          Ax = b. If necessary, these will be coerced to the proper
          dimensionality by appending dimensions with size 1.

        The string-based language has the standard arithmetic operators, / * +
        - and parentheses, plus "=" is used for equality and "," is used to
        AND together multiple constraint equations within a string. You can
        If no = appears in some expression, then that expression is assumed to
        be equal to zero. Division is always float-based, even if
        ``__future__.true_division`` isn't in effect.

        Returns a :class:`LinearConstraint` object.

        Examples::

          di = DesignInfo(["x1", "x2", "x3"])

          # Equivalent ways to write x1 == 0:
          di.linear_constraint({"x1": 0})  # by name
          di.linear_constraint({0: 0})  # by index
          di.linear_constraint("x1 = 0")  # string based
          di.linear_constraint("x1")  # can leave out "= 0"
          di.linear_constraint("2 * x1 = (x1 + 2 * x1) / 3")
          di.linear_constraint(([1, 0, 0], 0))  # constraint matrices

          # Equivalent ways to write x1 == 0 and x3 == 10
          di.linear_constraint({"x1": 0, "x3": 10})
          di.linear_constraint({0: 0, 2: 10})
          di.linear_constraint({0: 0, "x3": 10})
          di.linear_constraint("x1 = 0, x3 = 10")
          di.linear_constraint("x1, x3 = 10")
          di.linear_constraint(["x1", "x3 = 0"])  # list of strings
          di.linear_constraint("x1 = 0, x3 - 10 = x1")
          di.linear_constraint([[1, 0, 0], [0, 0, 1]], [0, 10])

          # You can also chain together equalities, just like Python:
          di.linear_constraint("x1 = x2 = 3")
        """
        return linear_constraint(constraint_likes, self.column_names)

    def describe(self):
        """Returns a human-readable string describing this design info.

        Example:

        .. ipython::

          In [1]: y, X = dmatrices("y ~ x1 + x2", demo_data("y", "x1", "x2"))

          In [2]: y.design_info.describe()
          Out[2]: 'y'

          In [3]: X.design_info.describe()
          Out[3]: '1 + x1 + x2'

        .. warning::
           There is no guarantee that the strings returned by this
           function can be parsed as formulas. They are best-effort descriptions
           intended for human users.
        """

        names = []
        for name in self.term_names:
            if name == "Intercept":
                names.append("1")
            else:
                names.append(name)
        return " + ".join(names)

    @classmethod
    def from_array(cls, array_like, default_column_prefix="column"):
        """Find or construct a DesignInfo appropriate for a given array_like.

        If the input `array_like` already has a ``.design_info``
        attribute, then it will be returned. Otherwise, a new DesignInfo
        object will be constructed, using names either taken from the
        `array_like` (e.g., for a pandas DataFrame with named columns), or
        constructed using `default_column_prefix`.

        This is how :func:`dmatrix` (for example) creates a DesignInfo object
        if an arbitrary matrix is passed in.

        :arg array_like: An ndarray or pandas container.
        :arg default_column_prefix: If it's necessary to invent column names,
          then this will be used to construct them.
        :returns: a DesignInfo object
        """
        if hasattr(array_like, "design_info") and isinstance(array_like.design_info, cls):
            return array_like.design_info
        arr = atleast_2d_column_default(array_like, preserve_pandas=True)
        if arr.ndim > 2:
            raise ValueError, "design matrix can't have >2 dimensions"
        columns = getattr(arr, "columns", xrange(arr.shape[1]))
        if (isinstance(columns, np.ndarray)
            and not np.issubdtype(columns.dtype, np.integer)):
            column_names = [str(obj) for obj in columns]
        else:
            column_names = ["%s%s" % (default_column_prefix, i)
                            for i in columns]
        return DesignInfo(column_names)