Repository: narwhals-dev/narwhals (python)

Issue:
bug: PyArrow Series.scatter returns wrong results
```python
data = {'a': [
    16,
    12,
    10,
    9,
    6,
    5,
    2
  ],
}
indices = [6, 1, 0, 5, 3, 2, 4]
df = nw.from_native(pa.table(data))
print(df['a'].scatter(indices, df['a']).to_native())
df = nw.from_native(pd.DataFrame(data))
print(df['a'].scatter(indices, df['a']).to_native())
df = nw.from_native(pl.DataFrame(data))
print(df['a'].scatter(indices, df['a']).to_native())
```

This outputs:
```
[
  [
    2,
    12,
    16,
    5,
    9,
    10,
    6
  ]
]
0    10
1    12
2     5
3     6
4     2
5     9
6    16
Name: a, dtype: int64
shape: (7,)
Series: 'a' [i64]
[
        10
        12
        5
        6
        2
        9
        16
]
```

Notice how the PyArrow one differs from the pandas/Polars ones

Pull request description:
fix: pyarrow scatter was returning incorrect results when indices were not sorted
closes #2148 

<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below


Patch:
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 40680de8..141c98fb 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -403,18 +403,28 @@ class ArrowSeries(CompliantSeries):
     def scatter(self: Self, indices: int | Sequence[int], values: Any) -> Self:
         import numpy as np  # ignore-banned-import
 
-        mask: _1DArray = np.zeros(self.len(), dtype=bool)
-        mask[indices] = True
-        if isinstance(values, self.__class__):
-            ser, values = extract_native(self, values)
+        if isinstance(indices, int):
+            indices_native = pa.array([indices])
+            values_native = pa.array([values])
         else:
-            ser = self._native_series
-        if isinstance(values, pa.ChunkedArray):
-            values = values.combine_chunks()
-        if not isinstance(values, pa.Array):
-            values = pa.array(values)
+            # TODO(unassigned): we may also want to let `indices` be a Series.
+            # https://github.com/narwhals-dev/narwhals/issues/2155
+            indices_native = pa.array(indices)
+            if isinstance(values, self.__class__):
+                values_native = values._native_series.combine_chunks()
+            else:
+                values_native = pa.array(values)
+
+        sorting_indices = pc.sort_indices(indices_native)  # type: ignore[call-overload]
+        indices_native = pc.take(indices_native, sorting_indices)
+        values_native = pc.take(values_native, sorting_indices)
+
+        mask: _1DArray = np.zeros(self.len(), dtype=bool)
+        mask[indices_native] = True
         result = pc.replace_with_mask(
-            ser, cast("list[bool]", mask), values.take(cast("Indices", indices))
+            self._native_series,
+            cast("list[bool]", mask),
+            values_native.take(cast("Indices", indices_native)),
         )
         return self._from_native_series(result)
 
diff --git a/pyproject.toml b/pyproject.toml
index 0ef18f69..33d0beb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -195,15 +195,16 @@ filterwarnings = [
   # This warning was temporarily raised by pandas but then reverted.
   'ignore:.*Passing a BlockManager to DataFrame:DeprecationWarning',
   # This warning was temporarily raised by Polars but then reverted.
-  'ignore:.*The default coalesce behavior of left join will change:DeprecationWarning',
+  'ignore:.*The default coalesce behavior of left join will change:DeprecationWarning:polars',
   'ignore: unclosed <socket.socket',
   'ignore:.*The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning:pyspark',
   'ignore:.*distutils Version classes are deprecated. Use packaging.version instead.*:DeprecationWarning:pyspark',
   'ignore:.*is_datetime64tz_dtype is deprecated and will be removed in a future version.*:DeprecationWarning:pyspark',
   # Warning raised by PyArrow nightly just by importing pandas
   'ignore:.*Python binding for RankQuantileOptions not exposed:RuntimeWarning:pyarrow',
-  'ignore:.*pandas only supports SQLAlchemy:UserWarning',
+  'ignore:.*pandas only supports SQLAlchemy:UserWarning:sqlframe',
   'ignore:.*numpy.core is deprecated and has been renamed to numpy._core.*:DeprecationWarning:sqlframe',
+  "ignore:.*__array__ implementation doesn't accept a copy keyword, so passing copy=False failed:DeprecationWarning:modin"
 ]
 xfail_strict = true
 markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]


Test patch:
diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py
index 11065ec9..d7f0aa77 100644
--- a/tests/series_only/scatter_test.py
+++ b/tests/series_only/scatter_test.py
@@ -49,3 +49,28 @@ def test_single_series(constructor_eager: ConstructorEager) -> None:
     s.scatter([0, 1], [999, 888])
     expected = {"a": [1, 2, 3]}
     assert_equal_data({"a": s}, expected)
+
+
+def test_scatter_integer(constructor_eager: ConstructorEager) -> None:
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3], "b": [142, 124, 132]}), eager_only=True
+    )
+    s = df["a"]
+    result = s.scatter(0, 999)
+    expected = {"a": [999, 2, 3]}
+    assert_equal_data({"a": result}, expected)
+
+
+def test_scatter_unordered_indices(
+    constructor_eager: ConstructorEager, request: pytest.FixtureRequest
+) -> None:
+    if "modin" in str(constructor_eager):
+        # bugged
+        request.applymarker(pytest.mark.xfail)
+    data = {
+        "a": [16, 12, 10, 9, 6, 5, 2],
+    }
+    indices = [6, 1, 0, 5, 3, 2, 4]
+    df = nw.from_native(constructor_eager(data))
+    result = df["a"].scatter(indices, df["a"])
+    assert_equal_data({"a": result}, {"a": [10, 12, 5, 6, 2, 9, 16]})