feat: add Selector.__xor__ and Expr.__xor__ for symmetric difference#3621
feat: add Selector.__xor__ and Expr.__xor__ for symmetric difference#3621SAY-5 wants to merge 9 commits into
Selector.__xor__ and Expr.__xor__ for symmetric difference#3621Conversation
|
@dangotbanned i'm confused, why is this deemed "AI SPAM"? |
Look at their profile, they've opened like 800+ PRs across 600+ repos this month |
|
This is a small and targeted PR which addresses an open issue, has tests, and a description of the changes I wouldn't class this as spam tbh @SAY-5 thanks for your PR, there's a couple of CI failures: uncovered lines typing could you address these please? the shiny downstream failure is unrelated |
|
Thanks for the review. Removed the two unused |
| msg = ( | ||
| f"unsupported operand type(s) for op: ('Selector' ^ '{type(other).__name__}')" | ||
| ) | ||
| raise TypeError(msg) |
There was a problem hiding this comment.
in __or__ we have
return self._to_expr()._append_node(
ExprNode(ExprKind.ELEMENTWISE, "__or__", exprs=(other,), str_as_lit=True)
)
why are we raising an error here?
There was a problem hiding this comment.
Good question. __and__/__or__ fall through to _to_expr() because narwhals Expr defines __and__ and __or__, so selector & expr is a valid elementwise boolean op. Expr has no __xor__, so there is no sensible expr-level fallback for ^, hence the TypeError. Happy to drop the branch and let the AttributeError surface instead if you prefer.
There was a problem hiding this comment.
ah thanks - i kinda feel like, if we're adding __xor__ for selectors, we should add it for Expr too.
i'd suggest
- either do it here
- or, open an issue to do that as a follow-up
There was a problem hiding this comment.
@MarcoGorelli circling back on your review thread (3269698893): I went with option A and added Expr.__xor__ plus __rxor__ in this PR rather than as a follow-up. The commit is c8e6cf9 (feat(expr): add __xor__ for symmetric difference); tests for the Expr path live in tests/expr_and_series/operators_test.py alongside the Selector ones. Happy to split into a separate PR if you would rather land the Selector work first.
|
Makes sense. I'll keep this PR scoped to selectors and open a follow-up issue to track adding |
| assert_equal_data(result, {"a": expected}) | ||
|
|
||
|
|
||
| def test_xor_operator_expr(constructor: Constructor) -> None: |
There was a problem hiding this comment.
can we also have a test_xor_operator_expr_nulls test, to check that null values propagate?
|
Added the null test and fixed the pre-1.0 emulation. The previous |
Selector.__xor__ for symmetric differenceSelector.__xor__ and Expr.__xor__ for symmetric difference
| assert_equal_data(result, {"a": expected}) | ||
|
|
||
|
|
||
| def test_xor_operator_expr(constructor: Constructor) -> None: |
There was a problem hiding this comment.
Added in commit c8e6cf9. The test covers True/True, True/None, False/None, None/None cases and checks that null propagates for pandas-style constructors differently (pandas treats nulls as False in boolean ops).
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
|
Fixed the mypy typing failure: added |
| # The narwhals-level Selector.__xor__ rejects non-selectors before reaching | ||
| # here, so this branch is a defensive guard only. | ||
| msg = f"unsupported operand type(s) for op: ('Selector' ^ '{type(other).__name__}')" # pragma: no cover | ||
| raise TypeError(msg) # pragma: no cover |
There was a problem hiding this comment.
is this still needed if you've implemented it for Expr?
There was a problem hiding this comment.
Not anymore, no. In 2e943a2 it falls back to self._to_expr() ^ other like __and__/__or__, and the narwhals-level Selector.__xor__ does the same instead of raising; added a test_xor_expr case. Polars needed a small tweak since it coerces the right operand to a selector and takes the set difference.
…tors Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
| # Polars coerces the right operand to a selector and takes the | ||
| # set difference; degrade to expressions for an elementwise xor. | ||
| return self._with_native(self.native.as_expr().__xor__(other_native)) |
There was a problem hiding this comment.
i'm confused by this, sorry - do you have an example please?
is it a bug which needs fixing in polars upstream?
|
Sure. It's not a polars bug, it's the selector operator overloading. When the left side is a selector, polars makes import polars as pl
import polars.selectors as cs
df = pl.DataFrame({"a": [True, False], "b": [True, True]})
# selector on the left: ^ becomes a set op over column *names*
df.select(cs.by_name("a", "b") ^ pl.col("b")).columns
# -> ['a'] ({a, b} xor {b})
# plain expr on the left: normal elementwise xor
df.select(cs.by_name("a", "b").as_expr() ^ pl.col("b"))
# -> shape (2, 1) column 'a' = [False, True]So when our left operand is backed by a selector but the user wrote an elementwise |
Description
Adds
Selector.__xor__for symmetric difference between selectors, rounding out the set operator set (&,|,-,~,^) as discussed in #2589.selA ^ selBreturns the columns in either selector but not both. Implementation mirrors the existing__or__/__sub__pattern; for Polars, falls back to(a | b) - (a & b)on versions before 1.0.0 where nativeSelector.__xor__was added.What type of PR is this? (check all applicable)
Related issues
Selector.__xor__(symmetric difference) #2589Checklist