Skip to content

Commit 31f58da

Browse files
committed
[SPARK-58249][PS] Use native functions for NumPy math ufuncs
### What changes were proposed in this pull request? Replace the scalar pandas UDF mappings or column expressions for NumPy `cosh`, `deg2rad`, `exp2`, `fabs`, `negative`, `positive`, `rad2deg`, `sign`, `sinh`, `square`, and `tanh` on pandas-on-Spark objects with native Spark SQL functions or expressions. `np.square` casts its input to `double` before multiplication, avoiding fixed-width integer overflow under ANSI mode. The end-to-end pandas-on-Spark coverage compares the mapped ufunc results with pandas; it includes the `int64` minimum value for `fabs` and `NaN` and signed-zero values for `sign`. ### Why are the changes needed? These mappings originated before the corresponding native Spark SQL functions and expressions were available. Current Spark provides native equivalents, including Spark Connect support, so this removes the Python worker boundary while preserving NumPy-compatible results. ### Does this PR introduce _any_ user-facing change? Yes. `np.square` on overflowing integer values now produces a double-valued result instead of NumPy fixed-width integer overflow. The other mapped ufuncs preserve their existing NumPy-compatible results. ### How was this patch tested? - Added end-to-end pandas-on-Spark coverage for the mapped ufuncs. - Ran `build/sbt -Phive package`. - Ran `SPARK_TESTING=1 SPARK_PREPEND_CLASSES=1 PYSPARK_PYTHON=.venv/bin/python PYSPARK_DRIVER_PYTHON=.venv/bin/python bin/pyspark pyspark.pandas.tests.test_numpy_compat`. - Ran `git diff --check`. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Codex (GPT-5) Closes #57417 from zhengruifeng/pandas-native-math-ufuncs-dev2. Authored-by: Ruifeng Zheng <ruifengz@apache.org> Signed-off-by: Ruifeng Zheng <ruifengz@apache.org>
1 parent c1d8707 commit 31f58da

2 files changed

Lines changed: 23 additions & 13 deletions

File tree

python/pyspark/pandas/numpy_compat.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939
"conj": lambda _: NotImplemented,
4040
"conjugate": lambda _: NotImplemented, # It requires complex type
4141
"cos": F.cos,
42-
"cosh": pandas_udf(lambda s: np.cosh(s), DoubleType()), # type: ignore[call-overload]
43-
"deg2rad": pandas_udf(lambda s: np.deg2rad(s), DoubleType()), # type: ignore[call-overload]
42+
"cosh": F.cosh,
43+
"deg2rad": F.radians,
4444
"degrees": F.degrees,
4545
"exp": F.exp,
46-
"exp2": pandas_udf(lambda s: np.exp2(s), DoubleType()), # type: ignore[call-overload]
46+
"exp2": lambda c: F.pow(F.lit(2.0), c),
4747
"expm1": F.expm1,
48-
"fabs": pandas_udf(lambda s: np.fabs(s), DoubleType()), # type: ignore[call-overload]
48+
"fabs": lambda c: F.abs(c.cast("double")),
4949
"floor": F.floor,
5050
"frexp": lambda _: NotImplemented, # 'frexp' output lengths become different
5151
# and it cannot be supported via pandas UDF.
@@ -57,26 +57,25 @@
5757
"log": F.log,
5858
"log10": F.log10,
5959
"log1p": F.log1p,
60-
"log2": pandas_udf(lambda s: np.log2(s), DoubleType()), # type: ignore[call-overload]
6160
"logical_not": lambda c: ~(c.cast(BooleanType())),
6261
"matmul": lambda _: NotImplemented, # Can return a NumPy array in pandas.
63-
"negative": lambda c: c * -1,
64-
"positive": lambda c: c,
65-
"rad2deg": pandas_udf(lambda s: np.rad2deg(s), DoubleType()), # type: ignore[call-overload]
62+
"negative": F.negative,
63+
"positive": F.positive,
64+
"rad2deg": F.degrees,
6665
"radians": F.radians,
6766
"reciprocal": pandas_udf( # type: ignore[call-overload]
6867
lambda s: np.reciprocal(s), DoubleType()
6968
),
7069
"rint": pandas_udf(lambda s: np.rint(s), DoubleType()), # type: ignore[call-overload]
71-
"sign": lambda c: F.when(c == 0, 0).when(c < 0, -1).otherwise(1),
70+
"sign": F.signum,
7271
"signbit": lambda c: F.when(c < 0, True).otherwise(False),
7372
"sin": F.sin,
74-
"sinh": pandas_udf(lambda s: np.sinh(s), DoubleType()), # type: ignore[call-overload]
73+
"sinh": F.sinh,
7574
"spacing": pandas_udf(lambda s: np.spacing(s), DoubleType()), # type: ignore[call-overload]
7675
"sqrt": F.sqrt,
77-
"square": pandas_udf(lambda s: np.square(s), DoubleType()), # type: ignore[call-overload]
76+
"square": lambda c: c.cast("double") * c,
7877
"tan": F.tan,
79-
"tanh": pandas_udf(lambda s: np.tanh(s), DoubleType()), # type: ignore[call-overload]
78+
"tanh": F.tanh,
8079
"trunc": pandas_udf(lambda s: np.trunc(s), DoubleType()), # type: ignore[call-overload]
8180
}
8281

python/pyspark/pandas/tests/test_numpy_compat.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,22 @@ def test_np_unsupported_frame(self):
8585
with self.assertRaisesRegex(ValueError, "cannot join with no overlapping index names"):
8686
np.left_shift(psdf1, psdf2)
8787

88-
def test_np_inverse_hyperbolic_series_uses_native_functions(self):
88+
def test_np_math_functions(self):
8989
for np_func, values in (
9090
(np.arccosh, [1.0, 2.0, 4.0]),
9191
(np.arcsinh, [-2.0, 0.0, 2.0]),
9292
(np.arctanh, [-0.5, 0.0, 0.5]),
93+
(np.cosh, [-2.0, 0.0, 2.0]),
94+
(np.deg2rad, [-180.0, 0.0, 180.0]),
95+
(np.exp2, [-2.0, 0.0, 2.0]),
96+
(np.fabs, [np.iinfo(np.int64).min, -2, 0, 2]),
97+
(np.negative, [-2.0, 0.0, 2.0]),
98+
(np.positive, [-2.0, 0.0, 2.0]),
99+
(np.rad2deg, [-np.pi, 0.0, np.pi]),
100+
(np.sign, [-2.0, -0.0, 0.0, 2.0, np.nan]),
101+
(np.sinh, [-2.0, 0.0, 2.0]),
102+
(np.square, [-2.0, 0.0, 2.0]),
103+
(np.tanh, [-2.0, 0.0, 2.0]),
93104
):
94105
with self.subTest(name=np_func.__name__):
95106
pdf = pd.DataFrame({"a": values})

0 commit comments

Comments
 (0)