diff --git a/doc/changes.rst b/doc/changes.rst index 878968f4..82de7aa5 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -8,6 +8,12 @@ All Patsy releases are archived at Zenodo: .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592075.svg :target: https://doi.org/10.5281/zenodo.592075 +v1.0.3 +------ + +* Fixed intercept handling so that adding ``+ -1`` (or ``+ 0``) to a formula + removes the intercept, consistently with ``- 1`` (gh-158). + v1.0.2 ------ diff --git a/patsy/desc.py b/patsy/desc.py index 210350df..d4cd0101 100644 --- a/patsy/desc.py +++ b/patsy/desc.py @@ -266,6 +266,19 @@ def _eval_binary_plus(evaluator, tree): False, left_expr.terms + right_expr.terms, ) + elif right_expr.intercept_removed and _is_bare_intercept(tree.args[1]): + # e.g. "a + b + -1": a bare intercept literal on the right (unary + # minus applied directly to 1, not wrapped in parentheses) removes + # the intercept, consistently with "- 1". This mirrors the direct + # "+ 0" (ZERO) case handled above. We deliberately do *not* + # propagate intercept removal out of a compound expression such as + # "1 + (a + 0)", which keeps the intercept by design. + return IntermediateExpr( + False, + None, + True, + left_expr.terms + right_expr.terms, + ) else: return IntermediateExpr( left_expr.intercept, @@ -275,6 +288,15 @@ def _eval_binary_plus(evaluator, tree): ) +# True if `node` is a *bare* intercept literal directly on the right of a "+", +# i.e. a chain of unary +/- ending in a numeric literal (as in "-1"), rather +# than a compound/parenthesized expression (as in "(a + 0)"). +def _is_bare_intercept(node): + while node.type in ("+", "-") and len(node.args) == 1: + node = node.args[0] + return node.type in ("ONE", "ZERO", "NUMBER") + + def _eval_binary_minus(evaluator, tree): left_expr = evaluator.eval(tree.args[0]) if tree.args[1].type == "ZERO": @@ -478,6 +500,12 @@ def eval(self, tree, require_evalexpr=True): "a - 1": (False, ["a"]), "a - 0": (True, ["a"]), "1 - a": (True, []), + # gh-158: "+ -1" (or "+ 0") must remove the intercept just like "- 1". + "a + -1": (False, ["a"]), + "a + b + -1": (False, ["a", "b"]), + "a + b + 0": (False, ["a", "b"]), + "a + -1 + b": (False, ["a", "b"]), + "a + -1 + 1": (True, ["a"]), "a + b": (True, ["a", "b"]), "(a + b)": (True, ["a", "b"]), "a + ((((b))))": (True, ["a", "b"]),