Coverage for /usr/lib/python3/dist-packages/scipy/stats/_axis_nan_policy.py: 25%
292 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1# Many scipy.stats functions support `axis` and `nan_policy` parameters.
2# When the two are combined, it can be tricky to get all the behavior just
3# right. This file contains utility functions useful for scipy.stats functions
4# that support `axis` and `nan_policy`, including a decorator that
5# automatically adds `axis` and `nan_policy` arguments to a function.
7import numpy as np
8from functools import wraps
9from scipy._lib._docscrape import FunctionDoc, Parameter
10from scipy._lib._util import _contains_nan
11import inspect
14def _broadcast_arrays(arrays, axis=None):
15 """
16 Broadcast shapes of arrays, ignoring incompatibility of specified axes
17 """
18 new_shapes = _broadcast_array_shapes(arrays, axis=axis)
19 if axis is None:
20 new_shapes = [new_shapes]*len(arrays)
21 return [np.broadcast_to(array, new_shape)
22 for array, new_shape in zip(arrays, new_shapes)]
25def _broadcast_array_shapes(arrays, axis=None):
26 """
27 Broadcast shapes of arrays, ignoring incompatibility of specified axes
28 """
29 shapes = [np.asarray(arr).shape for arr in arrays]
30 return _broadcast_shapes(shapes, axis)
33def _broadcast_shapes(shapes, axis=None):
34 """
35 Broadcast shapes, ignoring incompatibility of specified axes
36 """
37 if not shapes:
38 return shapes
40 # input validation
41 if axis is not None:
42 axis = np.atleast_1d(axis)
43 axis_int = axis.astype(int)
44 if not np.array_equal(axis_int, axis):
45 raise np.AxisError('`axis` must be an integer, a '
46 'tuple of integers, or `None`.')
47 axis = axis_int
49 # First, ensure all shapes have same number of dimensions by prepending 1s.
50 n_dims = max([len(shape) for shape in shapes])
51 new_shapes = np.ones((len(shapes), n_dims), dtype=int)
52 for row, shape in zip(new_shapes, shapes):
53 row[len(row)-len(shape):] = shape # can't use negative indices (-0:)
55 # Remove the shape elements of the axes to be ignored, but remember them.
56 if axis is not None:
57 axis[axis < 0] = n_dims + axis[axis < 0]
58 axis = np.sort(axis)
59 if axis[-1] >= n_dims or axis[0] < 0:
60 message = (f"`axis` is out of bounds "
61 f"for array of dimension {n_dims}")
62 raise np.AxisError(message)
64 if len(np.unique(axis)) != len(axis):
65 raise np.AxisError("`axis` must contain only distinct elements")
67 removed_shapes = new_shapes[:, axis]
68 new_shapes = np.delete(new_shapes, axis, axis=1)
70 # If arrays are broadcastable, shape elements that are 1 may be replaced
71 # with a corresponding non-1 shape element. Assuming arrays are
72 # broadcastable, that final shape element can be found with:
73 new_shape = np.max(new_shapes, axis=0)
74 # except in case of an empty array:
75 new_shape *= new_shapes.all(axis=0)
77 # Among all arrays, there can only be one unique non-1 shape element.
78 # Therefore, if any non-1 shape element does not match what we found
79 # above, the arrays must not be broadcastable after all.
80 if np.any(~((new_shapes == 1) | (new_shapes == new_shape))):
81 raise ValueError("Array shapes are incompatible for broadcasting.")
83 if axis is not None:
84 # Add back the shape elements that were ignored
85 new_axis = axis - np.arange(len(axis))
86 new_shapes = [tuple(np.insert(new_shape, new_axis, removed_shape))
87 for removed_shape in removed_shapes]
88 return new_shapes
89 else:
90 return tuple(new_shape)
93def _broadcast_array_shapes_remove_axis(arrays, axis=None):
94 """
95 Broadcast shapes of arrays, dropping specified axes
97 Given a sequence of arrays `arrays` and an integer or tuple `axis`, find
98 the shape of the broadcast result after consuming/dropping `axis`.
99 In other words, return output shape of a typical hypothesis test on
100 `arrays` vectorized along `axis`.
102 Examples
103 --------
104 >>> import numpy as np
105 >>> a = np.zeros((5, 2, 1))
106 >>> b = np.zeros((9, 3))
107 >>> _broadcast_array_shapes((a, b), 1)
108 (5, 3)
109 """
110 # Note that here, `axis=None` means do not consume/drop any axes - _not_
111 # ravel arrays before broadcasting.
112 shapes = [arr.shape for arr in arrays]
113 return _broadcast_shapes_remove_axis(shapes, axis)
116def _broadcast_shapes_remove_axis(shapes, axis=None):
117 """
118 Broadcast shapes, dropping specified axes
120 Same as _broadcast_array_shapes, but given a sequence
121 of array shapes `shapes` instead of the arrays themselves.
122 """
123 shapes = _broadcast_shapes(shapes, axis)
124 shape = shapes[0]
125 if axis is not None:
126 shape = np.delete(shape, axis)
127 return tuple(shape)
130def _broadcast_concatenate(arrays, axis):
131 """Concatenate arrays along an axis with broadcasting."""
132 arrays = _broadcast_arrays(arrays, axis)
133 res = np.concatenate(arrays, axis=axis)
134 return res
137# TODO: add support for `axis` tuples
138def _remove_nans(samples, paired):
139 "Remove nans from paired or unpaired 1D samples"
140 # potential optimization: don't copy arrays that don't contain nans
141 if not paired:
142 return [sample[~np.isnan(sample)] for sample in samples]
144 # for paired samples, we need to remove the whole pair when any part
145 # has a nan
146 nans = np.isnan(samples[0])
147 for sample in samples[1:]:
148 nans = nans | np.isnan(sample)
149 not_nans = ~nans
150 return [sample[not_nans] for sample in samples]
153def _remove_sentinel(samples, paired, sentinel):
154 "Remove sentinel values from paired or unpaired 1D samples"
155 # could consolidate with `_remove_nans`, but it's not quite as simple as
156 # passing `sentinel=np.nan` because `(np.nan == np.nan) is False`
158 # potential optimization: don't copy arrays that don't contain sentinel
159 if not paired:
160 return [sample[sample != sentinel] for sample in samples]
162 # for paired samples, we need to remove the whole pair when any part
163 # has a nan
164 sentinels = (samples[0] == sentinel)
165 for sample in samples[1:]:
166 sentinels = sentinels | (sample == sentinel)
167 not_sentinels = ~sentinels
168 return [sample[not_sentinels] for sample in samples]
171def _masked_arrays_2_sentinel_arrays(samples):
172 # masked arrays in `samples` are converted to regular arrays, and values
173 # corresponding with masked elements are replaced with a sentinel value
175 # return without modifying arrays if none have a mask
176 has_mask = False
177 for sample in samples:
178 mask = getattr(sample, 'mask', False)
179 has_mask = has_mask or np.any(mask)
180 if not has_mask:
181 return samples, None # None means there is no sentinel value
183 # Choose a sentinel value. We can't use `np.nan`, because sentinel (masked)
184 # values are always omitted, but there are different nan policies.
185 dtype = np.result_type(*samples)
186 dtype = dtype if np.issubdtype(dtype, np.number) else np.float64
187 for i in range(len(samples)):
188 # Things get more complicated if the arrays are of different types.
189 # We could have different sentinel values for each array, but
190 # the purpose of this code is convenience, not efficiency.
191 samples[i] = samples[i].astype(dtype, copy=False)
193 inexact = np.issubdtype(dtype, np.inexact)
194 info = np.finfo if inexact else np.iinfo
195 max_possible, min_possible = info(dtype).max, info(dtype).min
196 nextafter = np.nextafter if inexact else (lambda x, _: x - 1)
198 sentinel = max_possible
199 # For simplicity, min_possible/np.infs are not candidate sentinel values
200 while sentinel > min_possible:
201 for sample in samples:
202 if np.any(sample == sentinel): # choose a new sentinel value
203 sentinel = nextafter(sentinel, -np.inf)
204 break
205 else: # when sentinel value is OK, break the while loop
206 break
207 else:
208 message = ("This function replaces masked elements with sentinel "
209 "values, but the data contains all distinct values of this "
210 "data type. Consider promoting the dtype to `np.float64`.")
211 raise ValueError(message)
213 # replace masked elements with sentinel value
214 out_samples = []
215 for sample in samples:
216 mask = getattr(sample, 'mask', None)
217 if mask is not None: # turn all masked arrays into sentinel arrays
218 mask = np.broadcast_to(mask, sample.shape)
219 sample = sample.data.copy() if np.any(mask) else sample.data
220 sample = np.asarray(sample) # `sample.data` could be a memoryview?
221 sample[mask] = sentinel
222 out_samples.append(sample)
224 return out_samples, sentinel
227def _check_empty_inputs(samples, axis):
228 """
229 Check for empty sample; return appropriate output for a vectorized hypotest
230 """
231 # if none of the samples are empty, we need to perform the test
232 if not any(sample.size == 0 for sample in samples):
233 return None
234 # otherwise, the statistic and p-value will be either empty arrays or
235 # arrays with NaNs. Produce the appropriate array and return it.
236 output_shape = _broadcast_array_shapes_remove_axis(samples, axis)
237 output = np.ones(output_shape) * np.nan
238 return output
241def _add_reduced_axes(res, reduced_axes, keepdims):
242 """
243 Add reduced axes back to all the arrays in the result object
244 if keepdims = True.
245 """
246 return ([np.expand_dims(output, reduced_axes) for output in res]
247 if keepdims else res)
250# Standard docstring / signature entries for `axis`, `nan_policy`, `keepdims`
251_name = 'axis'
252_desc = (
253 """If an int, the axis of the input along which to compute the statistic.
254The statistic of each axis-slice (e.g. row) of the input will appear in a
255corresponding element of the output.
256If ``None``, the input will be raveled before computing the statistic."""
257 .split('\n'))
260def _get_axis_params(default_axis=0, _name=_name, _desc=_desc): # bind NOW
261 _type = f"int or None, default: {default_axis}"
262 _axis_parameter_doc = Parameter(_name, _type, _desc)
263 _axis_parameter = inspect.Parameter(_name,
264 inspect.Parameter.KEYWORD_ONLY,
265 default=default_axis)
266 return _axis_parameter_doc, _axis_parameter
269_name = 'nan_policy'
270_type = "{'propagate', 'omit', 'raise'}"
271_desc = (
272 """Defines how to handle input NaNs.
274- ``propagate``: if a NaN is present in the axis slice (e.g. row) along
275 which the statistic is computed, the corresponding entry of the output
276 will be NaN.
277- ``omit``: NaNs will be omitted when performing the calculation.
278 If insufficient data remains in the axis slice along which the
279 statistic is computed, the corresponding entry of the output will be
280 NaN.
281- ``raise``: if a NaN is present, a ``ValueError`` will be raised."""
282 .split('\n'))
283_nan_policy_parameter_doc = Parameter(_name, _type, _desc)
284_nan_policy_parameter = inspect.Parameter(_name,
285 inspect.Parameter.KEYWORD_ONLY,
286 default='propagate')
288_name = 'keepdims'
289_type = "bool, default: False"
290_desc = (
291 """If this is set to True, the axes which are reduced are left
292in the result as dimensions with size one. With this option,
293the result will broadcast correctly against the input array."""
294 .split('\n'))
295_keepdims_parameter_doc = Parameter(_name, _type, _desc)
296_keepdims_parameter = inspect.Parameter(_name,
297 inspect.Parameter.KEYWORD_ONLY,
298 default=False)
300_standard_note_addition = (
301 """\nBeginning in SciPy 1.9, ``np.matrix`` inputs (not recommended for new
302code) are converted to ``np.ndarray`` before the calculation is performed. In
303this case, the output will be a scalar or ``np.ndarray`` of appropriate shape
304rather than a 2D ``np.matrix``. Similarly, while masked elements of masked
305arrays are ignored, the output will be a scalar or ``np.ndarray`` rather than a
306masked array with ``mask=False``.""").split('\n')
309def _axis_nan_policy_factory(tuple_to_result, default_axis=0,
310 n_samples=1, paired=False,
311 result_to_tuple=None, too_small=0,
312 n_outputs=2, kwd_samples=[], override=None):
313 """Factory for a wrapper that adds axis/nan_policy params to a function.
315 Parameters
316 ----------
317 tuple_to_result : callable
318 Callable that returns an object of the type returned by the function
319 being wrapped (e.g. the namedtuple or dataclass returned by a
320 statistical test) provided the separate components (e.g. statistic,
321 pvalue).
322 default_axis : int, default: 0
323 The default value of the axis argument. Standard is 0 except when
324 backwards compatibility demands otherwise (e.g. `None`).
325 n_samples : int or callable, default: 1
326 The number of data samples accepted by the function
327 (e.g. `mannwhitneyu`), a callable that accepts a dictionary of
328 parameters passed into the function and returns the number of data
329 samples (e.g. `wilcoxon`), or `None` to indicate an arbitrary number
330 of samples (e.g. `kruskal`).
331 paired : {False, True}
332 Whether the function being wrapped treats the samples as paired (i.e.
333 corresponding elements of each sample should be considered as different
334 components of the same sample.)
335 result_to_tuple : callable, optional
336 Function that unpacks the results of the function being wrapped into
337 a tuple. This is essentially the inverse of `tuple_to_result`. Default
338 is `None`, which is appropriate for statistical tests that return a
339 statistic, pvalue tuple (rather than, e.g., a non-iterable datalass).
340 too_small : int, default: 0
341 The largest unnacceptably small sample for the function being wrapped.
342 For example, some functions require samples of size two or more or they
343 raise an error. This argument prevents the error from being raised when
344 input is not 1D and instead places a NaN in the corresponding element
345 of the result.
346 n_outputs : int or callable, default: 2
347 The number of outputs produced by the function given 1d sample(s). For
348 example, hypothesis tests that return a namedtuple or result object
349 with attributes ``statistic`` and ``pvalue`` use the default
350 ``n_outputs=2``; summary statistics with scalar output use
351 ``n_outputs=1``. Alternatively, may be a callable that accepts a
352 dictionary of arguments passed into the wrapped function and returns
353 the number of outputs corresponding with those arguments.
354 kwd_samples : sequence, default: []
355 The names of keyword parameters that should be treated as samples. For
356 example, `gmean` accepts as its first argument a sample `a` but
357 also `weights` as a fourth, optional keyword argument. In this case, we
358 use `n_samples=1` and kwd_samples=['weights'].
359 override : dict, default: {'vectorization': False, 'nan_propagation': True}
360 Pass a dictionary with ``'vectorization': True`` to ensure that the
361 decorator overrides the function's behavior for multimensional input.
362 Use ``'nan_propagation': False`` to ensure that the decorator does not
363 override the function's behavior for ``nan_policy='propagate'``.
364 (See `scipy.stats.mode`, for example.)
365 """
366 # Specify which existing behaviors the decorator must override
367 temp = override or {}
368 override = {'vectorization': False,
369 'nan_propagation': True}
370 override.update(temp)
372 if result_to_tuple is None:
373 def result_to_tuple(res):
374 return res
376 def is_too_small(samples):
377 for sample in samples:
378 if len(sample) <= too_small:
379 return True
380 return False
382 def axis_nan_policy_decorator(hypotest_fun_in):
383 @wraps(hypotest_fun_in)
384 def axis_nan_policy_wrapper(*args, _no_deco=False, **kwds):
386 if _no_deco: # for testing, decorator does nothing
387 return hypotest_fun_in(*args, **kwds)
389 # We need to be flexible about whether position or keyword
390 # arguments are used, but we need to make sure users don't pass
391 # both for the same parameter. To complicate matters, some
392 # functions accept samples with *args, and some functions already
393 # accept `axis` and `nan_policy` as positional arguments.
394 # The strategy is to make sure that there is no duplication
395 # between `args` and `kwds`, combine the two into `kwds`, then
396 # the samples, `nan_policy`, and `axis` from `kwds`, as they are
397 # dealt with separately.
399 # Check for intersection between positional and keyword args
400 params = list(inspect.signature(hypotest_fun_in).parameters)
401 if n_samples is None:
402 # Give unique names to each positional sample argument
403 # Note that *args can't be provided as a keyword argument
404 params = [f"arg{i}" for i in range(len(args))] + params[1:]
406 # raise if there are too many positional args
407 maxarg = (np.inf if inspect.getfullargspec(hypotest_fun_in).varargs
408 else len(inspect.getfullargspec(hypotest_fun_in).args))
409 if len(args) > maxarg: # let the function raise the right error
410 hypotest_fun_in(*args, **kwds)
412 # raise if multiple values passed for same parameter
413 d_args = dict(zip(params, args))
414 intersection = set(d_args) & set(kwds)
415 if intersection: # let the function raise the right error
416 hypotest_fun_in(*args, **kwds)
418 # Consolidate other positional and keyword args into `kwds`
419 kwds.update(d_args)
421 # rename avoids UnboundLocalError
422 if callable(n_samples):
423 # Future refactoring idea: no need for callable n_samples.
424 # Just replace `n_samples` and `kwd_samples` with a single
425 # list of the names of all samples, and treat all of them
426 # as `kwd_samples` are treated below.
427 n_samp = n_samples(kwds)
428 else:
429 n_samp = n_samples or len(args)
431 # get the number of outputs
432 n_out = n_outputs # rename to avoid UnboundLocalError
433 if callable(n_out):
434 n_out = n_out(kwds)
436 # If necessary, rearrange function signature: accept other samples
437 # as positional args right after the first n_samp args
438 kwd_samp = [name for name in kwd_samples
439 if kwds.get(name, None) is not None]
440 n_kwd_samp = len(kwd_samp)
441 if not kwd_samp:
442 hypotest_fun_out = hypotest_fun_in
443 else:
444 def hypotest_fun_out(*samples, **kwds):
445 new_kwds = dict(zip(kwd_samp, samples[n_samp:]))
446 kwds.update(new_kwds)
447 return hypotest_fun_in(*samples[:n_samp], **kwds)
449 # Extract the things we need here
450 try: # if something is missing
451 samples = [np.atleast_1d(kwds.pop(param))
452 for param in (params[:n_samp] + kwd_samp)]
453 except KeyError: # let the function raise the right error
454 # might need to revisit this if required arg is not a "sample"
455 hypotest_fun_in(*args, **kwds)
456 vectorized = True if 'axis' in params else False
457 vectorized = vectorized and not override['vectorization']
458 axis = kwds.pop('axis', default_axis)
459 nan_policy = kwds.pop('nan_policy', 'propagate')
460 keepdims = kwds.pop("keepdims", False)
461 del args # avoid the possibility of passing both `args` and `kwds`
463 # convert masked arrays to regular arrays with sentinel values
464 samples, sentinel = _masked_arrays_2_sentinel_arrays(samples)
466 # standardize to always work along last axis
467 reduced_axes = axis
468 if axis is None:
469 if samples:
470 # when axis=None, take the maximum of all dimensions since
471 # all the dimensions are reduced.
472 n_dims = np.max([sample.ndim for sample in samples])
473 reduced_axes = tuple(range(n_dims))
474 samples = [np.asarray(sample.ravel()) for sample in samples]
475 else:
476 samples = _broadcast_arrays(samples, axis=axis)
477 axis = np.atleast_1d(axis)
478 n_axes = len(axis)
479 # move all axes in `axis` to the end to be raveled
480 samples = [np.moveaxis(sample, axis, range(-len(axis), 0))
481 for sample in samples]
482 shapes = [sample.shape for sample in samples]
483 # New shape is unchanged for all axes _not_ in `axis`
484 # At the end, we append the product of the shapes of the axes
485 # in `axis`. Appending -1 doesn't work for zero-size arrays!
486 new_shapes = [shape[:-n_axes] + (np.prod(shape[-n_axes:]),)
487 for shape in shapes]
488 samples = [sample.reshape(new_shape)
489 for sample, new_shape in zip(samples, new_shapes)]
490 axis = -1 # work over the last axis
492 # if axis is not needed, just handle nan_policy and return
493 ndims = np.array([sample.ndim for sample in samples])
494 if np.all(ndims <= 1):
495 # Addresses nan_policy == "raise"
496 if nan_policy != 'propagate' or override['nan_propagation']:
497 contains_nan = [_contains_nan(sample, nan_policy)[0]
498 for sample in samples]
499 else:
500 # Behave as though there are no NaNs (even if there are)
501 contains_nan = [False]*len(samples)
503 # Addresses nan_policy == "propagate"
504 if any(contains_nan) and (nan_policy == 'propagate'
505 and override['nan_propagation']):
506 res = np.full(n_out, np.nan)
507 res = _add_reduced_axes(res, reduced_axes, keepdims)
508 return tuple_to_result(*res)
510 # Addresses nan_policy == "omit"
511 if any(contains_nan) and nan_policy == 'omit':
512 # consider passing in contains_nan
513 samples = _remove_nans(samples, paired)
515 # ideally, this is what the behavior would be:
516 # if is_too_small(samples):
517 # return tuple_to_result(np.nan, np.nan)
518 # but some existing functions raise exceptions, and changing
519 # behavior of those would break backward compatibility.
521 if sentinel:
522 samples = _remove_sentinel(samples, paired, sentinel)
523 res = hypotest_fun_out(*samples, **kwds)
524 res = result_to_tuple(res)
525 res = _add_reduced_axes(res, reduced_axes, keepdims)
526 return tuple_to_result(*res)
528 # check for empty input
529 # ideally, move this to the top, but some existing functions raise
530 # exceptions for empty input, so overriding it would break
531 # backward compatibility.
532 empty_output = _check_empty_inputs(samples, axis)
533 if empty_output is not None:
534 res = [empty_output.copy() for i in range(n_out)]
535 res = _add_reduced_axes(res, reduced_axes, keepdims)
536 return tuple_to_result(*res)
538 # otherwise, concatenate all samples along axis, remembering where
539 # each separate sample begins
540 lengths = np.array([sample.shape[axis] for sample in samples])
541 split_indices = np.cumsum(lengths)
542 x = _broadcast_concatenate(samples, axis)
544 # Addresses nan_policy == "raise"
545 if nan_policy != 'propagate' or override['nan_propagation']:
546 contains_nan, _ = _contains_nan(x, nan_policy)
547 else:
548 contains_nan = False # behave like there are no NaNs
550 if vectorized and not contains_nan and not sentinel:
551 res = hypotest_fun_out(*samples, axis=axis, **kwds)
552 res = result_to_tuple(res)
553 res = _add_reduced_axes(res, reduced_axes, keepdims)
554 return tuple_to_result(*res)
556 # Addresses nan_policy == "omit"
557 if contains_nan and nan_policy == 'omit':
558 def hypotest_fun(x):
559 samples = np.split(x, split_indices)[:n_samp+n_kwd_samp]
560 samples = _remove_nans(samples, paired)
561 if sentinel:
562 samples = _remove_sentinel(samples, paired, sentinel)
563 if is_too_small(samples):
564 return np.full(n_out, np.nan)
565 return result_to_tuple(hypotest_fun_out(*samples, **kwds))
567 # Addresses nan_policy == "propagate"
568 elif (contains_nan and nan_policy == 'propagate'
569 and override['nan_propagation']):
570 def hypotest_fun(x):
571 if np.isnan(x).any():
572 return np.full(n_out, np.nan)
574 samples = np.split(x, split_indices)[:n_samp+n_kwd_samp]
575 if sentinel:
576 samples = _remove_sentinel(samples, paired, sentinel)
577 if is_too_small(samples):
578 return np.full(n_out, np.nan)
579 return result_to_tuple(hypotest_fun_out(*samples, **kwds))
581 else:
582 def hypotest_fun(x):
583 samples = np.split(x, split_indices)[:n_samp+n_kwd_samp]
584 if sentinel:
585 samples = _remove_sentinel(samples, paired, sentinel)
586 if is_too_small(samples):
587 return np.full(n_out, np.nan)
588 return result_to_tuple(hypotest_fun_out(*samples, **kwds))
590 x = np.moveaxis(x, axis, 0)
591 res = np.apply_along_axis(hypotest_fun, axis=0, arr=x)
592 res = _add_reduced_axes(res, reduced_axes, keepdims)
593 return tuple_to_result(*res)
595 _axis_parameter_doc, _axis_parameter = _get_axis_params(default_axis)
596 doc = FunctionDoc(axis_nan_policy_wrapper)
597 parameter_names = [param.name for param in doc['Parameters']]
598 if 'axis' in parameter_names:
599 doc['Parameters'][parameter_names.index('axis')] = (
600 _axis_parameter_doc)
601 else:
602 doc['Parameters'].append(_axis_parameter_doc)
603 if 'nan_policy' in parameter_names:
604 doc['Parameters'][parameter_names.index('nan_policy')] = (
605 _nan_policy_parameter_doc)
606 else:
607 doc['Parameters'].append(_nan_policy_parameter_doc)
608 if 'keepdims' in parameter_names:
609 doc['Parameters'][parameter_names.index('keepdims')] = (
610 _keepdims_parameter_doc)
611 else:
612 doc['Parameters'].append(_keepdims_parameter_doc)
613 doc['Notes'] += _standard_note_addition
614 doc = str(doc).split("\n", 1)[1] # remove signature
615 axis_nan_policy_wrapper.__doc__ = str(doc)
617 sig = inspect.signature(axis_nan_policy_wrapper)
618 parameters = sig.parameters
619 parameter_list = list(parameters.values())
620 if 'axis' not in parameters:
621 parameter_list.append(_axis_parameter)
622 if 'nan_policy' not in parameters:
623 parameter_list.append(_nan_policy_parameter)
624 if 'keepdims' not in parameters:
625 parameter_list.append(_keepdims_parameter)
626 sig = sig.replace(parameters=parameter_list)
627 axis_nan_policy_wrapper.__signature__ = sig
629 return axis_nan_policy_wrapper
630 return axis_nan_policy_decorator