From b0f752523c5f2fc1bd53ec50ba1ea2e499118c5f Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:12:05 +0100 Subject: [PATCH 1/3] Bump version to 0.9.28 --- docs/installation.md | 2 +- narwhals/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 7c324b2f0..20cdf4515 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -11,6 +11,6 @@ Then, if you start the Python REPL and see the following: ```python >>> import narwhals >>> narwhals.__version__ -'0.9.27' +'0.9.28' ``` then installation worked correctly! diff --git a/narwhals/__init__.py b/narwhals/__init__.py index cb6e479ee..0f64dc731 100644 --- a/narwhals/__init__.py +++ b/narwhals/__init__.py @@ -42,7 +42,7 @@ from narwhals.utils import maybe_convert_dtypes from narwhals.utils import maybe_set_index -__version__ = "0.9.27" +__version__ = "0.9.28" __all__ = [ "selectors", diff --git a/pyproject.toml b/pyproject.toml index f3acfddee..b253209e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "narwhals" -version = "0.9.27" +version = "0.9.28" authors = [ { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" }, ] From b8bbe4d71b1eaa90625fab7ffb41dd12c21da1a8 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Fri, 5 Jul 2024 07:52:37 +0100 Subject: [PATCH 2/3] chore: return Self more (#408) * return Self more * a few more --- narwhals/dataframe.py | 8 +-- narwhals/expression.py | 116 +++++++++++++++++++++-------------------- narwhals/series.py | 62 +++++++++++----------- 3 files changed, 94 insertions(+), 92 deletions(-) diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py index 399b61cf0..a8d870278 100644 --- a/narwhals/dataframe.py +++ b/narwhals/dataframe.py @@ -399,9 +399,9 @@ def __getitem__(self, item: Sequence[int]) -> Series: ... def __getitem__(self, item: str) -> Series: ... @overload - def __getitem__(self, item: slice) -> DataFrame: ... + def __getitem__(self, item: slice) -> Self: ... - def __getitem__(self, item: str | slice | Sequence[int]) -> Series | DataFrame: + def __getitem__(self, item: str | slice | Sequence[int]) -> Series | Self: if isinstance(item, str): from narwhals.series import Series @@ -1649,7 +1649,7 @@ def is_unique(self: Self) -> Series: return Series(self._dataframe.is_unique()) - def null_count(self: Self) -> DataFrame: + def null_count(self: Self) -> Self: r""" Create a new DataFrame that shows the null counts per column. @@ -1814,7 +1814,7 @@ def __repr__(self) -> str: # pragma: no cover + "┘" ) - def __getitem__(self, item: str | slice) -> Series | DataFrame: + def __getitem__(self, item: str | slice) -> Series | Self: raise TypeError("Slicing is not supported on LazyFrame") def collect(self) -> DataFrame: diff --git a/narwhals/expression.py b/narwhals/expression.py index 658dbfcc9..9f12a3acb 100644 --- a/narwhals/expression.py +++ b/narwhals/expression.py @@ -14,6 +14,8 @@ from narwhals.utils import parse_version if TYPE_CHECKING: + from typing_extensions import Self + from narwhals.typing import IntoExpr @@ -33,7 +35,7 @@ def __init__(self, call: Callable[[Any], Any]) -> None: self._call = call # --- convert --- - def alias(self, name: str) -> Expr: + def alias(self, name: str) -> Self: """ Rename the expression. @@ -75,7 +77,7 @@ def alias(self, name: str) -> Expr: def cast( self, dtype: Any, - ) -> Expr: + ) -> Self: """ Redefine an object's data type. @@ -123,131 +125,131 @@ def cast( ) # --- binary --- - def __eq__(self, other: object) -> Expr: # type: ignore[override] + def __eq__(self, other: object) -> Self: # type: ignore[override] return self.__class__( lambda plx: self._call(plx).__eq__(extract_native(plx, other)) ) - def __ne__(self, other: object) -> Expr: # type: ignore[override] + def __ne__(self, other: object) -> Self: # type: ignore[override] return self.__class__( lambda plx: self._call(plx).__ne__(extract_native(plx, other)) ) - def __and__(self, other: Any) -> Expr: + def __and__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__and__(extract_native(plx, other)) ) - def __rand__(self, other: Any) -> Expr: + def __rand__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rand__(extract_native(plx, other)) ) - def __or__(self, other: Any) -> Expr: + def __or__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__or__(extract_native(plx, other)) ) - def __ror__(self, other: Any) -> Expr: + def __ror__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__ror__(extract_native(plx, other)) ) - def __add__(self, other: Any) -> Expr: + def __add__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__add__(extract_native(plx, other)) ) - def __radd__(self, other: Any) -> Expr: + def __radd__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__radd__(extract_native(plx, other)) ) - def __sub__(self, other: Any) -> Expr: + def __sub__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__sub__(extract_native(plx, other)) ) - def __rsub__(self, other: Any) -> Expr: + def __rsub__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rsub__(extract_native(plx, other)) ) - def __truediv__(self, other: Any) -> Expr: + def __truediv__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__truediv__(extract_native(plx, other)) ) - def __rtruediv__(self, other: Any) -> Expr: + def __rtruediv__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rtruediv__(extract_native(plx, other)) ) - def __mul__(self, other: Any) -> Expr: + def __mul__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__mul__(extract_native(plx, other)) ) - def __rmul__(self, other: Any) -> Expr: + def __rmul__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rmul__(extract_native(plx, other)) ) - def __le__(self, other: Any) -> Expr: + def __le__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__le__(extract_native(plx, other)) ) - def __lt__(self, other: Any) -> Expr: + def __lt__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__lt__(extract_native(plx, other)) ) - def __gt__(self, other: Any) -> Expr: + def __gt__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__gt__(extract_native(plx, other)) ) - def __ge__(self, other: Any) -> Expr: + def __ge__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__ge__(extract_native(plx, other)) ) - def __pow__(self, other: Any) -> Expr: + def __pow__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__pow__(extract_native(plx, other)) ) - def __rpow__(self, other: Any) -> Expr: + def __rpow__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rpow__(extract_native(plx, other)) ) - def __floordiv__(self, other: Any) -> Expr: + def __floordiv__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__floordiv__(extract_native(plx, other)) ) - def __rfloordiv__(self, other: Any) -> Expr: + def __rfloordiv__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rfloordiv__(extract_native(plx, other)) ) - def __mod__(self, other: Any) -> Expr: + def __mod__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__mod__(extract_native(plx, other)) ) - def __rmod__(self, other: Any) -> Expr: + def __rmod__(self, other: Any) -> Self: return self.__class__( lambda plx: self._call(plx).__rmod__(extract_native(plx, other)) ) # --- unary --- - def __invert__(self) -> Expr: + def __invert__(self) -> Self: return self.__class__(lambda plx: self._call(plx).__invert__()) - def any(self) -> Expr: + def any(self) -> Self: """ Return whether any of the values in the column are `True` @@ -281,7 +283,7 @@ def any(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).any()) - def all(self) -> Expr: + def all(self) -> Self: """ Return whether all values in the column are `True`. @@ -315,7 +317,7 @@ def all(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).all()) - def mean(self) -> Expr: + def mean(self) -> Self: """ Get mean value. @@ -349,7 +351,7 @@ def mean(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).mean()) - def std(self, *, ddof: int = 1) -> Expr: + def std(self, *, ddof: int = 1) -> Self: """ Get standard deviation. @@ -422,7 +424,7 @@ def sum(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).sum()) - def min(self) -> Expr: + def min(self) -> Self: """ Returns the minimum value(s) from a column(s). @@ -456,7 +458,7 @@ def min(self) -> Expr: return self.__class__(lambda plx: self._call(plx).min()) - def max(self) -> Expr: + def max(self) -> Self: """ Returns the maximum value(s) from a column(s). @@ -490,7 +492,7 @@ def max(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).max()) - def n_unique(self) -> Expr: + def n_unique(self) -> Self: """ Returns count of unique values @@ -524,7 +526,7 @@ def n_unique(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).n_unique()) - def unique(self) -> Expr: + def unique(self) -> Self: """ Return unique values @@ -562,7 +564,7 @@ def unique(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).unique()) - def cum_sum(self) -> Expr: + def cum_sum(self) -> Self: """ Return cumulative sum. @@ -604,7 +606,7 @@ def cum_sum(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).cum_sum()) - def diff(self) -> Expr: + def diff(self) -> Self: """ Returns the difference between each element and the previous one. @@ -655,7 +657,7 @@ def diff(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).diff()) - def shift(self, n: int) -> Expr: + def shift(self, n: int) -> Self: """ Shift values by `n` positions. @@ -706,7 +708,7 @@ def shift(self, n: int) -> Expr: """ return self.__class__(lambda plx: self._call(plx).shift(n)) - def sort(self, *, descending: bool = False) -> Expr: + def sort(self, *, descending: bool = False) -> Self: """ Sort this column. Place null values first. @@ -777,7 +779,7 @@ def sort(self, *, descending: bool = False) -> Expr: # --- transform --- def is_between( self, lower_bound: Any, upper_bound: Any, closed: str = "both" - ) -> Expr: + ) -> Self: """ Check if this expression is between the given lower and upper bounds. @@ -828,7 +830,7 @@ def is_between( lambda plx: self._call(plx).is_between(lower_bound, upper_bound, closed) ) - def is_in(self, other: Any) -> Expr: + def is_in(self, other: Any) -> Self: """ Check if elements of this expression are present in the other iterable. @@ -877,7 +879,7 @@ def is_in(self, other: Any) -> Expr: "Narwhals `is_in` doesn't accept expressions as an argument, as opposed to Polars. You should provide an iterable instead." ) - def filter(self, *predicates: Any) -> Expr: + def filter(self, *predicates: Any) -> Self: """ Filters elements based on a condition, returning a new expression. @@ -922,7 +924,7 @@ def filter(self, *predicates: Any) -> Expr: ) ) - def is_null(self) -> Expr: + def is_null(self) -> Self: """ Returns a boolean Series indicating which values are null. @@ -975,7 +977,7 @@ def is_null(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).is_null()) - def fill_null(self, value: Any) -> Expr: + def fill_null(self, value: Any) -> Self: """ Fill null values with given value. @@ -1027,7 +1029,7 @@ def fill_null(self, value: Any) -> Expr: return self.__class__(lambda plx: self._call(plx).fill_null(value)) # --- partial reduction --- - def drop_nulls(self) -> Expr: + def drop_nulls(self) -> Self: """ Remove missing values. @@ -1079,7 +1081,7 @@ def sample( fraction: float | None = None, *, with_replacement: bool = False, - ) -> Expr: + ) -> Self: """ Sample randomly from this expression. @@ -1129,7 +1131,7 @@ def sample( ) ) - def over(self, *keys: str | Iterable[str]) -> Expr: + def over(self, *keys: str | Iterable[str]) -> Self: """ Compute expressions over the given groups. @@ -1173,7 +1175,7 @@ def over(self, *keys: str | Iterable[str]) -> Expr: """ return self.__class__(lambda plx: self._call(plx).over(flatten(keys))) - def is_duplicated(self) -> Expr: + def is_duplicated(self) -> Self: r""" Return a boolean mask indicating duplicated values. @@ -1214,7 +1216,7 @@ def is_duplicated(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).is_duplicated()) - def is_unique(self) -> Expr: + def is_unique(self) -> Self: r""" Return a boolean mask indicating unique values. @@ -1256,7 +1258,7 @@ def is_unique(self) -> Expr: return self.__class__(lambda plx: self._call(plx).is_unique()) - def null_count(self) -> Expr: + def null_count(self) -> Self: r""" Count null values. @@ -1295,7 +1297,7 @@ def null_count(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).null_count()) - def is_first_distinct(self) -> Expr: + def is_first_distinct(self) -> Self: r""" Return a boolean mask indicating the first occurrence of each distinct value. @@ -1336,7 +1338,7 @@ def is_first_distinct(self) -> Expr: """ return self.__class__(lambda plx: self._call(plx).is_first_distinct()) - def is_last_distinct(self) -> Expr: + def is_last_distinct(self) -> Self: r"""Return a boolean mask indicating the last occurrence of each distinct value. Examples: @@ -1380,7 +1382,7 @@ def quantile( self, quantile: float, interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"], - ) -> Expr: + ) -> Self: r"""Get quantile value. Note: @@ -1426,7 +1428,7 @@ def quantile( lambda plx: self._call(plx).quantile(quantile, interpolation) ) - def head(self, n: int = 10) -> Expr: + def head(self, n: int = 10) -> Self: r""" Get the first `n` rows. @@ -1470,7 +1472,7 @@ def head(self, n: int = 10) -> Expr: return self.__class__(lambda plx: self._call(plx).head(n)) - def tail(self, n: int = 10) -> Expr: + def tail(self, n: int = 10) -> Self: r""" Get the last `n` rows. @@ -1514,7 +1516,7 @@ def tail(self, n: int = 10) -> Expr: return self.__class__(lambda plx: self._call(plx).tail(n)) - def round(self, decimals: int = 0) -> Expr: + def round(self, decimals: int = 0) -> Self: r""" Round underlying floating point data by `decimals` digits. @@ -1566,7 +1568,7 @@ def round(self, decimals: int = 0) -> Expr: return self.__class__(lambda plx: self._call(plx).round(decimals)) - def len(self) -> Expr: + def len(self) -> Self: r""" Return the number of elements in the column. diff --git a/narwhals/series.py b/narwhals/series.py index a5411bf7c..44a7556ea 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -1172,74 +1172,74 @@ def to_pandas(self) -> Any: """ return self._series.to_pandas() - def __add__(self, other: object) -> Series: + def __add__(self, other: object) -> Self: return self._from_series(self._series.__add__(self._extract_native(other))) - def __radd__(self, other: object) -> Series: + def __radd__(self, other: object) -> Self: return self._from_series(self._series.__radd__(self._extract_native(other))) - def __sub__(self, other: object) -> Series: + def __sub__(self, other: object) -> Self: return self._from_series(self._series.__sub__(self._extract_native(other))) - def __rsub__(self, other: object) -> Series: + def __rsub__(self, other: object) -> Self: return self._from_series(self._series.__rsub__(self._extract_native(other))) - def __mul__(self, other: object) -> Series: + def __mul__(self, other: object) -> Self: return self._from_series(self._series.__mul__(self._extract_native(other))) - def __rmul__(self, other: object) -> Series: + def __rmul__(self, other: object) -> Self: return self._from_series(self._series.__rmul__(self._extract_native(other))) - def __truediv__(self, other: object) -> Series: + def __truediv__(self, other: object) -> Self: return self._from_series(self._series.__truediv__(self._extract_native(other))) - def __floordiv__(self, other: object) -> Series: + def __floordiv__(self, other: object) -> Self: return self._from_series(self._series.__floordiv__(self._extract_native(other))) - def __rfloordiv__(self, other: object) -> Series: + def __rfloordiv__(self, other: object) -> Self: return self._from_series(self._series.__rfloordiv__(self._extract_native(other))) - def __pow__(self, other: object) -> Series: + def __pow__(self, other: object) -> Self: return self._from_series(self._series.__pow__(self._extract_native(other))) - def __rpow__(self, other: object) -> Series: + def __rpow__(self, other: object) -> Self: return self._from_series(self._series.__rpow__(self._extract_native(other))) - def __mod__(self, other: object) -> Series: + def __mod__(self, other: object) -> Self: return self._from_series(self._series.__mod__(self._extract_native(other))) - def __rmod__(self, other: object) -> Series: + def __rmod__(self, other: object) -> Self: return self._from_series(self._series.__rmod__(self._extract_native(other))) - def __eq__(self, other: object) -> Series: # type: ignore[override] + def __eq__(self, other: object) -> Self: # type: ignore[override] return self._from_series(self._series.__eq__(self._extract_native(other))) - def __ne__(self, other: object) -> Series: # type: ignore[override] + def __ne__(self, other: object) -> Self: # type: ignore[override] return self._from_series(self._series.__ne__(self._extract_native(other))) - def __gt__(self, other: Any) -> Series: + def __gt__(self, other: Any) -> Self: return self._from_series(self._series.__gt__(self._extract_native(other))) - def __ge__(self, other: Any) -> Series: # pragma: no cover (todo) + def __ge__(self, other: Any) -> Self: # pragma: no cover (todo) return self._from_series(self._series.__ge__(self._extract_native(other))) - def __lt__(self, other: Any) -> Series: # pragma: no cover (todo) + def __lt__(self, other: Any) -> Self: # pragma: no cover (todo) return self._from_series(self._series.__lt__(self._extract_native(other))) - def __le__(self, other: Any) -> Series: # pragma: no cover (todo) + def __le__(self, other: Any) -> Self: # pragma: no cover (todo) return self._from_series(self._series.__le__(self._extract_native(other))) - def __and__(self, other: Any) -> Series: # pragma: no cover (todo) + def __and__(self, other: Any) -> Self: # pragma: no cover (todo) return self._from_series(self._series.__and__(self._extract_native(other))) - def __or__(self, other: Any) -> Series: # pragma: no cover (todo) + def __or__(self, other: Any) -> Self: # pragma: no cover (todo) return self._from_series(self._series.__or__(self._extract_native(other))) # unary - def __invert__(self) -> Series: + def __invert__(self) -> Self: return self._from_series(self._series.__invert__()) - def filter(self, other: Any) -> Series: + def filter(self, other: Any) -> Self: """ Filter elements in the Series based on a condition. @@ -1276,7 +1276,7 @@ def filter(self, other: Any) -> Series: return self._from_series(self._series.filter(self._extract_native(other))) # --- descriptive --- - def is_duplicated(self: Self) -> Series: + def is_duplicated(self: Self) -> Self: r""" Get a mask of all duplicated rows in the Series. @@ -1311,7 +1311,7 @@ def is_duplicated(self: Self) -> Series: true ] """ - return Series(self._series.is_duplicated()) + return self.__class__(self._series.is_duplicated()) def is_empty(self: Self) -> bool: r""" @@ -1343,7 +1343,7 @@ def is_empty(self: Self) -> bool: """ return self._series.is_empty() # type: ignore[no-any-return] - def is_unique(self: Self) -> Series: + def is_unique(self: Self) -> Self: r""" Get a mask of all unique rows in the Series. @@ -1379,7 +1379,7 @@ def is_unique(self: Self) -> Series: false ] """ - return Series(self._series.is_unique()) + return self.__class__(self._series.is_unique()) def null_count(self: Self) -> int: r""" @@ -1412,7 +1412,7 @@ def null_count(self: Self) -> int: return self._series.null_count() # type: ignore[no-any-return] - def is_first_distinct(self: Self) -> Series: + def is_first_distinct(self: Self) -> Self: r""" Return a boolean mask indicating the first occurrence of each distinct value. @@ -1450,9 +1450,9 @@ def is_first_distinct(self: Self) -> Series: false ] """ - return Series(self._series.is_first_distinct()) + return self.__class__(self._series.is_first_distinct()) - def is_last_distinct(self: Self) -> Series: + def is_last_distinct(self: Self) -> Self: r""" Return a boolean mask indicating the last occurrence of each distinct value. @@ -1490,7 +1490,7 @@ def is_last_distinct(self: Self) -> Series: true ] """ - return Series(self._series.is_last_distinct()) + return self.__class__(self._series.is_last_distinct()) def is_sorted(self: Self, *, descending: bool = False) -> bool: r""" From cc2c1f11983f48122d102d22382468192c13737e Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Fri, 5 Jul 2024 08:20:16 +0100 Subject: [PATCH 3/3] feat: Expr.Abs and Series.abs (#409) * feat: add Expr.abs and Series.abs * cov --- docs/api-reference/expressions.md | 1 + docs/api-reference/series.md | 1 + narwhals/_arrow/expr.py | 3 +++ narwhals/_arrow/series.py | 4 ++++ narwhals/_pandas_like/expr.py | 3 +++ narwhals/_pandas_like/series.py | 3 +++ narwhals/expression.py | 37 +++++++++++++++++++++++++++++++ narwhals/series.py | 36 ++++++++++++++++++++++++++++++ tests/expr/abs_test.py | 15 +++++++++++++ 9 files changed, 103 insertions(+) create mode 100644 tests/expr/abs_test.py diff --git a/docs/api-reference/expressions.md b/docs/api-reference/expressions.md index a717fed78..9068b5839 100644 --- a/docs/api-reference/expressions.md +++ b/docs/api-reference/expressions.md @@ -4,6 +4,7 @@ handler: python options: members: + - abs - alias - all - any diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md index 2687eead5..525f5af6e 100644 --- a/docs/api-reference/series.md +++ b/docs/api-reference/series.md @@ -4,6 +4,7 @@ handler: python options: members: + - abs - alias - all - any diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py index 775168135..c0e587b69 100644 --- a/narwhals/_arrow/expr.py +++ b/narwhals/_arrow/expr.py @@ -71,6 +71,9 @@ def __narwhals_namespace__(self) -> ArrowNamespace: def cast(self, dtype: DType) -> Self: return reuse_series_implementation(self, "cast", dtype) # type: ignore[type-var] + def abs(self) -> Self: + return reuse_series_implementation(self, "abs") # type: ignore[type-var] + def cum_sum(self) -> Self: return reuse_series_implementation(self, "cum_sum") # type: ignore[type-var] diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py index 356412470..d48fcd84e 100644 --- a/narwhals/_arrow/series.py +++ b/narwhals/_arrow/series.py @@ -78,6 +78,10 @@ def alias(self, name: str) -> Self: def dtype(self) -> DType: return translate_dtype(self._series.type) + def abs(self) -> Self: + pc = get_pyarrow_compute() + return self._from_series(pc.abs(self._series)) + def cum_sum(self) -> Self: pc = get_pyarrow_compute() return self._from_series(pc.cumulative_sum(self._series)) diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py index 0b7a1ea79..162c0d172 100644 --- a/narwhals/_pandas_like/expr.py +++ b/narwhals/_pandas_like/expr.py @@ -216,6 +216,9 @@ def drop_nulls(self) -> Self: def sort(self, *, descending: bool = False) -> Self: return reuse_series_implementation(self, "sort", descending=descending) + def abs(self) -> Self: + return reuse_series_implementation(self, "abs") + def cum_sum(self) -> Self: return reuse_series_implementation(self, "cum_sum") diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py index abfd49fb9..3b92a5aa2 100644 --- a/narwhals/_pandas_like/series.py +++ b/narwhals/_pandas_like/series.py @@ -401,6 +401,9 @@ def sample( ser = self._series return self._from_series(ser.sample(n=n, frac=fraction, replace=with_replacement)) + def abs(self) -> PandasSeries: + return self._from_series(self._series.abs()) + def cum_sum(self) -> PandasSeries: return self._from_series(self._series.cumsum()) diff --git a/narwhals/expression.py b/narwhals/expression.py index 9f12a3acb..68fec6660 100644 --- a/narwhals/expression.py +++ b/narwhals/expression.py @@ -564,6 +564,43 @@ def unique(self) -> Self: """ return self.__class__(lambda plx: self._call(plx).unique()) + def abs(self) -> Self: + """ + Return absolute value of each element. + + Examples: + >>> import polars as pl + >>> import pandas as pd + >>> import narwhals as nw + >>> data = {"a": [1, -2], "b": [-3, 4]} + >>> df_pd = pd.DataFrame(data) + >>> df_pl = pl.DataFrame(data) + + Let's define a dataframe-agnostic function: + + >>> @nw.narwhalify + ... def func(df): + ... return df.select(nw.col("a", "b").abs()) + + We can then pass either pandas or Polars to `func`: + + >>> func(df_pd) + a b + 0 1 3 + 1 2 4 + >>> func(df_pl) + shape: (2, 2) + ┌─────┬─────┐ + │ a ┆ b │ + │ --- ┆ --- │ + │ i64 ┆ i64 │ + ╞═════╪═════╡ + │ 1 ┆ 3 │ + │ 2 ┆ 4 │ + └─────┴─────┘ + """ + return self.__class__(lambda plx: self._call(plx).abs()) + def cum_sum(self) -> Self: """ Return cumulative sum. diff --git a/narwhals/series.py b/narwhals/series.py index 44a7556ea..ed8e146e7 100644 --- a/narwhals/series.py +++ b/narwhals/series.py @@ -625,6 +625,42 @@ def drop_nulls(self) -> Self: """ return self._from_series(self._series.drop_nulls()) + def abs(self) -> Self: + """ + Calculate the absolute value of each element. + + Examples: + >>> import pandas as pd + >>> import polars as pl + >>> import narwhals as nw + >>> s = [2, -4, 3] + >>> s_pd = pd.Series(s) + >>> s_pl = pl.Series(s) + + We define a dataframe-agnostic function: + + >>> @nw.narwhalify + ... def func(s_any): + ... return s_any.abs() + + We can then pass either pandas or Polars to `func`: + + >>> func(s_pd) + 0 2 + 1 4 + 2 3 + dtype: int64 + >>> func(s_pl) # doctest: +NORMALIZE_WHITESPACE + shape: (3,) + Series: '' [i64] + [ + 2 + 4 + 3 + ] + """ + return self._from_series(self._series.abs()) + def cum_sum(self) -> Self: """ Calculate the cumulative sum. diff --git a/tests/expr/abs_test.py b/tests/expr/abs_test.py new file mode 100644 index 000000000..eb558d139 --- /dev/null +++ b/tests/expr/abs_test.py @@ -0,0 +1,15 @@ +from typing import Any + +import narwhals as nw +from tests.utils import compare_dicts + + +def test_abs(constructor_with_pyarrow: Any) -> None: + df = nw.from_native( + constructor_with_pyarrow({"a": [1, 2, 3, -4, 5]}), eager_only=True + ) + result = df.select(b=nw.col("a").abs()) + expected = {"b": [1, 2, 3, 4, 5]} + compare_dicts(result, expected) + result = df.select(b=df["a"].abs()) + compare_dicts(result, expected)