-
Notifications
You must be signed in to change notification settings - Fork 210
feat: add nw.factorize #3809
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
base: main
Are you sure you want to change the base?
feat: add nw.factorize #3809
Changes from all commits
d38d4cb
dfcc905
9a5dacf
7d8459a
8b798aa
e3b4cb4
e8d73e9
2abe03d
9d7a132
44c3b55
571ce63
f6510a0
fa085fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| - dtype | ||
| - ewm_mean | ||
| - exp | ||
| - factorize | ||
| - fill_nan | ||
| - fill_null | ||
| - filter | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1038,6 +1038,57 @@ def hist_from_bin_count( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .to_frame() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def factorize( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, *, null_as_value: bool = False, sort: bool = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> tuple[Self, Self]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(self.native) == 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codes = pa.chunked_array([[]], type=pa.int32()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uniques = pa.chunked_array([[]], type=self.native.type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (self._with_native(codes), self._with_native(uniques)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # https://github.com/apache/arrow/issues/33297; input pa.NullArray's don't dictionary_encode properly | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pa.types.is_null(self.native.type): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if null_as_value: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codes, uniques = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pa.repeat(0, len(self.native)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pa.nulls(1, type=self.native.type), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codes, uniques = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pa.repeat(-1, len(self.native)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pa.nulls(0, type=self.native.type), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (self._with_native(codes.cast(pa.int32())), self._with_native(uniques)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| native = self.native | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pa.types.is_dictionary(native.type): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # re-encode if already dict encoded; can't be certain how the original dict encoding was done | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| native = native.cast(native.type.value_type) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| null_encoding: Literal["encode", "mask"] = "encode" if null_as_value else "mask" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encoded = pc.dictionary_encode( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| native, null_encoding=null_encoding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).unify_dictionaries() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uniques = encoded.chunk(0).dictionary # type: ignore[attr-defined] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codes = pa.chunked_array([c.indices for c in encoded.chunks], type=pa.int32()) # type: ignore[attr-defined] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codes = cast("pa.ChunkedArray[pa.Int32Scalar]", codes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not sort: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._with_native(pc.fill_null(codes, pa.scalar(-1))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._with_native(uniques), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sorted_uniques = pc.take(uniques, pc.sort_indices(uniques)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_mapping = pc.index_in(uniques, value_set=sorted_uniques) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_codes = pc.take(new_mapping, codes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._with_native(pc.fill_null(new_codes, pa.scalar(-1))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._with_native(sorted_uniques), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1069
to
+1090
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As somebody who has battled against I'm not 100% sure what is going on here, so I don't a concrete suggestion. But I do have some code that may be helpful to look at. If only inspire an oh that's how these bits could fit together moment π Show (maybe related) pyarrow functions
Definitions
Some usagenarwhals/src/narwhals/_plan/arrow/dataframe.py Lines 301 to 313 in c179a78
narwhals/src/narwhals/_plan/arrow/functions/_aggregation.py Lines 152 to 159 in c179a78
narwhals/src/narwhals/_plan/arrow/series.py Lines 183 to 191 in c179a78
narwhals/src/narwhals/_plan/arrow/functions/_lists.py Lines 352 to 365 in c179a78
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested Timing codeimport pyarrow as pa
import pyarrow.compute as pc
def factorize_via_replace(arr, *, sort=False):
uniques = pc.unique(arr)
if sort:
uniques = pc.take(uniques, pc.sort_indices(uniques))
return pc.index_in(arr, value_set=uniques), uniques
def factorize_via_dictionary(arr, sort=False):
encoded = pc.dictionary_encode(arr, null_encoding='mask').combine_chunks()
uniques = encoded.dictionary
codes = pa.chunked_array(encoded.indices)
if not sort:
return codes, uniques
sorted_uniques = pc.take(uniques, pc.sort_indices(uniques))
new_mapping = pc.index_in(uniques, value_set=sorted_uniques)
new_codes = pc.take(new_mapping, codes)
return new_codes, sorted_uniques
import numpy as np
from numpy.random import default_rng
from string import ascii_lowercase
from timeit import timeit
from itertools import product
rng = default_rng(0)
n = 10
for sz, n_chunks, sort in product([10_000, 1_000_000], [5, 100, 10_000], [True, False]):
n_chunks = min(n_chunks, sz)
arr = rng.choice([*ascii_lowercase], size=(sz, 4)).view('<U4').ravel()
arr = pa.chunked_array(np.array_split(arr, n_chunks), type=pa.string())
c1, u1 = factorize_via_replace(arr, sort=sort)
c2, u2 = factorize_via_dictionary(arr, sort=sort)
assert u1.equals(u2)
assert c1.equals(c2)
print(f'N={sz:<10,}| Chunks={n_chunks:<10,}| sort={sort!r:<5}| Unique={len(u1):,}')
print(f"{timeit(lambda: factorize_via_replace(arr, sort=sort), number=n) = :.6f}")
print(f"{timeit(lambda: factorize_via_dictionary(arr, sort=sort), number=n) = :.6f}")
print()Timing results, stepping through dictionary_encoding is *generally* faster, but not in every scenario. ~2Γ faster for the largest array size (1M values, 10k chunks) when `sort=False`.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (do note that in my timing I used |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __iter__(self) -> Iterator[Any]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for x in self.native: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield maybe_extract_py_scalar(x, return_py_scalar=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -670,6 +670,27 @@ def last(self) -> PythonLiteral: | |||||||||
| def any_value(self, *, ignore_nulls: bool) -> PythonLiteral: | ||||||||||
| return self.drop_nulls().first() if ignore_nulls else self.first() | ||||||||||
|
|
||||||||||
| def factorize( | ||||||||||
| self, *, null_as_value: bool = False, sort: bool = False | ||||||||||
| ) -> tuple[Self, Self]: | ||||||||||
|
Comment on lines
+673
to
+675
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
pl.Categorical>>> s1 = pl.Series("X", [*"abc"]).cast(pl.Categorical)
>>> s2 = pl.Series("X", [*"abd"]).cast(pl.Categorical)
>>> s1.to_physical()
shape: (3,)
Series: 'X' [u32]
[
0
1
2
]
>>> s2.to_physical()
shape: (3,)
Series: 'X' [u32]
[
0
1
3
]pl.Enum>>> import polars as pl
>>> e1 = pl.Enum(['a', 'b', 'c'])
>>> e2 = pl.Enum(['a', 'b', 'd'])
>>> s1 = pl.Series("X", [*"abc"]).cast(e1)
>>> s2 = pl.Series("X", [*"abd"]).cast(e2)
>>> s1.to_physical()
shape: (3,)
Series: 'X' [u8]
[
0
1
2
]
>>> s2.to_physical()
shape: (3,)
Series: 'X' [u8]
[
0
1
2
] |
||||||||||
| uniques = self.unique() if null_as_value else self.unique().drop_nulls() | ||||||||||
| if sort: | ||||||||||
| uniques = uniques.sort(descending=False, nulls_last=True) | ||||||||||
|
|
||||||||||
| if null_as_value: | ||||||||||
| codes = self.native.replace_strict( | ||||||||||
| old=uniques.native, new=range(len(uniques)), return_dtype=pl.Int32() | ||||||||||
| ) | ||||||||||
|
Comment on lines
+676
to
+683
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered starting this with a Show pattern
This is based on the magic polars does for most narwhals/src/narwhals/_polars/series.py Lines 808 to 811 in 05ce363
Chaining For example, what if import polars as pl
N = 10_000_000
N_UNIQUE = 100
native = (
pl.select(big_boi=pl.int_range(N), eager=False)
.gather((pl.col.big_boi % N_UNIQUE).shuffle(1))
.collect()
.to_series()
)
nativeShow output
Would there be a benefit to not materializing until as late as possible? uniques = native.unique().drop_nulls().sort(nulls_last=True)
codes = native.replace_strict(uniques, range(len(uniques)), return_dtype=pl.Int32()).cast(
pl.Int32()
)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had intentionally re-used our
Probably, but "as late as possible" comes pretty early as we must materialize before calling replace (otherwise |
||||||||||
| else: | ||||||||||
| codes = self.native.replace_strict( | ||||||||||
| old=uniques.native, | ||||||||||
| new=range(len(uniques)), | ||||||||||
| default=-1, | ||||||||||
| return_dtype=pl.Int32(), | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| return self._with_native(codes.cast(pl.Int32())), uniques | ||||||||||
|
|
||||||||||
| @property | ||||||||||
| def dt(self) -> PolarsSeriesDateTimeNamespace: | ||||||||||
| return PolarsSeriesDateTimeNamespace(self) | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||
| from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence | ||||||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||||||
| from functools import partial | ||||||||||||||||||||||||||||||||
| from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, cast, overload | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -2905,6 +2906,68 @@ def is_close( | |||||||||||||||||||||||||||||||
| result = result.rename(orig_name) if name_is_none else result | ||||||||||||||||||||||||||||||||
| return cast("Self", result) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def factorize( | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we please mark this as unstable? |
||||||||||||||||||||||||||||||||
| self, *, null_as_value: bool = False, sort: bool = False | ||||||||||||||||||||||||||||||||
| ) -> Encoded[IntoSeriesT]: | ||||||||||||||||||||||||||||||||
| """Encode values as integer codes and unique values. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| The integer codes are index locations that map the unique values back to their | ||||||||||||||||||||||||||||||||
| positions within the original array. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Arguments: | ||||||||||||||||||||||||||||||||
| null_as_value: Whether to treat null as a regular value. When False, | ||||||||||||||||||||||||||||||||
| nulls are removed from the returned unique values and the code -1 is | ||||||||||||||||||||||||||||||||
| used to indicate the location of null values in the original array. | ||||||||||||||||||||||||||||||||
| When True, nulls are preserved in the returned unique values and a | ||||||||||||||||||||||||||||||||
| positive integer is used to indicate their location in the original | ||||||||||||||||||||||||||||||||
| array. | ||||||||||||||||||||||||||||||||
|
Comment on lines
+2918
to
+2923
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would still hold against having a sentinel value unless provided. I understand the ergonomic, but in polars spirit, I think there should be a way to preserve null values.
I am curious to know how others feel about this
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Beyond ergonomics, preserving the null value in the returned codes breaks base pandas as the returned NaN upcasts the integer codes to become floats. I'd prefer not to have the default case create a surprise for the backend where this function is likely to be used most often. I am fine with the remaining semantics, just would advocate to make the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is it not possible to use
I understand this concern, but I agree with @FBruzzesi that the default behavior should feel like polars. Third optionWhat if we held back on This feels pretty related to discord thread and would slide in perfectly into a whole section about transitioning from @camriddell, correct me if I'm wrong, but that feels like something you'd have a lot to speak on π
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@dangotbanned yeah we can return an
I am biased as I have already sunk a few hours into this, but I would like to see it land. This is also a fairly common operation for our array-based libraries downstream users, and the backend implementations are all quite different from one another. @FBruzzesi & @dangotbanned how do you feel about this as an API? I would love it if the SENTINEL policy was its own struct, so one could embed the sentinel value inside of it but we're using the wrong language for that. class NullEncoding(StrEnum):
PRESERVE = auto() # keep the Nulls in the resultant codes
ENCODE = auto() # encode Nulls as one would like any other value
SENTINEL = auto() # replace Nulls with a particular `sentinel` value
class Series:
def factorize(
self, *, sort: bool = False, null_policy: NullEncoding = NullEncoding.PRESERVE, sentinel: Any | NoDefault = NO_DEFAULT,
) -> Encoded[IntoSeriesT]:
if null_policy is NullEncoding.SENTINEL:
if sentinel is NO_DEFAULT:
msg = "Must supply `sentinel` when null_policy=NullEncoding.SENTINEL"
raise TypeError(msg)
elif sentinel is not NO_DEFAULT:
msg = f"Argument `sentinel` is ignored when null_policy={null_policy}"
raise TypeError(msg)
...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey @camriddell sorry for the late feedback, I completely lost this thread! I like having On the larger picture of having the feature vs documenting it, I would be in favor of having it with the unstable tag. As Cam mentioned, this topic came up quite a lot recently but not only, and there is real need for it. On the Int64 dtype for pandas: I am quite ok with that, keep in mind that one day pyarrow might be a default/requirement if I remember correctly. I am sure Marco knows much more about it. |
||||||||||||||||||||||||||||||||
| sort: Whether to sort the unique values before assigning codes. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||
| codes: An integer series where each value represents the index | ||||||||||||||||||||||||||||||||
| of the corresponding value in `uniques`. Null values are encoded | ||||||||||||||||||||||||||||||||
| as -1. | ||||||||||||||||||||||||||||||||
|
Comment on lines
+2928
to
+2929
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of sync with the new behaviour |
||||||||||||||||||||||||||||||||
| uniques: A series containing the unique non-null values. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Examples: | ||||||||||||||||||||||||||||||||
| >>> import polars as pl | ||||||||||||||||||||||||||||||||
| >>> import narwhals as nw | ||||||||||||||||||||||||||||||||
| >>> df = pl.DataFrame({"groups": ["a", "b", "a", None]}) | ||||||||||||||||||||||||||||||||
| >>> nw_df = nw.from_native(df) | ||||||||||||||||||||||||||||||||
| >>> codes, uniques = nw_df["groups"].factorize(sort=True) | ||||||||||||||||||||||||||||||||
| >>> codes | ||||||||||||||||||||||||||||||||
| ββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||
| | Narwhals Series | | ||||||||||||||||||||||||||||||||
| |----------------------| | ||||||||||||||||||||||||||||||||
| |shape: (4,) | | ||||||||||||||||||||||||||||||||
| |Series: 'groups' [i32]| | ||||||||||||||||||||||||||||||||
| |[ | | ||||||||||||||||||||||||||||||||
| | 0 | | ||||||||||||||||||||||||||||||||
| | 1 | | ||||||||||||||||||||||||||||||||
| | 0 | | ||||||||||||||||||||||||||||||||
| | -1 | | ||||||||||||||||||||||||||||||||
| |] | | ||||||||||||||||||||||||||||||||
| ββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||
| >>> uniques | ||||||||||||||||||||||||||||||||
| ββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||
| | Narwhals Series | | ||||||||||||||||||||||||||||||||
| |----------------------| | ||||||||||||||||||||||||||||||||
| |shape: (2,) | | ||||||||||||||||||||||||||||||||
| |Series: 'groups' [str]| | ||||||||||||||||||||||||||||||||
| |[ | | ||||||||||||||||||||||||||||||||
| | "a" | | ||||||||||||||||||||||||||||||||
| | "b" | | ||||||||||||||||||||||||||||||||
| |] | | ||||||||||||||||||||||||||||||||
| ββββββββββββββββββββββββ | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| codes, uniques = self._compliant_series.factorize( | ||||||||||||||||||||||||||||||||
| null_as_value=null_as_value, sort=sort | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| return Encoded( | ||||||||||||||||||||||||||||||||
| self._with_compliant(codes).alias("codes"), | ||||||||||||||||||||||||||||||||
| self._with_compliant(uniques).alias("uniques"), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @unstable | ||||||||||||||||||||||||||||||||
| def any_value(self, *, ignore_nulls: bool = False) -> PythonLiteral: | ||||||||||||||||||||||||||||||||
| """Get a random value from the column. | ||||||||||||||||||||||||||||||||
|
|
@@ -2946,3 +3009,26 @@ def list(self) -> SeriesListNamespace[Self]: | |||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||
| def struct(self) -> SeriesStructNamespace[Self]: | ||||||||||||||||||||||||||||||||
| return SeriesStructNamespace(self) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @dataclass(frozen=True) | ||||||||||||||||||||||||||||||||
| class Encoded(Generic[IntoSeriesT]): | ||||||||||||||||||||||||||||||||
|
Comment on lines
+3014
to
+3015
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this also be marked as unstable and/or private please? It is mostly an implementation detail |
||||||||||||||||||||||||||||||||
| """Result of `factorize`. Unpacks as `(codes, uniques)` like pandas.""" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| codes: Series[IntoSeriesT] | ||||||||||||||||||||||||||||||||
| uniques: Series[IntoSeriesT] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def __iter__(self) -> Iterator[Series[IntoSeriesT]]: | ||||||||||||||||||||||||||||||||
| yield self.codes | ||||||||||||||||||||||||||||||||
| yield self.uniques | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||
| def mapping(self) -> Mapping[Any, int]: | ||||||||||||||||||||||||||||||||
| """Forward map as a joinable ``(value, code)`` frame; works for any dtype.""" | ||||||||||||||||||||||||||||||||
| name = self.uniques.name | ||||||||||||||||||||||||||||||||
| return dict( | ||||||||||||||||||||||||||||||||
| self.uniques.to_frame() | ||||||||||||||||||||||||||||||||
| .with_row_index("code") | ||||||||||||||||||||||||||||||||
| .select(name, "code") | ||||||||||||||||||||||||||||||||
| .iter_rows() | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+3025
to
+3034
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am basically self-reviewing this bit - I ended up suggesting:
Suggested change
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you tell by the presence of nulls?
IIRC, one version keeps nulls in the indices 1.
If there aren't any nulls, then I would assume the kind of encoding doesn't matter
Footnotes
It could also be the other property β©
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally had a version that dispatched based on the presense of nulls, but it felt a bit more like an implementation detail that produced a surprising behavior. Was planning to open an upstream issue.