Repository: narwhals-dev/narwhals
Pull request #191 (2024-05-19 16:28:56)

add By dtype selector (flixbus PR)
first step towards #139 

me RN:

![image](https://github.com/narwhals-dev/narwhals/assets/33491632/611f483a-b815-4c0e-8b97-6cac35610f64)



Patch:
diff --git a/docs/api-reference/selectors.md b/docs/api-reference/selectors.md
new file mode 100644
index 00000000..cce2d2c7
--- /dev/null
+++ b/docs/api-reference/selectors.md
@@ -0,0 +1,11 @@
+# `narwhals.selectors`
+
+::: narwhals.selectors
+    handler: python
+    options:
+      members:
+        - by_dtype
+        - numeric
+      show_root_heading: false
+      show_source: false
+      show_bases: false
diff --git a/mkdocs.yml b/mkdocs.yml
index ea3e5fac..3a1ccce3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -27,6 +27,7 @@ nav:
     - api-reference/expressions_str.md
     - api-reference/dtypes.md
     - api-reference/dependencies.md
+    - api-reference/selectors.md
 theme:
   name: material
   font: false
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 54c63311..b13f4097 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -150,7 +150,7 @@ def agg_pandas(  # noqa: PLR0913
             for result_keys in results_keys:
                 out_group.append(item(result_keys._series))
                 out_names.append(result_keys.name)
-        return plx.make_native_series(name="", data=out_group, index=out_names)
+        return plx._make_native_series(name="", data=out_group, index=out_names)
 
     if implementation == "pandas":
         import pandas as pd
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index bc8ef788..06f5f03f 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -9,6 +9,7 @@ from typing import Iterable
 from narwhals import dtypes
 from narwhals._pandas_like.dataframe import PandasDataFrame
 from narwhals._pandas_like.expr import PandasExpr
+from narwhals._pandas_like.selectors import PandasSelector
 from narwhals._pandas_like.series import PandasSeries
 from narwhals._pandas_like.utils import horizontal_concat
 from narwhals._pandas_like.utils import parse_into_exprs
@@ -36,7 +37,11 @@ class PandasNamespace:
     String = dtypes.String
     Datetime = dtypes.Datetime
 
-    def make_native_series(self, name: str, data: list[Any], index: Any) -> Any:
+    @property
+    def selectors(self) -> PandasSelector:
+        return PandasSelector(self._implementation)
+
+    def _make_native_series(self, name: str, data: list[Any], index: Any) -> Any:
         if self._implementation == "pandas":
             import pandas as pd
 
diff --git a/narwhals/_pandas_like/selectors.py b/narwhals/_pandas_like/selectors.py
new file mode 100644
index 00000000..df065517
--- /dev/null
+++ b/narwhals/_pandas_like/selectors.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from narwhals._pandas_like.expr import PandasExpr
+
+if TYPE_CHECKING:
+    from narwhals._pandas_like.dataframe import PandasDataFrame
+    from narwhals._pandas_like.series import PandasSeries
+    from narwhals.dtypes import DType
+
+
+class PandasSelector:
+    def __init__(self, implementation: str) -> None:
+        self._implementation = implementation
+
+    def by_dtype(self, dtypes: list[DType]) -> PandasExpr:
+        def func(df: PandasDataFrame) -> list[PandasSeries]:
+            return [df[col] for col in df.columns if df.schema[col] in dtypes]
+
+        return PandasExpr(
+            func,
+            depth=0,
+            function_name="type_selector",
+            root_names=None,
+            output_names=None,
+            implementation=self._implementation,
+        )
diff --git a/narwhals/selectors.py b/narwhals/selectors.py
new file mode 100644
index 00000000..8e170a91
--- /dev/null
+++ b/narwhals/selectors.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+from typing import Any
+
+from narwhals import dtypes
+from narwhals.dtypes import translate_dtype
+from narwhals.expression import Expr
+from narwhals.utils import flatten
+
+
+def by_dtype(*dtypes: Any) -> Expr:
+    """
+    Select columns based on their dtype.
+
+    Arguments:
+        dtypes: one or data types to select
+
+    Examples:
+        >>> import narwhals as nw
+        >>> import narwhals.selectors as ncs
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>>
+        >>> data = {'a': [1, 2], 'b': ['x', 'y'], 'c': [4.1, 2.3]}
+        >>> df_pd = pd.DataFrame(data)
+        >>> df_pl = pl.DataFrame(data)
+
+        Let's define a dataframe-agnostic function to select int64 and float64
+        dtypes and multiplies each value by 2:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(ncs.by_dtype(nw.Int64, nw.Float64)*2)
+        ...     return nw.to_native(df)
+
+        We can then pass either pandas or Polars dataframes:
+
+        >>> func(df_pd)
+           a    c
+        0  2  8.2
+        1  4  4.6
+        >>> func(df_pl)
+        shape: (2, 2)
+        ┌─────┬─────┐
+        │ a   ┆ c   │
+        │ --- ┆ --- │
+        │ i64 ┆ f64 │
+        ╞═════╪═════╡
+        │ 2   ┆ 8.2 │
+        │ 4   ┆ 4.6 │
+        └─────┴─────┘
+    """
+    return Expr(
+        lambda plx: plx.selectors.by_dtype(
+            [translate_dtype(plx, dtype) for dtype in flatten(dtypes)]
+        )
+    )
+
+
+def numeric() -> Expr:
+    """
+    Select numeric columns.
+
+    Examples:
+        >>> import narwhals as nw
+        >>> import narwhals.selectors as ncs
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>>
+        >>> data = {'a': [1, 2], 'b': ['x', 'y'], 'c': [4.1, 2.3]}
+        >>> df_pd = pd.DataFrame(data)
+        >>> df_pl = pl.DataFrame(data)
+
+        Let's define a dataframe-agnostic function to select numeric
+        dtypes and multiplies each value by 2:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(ncs.by_dtype(nw.Int64, nw.Float64)*2)
+        ...     return nw.to_native(df)
+
+        We can then pass either pandas or Polars dataframes:
+
+        >>> func(df_pd)
+           a    c
+        0  2  8.2
+        1  4  4.6
+        >>> func(df_pl)
+        shape: (2, 2)
+        ┌─────┬─────┐
+        │ a   ┆ c   │
+        │ --- ┆ --- │
+        │ i64 ┆ f64 │
+        ╞═════╪═════╡
+        │ 2   ┆ 8.2 │
+        │ 4   ┆ 4.6 │
+        └─────┴─────┘
+    """
+    return by_dtype(
+        dtypes.Int64,
+        dtypes.Int32,
+        dtypes.Int16,
+        dtypes.Int8,
+        dtypes.UInt64,
+        dtypes.UInt32,
+        dtypes.UInt16,
+        dtypes.UInt8,
+        dtypes.Float64,
+        dtypes.Float32,
+    )
diff --git a/narwhals/series.py b/narwhals/series.py
index aa902879..9954f890 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -437,34 +437,6 @@ class Series:
         return self._series.sum()
 
     def std(self, *, ddof: int = 1) -> Any:
-        """
-        Get the standard deviation of this Series.
-
-        Arguments:
-            ddof: “Delta Degrees of Freedom”: the divisor used in the calculation is N - ddof,
-                     where N represents the number of elements.
-
-        Examples:
-            >>> import pandas as pd
-            >>> import polars as pl
-            >>> import narwhals as nw
-            >>> s = [1, 2, 3]
-            >>> s_pd = pd.Series(s)
-            >>> s_pl = pl.Series(s)
-
-            We define a library agnostic function:
-
-            >>> def func(s_any):
-            ...     s = nw.from_native(s_any, series_only=True)
-            ...     return s.std()
-
-            We can then pass either pandas or Polars to `func`:
-
-            >>> func(s_pd)
-            1.0
-            >>> func(s_pl)
-            1.0
-        """
         return self._series.std(ddof=ddof)
 
     def is_in(self, other: Any) -> Self:


Test patch:
diff --git a/tests/selectors_test.py b/tests/selectors_test.py
new file mode 100644
index 00000000..6d9fca87
--- /dev/null
+++ b/tests/selectors_test.py
@@ -0,0 +1,28 @@
+from typing import Any
+
+import pandas as pd
+import polars as pl
+import pytest
+
+import narwhals as nw
+from narwhals.selectors import by_dtype
+from narwhals.selectors import numeric
+from tests.utils import compare_dicts
+
+data = {"a": [1, 1, 2], "b": ["a", "b", "c"], "c": [4.0, 5.0, 6.0]}
+
+
+@pytest.mark.parametrize("constructor", [pd.DataFrame, pl.DataFrame])
+def test_selecctors(constructor: Any) -> None:
+    df = nw.from_native(constructor(data))
+    result = nw.to_native(df.select(by_dtype([nw.Int64, nw.Float64]) + 1))
+    expected = {"a": [2, 2, 3], "c": [5.0, 6.0, 7.0]}
+    compare_dicts(result, expected)
+
+
+@pytest.mark.parametrize("constructor", [pd.DataFrame, pl.DataFrame])
+def test_numeric(constructor: Any) -> None:
+    df = nw.from_native(constructor(data))
+    result = nw.to_native(df.select(numeric() + 1))
+    expected = {"a": [2, 2, 3], "c": [5.0, 6.0, 7.0]}
+    compare_dicts(result, expected)