BUG: `Styler.apply` consistently manages Series return objects aligning labels.
State: closed

- [x] closes #13657 

This issue is due to the asymmetric application of `DataFrame.apply` (#42005). 
**The bug** is that when using `Styler.apply` the treatment of Series return objects is applied for `axis=0` and overwritten for `axis=1`.

```python
df = pd.DataFrame([[1,2],[3,4]], index=["X", "Y"], columns=["X", "Y"])
s = df.style

def series_ret(s):
    return pd.Series(["color:red;", "color:blue;"], index=["Y", "X"])  # note X-Y order
```

| Command    | Current Behaviour             | New Behaviour            |
| ------------ | ------------------------- | ---------------------- |
| `s.apply(series_ret, axis=0)` | ![][image5] | ![][image5] |
| `s.apply(series_ret, axis=1)` | ![][image6] | ![][image7] |

Also the application has been expanded to **allow** (for axis=0 | 1) Series return objects that are **not** the same size, or (for axis=None) DataFrame return objects that are **not** the same size, if they contain only relevant index labels. This allows for more flexible user defined functions. Potentially it also increases performance by not having to loop over labels that no longer exist. 

```python
def series_ret(s):
    return pd.Series(["color:red;"], index=["Y"])  # note only returning 1 element

def array_ret(s):
    return ["color:red;"]

def ser_no_idx_ret(s):
    return pd.Series(["color:red;", "color:blue;"]) # note returning a Series of right shape with no index
```

| Command    | Current Behaviour             | New Behaviour            |
| ------------ | ------------------------- | ---------------------- |
| `s.apply(series_ret, axis=0)` | `ValueError: Function returned the wrong shape.` | ![][image1] |
| `s.apply(series_ret, axis=1)` | `ValueError: Length mismatch: Expected axis has 1 elements, new values have 2 elements` | ![][image2] |
| `s.apply(array_ret, axis=0)` | `ValueError: Function returned the wrong shape.` | `ValueError: Function created invalid index labels. Usually, this is the result of the function returning a Series which contains invalid labels, or returning an incorrectly shaped, list-like object which cannot be mapped to labels, possibly due to applying the function along the wrong axis.` |
| `s.apply(array_ret, axis=1)` |`ValueError: Length mismatch: Expected axis has 1 elements, new values have 2 elements`                                |`ValueError: Function created invalid index labels. Usually, this is the result of the function returning a Series which contains invalid labels, or returning an incorrectly shaped, list-like object which cannot be mapped to labels, possibly due to applying the function along the wrong axis.` |
| `s.apply(ser_no_idx_ret, axis=0)` | `KeyError: 0` | `ValueError: Function created invalid index labels. Usually, this is the result of the function returning a Series which contains invalid labels, or returning an incorrectly shaped, list-like object which cannot be mapped to labels, possibly due to applying the function along the wrong axis.` |
| `s.apply(ser_no_idx_ret, axis=1)` | ![][image6] |`ValueError: Function created invalid index labels. Usually, this is the result of the function returning a Series which contains invalid labels, or returning an incorrectly shaped, list-like object which cannot be mapped to labels, possibly due to applying the function along the wrong axis.` |

Note all errors do report the shapes, I just didn't include in the table for brevity.

### Edge Cases

The edge case here is that if a user has a DataFrame with a default range index (or a DataFrame with some indexes the same as those in default, e.g. 0 or 1) then even returning arrays of the wrong size may work, since `DataFrame.apply` assigns them to a RangeIndex.

To be consistent with the above this should raise, but it is impossible to hook into the return value of the user-defined function and determine if it is a Series (where non-matching shapes are OK) or list-like (where matching shapes should be enforced)

```python
df = pd.DataFrame([[1,2],[3,4]])
s = df.style
```
| Command    | Current Behaviour             | New Behaviour            |
| ------------ | ------------------------- | ---------------------- |
| `s.apply(array_ret, axis=0)` | `ValueError: Function returned the wrong shape.` | ![][image3] |
| `s.apply(array_ret, axis=1)` | `ValueError: Length mismatch: Expected axis has 1 elements, new values have 2 elements` |  ![][image4] |

### ASV
Benchmarks unchanged.

[image1]: https://user-images.githubusercontent.com/24256554/122002332-e59b5580-cdb1-11eb-85c2-993c7e20bdfc.png
[image2]: https://user-images.githubusercontent.com/24256554/122002344-e9c77300-cdb1-11eb-8a01-f50ec660b300.png
[image3]: https://user-images.githubusercontent.com/24256554/122004954-57c16980-cdb5-11eb-8ba4-ec14b1a6b06f.png
[image4]: https://user-images.githubusercontent.com/24256554/122005013-69a30c80-cdb5-11eb-900c-0d4be813cb58.png
[image5]: https://user-images.githubusercontent.com/24256554/122006522-3497b980-cdb7-11eb-892e-f34c80e3c7ff.png
[image6]: https://user-images.githubusercontent.com/24256554/122006532-36fa1380-cdb7-11eb-8b08-f89d6991536c.png
[image7]: https://user-images.githubusercontent.com/24256554/122006816-93f5c980-cdb7-11eb-8aff-09b658c594d9.png




Labels:
name: Bug
name: Apply
description: Apply, Aggregate, Transform, Map
name: Styler
description: conditional formatting using DataFrame.style

Comments:
This pull request is stale because it has been open for thirty days with no activity. Please update or respond to this comment if you're still interested in working on this.
thanks @attack68