Repository: narwhals-dev/narwhals

Issue:
[Enh]: clarify and/or expand support for `int` column names for Pandas-like dataframes
### We would like to learn about your use case. For example, if this feature is needed  to adopt Narwhals in an open source project, could you please enter the link to it below?


I was looking at how we support `int` column names while working on #355.

### Please describe the purpose of the new feature or describe the problem to solve.

ML libraries like `scikit-learn` currently support pandas dataframes created from numpy arrays with no column names. By default, pandas assigns an `int` index for each column in the array.

Certain methods already work with these int columns, others don't. For example, see:
```python
>>> import numpy as np
>>> import pandas as pd
>>> import narwhals as nw
>>> np_matrix = np.array([[1,2,3], [4,5,6], [7,8,9]])
>>> df = pd.DataFrame(np_matrix)
>>> nw_df = nw.from_native(df, eager_only=True)
```
- `nw_df get_columns(1)` works and also documents the use with `int` columns
- `nw.col(1)` works
- `nw.col(1).alias(4)` works
- `nw_df.rename({1 : 4})` works
- `nw_df.select(1)` or `DataFrame.select([0,1])` don't work: `AssertionError: Expected IntoExpr, got <class 'int'>` ❌
- `DataFrame.with_columns` works with `Expr` and doesn't work with `Series` ⚠️
```python
>>> nw.to_native(nw_df.with_columns(nw.col(1).alias(4)))
	0	1	2	4
0	1	2	3	2
1	4	5	6	5
2	7	8	9	8

>>> nw.to_native(nw_df.with_columns(nw_df.get_column(1).alias(4)))
TypeError: keywords must be strings
```

### Suggest a solution if possible.

We should decide whether we should fix the methods that don't work and also add a documentation page to clarify how to deal with `int` columns (and add the tests if they are missing to ensure we don't drop support for int columns).


My thoughts:
- `select` : allowing `DataFrame.select(<int>)` may be a bit weird. But I think we should suggest an alternative for example `DataFrame.select(nw.col(<int>))`
- `with_columns`:`nw_df.with_columns(nw_df.get_column(1).alias(4))` should be allowed. One may want to do some manipulation to a series before adding it to the dataframe


### If you have tried alternatives, please describe them below.

_No response_

### Additional information that may help us understand your needs.

Please let me know if I missed something. There may be some decision or feature I missed 👀

What do you think @MarcoGorelli and @FBruzzesi ?

Discussion:

Patch:
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index db40fb65..d214a1f4 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -285,9 +285,9 @@ class PandasLikeDataFrame:
                 backend_version=self._backend_version,
             )
         else:
-            df = self._native_dataframe.assign(
-                **{s.name: validate_dataframe_comparand(index, s) for s in new_columns}
-            )
+            df = self._native_dataframe.copy(deep=False)
+            for s in new_columns:
+                df[s.name] = validate_dataframe_comparand(index, s)
         return self._from_native_dataframe(df)
 
     def rename(self, mapping: dict[str, str]) -> Self:


Test patch:
diff --git a/tests/frame/with_columns_test.py b/tests/frame/with_columns_test.py
new file mode 100644
index 00000000..605688fa
--- /dev/null
+++ b/tests/frame/with_columns_test.py
@@ -0,0 +1,15 @@
+import numpy as np
+import pandas as pd
+
+import narwhals as nw
+
+
+def test_with_columns_int_col_name_pandas() -> None:
+    np_matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
+    df = pd.DataFrame(np_matrix, dtype="int64")
+    nw_df = nw.from_native(df, eager_only=True)
+    result = nw_df.with_columns(nw_df.get_column(1).alias(4)).pipe(nw.to_native)  # type: ignore[arg-type]
+    expected = pd.DataFrame(
+        {0: [1, 4, 7], 1: [2, 5, 8], 2: [3, 6, 9], 4: [2, 5, 8]}, dtype="int64"
+    )
+    pd.testing.assert_frame_equal(result, expected)
diff --git a/tests/series_only/value_counts_test.py b/tests/series_only/value_counts_test.py
index abda5979..2a6825c1 100644
--- a/tests/series_only/value_counts_test.py
+++ b/tests/series_only/value_counts_test.py
@@ -1,19 +1,20 @@
-import sys
 from typing import Any
 
+import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
 from tests.utils import compare_dicts
 
 data = {"a": [4, 4, 6, 4, 1, 1]}
 
 
 def test_value_counts(request: Any, constructor: Any) -> None:
-    if "pandas_nullable_constructor" in str(constructor) and sys.version_info < (
-        3,
-        9,
-    ):  # fails for py3.8
+    if "pandas_nullable_constructor" in str(constructor) and parse_version(
+        pd.__version__
+    ) < (2, 2):
+        # bug in old pandas
         request.applymarker(pytest.mark.xfail)
 
     series = nw.from_native(constructor(data), eager_only=True)["a"]