Skip to content

Commit b11463b

Browse files
committed
docs: add examples of when haystack includes NULL in .isin() and .notin()
1 parent 15ff868 commit b11463b

File tree

1 file changed

+136
-114
lines changed

1 file changed

+136
-114
lines changed

ibis/expr/types/generic.py

Lines changed: 136 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -585,19 +585,23 @@ def between(
585585
return ops.Between(self, lower, upper).to_expr()
586586

587587
def isin(self, values: Value | Sequence[Value], /) -> ir.BooleanValue:
588-
"""Check whether this expression's values are in `values`.
588+
"""Check whether this expression is in `values`.
589589
590-
`NULL` values are propagated in the output. See examples for details.
590+
`NULL` values in the input are propagated in the output.
591+
If the `values` argument contains any `NULL` values,
592+
then ibis follows the SQL behavior of returning `NULL` (not False)
593+
when `self` is not present.
594+
See examples below for details.
591595
592596
Parameters
593597
----------
594598
values
595-
Values or expression to check for membership
599+
Values or expression to check for membership.
596600
597601
Returns
598602
-------
599603
BooleanValue
600-
Expression indicating membership
604+
True if `self` is contained in `values`, False otherwise.
601605
602606
See Also
603607
--------
@@ -607,91 +611,67 @@ def isin(self, values: Value | Sequence[Value], /) -> ir.BooleanValue:
607611
--------
608612
>>> import ibis
609613
>>> ibis.options.interactive = True
610-
>>> t = ibis.memtable({"a": [1, 2, 3], "b": [2, 3, 4]})
611-
>>> t
612-
┏━━━━━━━┳━━━━━━━┓
613-
┃ a ┃ b ┃
614-
┡━━━━━━━╇━━━━━━━┩
615-
│ int64 │ int64 │
616-
├───────┼───────┤
617-
│ 1 │ 2 │
618-
│ 2 │ 3 │
619-
│ 3 │ 4 │
620-
└───────┴───────┘
621-
622-
Check against a literal sequence of values
623-
624-
>>> t.a.isin([1, 2])
625-
┏━━━━━━━━━━━━━━━━━━━━━┓
626-
┃ InValues(a, (1, 2)) ┃
627-
┡━━━━━━━━━━━━━━━━━━━━━┩
628-
│ boolean │
629-
├─────────────────────┤
630-
│ True │
631-
│ True │
632-
│ False │
633-
└─────────────────────┘
634-
635-
Check against a derived expression
636-
637-
>>> t.a.isin(t.b + 1)
638-
┏━━━━━━━━━━━━━━━┓
639-
┃ InSubquery(a) ┃
640-
┡━━━━━━━━━━━━━━━┩
641-
│ boolean │
642-
├───────────────┤
643-
│ False │
644-
│ False │
645-
│ True │
646-
└───────────────┘
614+
>>> t = ibis.memtable(
615+
... {
616+
... "a": [1, 2, 3, None],
617+
... "b": [1, 2, 9, None],
618+
... },
619+
... schema={"a": int, "b": int},
620+
... )
647621
648-
Check against a column from a different table
622+
Checking for values in literals:
649623
650-
>>> t2 = ibis.memtable({"x": [99, 2, 99]})
651-
>>> t.a.isin(t2.x)
652-
┏━━━━━━━━━━━━━━━┓
653-
┃ InSubquery(a) ┃
654-
┡━━━━━━━━━━━━━━━┩
655-
│ boolean │
656-
├───────────────┤
657-
│ False │
658-
│ True │
659-
│ False │
660-
└───────────────┘
661-
662-
`NULL` behavior
624+
>>> t.mutate(
625+
... a_in_12=t.a.isin([1, 2]),
626+
... a_in_12None=t.a.isin([1, 2, None]),
627+
... )
628+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━┓
629+
┃ a ┃ b ┃ a_in_12 ┃ a_in_12None ┃
630+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━┩
631+
│ int64 │ int64 │ boolean │ boolean │
632+
├───────┼───────┼─────────┼─────────────┤
633+
│ 1 │ 1 │ True │ True │
634+
│ 2 │ 2 │ True │ True │
635+
│ 3 │ 9 │ False │ NULL │
636+
│ NULL │ NULL │ NULL │ NULL │
637+
└───────┴───────┴─────────┴─────────────┘
638+
639+
Checking for values in columns of the same table:
663640
664-
>>> t = ibis.memtable({"x": [1, 2]})
665-
>>> t.x.isin([1, None])
666-
┏━━━━━━━━━━━━━━━━━━━━━━━━┓
667-
┃ InValues(x, (1, None)) ┃
668-
┡━━━━━━━━━━━━━━━━━━━━━━━━┩
669-
│ boolean │
670-
├────────────────────────┤
671-
│ True │
672-
│ NULL │
673-
└────────────────────────┘
674-
>>> t = ibis.memtable({"x": [1, None, 2]})
675-
>>> t.x.isin([1])
676-
┏━━━━━━━━━━━━━━━━━━━┓
677-
┃ InValues(x, (1,)) ┃
678-
┡━━━━━━━━━━━━━━━━━━━┩
679-
│ boolean │
680-
├───────────────────┤
681-
│ True │
682-
│ NULL │
683-
│ False │
684-
└───────────────────┘
685-
>>> t.x.isin([3])
686-
┏━━━━━━━━━━━━━━━━━━━┓
687-
┃ InValues(x, (3,)) ┃
688-
┡━━━━━━━━━━━━━━━━━━━┩
689-
│ boolean │
690-
├───────────────────┤
691-
│ False │
692-
│ NULL │
693-
│ False │
694-
└───────────────────┘
641+
>>> t.mutate(
642+
... a_in_b=t.a.isin(t.b),
643+
... a_in_b_no_null=t.a.isin(t.b.fill_null(0)),
644+
... a_in_b_plus_1=t.a.isin(t.b + 1),
645+
... )
646+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
647+
┃ a ┃ b ┃ a_in_b ┃ a_in_b_no_null ┃ a_in_b_plus_1 ┃
648+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
649+
│ int64 │ int64 │ boolean │ boolean │ boolean │
650+
├───────┼───────┼─────────┼────────────────┼───────────────┤
651+
│ 1 │ 1 │ True │ True │ NULL │
652+
│ 2 │ 2 │ True │ True │ True │
653+
│ 3 │ 9 │ NULL │ False │ True │
654+
│ NULL │ NULL │ NULL │ NULL │ NULL │
655+
└───────┴───────┴─────────┴────────────────┴───────────────┘
656+
657+
Checking for values in a column from a different table:
658+
659+
>>> t2 = ibis.memtable({"x": [1, 2, 99], "y": [1, 2, None]})
660+
>>> t.mutate(
661+
... a_in_x=t.a.isin(t2.x),
662+
... a_in_y=t.a.isin(t2.y),
663+
... a_in_y_plus_1=t.a.isin(t2.y + 1),
664+
... )
665+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━┓
666+
┃ a ┃ b ┃ a_in_x ┃ a_in_y ┃ a_in_y_plus_1 ┃
667+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━┩
668+
│ int64 │ int64 │ boolean │ boolean │ boolean │
669+
├───────┼───────┼─────────┼─────────┼───────────────┤
670+
│ 1 │ 1 │ True │ True │ NULL │
671+
│ 2 │ 2 │ True │ True │ True │
672+
│ 3 │ 9 │ False │ NULL │ True │
673+
│ NULL │ NULL │ NULL │ NULL │ NULL │
674+
└───────┴───────┴─────────┴─────────┴───────────────┘
695675
"""
696676
from ibis.expr.types import ArrayValue
697677

@@ -703,49 +683,91 @@ def isin(self, values: Value | Sequence[Value], /) -> ir.BooleanValue:
703683
return ops.InValues(self, values).to_expr()
704684

705685
def notin(self, values: Value | Sequence[Value], /) -> ir.BooleanValue:
706-
"""Check whether this expression's values are not in `values`.
686+
"""Check whether this expression is not in `values`.
707687
708688
Opposite of [`Value.isin()`](./expression-generic.qmd#ibis.expr.types.generic.Value.isin).
709689
690+
`NULL` values in the input are propagated in the output.
691+
If the `values` argument contains any `NULL` values,
692+
then ibis follows the SQL behavior of returning `NULL` (not False)
693+
when `self` is present.
694+
See examples below for details.
695+
710696
Parameters
711697
----------
712698
values
713-
Values or expression to check for lack of membership
699+
Values or expression to check for lack of membership.
714700
715701
Returns
716702
-------
717703
BooleanValue
718-
Whether `self`'s values are not contained in `values`
704+
True if self is not in `values`, False otherwise.
719705
720706
Examples
721707
--------
722708
>>> import ibis
723709
>>> ibis.options.interactive = True
724-
>>> t = ibis.examples.penguins.fetch().limit(5)
725-
>>> t.bill_depth_mm
726-
┏━━━━━━━━━━━━━━━┓
727-
┃ bill_depth_mm ┃
728-
┡━━━━━━━━━━━━━━━┩
729-
│ float64 │
730-
├───────────────┤
731-
│ 18.7 │
732-
│ 17.4 │
733-
│ 18.0 │
734-
│ NULL │
735-
│ 19.3 │
736-
└───────────────┘
737-
>>> t.bill_depth_mm.notin([18.7, 18.1])
738-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
739-
┃ Not(InValues(bill_depth_mm, (18.7, 18.1))) ┃
740-
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
741-
│ boolean │
742-
├────────────────────────────────────────────┤
743-
│ False │
744-
│ True │
745-
│ True │
746-
│ NULL │
747-
│ True │
748-
└────────────────────────────────────────────┘
710+
>>> t = ibis.memtable(
711+
... {
712+
... "a": [1, 2, 3, None],
713+
... "b": [1, 2, 9, None],
714+
... },
715+
... schema={"a": int, "b": int},
716+
... )
717+
718+
Checking for values in literals:
719+
720+
>>> t.mutate(
721+
... a_notin_12=t.a.notin([1, 2]),
722+
... a_notin_12None=t.a.notin([1, 2, None]),
723+
... )
724+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
725+
┃ a ┃ b ┃ a_notin_12 ┃ a_notin_12None ┃
726+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
727+
│ int64 │ int64 │ boolean │ boolean │
728+
├───────┼───────┼────────────┼────────────────┤
729+
│ 1 │ 1 │ False │ False │
730+
│ 2 │ 2 │ False │ False │
731+
│ 3 │ 9 │ True │ NULL │
732+
│ NULL │ NULL │ NULL │ NULL │
733+
└───────┴───────┴────────────┴────────────────┘
734+
735+
Checking for values in columns of the same table:
736+
737+
>>> t.mutate(
738+
... a_notin_b=t.a.notin(t.b),
739+
... a_notin_b_no_null=t.a.notin(t.b.fill_null(0)),
740+
... a_notin_b_plus_1=t.a.notin(t.b + 1),
741+
... )
742+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
743+
┃ a ┃ b ┃ a_notin_b ┃ a_notin_b_no_null ┃ a_notin_b_plus_1 ┃
744+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
745+
│ int64 │ int64 │ boolean │ boolean │ boolean │
746+
├───────┼───────┼───────────┼───────────────────┼──────────────────┤
747+
│ 1 │ 1 │ False │ False │ NULL │
748+
│ 2 │ 2 │ False │ False │ False │
749+
│ 3 │ 9 │ NULL │ True │ False │
750+
│ NULL │ NULL │ NULL │ NULL │ NULL │
751+
└───────┴───────┴───────────┴───────────────────┴──────────────────┘
752+
753+
Checking for values in a column from a different table:
754+
755+
>>> t2 = ibis.memtable({"x": [1, 2, 99], "y": [1, 2, None]})
756+
>>> t.mutate(
757+
... a_notin_x=t.a.notin(t2.x),
758+
... a_notin_y=t.a.notin(t2.y),
759+
... a_notin_y_plus_1=t.a.notin(t2.y + 1),
760+
... )
761+
┏━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
762+
┃ a ┃ b ┃ a_notin_x ┃ a_notin_y ┃ a_notin_y_plus_1 ┃
763+
┡━━━━━━━╇━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
764+
│ int64 │ int64 │ boolean │ boolean │ boolean │
765+
├───────┼───────┼───────────┼───────────┼──────────────────┤
766+
│ 1 │ 1 │ False │ False │ NULL │
767+
│ 2 │ 2 │ False │ False │ False │
768+
│ 3 │ 9 │ True │ NULL │ False │
769+
│ NULL │ NULL │ NULL │ NULL │ NULL │
770+
└───────┴───────┴───────────┴───────────┴──────────────────┘
749771
"""
750772
return ~self.isin(values)
751773

0 commit comments

Comments
 (0)