From 36f8bc863dbf174c570d7aa9166ea563b181c7e7 Mon Sep 17 00:00:00 2001 From: ktz03 <2484593937@qq.com> Date: Mon, 12 Oct 2026 17:25:26 +0800 Subject: [PATCH] Preserve fractional cyclic coordinates for integer input --- patsy/mgcv_cubic_splines.py | 22 +++++++++++++++++++++ patsy/test_highlevel.py | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/patsy/mgcv_cubic_splines.py b/patsy/mgcv_cubic_splines.py index 5f936809..a3e675a3 100644 --- a/patsy/mgcv_cubic_splines.py +++ b/patsy/mgcv_cubic_splines.py @@ -75,6 +75,8 @@ def _map_cyclic(x, lbound, ubound): ) x = np.copy(x) + if np.issubdtype(x.dtype, np.integer): + x = x.astype(float) x[x > ubound] = lbound + (x[x > ubound] - ubound) % (ubound - lbound) x[x < lbound] = ubound - (lbound - x[x < lbound]) % (ubound - lbound) @@ -98,6 +100,26 @@ def test__map_cyclic_errors(): pytest.raises(ValueError, _map_cyclic, x, 4.5, 4.5) +def test__map_cyclic_integer(): + x = np.arange(-1, 6) + original = x.copy() + mapped = _map_cyclic(x, 0.1, 3.4) + np.testing.assert_allclose(mapped, [2.3, 3.3, 1.0, 2.0, 3.0, 0.7, 1.7]) + np.testing.assert_array_equal(x, original) + + +def test__map_cyclic_float_dtype(): + for dtype in [np.float32, np.float64]: + x = np.arange(-1, 6, dtype=dtype) + original = x.copy() + mapped = _map_cyclic(x, 0.1, 3.4) + assert mapped.dtype == x.dtype + np.testing.assert_allclose( + mapped, [2.3, 3.3, 1.0, 2.0, 3.0, 0.7, 1.7], rtol=2 * np.finfo(dtype).eps + ) + np.testing.assert_array_equal(x, original) + + def _get_cyclic_f(knots): """Returns mapping of cyclic cubic spline values to 2nd derivatives. diff --git a/patsy/test_highlevel.py b/patsy/test_highlevel.py index 2fad925c..e2425c46 100644 --- a/patsy/test_highlevel.py +++ b/patsy/test_highlevel.py @@ -997,3 +997,41 @@ def test_NAActioon_pandas_string_index(): dm2 = dmatrix(di, data2, 0, return_type="dataframe") assert np.allclose(dm2, [[1.0, 6.0, 4.0], [1.0, 7.0, 5.0]]) assert list(dm2.columns) == ["Intercept", "x", "z"] + + +@pytest.mark.parametrize( + "dtype", [np.int32, np.int64, np.uint64, np.float32, np.float64] +) +@pytest.mark.parametrize("constraints", [None, "center"]) +def test_cc_integer_input(dtype, constraints): + from patsy import cc + + x = np.arange(6, dtype=dtype) + original = x.copy() + kwargs = dict( + knots=[1.0, 2.0, 3.0], + lower_bound=0.1, + upper_bound=3.4, + constraints=constraints, + ) + np.testing.assert_allclose( + cc(x, **kwargs), cc(x.astype(float), **kwargs), rtol=1e-6, atol=1e-6 + ) + formula = ( + "0 + cc(x, knots=[1., 2., 3.], lower_bound=0.1, upper_bound=3.4, " + "constraints=%r)" % constraints + ) + np.testing.assert_allclose( + dmatrix(formula, {"x": x}), + dmatrix(formula, {"x": x.astype(float)}), + rtol=1e-6, + atol=1e-6, + ) + training = dmatrix(formula, {"x": np.array([1.0, 2.0, 3.0])}) + np.testing.assert_allclose( + build_design_matrices([training.design_info], {"x": x})[0], + build_design_matrices([training.design_info], {"x": x.astype(float)})[0], + rtol=1e-6, + atol=1e-6, + ) + np.testing.assert_array_equal(x, original)