Esempio n. 1
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)
Esempio n. 2
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)