Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
28 changes: 28 additions & 0 deletions patsy/desc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this is a "won't fix" type of bug given where the project is. I think a warning would be OK, though. To fix this, one would need to go through a reasonably long deprecation cycle since this is a change to existing code, even if the original implementation is not great. Something that explains to the user what happens here and then tells the user how they can fix it (by slightly altering their formula) is better than a silent behaviour change.

statsmodels will be out with formulaic support next month, so this is moving to lower importance.

False,
None,
True,
left_expr.terms + right_expr.terms,
)
else:
return IntermediateExpr(
left_expr.intercept,
Expand All @@ -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":
Expand Down Expand Up @@ -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"]),
Expand Down