Skip to content

Refactor string methods for StringArray + return IntegerArray for numeric results #29640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Nov 19, 2019

Conversation

TomAugspurger
Copy link
Contributor

@TomAugspurger TomAugspurger commented Nov 15, 2019

Intended as an alternative to #29637.

This is a much smaller change. It only changes the codepath for StringDtype.
I think this is OK since someday (well down the road) we'll want to deprecate
the .str accessor on object-dtype Series. When we enforce that, we can just
delete the entire old implementation.

The API change is limited to always returning Int64Dtype for numeric outputs, rather than int if there's no NAs and float if there are any.
When BoolArray is done, we'll change that for the boolean-returning ones.

As a side benefit, we get a nice perf boost, since we have deterministic output dtypes we
can skip an object-dtype allocation.

In [2]: s = pd.Series(['a'] * 100_000 + [None], dtype="string")

In [3]: t = s.astype(object)

In [4]: %timeit s.str.count("a")
50.7 ms ± 2.21 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)


In [5]: %timeit t.str.count("a")
62.7 ms ± 742 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

@TomAugspurger TomAugspurger added Performance Memory or execution speed performance Strings String extension data type and string data labels Nov 15, 2019
@TomAugspurger TomAugspurger added this to the 1.0 milestone Nov 15, 2019
@jorisvandenbossche
Copy link
Member

As a side benefit, we get a nice perf boost

Maybe a copy paste error, but the example doesn't show much performance boost :) (less than a percent)

@jorisvandenbossche
Copy link
Member

This looks good to me! I suppose this is simpler than your other PR

@jbrockmendel
Copy link
Member

Is this time-sensitive or can i wait and look at it on Monday?

@TomAugspurger
Copy link
Contributor Author

No rush on this.

I did fail on the copy paste. The speed up is about 15% IIRC.

@@ -109,9 +118,51 @@ def cat_safe(list_of_columns: List, sep: str):

def _na_map(f, arr, na_result=np.nan, dtype=object):
# should really _check_ for NA
if is_extension_array_dtype(arr.dtype):
Copy link
Contributor

Choose a reason for hiding this comment

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

can you sync the signatures up with _map, also rename _map -> _map_arraylike (or similar)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, don't follow this. I think they do match, aside from @jbrockmendel's request to call it func rather than f.

I guess na_map calls it na_result while _map calls it na_value.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am really -1 on 2 different branches here. If they have exactly the same signature a little less negative. again I would rename _map to be more informative here

Copy link
Member

Choose a reason for hiding this comment

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

What do you mean with "-1 on 2 different branches here" ?
The whole purpose of this PR is to add a separate branch in case of StringArray (because we can be more efficient and want to be more specific in the result dtype)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's why my initial PR was so much more complex, since it tried to handle both cases similarly. I think that was more complex than this.

As Joris says, the main point of divergence is that for StringArray we usually know the result dtype exactly. It doesn't depend on the presence of NAs. Additionally,

  1. We're still using map_infer_dtype for both, so the core implementation is the same.
  2. We'll eventually deprecate .str on object-dtype, so we will end up with just this implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, at the very least these signatures for what you call _map and _stringarray_map should be exactly the same.
and _map -> _map_object and _stringarray_map -> _map_stringarray.

I think this is crucial for not adding technical debt.

Copy link
Member

Choose a reason for hiding this comment

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

@jreback is your suggestion to add na_mask to the _stringarray_map signature and have it just not be used? I think this relates to the "this should be a StringArray method" discussion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_map and _na_map already have inconsistent signatures. I'm not sure why it's that way on master, but I'm a bit against adding unused arguments in this case.

What's the technical debt we're adding here? By definition, we need to handle StringArray differently, since its result type doesn't depend on the presence of NAs.

Copy link
Member

Choose a reason for hiding this comment

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

_map and _na_map already have inconsistent signatures. I'm not sure why it's that way on master, but I'm a bit against adding unused arguments in this case.

And there is also a good reason for that, as _map has an additional argument na_mask that is used internally in _map (for a recursive call).
I think refactoring _map is outside of the scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

can you create an issue to clean this up (_map and _na_map), and/or refactor of this, post this PR.

@TomAugspurger
Copy link
Contributor Author

Just the clipboard failure: #29676

@@ -109,9 +118,51 @@ def cat_safe(list_of_columns: List, sep: str):

def _na_map(f, arr, na_result=np.nan, dtype=object):
# should really _check_ for NA
if is_extension_array_dtype(arr.dtype):
Copy link
Contributor

Choose a reason for hiding this comment

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

I am really -1 on 2 different branches here. If they have exactly the same signature a little less negative. again I would rename _map to be more informative here

@TomAugspurger
Copy link
Contributor Author

CI failure is from #29514

return _map(f, arr, na_mask=True, na_value=na_result, dtype=dtype)


def _stringarray_map(
func: Callable[[str], Any], arr: "StringArray", na_value: Any, dtype: Dtype
Copy link
Member

Choose a reason for hiding this comment

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

na_value restricted to scalar?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I don't think so.

na_value : Any
The value to use for missing values. By default, this is
the original value (NA).
dtype : Dtype
Copy link
Member

Choose a reason for hiding this comment

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

Elsewhere we say "np.dtype or ExtensionDtype". Is this the new policy?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if we have a policy. If it matters, this is an internal docstring, so I'm OK to use things from pandas._typing that we wouldn't have in a public docstring yet.

@TomAugspurger
Copy link
Contributor Author

Thanks for the review. Addressed all the comments I think.

After this, I'll have a small change at #29597 to get it using the new methods (there's some no unnecessary changes in #29597 that need to be removed).

@TomAugspurger
Copy link
Contributor Author

Just the clipboard CI failure again.

@TomAugspurger
Copy link
Contributor Author

TomAugspurger commented Nov 19, 2019 via email

Copy link
Contributor

@jreback jreback left a comment

Choose a reason for hiding this comment

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

very minor comments, rebase and ping on green.

@@ -63,7 +63,7 @@ Previously, strings were typically stored in object-dtype NumPy arrays.
``StringDtype`` is currently considered experimental. The implementation
and parts of the API may change without warning.

The text extension type solves several issues with object-dtype NumPy arrays:
The ``'string'`` extension type solves several issues with object-dtype NumPy arrays:
Copy link
Contributor

Choose a reason for hiding this comment

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

does this need any updating an/or reference to your new section that you added in text.rst?

Copy link
Member

Choose a reason for hiding this comment

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

@TomAugspurger you can probably update the sentence "The usual string accessor methods work. Where appropriate, the return type of the Series or columns of a DataFrame will also have string dtype." 20 lines below this line, to include that it can also return IntegerDtype in certain cases.

@@ -109,9 +118,51 @@ def cat_safe(list_of_columns: List, sep: str):

def _na_map(f, arr, na_result=np.nan, dtype=object):
# should really _check_ for NA
if is_extension_array_dtype(arr.dtype):
Copy link
Contributor

Choose a reason for hiding this comment

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

can you create an issue to clean this up (_map and _na_map), and/or refactor of this, post this PR.

if is_extension_array_dtype(arr.dtype):
# just StringDtype
arr = extract_array(arr)
return _stringarray_map(f, arr, na_value=na_result, dtype=dtype)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rename these as indicated above.

Copy link
Member

Choose a reason for hiding this comment

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

I would rename these as indicated above.

Can you be explicit in what exact names you want to propose? (I am getting a bit lost in all the comments)
Tom already renamed _ea_map to _stringarray_map

Copy link
Contributor

Choose a reason for hiding this comment

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

right we should change _map -> _object_map (though I actually prefer the opposite, _map_object and _map_stringarray), but either is ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, make sense. I renamed to _map_object and _map_stringarray

@jorisvandenbossche jorisvandenbossche changed the title String Methods refactor, take 2 Refactor string methods for StringArray + return IntegerArray for numeric results Nov 19, 2019
@TomAugspurger
Copy link
Contributor Author

Merged master, and writing up an issue for the followup.

@TomAugspurger
Copy link
Contributor Author

#29710 for followup.

@TomAugspurger
Copy link
Contributor Author

Thanks all!

@TomAugspurger TomAugspurger merged commit 6b1f73e into pandas-dev:master Nov 19, 2019
@TomAugspurger TomAugspurger deleted the masked-string-2 branch November 19, 2019 16:22
proost pushed a commit to proost/pandas that referenced this pull request Dec 19, 2019
proost pushed a commit to proost/pandas that referenced this pull request Dec 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Performance Memory or execution speed performance Strings String extension data type and string data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants