Repository: dask/dask

Issue:
handling of negative drop_axis in map_blocks and map_overlap
**What happened**:

If a negative value for `drop_axis` is passed into either `map_blocks` or `map_overlap` a non-informative exception is raised.

**What you expected to happen**:

I would expect this would work as in NumPy for negative axis arguments where axis becomes `axis = axis % array.ndim`. If it is not intended to work, then it should raise a user-friendly `AxisError`. This came up while working on #7894 and it was decided it should be addressed in a separate PR.

I am interested in submitting a PR to fix this, but have a question on intended behavior:
- The `map_blocks` and `map_overlap` functions can take multiple array arguments. If that is the case, what should happen in the case where both arrays do not have the same number of dimensions? (This is important because we need to use ndim in order to convert a negative `drop_axis` into a non-negative one). Should it error in this case, use `ndim` of the first array, or something else? 

**Minimal Complete Verifiable Example**:
```Python
import dask.array as da

drop_axis = (-1,)
x = da.random.standard_normal((5, 10, 8), chunks=(2, 5, 4))
y = da.map_blocks(lambda x: x.mean(axis=drop_axis), x, drop_axis=drop_axis, dtype=float).compute()
```

<details>
<summary>Traceback</summary>

```
gives an `IndexError` with the following extended traceback

Traceback (most recent call last):

  File "/tmp/ipykernel_1277728/1452951085.py", line 9, in <module>
    y = da.map_blocks(_mean, x, drop_axis=drop_axis, dtype=float).compute()

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/base.py", line 285, in compute
    (result,) = compute(self, traverse=False, **kwargs)

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/base.py", line 568, in compute
    return repack([f(r, *a) for r, (f, a) in zip(results, postcomputes)])

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/base.py", line 568, in <listcomp>
    return repack([f(r, *a) for r, (f, a) in zip(results, postcomputes)])

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/array/core.py", line 1070, in finalize
    return concatenate3(results)

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/array/core.py", line 4799, in concatenate3
    chunks = chunks_from_arrays(arrays)

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/array/core.py", line 4583, in chunks_from_arrays
    result.append(tuple([shape(deepfirst(a))[dim] for a in arrays]))

  File "/home/lee8rx/mambaforge/envs/skimage-dev/lib/python3.8/site-packages/dask/array/core.py", line 4583, in <listcomp>
    result.append(tuple([shape(deepfirst(a))[dim] for a in arrays]))

IndexError: tuple index out of range
```

**Anything else we need to know?**:

**Environment**:

- Dask version: 2021.06.2
- Python version: 3.8
- Operating System: linux
- Install method (conda, pip, source): conda-forge



Discussion:
Thanks for taking this on! The `drop_index` should operate out the output indices, so I don't think you need to worry about the input `ndims`

This code looks like:

```python 
    if drop_axis:
        out_ind = tuple(x for i, x in enumerate(out_ind) if i not in drop_axis)
```
https://github.com/dask/dask/blob/212c1058c19fa543d60d9a294f8f9feda3e1c9dc/dask/array/core.py#L685:L686

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -694,6 +694,8 @@ def map_blocks(
         dtype = apply_infer_dtype(func, args, original_kwargs, "map_blocks")
 
     if drop_axis:
+        if any(i < 0 for i in drop_axis):
+            raise ValueError(f"Expected drop_axis to be non-negative; got {drop_axis}.")
         out_ind = tuple(x for i, x in enumerate(out_ind) if i not in drop_axis)
     if new_axis is None and chunks is not None and len(out_ind) < len(chunks):
         new_axis = range(len(chunks) - len(out_ind))


Test patch: