Skip to content
43 changes: 20 additions & 23 deletions narwhals/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
get_pyspark_connect,
get_pyspark_sql,
get_sqlframe,
is_cudf_series,
is_modin_series,
is_narwhals_series,
is_narwhals_series_int,
is_numpy_array_1d,
Expand All @@ -62,6 +60,7 @@
from typing import AbstractSet as Set

import pandas as pd
import polars as pl
import pyarrow as pa
from typing_extensions import (
Concatenate,
Expand Down Expand Up @@ -1147,7 +1146,7 @@ def scale_bytes(sz: int, unit: SizeUnit) -> int | float:
raise ValueError(msg)


def is_ordered_categorical(series: Series[Any]) -> bool: # noqa: PLR0911
def is_ordered_categorical(series: Series[Any]) -> bool:
"""Return whether indices of categories are semantically meaningful.

This is a convenience function to accessing what would otherwise be
Expand Down Expand Up @@ -1193,29 +1192,27 @@ def is_ordered_categorical(series: Series[Any]) -> bool: # noqa: PLR0911

dtypes = series._compliant_series._version.dtypes
compliant = series._compliant_series
# If it doesn't match any branches, let's just play it safe and return False.
result: bool = False
if isinstance(compliant, InterchangeSeries) and isinstance(
series.dtype, dtypes.Categorical
):
return compliant.native.describe_categorical["is_ordered"]
if series.dtype == dtypes.Enum:
return True
if series.dtype != dtypes.Categorical:
return False
native_series = series.to_native()
if is_polars_series(native_series):
return native_series.dtype.ordering == "physical" # type: ignore[attr-defined]
if is_pandas_series(native_series):
return bool(native_series.cat.ordered)
if is_modin_series(native_series): # pragma: no cover
return native_series.cat.ordered
if is_cudf_series(native_series): # pragma: no cover
return native_series.cat.ordered
if is_pyarrow_chunked_array(native_series):
from narwhals._arrow.utils import is_dictionary

return is_dictionary(native_series.type) and native_series.type.ordered
# If it doesn't match any of the above, let's just play it safe and return False.
return False # pragma: no cover
result = compliant.native.describe_categorical["is_ordered"]
elif series.dtype == dtypes.Enum:
result = True
elif series.dtype != dtypes.Categorical:
result = False
else:
native = series.to_native()
if is_polars_series(native):
result = cast("pl.Categorical", native.dtype).ordering == "physical"
elif is_pandas_like_series(native):
result = bool(native.cat.ordered)
elif is_pyarrow_chunked_array(native):
from narwhals._arrow.utils import is_dictionary

result = is_dictionary(native.type) and native.type.ordered
return result


def generate_unique_token(
Expand Down
21 changes: 21 additions & 0 deletions tests/series_only/is_ordered_categorical_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@
import pytest

import narwhals as nw
from narwhals.utils import Version

if TYPE_CHECKING:
from narwhals.typing import IntoSeries
from tests.utils import ConstructorEager


class MockCompliantSeries:
_version = Version.MAIN

def __narwhals_series__(self) -> Any:
return self

@property
def native(self) -> tuple[()]:
return ()

@property
def dtype(self) -> nw.Categorical:
return nw.Categorical()


def test_is_ordered_categorical_polars() -> None:
pytest.importorskip("polars")
import polars as pl
Expand Down Expand Up @@ -62,3 +78,8 @@ def test_is_ordered_categorical_pyarrow() -> None:
assert nw.is_ordered_categorical(
nw.from_native(s, series_only=True)
) # pragma: no cover


def test_is_ordered_categorical_unknown_series() -> None:
series: nw.Series[Any] = nw.Series(MockCompliantSeries(), level="full")
assert nw.is_ordered_categorical(series) is False
Loading