def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs): # For BooleanArray inputs, we apply the ufunc to ._data # and mask the result. if method == "reduce": # Not clear how to handle missing values in reductions. Raise. raise NotImplementedError("The 'reduce' method is not supported.") out = kwargs.get("out", ()) for x in inputs + out: if not isinstance(x, self._HANDLED_TYPES + (BooleanArray, )): return NotImplemented # for binary ops, use our custom dunder methods result = ops.maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result mask = np.zeros(len(self), dtype=bool) inputs2 = [] for x in inputs: if isinstance(x, BooleanArray): mask |= x._mask inputs2.append(x._data) else: inputs2.append(x) def reconstruct(x): # we don't worry about scalar `x` here, since we # raise for reduce up above. if is_bool_dtype(x.dtype): m = mask.copy() return BooleanArray(x, m) else: x[mask] = np.nan return x result = getattr(ufunc, method)(*inputs2, **kwargs) if isinstance(result, tuple): tuple(reconstruct(x) for x in result) else: return reconstruct(result)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): # Lightly modified version of # https://numpy.org/doc/stable/reference/generated/numpy.lib.mixins.NDArrayOperatorsMixin.html # The primary modification is not boxing scalar return values # in PandasArray, since pandas' ExtensionArrays are 1-d. out = kwargs.get("out", ()) result = ops.maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result if method == "reduce": result = arraylike.dispatch_reduction_ufunc( self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: # e.g. tests.series.test_ufunc.TestNumpyReductions return result # Defer to the implementation of the ufunc on unwrapped values. inputs = tuple(x._ndarray if isinstance(x, PandasArray) else x for x in inputs) if out: kwargs["out"] = tuple( x._ndarray if isinstance(x, PandasArray) else x for x in out) result = getattr(ufunc, method)(*inputs, **kwargs) if ufunc.nout > 1: # multiple return values; re-box array-like results return tuple(type(self)(x) for x in result) elif method == "at": # no return value return None elif method == "reduce": if isinstance(result, np.ndarray): # e.g. test_np_reduce_2d return type(self)(result) # e.g. test_np_max_nested_tuples return result else: # one return value; re-box array-like results return type(self)(result)
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): # For MaskedArray inputs, we apply the ufunc to ._data # and mask the result. out = kwargs.get("out", ()) for x in inputs + out: if not isinstance(x, self._HANDLED_TYPES + (BaseMaskedArray,)): return NotImplemented # for binary ops, use our custom dunder methods result = ops.maybe_dispatch_ufunc_to_dunder_op( self, ufunc, method, *inputs, **kwargs ) if result is not NotImplemented: return result if "out" in kwargs: # e.g. test_ufunc_with_out return arraylike.dispatch_ufunc_with_out( self, ufunc, method, *inputs, **kwargs ) if method == "reduce": result = arraylike.dispatch_reduction_ufunc( self, ufunc, method, *inputs, **kwargs ) if result is not NotImplemented: return result mask = np.zeros(len(self), dtype=bool) inputs2 = [] for x in inputs: if isinstance(x, BaseMaskedArray): mask |= x._mask inputs2.append(x._data) else: inputs2.append(x) def reconstruct(x): # we don't worry about scalar `x` here, since we # raise for reduce up above. from pandas.core.arrays import ( BooleanArray, FloatingArray, IntegerArray, ) if is_bool_dtype(x.dtype): m = mask.copy() return BooleanArray(x, m) elif is_integer_dtype(x.dtype): m = mask.copy() return IntegerArray(x, m) elif is_float_dtype(x.dtype): m = mask.copy() if x.dtype == np.float16: # reached in e.g. np.sqrt on BooleanArray # we don't support float16 x = x.astype(np.float32) return FloatingArray(x, m) else: x[mask] = np.nan return x result = getattr(ufunc, method)(*inputs2, **kwargs) if ufunc.nout > 1: # e.g. np.divmod return tuple(reconstruct(x) for x in result) elif method == "reduce": # e.g. np.add.reduce; test_ufunc_reduce_raises if self._mask.any(): return self._na_value return result else: return reconstruct(result)
def array_ufunc(self, ufunc: Callable, method: str, *inputs: Any, **kwargs: Any): """ Compatibility with numpy ufuncs. See also -------- numpy.org/doc/stable/reference/arrays.classes.html#numpy.class.__array_ufunc__ """ from pandas.core.generic import NDFrame from pandas.core.internals import BlockManager cls = type(self) # for backwards compatibility check and potentially fallback for non-aligned frames result = _maybe_fallback(ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result # for binary ops, use our custom dunder methods result = maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result # Determine if we should defer. no_defer = (np.ndarray.__array_ufunc__, cls.__array_ufunc__) for item in inputs: higher_priority = (hasattr(item, "__array_priority__") and item.__array_priority__ > self.__array_priority__) has_array_ufunc = (hasattr(item, "__array_ufunc__") and type(item).__array_ufunc__ not in no_defer and not isinstance(item, self._HANDLED_TYPES)) if higher_priority or has_array_ufunc: return NotImplemented # align all the inputs. types = tuple(type(x) for x in inputs) alignable = [x for x, t in zip(inputs, types) if issubclass(t, NDFrame)] if len(alignable) > 1: # This triggers alignment. # At the moment, there aren't any ufuncs with more than two inputs # so this ends up just being x1.index | x2.index, but we write # it to handle *args. if len(set(types)) > 1: # We currently don't handle ufunc(DataFrame, Series) # well. Previously this raised an internal ValueError. We might # support it someday, so raise a NotImplementedError. raise NotImplementedError( "Cannot apply ufunc {} to mixed DataFrame and Series " "inputs.".format(ufunc)) axes = self.axes for obj in alignable[1:]: # this relies on the fact that we aren't handling mixed # series / frame ufuncs. for i, (ax1, ax2) in enumerate(zip(axes, obj.axes)): axes[i] = ax1.union(ax2) reconstruct_axes = dict(zip(self._AXIS_ORDERS, axes)) inputs = tuple( x.reindex(**reconstruct_axes) if issubclass(t, NDFrame) else x for x, t in zip(inputs, types)) else: reconstruct_axes = dict(zip(self._AXIS_ORDERS, self.axes)) if self.ndim == 1: names = [getattr(x, "name") for x in inputs if hasattr(x, "name")] name = names[0] if len(set(names)) == 1 else None reconstruct_kwargs = {"name": name} else: reconstruct_kwargs = {} def reconstruct(result): if lib.is_scalar(result): return result if result.ndim != self.ndim: if method == "outer": if self.ndim == 2: # we already deprecated for Series msg = ("outer method for ufunc {} is not implemented on " "pandas objects. Returning an ndarray, but in the " "future this will raise a 'NotImplementedError'. " "Consider explicitly converting the DataFrame " "to an array with '.to_numpy()' first.") warnings.warn(msg.format(ufunc), FutureWarning, stacklevel=4) return result raise NotImplementedError return result if isinstance(result, BlockManager): # we went through BlockManager.apply result = self._constructor(result, **reconstruct_kwargs, copy=False) else: # we converted an array, lost our axes result = self._constructor(result, **reconstruct_axes, **reconstruct_kwargs, copy=False) # TODO: When we support multiple values in __finalize__, this # should pass alignable to `__fianlize__` instead of self. # Then `np.add(a, b)` would consider attrs from both a and b # when a and b are NDFrames. if len(alignable) == 1: result = result.__finalize__(self) return result if self.ndim > 1 and (len(inputs) > 1 or ufunc.nout > 1 # type: ignore[attr-defined] ): # Just give up on preserving types in the complex case. # In theory we could preserve them for them. # * nout>1 is doable if BlockManager.apply took nout and # returned a Tuple[BlockManager]. # * len(inputs) > 1 is doable when we know that we have # aligned blocks / dtypes. inputs = tuple(np.asarray(x) for x in inputs) result = getattr(ufunc, method)(*inputs, **kwargs) elif self.ndim == 1: # ufunc(series, ...) inputs = tuple(extract_array(x, extract_numpy=True) for x in inputs) result = getattr(ufunc, method)(*inputs, **kwargs) else: # ufunc(dataframe) if method == "__call__" and not kwargs: # for np.<ufunc>(..) calls # kwargs cannot necessarily be handled block-by-block, so only # take this path if there are no kwargs mgr = inputs[0]._mgr result = mgr.apply(getattr(ufunc, method)) else: # otherwise specific ufunc methods (eg np.<ufunc>.accumulate(..)) # Those can have an axis keyword and thus can't be called block-by-block result = getattr(ufunc, method)(np.asarray(inputs[0]), **kwargs) if ufunc.nout > 1: # type: ignore[attr-defined] result = tuple(reconstruct(x) for x in result) else: result = reconstruct(result) return result
def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): # For MaskedArray inputs, we apply the ufunc to ._data # and mask the result. if method == "reduce" and ufunc not in [np.maximum, np.minimum]: # Not clear how to handle missing values in reductions. Raise. raise NotImplementedError("The 'reduce' method is not supported.") out = kwargs.get("out", ()) for x in inputs + out: if not isinstance(x, self._HANDLED_TYPES + (BaseMaskedArray, )): return NotImplemented # for binary ops, use our custom dunder methods result = ops.maybe_dispatch_ufunc_to_dunder_op(self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result if method == "reduce": result = arraylike.dispatch_reduction_ufunc( self, ufunc, method, *inputs, **kwargs) if result is not NotImplemented: return result mask = np.zeros(len(self), dtype=bool) inputs2 = [] for x in inputs: if isinstance(x, BaseMaskedArray): mask |= x._mask inputs2.append(x._data) else: inputs2.append(x) def reconstruct(x): # we don't worry about scalar `x` here, since we # raise for reduce up above. from pandas.core.arrays import ( BooleanArray, FloatingArray, IntegerArray, ) if is_bool_dtype(x.dtype): m = mask.copy() return BooleanArray(x, m) elif is_integer_dtype(x.dtype): m = mask.copy() return IntegerArray(x, m) elif is_float_dtype(x.dtype): m = mask.copy() if x.dtype == np.float16: # reached in e.g. np.sqrt on BooleanArray # we don't support float16 x = x.astype(np.float32) return FloatingArray(x, m) else: x[mask] = np.nan return x result = getattr(ufunc, method)(*inputs2, **kwargs) if isinstance(result, tuple): return tuple(reconstruct(x) for x in result) else: return reconstruct(result)