Skip to content

Commit c96ec19

Browse files
authored
test: check pandas dtype backends are preserved for floordiv by zero
Refactor imports and add dtype backend tests for floor division by zero in pandas-like backends.
1 parent f8fe578 commit c96ec19

1 file changed

Lines changed: 114 additions & 1 deletion

File tree

‎tests/expr_and_series/division_by_zero_test.py‎

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@
55
import pytest
66

77
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+
)
916

1017
if TYPE_CHECKING:
1118
from collections.abc import Callable
1219

20+
from narwhals.typing import DTypeBackend
21+
1322
data: dict[str, list[float]] = {
1423
"int": [-2, 0, 2],
1524
"float": [-2.1, 0.0, 2.1],
@@ -143,3 +152,107 @@ def test_expr_rfloordiv_by_zero(
143152
result = df.select(result=numerator // nw.col("denominator"))
144153
expected = {"result": expected_floordiv}
145154
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

Comments
 (0)