ENH: Add support for multiple conditions assign statement
State: closed

Used to support conditional assignment operation.

Usage example: 
```
df.assign(d = pd.case_when(lambda x: <some conditions>, 0,            # first condition, result   
                           (other_df.z > 4) | (other_df.x.isna()), 1, # second condition, result 
                           "some_value"))                             # default (optional)
```

Continued discussion [here](https://github.com/pandas-dev/pandas/issues/39154#issuecomment-1357794823).

Credit: 
Some of the logic is inspired by [this implementation](https://github.com/pyjanitor-devs/pyjanitor/blob/cdf62dac8696aabae0946b401de2b94313c23fc1/janitor/functions/case_when.py) which at the time of constructing the PR was contributed to by @samukweku @Zeroto521 @thatlittleboy and @ericmjl 


- [x] closes #39154 
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Labels:
name: Enhancement

Comments:
I am not sure if I would've implemented it with an anonymous function (eg. lambda). Since `np.select` expects boolean arrays, so we could just use a standard pandas condition `df["a"] == "some value"`
@erfannariman Thanks for taking a look. 

> I am not sure if I would've implemented it with an anonymous function (eg. lambda).

Indeed, here is a fixup: [7ad6b10](https://github.com/pandas-dev/pandas/pull/50343/commits/7ad6b10abff4bec64a4422f818270d9b5fbcba27)

>  Since np.select expects boolean arrays, so we could just use a standard pandas condition df["a"] == "some value"

I didn't understand your statement fully, can you please elaborate? 
This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
@ELHoussineT how's it going?
Sorry I was away. Will push commits this weekend. @samukweku 
@rhshadrach @samukweku 

Hey guys, sorry for the late reply. 

Thanks a lot for the valuable feedback. 

Since the feedback contained core changed to the logic, let's first agree that [this](https://github.com/pandas-dev/pandas/pull/50343/files#diff-7bb01d4a7a19df9dc39717fadf6e6e9aa10244326273c69c76cdf7e89a484a06) is how we desire the API to behave. Once agreed, I will proceed with adjusting the tests, docs, ...etc. 

As you can see in the last commit, the following changes are in effect: 
1. `default` is required 
2. The API returns a `Series`
3. The API uses `Series.mask` under the hood which give us a lot of advantages specially dealing with EAs.

@rhshadrach I included in the docstring of `case_when` a lot of example to address your points such as passing a `Dataframe` vs `Series` to the API as well as the default being a `Series` or a single value. Later on, we can create an alias of this `pd.case_when` to support the use case you mentioned [here](https://github.com/pandas-dev/pandas/pull/50343#discussion_r1064193569).

If you agree on that logic, I will proceed with the tests & docs; otherwise, we can further iterate on the logic until it meets our desired. 

Looking forward to your response. 
@rhshadrach Answering your question [here](https://github.com/pandas-dev/pandas/pull/50343#discussion_r1131657169) made me realize that the testing should be a bit extensive here as a lot of combos can face this API. To illustrate: 

<img src="https://user-images.githubusercontent.com/20634657/224187005-8da15dd3-488f-43fd-8afb-390b4b4cff7c.png" width=500 />


Anyway, once you agree on the logic, I will proceed to testing. 
@rhshadrach How is it looking, shall I proceed and write the tests and docs to wrap up this PR? 
@rhshadrach PTAL.
@rhshadrach I also had to add `.convert_dtypes()` [a5d678f](https://github.com/pandas-dev/pandas/pull/50343/commits/a5d678f067b331c9670ee6de2073f9fd79a3a6b4). 

This is necessary since the default always determines the first dtype of the series even if the final series does not have any default value. 

To illustrate, see [1] which does not have `.convert_dtypes()` while [2] has it. Notice how [2] has the correct dtype in the result series. 

[1] : 
```
>>> df 
         a  b
index 1  1  4
index 2  2  5
index 3  3  6

>>> pd.case_when(
... df,
... lambda x: x.a > 0,
... 1,
... lambda x: x.a == 1, 
... -1,
... default='default'
)
index 1    1
index 2    1
index 3    1
dtype: object

```

[2] : 

```
>>> df 
         a  b
index 1  1  4
index 2  2  5
index 3  3  6

>>> pd.case_when(
... df,
... lambda x: x.a > 0,
... 1,
... lambda x: x.a == 1, 
... -1,
... default='default'
)
index 1    1
index 2    1
index 3    1
dtype: Int64
```

> @rhshadrach I also had to add `.convert_dtypes()` [a5d678f](https://github.com/pandas-dev/pandas/pull/50343/commits/a5d678f067b331c9670ee6de2073f9fd79a3a6b4).
> 
> This is necessary since the default always determines the first dtype of the series even if the final series does not have any default value.

Thanks for identifying this, it is indeed something I had not considered. I do not think Int64 is the right answer in the example you gave. As values of the input change, the dtype of the result can change, which can then lead to radically different answers later on. It is an example of "values-dependent behavior" which we try to avoid if at all possible.

I think we should be using something like `find_common_type` to determine what the output dtype is based on the input dtypes alone (and not values!). However I don't know if this is readily used since this function accepts scalars. In short, I'm not sure of what a good solution looks like here. 
@rhshadrach 

> I do not think Int64 is the right answer in the example you gave.

May I ask why? It is still not super clear to me why can't we proceed with `.convert_dtypes()`. 

Thanks 


> May I ask why? 

I included why I think to be the case in my initial response.
@rhshadrach 

Understood and I agree. 

In this case, I would suggest to remove `.convert_dtypes()` (see [072bc0c](https://github.com/pandas-dev/pandas/pull/50343/commits/072bc0c648a11fcb9fcb27b6dd3276a9a3997b59)) and mention this limitation in the docstring (see [747b379](https://github.com/pandas-dev/pandas/pull/50343/commits/747b379070f2bbcae3b5ca24e4306ffdc7a919b0)). 

The reason why I suggested the above is that this issue is built in `Series.mask()` and keeping the behavior consistent for now might be beneficial. 

To illustrate how the issue is in `Series.mask()`, see the dtype of the result `Series` below: 

```
>>> s = pd.Series(['a','b'])
0    a
1    b
dtype: object

>>> s.mask([True, True], 1)
0    1
1    1
dtype: object
```

Keeping that behavior as is in `pd.case_when` will maintain the consistency (even through one could argue it is an undesired behavior) and if this gets fixed in `Series.mask` the changes will propagate to `pd.case_when` as it uses `Series.mask` under the hood. 

I am also happy to tackle this issue in `Series.mask` after we are done with this PR. I am optimistic that we can find a solution. 

Looking forward to your reply. If you agree with the above, please indicate that so I proceed with tests & docs. 

Best, 
@rhshadrach FYI https://github.com/pandas-dev/pandas/issues/52662
@rhshadrach PTAL.
Thanks for the pull request, but it appears to have gone stale. If interested in continuing, please merge in the main branch, address any review comments and/or failing tests, and we can reopen.