Skip to content

ENH: speed up StringDType isin - #32217

Closed
ngoldbaum wants to merge 2 commits into
numpy:mainfrom
ngoldbaum:speedup-stringdtype-isin
Closed

ngoldbaum wants to merge 2 commits into
numpy:mainfrom
ngoldbaum:speedup-stringdtype-isin

Conversation

@ngoldbaum

Copy link
Copy Markdown
Member

PR summary

Fixes #32161. Opening as a draft for now while I test it.

AI Disclosure

I used an AI to iterate on this PR.

if na_is_nan:
selection = np.isnan(ar)
else:
selection = np.equal(ar, typed_na)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ikrommyd says this causes an issue:

In [8]: import numpy as np
In [9]: from numpy.dtypes import StringDType
In [10]: dt = StringDType(na_object=None)
In [11]: cands = np.array([""] + [f"s{i}" for i in range(30)], dtype="U5")
In [12]: np.isin(np.array([""], dtype=dt), cands)
Out[12]: array([ False])

@ngoldbaum

Copy link
Copy Markdown
Member Author

We chatted a little bit about this at a triage review meeting and agreed that it's OK to have a bit of complexity if it buys a really significant speedup like this.

@ikrommyd ikrommyd left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's overall in a very good track but with the help of AI I have a few comments about specific bugs or edge cases that would be very very difficult to spot otherwise.

if na_is_nan:
selection = np.isnan(ar)
else:
selection = np.equal(ar, typed_na)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

np.equal(NA, "") returns True which corrupts the results when you are searching if the empty string is in an array. The snippet above returns True on main.

return ret[:len(ar1)]
mask = np.ones(len(ar1), dtype=bool)
for a in ar2:
mask &= (ar1 != a)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there is a problem with invert=True here depending on whether arr2 is small or large

dt = StringDType(na_object=np.nan)
v = np.array(["a", np.nan], dtype=dt)
print(np.isin(v, np.array(["a"], dtype="U1"), invert=True))
# [False False]                          ← small ar2: old loop
print(np.isin(v, np.array(["a"] + [f"s{i}" for i in range(40)], dtype="U3"), invert=True))
# [False  True]                          ← large ar2: new path


# Unequal descriptors only need a compatibility check when it
# unlocks sorting; small arrays use the scalar fallback below.
if (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again another dependence on the length of arr2 is whether pandas NA sentinels raise.

values = np.array(["a", "b"] * 200, dtype=StringDType(na_object=pd.NA))
cands  = np.array(["a", pd.NA], dtype=StringDType(na_object=pd.NA, coerce=False))

np.isin(values, cands)         # Error
np.isin(values, np.concatenate([cands, np.array([f"s{i}" for i in range(40)], dtype=cands.dtype)]))   # works fine

non_null_ar2, ar2_selection = _filter_stringdtype_nulls(
ar2, typed_na, na_is_nan
)
non_null_result = _isin_sorting(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The decision to use sorting is made before _filter_stringdtype_nulls is called. But filtering can reduce the length. I think it's more efficient for speedup to decide after filtering which method to use.

for value in ar2:
# A NaN-like sentinel never matches. Skipping it also avoids
# scalar object comparison for objects such as pandas.NA.
if na_is_nan and value is na_object:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

na_object comes from ar1 here and na_objects are not equal from different arrays.

dt1 = StringDType(na_object=np.nan)
dt2 = StringDType(na_object=float("nan"))
print(dt1 == dt2) # True
print(dt1.na_object is dt2.na_object) #False


import numpy as np
from numpy._core.tests._natype import pd_NA
from numpy._core.tests.test_stringdtype import string_list # noqa: F401

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm getting linting failures from spin lint locally about redefinition of string_list. Don't know why they're not in CI

Comment on lines +955 to +956
PyObject *
stringdtype_compatible_na_py(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add NPY_NO_EXPORT to match the header file

{NULL, NULL, 0, NULL},
};

PyObject *

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add NPY_NO_EXPORT to match the header file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My AI model informs me that both new exports in this file are reachable from Python already:

np.result_type(StringDType(na_object=None), StringDType(na_object=np.nan))  # TypeError iff incompatible
np.result_type(StringDType(), StringDType(na_object=None)).na_object        # effective NA

and that

try: np.result_type(...) except TypeError

replaces both functions and the getattr/_NoValue chain at _arraysetops_impl.py:855-863


param_names = ["size", "na_kind"]
params = [
[10, 30_000],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

size 10 is under the cutoff no? 10 < 10*10**0.145. So it measures the old scalar loop.

@ngoldbaum

Copy link
Copy Markdown
Member Author

Closing this in favor of #32693.

@ngoldbaum ngoldbaum closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PERF: np.isin is very slow for StringDType

2 participants