Spenserrrr opened a new pull request, #58485:
URL: https://github.com/apache/spark/pull/58485
### 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 adds a table of the
operand types NumPy itself accepts and consults it in
`maybe_dispatch_ufunc_to_spark_func`, raising `TypeError` before any `Column`
is built.
The new pieces, all in `numpy_compat.py`:
- `_np_spark_accepted_types`, a dict from ufunc name to **one tuple of Spark
types per operand**. Per-operand rather than per-ufunc because the sets are not
always symmetric: `np.ldexp` takes its exponent from an integer loop only
(`ufunc.types` is `['ei->e', 'fi->f', 'di->d', ...]`), so `np.ldexp(float_col,
2.5)` must raise while the first operand stays numeric.
- Five shared type sets built from Spark's abstract bases (`IntegralType`,
`NumericType`) rather than enumerated widths. Boolean is named explicitly
because Spark does not classify `BooleanType` as a `NumericType`, while NumPy
accepts a boolean wherever it accepts an integer.
- `_check_operand_types`, which reads `inp.spark.data_type` for a `Series`
or `Index` and `as_spark_type(type(inp))` for a scalar, so `np.fmod(psser,
"8")` is caught as well. An operand whose type cannot be determined is left
unchecked.
The check is deliberately placed in the dispatch rather than inside each
mapping function. A mapping function receives `Column`s, whose type is visible
only to the plan, so it could only emit a `RaiseError` node that fires per row
at collect time; the dispatch still holds the `Series`, so a Python `TypeError`
is raised at the ufunc call, as pandas does.
The two commits split the change by risk, and are worth reviewing separately:
1. **The first covers only the 27 mappings converted from a `pandas_udf`.**
Those UDFs called NumPy per value and so enforced its type rules for free;
converting them to native expressions dropped that. Every one of those
conversions is unreleased (`git tag --contains` finds no final tag for any of
them), so this commit restores prior behaviour and changes nothing users have
seen.
2. **The second extends the table to all 58 mappings whose accepted types
can be listed per operand**, including those that were already native
expressions. This is a user-facing change on released versions, and it is here
because gating only the converted half leaves two spellings of the same
operation disagreeing: `np.rad2deg` on a string column raised while
`np.degrees` returned `401.07`. Happy to drop this commit if you would rather
ship the regression fix alone.
A mapping stays out of the table when its accepted operand *pairs* are not a
rectangle, so no per-operand set describes them: `np.fmax`, `np.fmin`,
`np.maximum` and `np.minimum` accept `(int, int)`, `(str, str)` and
`(timestamp, timestamp)` but no mixed pair. They still compute on an all-null
column where pandas raises. Also out: mappings that are still a `pandas_udf`
(NumPy runs per value there), `np.floor_divide` (the `floordiv` dunder wins
before this registry), and alias names, since the lookup uses `ufunc.__name__`
and `np.abs` arrives as `absolute`.
### Why are the changes needed?
A ufunc silently computes a plausible number for operands NumPy rejects:
`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. A ufunc called on an operand type NumPy does not accept now raises
`TypeError` instead of returning a value:
```python
>>> psdf = ps.DataFrame({"a": ["7", "8"]})
>>> np.fmod(psdf.a, psdf.a)
TypeError: ufunc 'fmod' is not supported for the input types (string,
string).
```
For the 27 mappings in the first commit this restores the behaviour of the
released `pandas_udf` implementations. For the rest it is a change against
released versions, matching pandas in every case.
### How was this patch tested?
Four tests in `NumPyCompatTestsMixin`: one loops the table so every entry is
covered as it grows, plus rows for the types only some ufuncs reject, for
scalar operands, and for the accepted types that must still compute. All fail
on a pristine `numpy_compat.py`. Each entry's accepted set was checked against
pandas and against `ufunc.types` for all 58 ufuncs.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]