[SPARK-59186][PS] Validate NumPy ufunc operand types instead of silently casting them - #58485
Closed
Spenserrrr wants to merge 2 commits into
Closed
[SPARK-59186][PS] Validate NumPy ufunc operand types instead of silently casting them#58485Spenserrrr wants to merge 2 commits into
Spenserrrr wants to merge 2 commits into
Conversation
…rejects The pandas_udf implementations converted to native expressions under SPARK-58532 called NumPy per value, so NumPy's operand type rules applied for free. The expressions that replaced them accept whatever Spark can cast, so np.fmod on a string column returns 1.0 where pandas raises TypeError, and np.ldexp with a float exponent computes 2**2.5. Add a per-ufunc table of accepted Spark types, consulted in maybe_dispatch_ufunc_to_spark_func before any Column is built, so the error comes from the offending call rather than from a later action. A ufunc absent from the table is unchecked.
…ricts them Gating only the mappings converted from a pandas_udf left two spellings of the same operation disagreeing: np.rad2deg raised on a string column while np.degrees returned 401.07, and np.cosh raised where np.sin computed. Extend the table to all 58 mappings whose accepted types can be listed per operand. This changes behaviour for the mappings that were already native expressions, which needs a migration note. A mapping stays out when its accepted operand PAIRS are not a rectangle, so no per-operand set describes them: np.fmax and friends accept (int, int), (str, str) and (timestamp, timestamp) but no mixed pair. They keep computing on an all-null column where pandas raises.
Spenserrrr
marked this pull request as ready for review
September 2, 2026 20:28
Contributor
Author
|
Hi @zhengruifeng! This PR adds a per-ufunc table of the operand types NumPy accepts and checks it in the dispatch, so an unsupported type raises TypeError instead of being cast and computed. There are three things I want to point out:
Could you take a look when you have time? Thanks! |
zhengruifeng
approved these changes
Sep 3, 2026
zhengruifeng
left a comment
Contributor
There was a problem hiding this comment.
shall we add a upstream test to monitor numpy's type coersion?
zhengruifeng
pushed a commit
that referenced
this pull request
Sep 3, 2026
…tly casting them ### What changes were proposed in this pull request? `numpy_compat.py` maps each NumPy ufunc to a Spark expression, and that expression accepts any operand Spark can cast. This PR records the operand types NumPy itself accepts and checks them in the dispatch: - `_np_spark_accepted_types`, mapping each ufunc name to the Spark types it accepts, listed once per operand. `np.ldexp` shows why per operand: it accepts a float first operand, but its exponent must be an integer. - `_check_operand_types`, called from `maybe_dispatch_ufunc_to_spark_func`. It raises `TypeError` at the ufunc call itself, rather than when the result is later collected. `Series`, `Index` and Python scalar operands are all checked, so `np.fmod(psser, "8")` is caught too. - Not in the table: `np.fmax`, `np.fmin`, `np.maximum` and `np.minimum`, which accept two integers or two strings but not one of each, so a list per operand cannot describe them; the four mappings still implemented as a `pandas_udf`, where NumPy checks the types itself; `np.floor_divide`, which the `floordiv` operator handles before this table is consulted; and `np.abs` and `np.bitwise_not`, which arrive under the name of the ufunc they alias. **Why two commits.** The first covers the 27 ufuncs whose native expression is **unreleased** — they were `pandas_udf`s until recently, and the UDF rejected these operand types, so the check restores what they did before the conversion. The second covers the 31 ufuncs that were **already native in released versions**, so that every ufunc behaves the same way; there, adding the check is a behaviour change. Happy to drop the second commit if you would rather ship only the first. Boolean columns are left to a follow-up. Most of these ufuncs raise an `AnalysisException` on one today, where pandas returns a value, because Spark does not implicitly cast a boolean to a numeric type. The follow-up would cast the boolean operands, which was reverted before because it also admitted timestamps and computed a result for them; behind this check it becomes safe. NumPy returns `float16` or `int8` for a boolean input, so the values will match pandas and the dtype will not. ### Why are the changes needed? `np.fmod` on a string column returns `1.0`, and `np.ldexp(float_col, 2.5)` returns `42.4`, where pandas raises `TypeError`. ### Does this PR introduce _any_ user-facing change? Yes, for an operand type NumPy does not accept: ```python >>> np.fmod(psdf.a, psdf.a) # a is a string column TypeError: ufunc 'fmod' is not supported for the input types (string, string). ``` For the 27 ufuncs in the first commit there is no change from any released version, since the `pandas_udf` they replaced raised as well. For the 31 in the second commit it is a change from released versions. The new behaviour matches pandas in both cases. ### How was this patch tested? Four tests in `NumPyCompatTestsMixin`, all failing on a pristine `numpy_compat.py`; one loops the table so later entries need no new row. Every accepted set was checked against pandas and `ufunc.types`. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5) Closes #58485 from Spenserrrr/numpy-ufunc-type-gate. Authored-by: Spenser Sun <hsun112358@gmail.com> Signed-off-by: Ruifeng Zheng <ruifengz@foxmail.com> (cherry picked from commit 0300805) Signed-off-by: Ruifeng Zheng <ruifengz@foxmail.com>
Contributor
Contributor
Author
Yeah I think we can add one. Will create a new PR for this. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
numpy_compat.pymaps each NumPy ufunc to a Spark expression, and that expression accepts any operand Spark can cast. This PR records the operand types NumPy itself accepts and checks them in the dispatch:_np_spark_accepted_types, mapping each ufunc name to the Spark types it accepts, listed once per operand.np.ldexpshows why per operand: it accepts a float first operand, but its exponent must be an integer._check_operand_types, called frommaybe_dispatch_ufunc_to_spark_func. It raisesTypeErrorat the ufunc call itself, rather than when the result is later collected.Series,Indexand Python scalar operands are all checked, sonp.fmod(psser, "8")is caught too.np.fmax,np.fmin,np.maximumandnp.minimum, which accept two integers or two strings but not one of each, so a list per operand cannot describe them; the four mappings still implemented as apandas_udf, where NumPy checks the types itself;np.floor_divide, which thefloordivoperator handles before this table is consulted; andnp.absandnp.bitwise_not, which arrive under the name of the ufunc they alias.Why two commits. The first covers the 27 ufuncs whose native expression is unreleased — they were
pandas_udfs until recently, and the UDF rejected these operand types, so the check restores what they did before the conversion. The second covers the 31 ufuncs that were already native in released versions, so that every ufunc behaves the same way; there, adding the check is a behaviour change. Happy to drop the second commit if you would rather ship only the first.Boolean columns are left to a follow-up. Most of these ufuncs raise an
AnalysisExceptionon one today, where pandas returns a value, because Spark does not implicitly cast a boolean to a numeric type. The follow-up would cast the boolean operands, which was reverted before because it also admitted timestamps and computed a result for them; behind this check it becomes safe. NumPy returnsfloat16orint8for a boolean input, so the values will match pandas and the dtype will not.Why are the changes needed?
np.fmodon a string column returns1.0, andnp.ldexp(float_col, 2.5)returns42.4, where pandas raisesTypeError.Does this PR introduce any user-facing change?
Yes, for an operand type NumPy does not accept:
For the 27 ufuncs in the first commit there is no change from any released version, since the
pandas_udfthey replaced raised as well. For the 31 in the second commit it is a change from released versions. The new behaviour matches pandas in both cases.How was this patch tested?
Four tests in
NumPyCompatTestsMixin, all failing on a pristinenumpy_compat.py; one loops the table so later entries need no new row. Every accepted set was checked against pandas andufunc.types.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)