|
5 | 5 | import pytest |
6 | 6 |
|
7 | 7 | import narwhals as nw |
8 | | -from tests.utils import POLARS_VERSION, Constructor, ConstructorEager, assert_equal_data |
| 8 | +from tests.utils import ( |
| 9 | + PANDAS_VERSION, |
| 10 | + POLARS_VERSION, |
| 11 | + PYARROW_VERSION, |
| 12 | + Constructor, |
| 13 | + ConstructorEager, |
| 14 | + assert_equal_data, |
| 15 | +) |
9 | 16 |
|
10 | 17 | if TYPE_CHECKING: |
11 | 18 | from collections.abc import Callable |
12 | 19 |
|
| 20 | + from narwhals.typing import DTypeBackend |
| 21 | + |
13 | 22 | data: dict[str, list[float]] = { |
14 | 23 | "int": [-2, 0, 2], |
15 | 24 | "float": [-2.1, 0.0, 2.1], |
@@ -143,3 +152,107 @@ def test_expr_rfloordiv_by_zero( |
143 | 152 | result = df.select(result=numerator // nw.col("denominator")) |
144 | 153 | expected = {"result": expected_floordiv} |
145 | 154 | assert_equal_data(result, expected) |
| 155 | + |
| 156 | + |
| 157 | +# Floor division by zero on pandas-like backends must keep the dtype backend of the |
| 158 | +# input: nullable stays nullable, pyarrow-backed stays pyarrow-backed. numpy-backed |
| 159 | +# integers cannot hold missing values, so they become float64 with NaN. |
| 160 | +PYARROW_UNAVAILABLE = PYARROW_VERSION == (0, 0, 0) |
| 161 | +require_pd_2_1 = pytest.mark.skipif( |
| 162 | + PANDAS_VERSION < (2, 1, 0) or PYARROW_UNAVAILABLE, |
| 163 | + reason="pyarrow-backed dtypes require pandas>=2.1 and pyarrow", |
| 164 | +) |
| 165 | +require_pd_2_0 = pytest.mark.skipif( |
| 166 | + PANDAS_VERSION < (2, 0, 0), reason="nullable dtypes require pandas>=2.0" |
| 167 | +) |
| 168 | +DTYPE_BACKENDS = [ |
| 169 | + pytest.param("pyarrow", marks=require_pd_2_1), |
| 170 | + pytest.param("numpy_nullable", marks=require_pd_2_0), |
| 171 | + None, |
| 172 | +] |
| 173 | +# (input dtype, expected output dtype) per dtype backend and numeric kind. |
| 174 | +FLOORDIV_DTYPES: dict[DTypeBackend, dict[str, tuple[str, str]]] = { |
| 175 | + None: {"int": ("int64", "float64"), "float": ("float64", "float64")}, |
| 176 | + "numpy_nullable": {"int": ("Int64", "Int64"), "float": ("Float64", "Float64")}, |
| 177 | + "pyarrow": { |
| 178 | + "int": ("int64[pyarrow]", "int64[pyarrow]"), |
| 179 | + "float": ("double[pyarrow]", "double[pyarrow]"), |
| 180 | + }, |
| 181 | +} |
| 182 | +INDEX = [8, 7, 6] |
| 183 | + |
| 184 | + |
| 185 | +@pytest.mark.parametrize("dtype_backend", DTYPE_BACKENDS) |
| 186 | +@pytest.mark.parametrize("kind", ["int", "float"]) |
| 187 | +def test_floordiv_by_zero_keeps_dtype_backend_pandas( |
| 188 | + dtype_backend: DTypeBackend, kind: str |
| 189 | +) -> None: |
| 190 | + pytest.importorskip("pandas") |
| 191 | + import pandas as pd |
| 192 | + |
| 193 | + in_dtype, out_dtype = FLOORDIV_DTYPES[dtype_backend][kind] |
| 194 | + numerator = pd.Series([-3, 0, 5], name="a", index=INDEX).astype(in_dtype) |
| 195 | + denominator = pd.Series([0, 2, 2], name="b", index=INDEX).astype(in_dtype) |
| 196 | + series = nw.from_native(numerator, series_only=True) |
| 197 | + |
| 198 | + result = (series // nw.from_native(denominator, series_only=True)).to_native() |
| 199 | + expected = pd.Series([None, 0, 2], name="a", index=INDEX, dtype=out_dtype) |
| 200 | + pd.testing.assert_series_equal(result, expected) |
| 201 | + |
| 202 | + result = (series // 0).to_native() |
| 203 | + expected = pd.Series([None] * 3, name="a", index=INDEX, dtype=out_dtype) |
| 204 | + pd.testing.assert_series_equal(result, expected) |
| 205 | + |
| 206 | + |
| 207 | +@pytest.mark.parametrize("dtype_backend", DTYPE_BACKENDS) |
| 208 | +@pytest.mark.parametrize("kind", ["int", "float"]) |
| 209 | +def test_rfloordiv_by_zero_keeps_dtype_backend_pandas( |
| 210 | + dtype_backend: DTypeBackend, kind: str |
| 211 | +) -> None: |
| 212 | + pytest.importorskip("pandas") |
| 213 | + import pandas as pd |
| 214 | + |
| 215 | + in_dtype, out_dtype = FLOORDIV_DTYPES[dtype_backend][kind] |
| 216 | + denominator = pd.Series([0, 2, -3], name="a", index=INDEX).astype(in_dtype) |
| 217 | + series = nw.from_native(denominator, series_only=True) |
| 218 | + |
| 219 | + result = (7 // series).to_native() |
| 220 | + expected = pd.Series([None, 3, -3], name="a", index=INDEX, dtype=out_dtype) |
| 221 | + pd.testing.assert_series_equal(result, expected) |
| 222 | + |
| 223 | + |
| 224 | +@pytest.mark.parametrize("dtype_backend", DTYPE_BACKENDS) |
| 225 | +def test_expr_floordiv_by_zero_keeps_dtype_backend_pandas( |
| 226 | + dtype_backend: DTypeBackend, |
| 227 | +) -> None: |
| 228 | + pytest.importorskip("pandas") |
| 229 | + import pandas as pd |
| 230 | + |
| 231 | + dtypes = FLOORDIV_DTYPES[dtype_backend] |
| 232 | + native = pd.DataFrame( |
| 233 | + {"int": [-3, 0, 5], "float": [-3.5, 0.0, 5.5], "denominator": [0, 2, 2]}, |
| 234 | + index=INDEX, |
| 235 | + ).astype( |
| 236 | + { |
| 237 | + "int": dtypes["int"][0], |
| 238 | + "float": dtypes["float"][0], |
| 239 | + "denominator": dtypes["int"][0], |
| 240 | + } |
| 241 | + ) |
| 242 | + df = nw.from_native(native, eager_only=True) |
| 243 | + |
| 244 | + result = df.select( |
| 245 | + int_col=nw.col("int") // nw.col("denominator"), |
| 246 | + int_lit=nw.col("int") // nw.lit(0), |
| 247 | + float_col=nw.col("float") // nw.col("denominator"), |
| 248 | + rfloordiv=7 // nw.col("denominator"), |
| 249 | + ).to_native() |
| 250 | + expected = pd.DataFrame( |
| 251 | + { |
| 252 | + "int_col": pd.Series([None, 0, 2], dtype=dtypes["int"][1]), |
| 253 | + "int_lit": pd.Series([None] * 3, dtype=dtypes["int"][1]), |
| 254 | + "float_col": pd.Series([None, 0.0, 2.0], dtype=dtypes["float"][1]), |
| 255 | + "rfloordiv": pd.Series([None, 3, 3], dtype=dtypes["int"][1]), |
| 256 | + } |
| 257 | + ).set_axis(INDEX) |
| 258 | + pd.testing.assert_frame_equal(result, expected) |
0 commit comments