Repository: pyjanitor-devs/pyjanitor

Issue:
[ENH] Coalesce function that doesn't delete columns
# Brief Description

It seems the coalesce function must always replace all input columns with one output column. While this makes sense from the meaning of "coalesce" I can think of cases when this is not the desired outcome.

For example, I have a column D that is a good default value for a few other columns and I might want to coalesce([A,D],A), coalesce([B, D], B) etc. This would break currently as D would disappear after the first coalesce

In this case, I could use A.fillna(D, inplace=True) but if I have multiple default columns (e.g. I want to coalesce([A,B,D]) and coalesce([B,C,D]) then it becomes messy. Maybe there is a better way I am not aware of.

I'm happy to try a PR for this but wanted to confirm that this is a reasonable thing to do.

# Example API

I would propose adding a delete_columns option which is defaulted to be True so existing behaviour is preserved. Or keep_columns which defaults to False.

```python
def coalesce(
    df: pd.DataFrame,
    column_names: Union[str, Iterable[str], Any],
    new_column_name: str = None,
    delete_columns: bool = True
) -> pd.DataFrame:
...
if delete_columns:
     df = df.drop(columns=column_names)
```
It would still replace an existing column if no column name is specified or the new column already exists.


Discussion:
@gddcunh I like the idea! Would you like to go ahead and make the PR? Don't forget to add a test for this new case and to update the docstring :smile:. 
@ericmjl sure I will give it a shot

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -568,6 +568,7 @@ def coalesce(
     df: pd.DataFrame,
     column_names: Union[str, Iterable[str], Any],
     new_column_name: str = None,
+    delete_columns: bool = True,
 ) -> pd.DataFrame:
     """
 
@@ -601,6 +602,7 @@ def coalesce(
     :param df: A pandas DataFrame.
     :param column_names: A list of column names.
     :param str new_column_name: The new column name after combining.
+    :param bool delete_columns: Whether to delete the columns being coalesced
     :returns: A pandas DataFrame with coalesced columns.
     """
     series = [df[c] for c in column_names]
@@ -608,7 +610,8 @@ def coalesce(
     def _coalesce(series1, series2):
         return series1.combine_first(series2)
 
-    df = df.drop(columns=column_names)
+    if delete_columns:
+        df = df.drop(columns=column_names)
     if not new_column_name:
         new_column_name = column_names[0]
     df[new_column_name] = reduce(_coalesce, series)  # noqa: F821


Test patch:
diff --git a/tests/functions/test_coalesce.py b/tests/functions/test_coalesce.py
--- a/tests/functions/test_coalesce.py
+++ b/tests/functions/test_coalesce.py
@@ -22,3 +22,15 @@ def test_coalesce_without_title():
     assert "b" not in df.columns
     assert df.shape == (3, 1)
     assert pd.isnull(df).sum().sum() == 0
+
+
+@pytest.mark.functions
+def test_coalesce_without_delete():
+    df = pd.DataFrame(
+        {"a": [1, np.nan, 3], "b": [2, 3, 1], "c": [2, np.nan, 9]}
+    ).coalesce(["a", "b", "c"], delete_columns=False)
+    assert "a" in df.columns
+    assert "b" in df.columns
+    assert "c" in df.columns
+    assert df.shape == (3, 3)
+    assert pd.isnull(df).sum().sum() == 1