|
87 | 87 | from pandas.core.dtypes.missing import isna, notna
|
88 | 88 |
|
89 | 89 | import pandas as pd
|
90 |
| -from pandas.core import missing, nanops |
| 90 | +from pandas.core import missing, nanops, ops |
91 | 91 | import pandas.core.algorithms as algos
|
92 | 92 | from pandas.core.base import PandasObject, SelectionMixin
|
93 | 93 | import pandas.core.common as com
|
94 |
| -from pandas.core.construction import create_series_with_explicit_dtype |
| 94 | +from pandas.core.construction import create_series_with_explicit_dtype, extract_array |
95 | 95 | from pandas.core.flags import Flags
|
96 | 96 | from pandas.core.indexes import base as ibase
|
97 | 97 | from pandas.core.indexes.api import Index, MultiIndex, RangeIndex, ensure_index
|
@@ -1912,6 +1912,102 @@ def __array_wrap__(
|
1912 | 1912 | self, method="__array_wrap__"
|
1913 | 1913 | )
|
1914 | 1914 |
|
| 1915 | + @ops.defer_or_dispatch_ufunc |
| 1916 | + def __array_ufunc__( |
| 1917 | + self, ufunc: Callable, method: str, *inputs: Any, **kwargs: Any |
| 1918 | + ): |
| 1919 | + # XXX: check outer |
| 1920 | + # align all the inputs. |
| 1921 | + types = tuple(type(x) for x in inputs) |
| 1922 | + alignable = [x for x, t in zip(inputs, types) if issubclass(t, NDFrame)] |
| 1923 | + |
| 1924 | + if len(alignable) > 1: |
| 1925 | + # This triggers alignment. |
| 1926 | + # At the moment, there aren't any ufuncs with more than two inputs |
| 1927 | + # so this ends up just being x1.index | x2.index, but we write |
| 1928 | + # it to handle *args. |
| 1929 | + |
| 1930 | + if len(set(types)) > 1: |
| 1931 | + # We currently don't handle ufunc(DataFrame, Series) |
| 1932 | + # well. Previously this raised an internal ValueError. We might |
| 1933 | + # support it someday, so raise a NotImplementedError. |
| 1934 | + raise NotImplementedError( |
| 1935 | + "Cannot apply ufunc {} to mixed DataFrame and Series " |
| 1936 | + "inputs.".format(ufunc) |
| 1937 | + ) |
| 1938 | + axes = self.axes |
| 1939 | + for obj in alignable[1:]: |
| 1940 | + # this relies on the fact that we aren't handling mixed |
| 1941 | + # series / frame ufuncs. |
| 1942 | + for i, (ax1, ax2) in enumerate(zip(axes, obj.axes)): |
| 1943 | + axes[i] = ax1 | ax2 |
| 1944 | + |
| 1945 | + reconstruct_axes = dict(zip(self._AXIS_ORDERS, axes)) |
| 1946 | + inputs = tuple( |
| 1947 | + x.reindex(**reconstruct_axes) if issubclass(t, NDFrame) else x |
| 1948 | + for x, t in zip(inputs, types) |
| 1949 | + ) |
| 1950 | + else: |
| 1951 | + reconstruct_axes = dict(zip(self._AXIS_ORDERS, self.axes)) |
| 1952 | + |
| 1953 | + if self.ndim == 1: |
| 1954 | + names = [getattr(x, "name") for x in inputs if hasattr(x, "name")] |
| 1955 | + name = names[0] if len(set(names)) == 1 else None |
| 1956 | + reconstruct_kwargs = {"name": name} |
| 1957 | + else: |
| 1958 | + reconstruct_kwargs = {} |
| 1959 | + |
| 1960 | + def reconstruct(result): |
| 1961 | + if lib.is_scalar(result): |
| 1962 | + return result |
| 1963 | + if result.ndim != self.ndim: |
| 1964 | + if method == "outer": |
| 1965 | + if self.ndim == 2: |
| 1966 | + # we already deprecated for Series |
| 1967 | + msg = ( |
| 1968 | + "outer method for ufunc {} is not implemented on " |
| 1969 | + "pandas objects. Returning an ndarray, but in the " |
| 1970 | + "future this will raise a 'NotImplementedError'. " |
| 1971 | + "Consider explicitly converting the DataFrame " |
| 1972 | + "to an array with '.to_numpy()' first." |
| 1973 | + ) |
| 1974 | + warnings.warn(msg.format(ufunc), FutureWarning, stacklevel=4) |
| 1975 | + return result |
| 1976 | + raise NotImplementedError |
| 1977 | + return result |
| 1978 | + if isinstance(result, BlockManager): |
| 1979 | + # we went through BlockManager.apply |
| 1980 | + return self._constructor(result, **reconstruct_kwargs, copy=False) |
| 1981 | + else: |
| 1982 | + # we converted an array, lost our axes |
| 1983 | + return self._constructor( |
| 1984 | + result, **reconstruct_axes, **reconstruct_kwargs, copy=False |
| 1985 | + ) |
| 1986 | + |
| 1987 | + if self.ndim > 1 and (len(inputs) > 1 or ufunc.nout > 1): |
| 1988 | + # Just give up on preserving types in the complex case. |
| 1989 | + # In theory we could preserve them for them. |
| 1990 | + # * nout>1 is doable if BlockManager.apply took nout and |
| 1991 | + # returned a Tuple[BlockManager]. |
| 1992 | + # * len(inputs) > 1 is doable when we know that we have |
| 1993 | + # aligned blocks / dtypes. |
| 1994 | + inputs = tuple(np.asarray(x) for x in inputs) |
| 1995 | + result = getattr(ufunc, method)(*inputs) |
| 1996 | + elif self.ndim == 1: |
| 1997 | + # ufunc(series, ...) |
| 1998 | + inputs = tuple(extract_array(x, extract_numpy=True) for x in inputs) |
| 1999 | + result = getattr(ufunc, method)(*inputs, **kwargs) |
| 2000 | + else: |
| 2001 | + # ufunc(dataframe) |
| 2002 | + mgr = inputs[0]._mgr |
| 2003 | + result = mgr.apply(getattr(ufunc, method)) |
| 2004 | + |
| 2005 | + if ufunc.nout > 1: |
| 2006 | + result = tuple(reconstruct(x) for x in result) |
| 2007 | + else: |
| 2008 | + result = reconstruct(result) |
| 2009 | + return result |
| 2010 | + |
1915 | 2011 | # ideally we would define this to avoid the getattr checks, but
|
1916 | 2012 | # is slower
|
1917 | 2013 | # @property
|
|
0 commit comments