1"""
2masked_reductions.py is for reduction algorithms using a mask-based approach
3for missing values.
4"""
5
6from __future__ import annotations
7
8from typing import (
9 TYPE_CHECKING,
10 Any,
11)
12import warnings
13
14import numpy as np
15
16from pandas._libs import (
17 lib,
18 missing as libmissing,
19)
20
21from pandas.core.nanops import check_below_min_count
22
23if TYPE_CHECKING:
24 from collections.abc import Callable
25
26 from pandas._typing import (
27 AxisInt,
28 npt,
29 )
30
31
32def _get_min_value(dtype: np.dtype) -> int | float:
33 """Get the minimum value for a given numpy dtype."""
34 if dtype.kind == "f":
35 return -np.inf
36 elif dtype.kind == "u":
37 return 0
38 else:
39 return np.iinfo(dtype).min
40
41
42def _get_max_value(dtype: np.dtype) -> int | float:
43 """Get the maximum value for a given numpy dtype."""
44 if dtype.kind == "f":
45 return np.inf
46 else:
47 return np.iinfo(dtype).max
48
49
50def _reductions(
51 func: Callable[..., Any],
52 values: np.ndarray,
53 mask: npt.NDArray[np.bool_],
54 *,
55 skipna: bool = True,
56 min_count: int = 0,
57 axis: AxisInt | None = None,
58 initial: object | lib.NoDefault = lib.no_default,
59 **kwargs,
60):
61 """
62 Sum, mean or product for 1D masked array.
63
64 Parameters
65 ----------
66 func : np.sum or np.prod
67 values : np.ndarray
68 Numpy array with the values (can be of any dtype that support the
69 operation).
70 mask : np.ndarray[bool]
71 Boolean numpy array (True values indicate missing values).
72 skipna : bool, default True
73 Whether to skip NA.
74 min_count : int, default 0
75 The required number of valid values to perform the operation. If fewer than
76 ``min_count`` non-NA values are present the result will be NA.
77 axis : int, optional, default None
78 initial : scalar, optional
79 Starting value for the reduction. NumPy has a default value for most
80 data types, but for object-dtype arrays we need to specify it explicitly
81 """
82 if initial is not lib.no_default:
83 kwargs["initial"] = initial
84
85 if not skipna:
86 if axis is not None and values.ndim > 1:
87 # For 2D with axis, compute the reduction and let the caller
88 # handle per-row/column masking via _wrap_reduction_result.
89 return func(values, axis=axis, **kwargs)
90 if mask.any() or check_below_min_count(values.shape, None, min_count):
91 return libmissing.NA
92 else:
93 return func(values, axis=axis, **kwargs)
94 else:
95 if check_below_min_count(values.shape, mask, min_count) and (
96 axis is None or values.ndim == 1
97 ):
98 return libmissing.NA
99
100 return func(values, where=~mask, axis=axis, **kwargs)
101
102
103def sum(
104 values: np.ndarray,
105 mask: npt.NDArray[np.bool_],
106 *,
107 skipna: bool = True,
108 min_count: int = 0,
109 axis: AxisInt | None = None,
110 initial: object | lib.NoDefault = lib.no_default,
111):
112 return _reductions(
113 np.sum,
114 values=values,
115 mask=mask,
116 skipna=skipna,
117 min_count=min_count,
118 axis=axis,
119 initial=initial,
120 )
121
122
123def prod(
124 values: np.ndarray,
125 mask: npt.NDArray[np.bool_],
126 *,
127 skipna: bool = True,
128 min_count: int = 0,
129 axis: AxisInt | None = None,
130):
131 return _reductions(
132 np.prod, values=values, mask=mask, skipna=skipna, min_count=min_count, axis=axis
133 )
134
135
136def _minmax(
137 func: Callable[..., Any],
138 values: np.ndarray,
139 mask: npt.NDArray[np.bool_],
140 *,
141 skipna: bool = True,
142 axis: AxisInt | None = None,
143):
144 """
145 Reduction for 1D masked array.
146
147 Parameters
148 ----------
149 func : np.min or np.max
150 values : np.ndarray
151 Numpy array with the values (can be of any dtype that support the
152 operation).
153 mask : np.ndarray[bool]
154 Boolean numpy array (True values indicate missing values).
155 skipna : bool, default True
156 Whether to skip NA.
157 axis : int, optional, default None
158 """
159 if not skipna:
160 if axis is not None and values.ndim > 1:
161 # For 2D with axis, compute the reduction and let the caller
162 # handle per-row/column masking via _wrap_reduction_result.
163 return func(values, axis=axis)
164 if mask.any() or not values.size:
165 # min/max with empty array raise in numpy, pandas returns NA
166 return libmissing.NA
167 else:
168 return func(values, axis=axis)
169 else:
170 if axis is not None and values.ndim > 1:
171 # Can't use values[~mask] which would flatten the array.
172 # Instead, replace masked values with the identity element
173 # and then reduce.
174 if func is np.min:
175 fill_value = _get_max_value(values.dtype)
176 else:
177 fill_value = _get_min_value(values.dtype)
178 filled = np.where(mask, fill_value, values)
179 return func(filled, axis=axis)
180 subset = values[~mask]
181 if subset.size:
182 return func(subset, axis=axis)
183 else:
184 # min/max with empty array raise in numpy, pandas returns NA
185 return libmissing.NA
186
187
188def min(
189 values: np.ndarray,
190 mask: npt.NDArray[np.bool_],
191 *,
192 skipna: bool = True,
193 axis: AxisInt | None = None,
194):
195 return _minmax(np.min, values=values, mask=mask, skipna=skipna, axis=axis)
196
197
198def max(
199 values: np.ndarray,
200 mask: npt.NDArray[np.bool_],
201 *,
202 skipna: bool = True,
203 axis: AxisInt | None = None,
204):
205 return _minmax(np.max, values=values, mask=mask, skipna=skipna, axis=axis)
206
207
208def mean(
209 values: np.ndarray,
210 mask: npt.NDArray[np.bool_],
211 *,
212 skipna: bool = True,
213 axis: AxisInt | None = None,
214):
215 if not values.size or (mask.all() and (axis is None or values.ndim == 1)):
216 return libmissing.NA
217 with warnings.catch_warnings():
218 warnings.simplefilter("ignore", RuntimeWarning)
219 return _reductions(np.mean, values=values, mask=mask, skipna=skipna, axis=axis)
220
221
222def var(
223 values: np.ndarray,
224 mask: npt.NDArray[np.bool_],
225 *,
226 skipna: bool = True,
227 axis: AxisInt | None = None,
228 ddof: int = 1,
229):
230 if not values.size or (mask.all() and (axis is None or values.ndim == 1)):
231 return libmissing.NA
232
233 with warnings.catch_warnings():
234 warnings.simplefilter("ignore", RuntimeWarning)
235 return _reductions(
236 np.var, values=values, mask=mask, skipna=skipna, axis=axis, ddof=ddof
237 )
238
239
240def std(
241 values: np.ndarray,
242 mask: npt.NDArray[np.bool_],
243 *,
244 skipna: bool = True,
245 axis: AxisInt | None = None,
246 ddof: int = 1,
247):
248 if not values.size or (mask.all() and (axis is None or values.ndim == 1)):
249 return libmissing.NA
250
251 with warnings.catch_warnings():
252 warnings.simplefilter("ignore", RuntimeWarning)
253 return _reductions(
254 np.std, values=values, mask=mask, skipna=skipna, axis=axis, ddof=ddof
255 )