Repository: pandas-dev/pandas
Pull request: ENH: add result_names argument to DataFrame.compare #44354
This is following up on the pull request #44365
- [X] closes #44354 (Replace xxxx with the Github issue number)
- [X] [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
- [X] 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/v1.5.0.rst` file if fixing a bug or adding a new feature.


Problem statement:
ENH: add suffixes argument to DataFrame.compare
#### Is your feature request related to a problem?

It would be convenient to choose other suffixes in `DataFrame.compare` than `self, other`

#### Describe the solution you'd like

Add argument `suffixes` so users can specify them.

#### API breaking implications

None

#### Additional context

```python
df = pd.DataFrame(
    {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
    columns=["col1", "col2", "col3"],
)
df2 = df.copy()
df2.loc[0, "col1"] = "c"
df2.loc[2, "col3"] = np.nan
diff = df.compare(df2, suffixes=["left", "right"])
print(diff)

  col1       col3      
  left right left right
0    a     c  NaN   NaN
2  NaN   NaN  3.0   NaN
```

Hints from issue discussion:
seems reasonable
In the source code, suffixes is fixed to keys=['self', 'other'],  change the value of keys to the value of suffixes transferred by the user.

Commit message: Merge branch 'main' into issue2

Patch to review:
diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst
index c70acc0a0b18c..e9d5bd1ffd1e2 100644
--- a/doc/source/whatsnew/v1.5.0.rst
+++ b/doc/source/whatsnew/v1.5.0.rst
@@ -277,6 +277,7 @@ Other enhancements
 - Allow reading compressed SAS files with :func:`read_sas` (e.g., ``.sas7bdat.gz`` files)
 - :meth:`DatetimeIndex.astype` now supports casting timezone-naive indexes to ``datetime64[s]``, ``datetime64[ms]``, and ``datetime64[us]``, and timezone-aware indexes to the corresponding ``datetime64[unit, tzname]`` dtypes (:issue:`47579`)
 - :class:`Series` reducers (e.g. ``min``, ``max``, ``sum``, ``mean``) will now successfully operate when the dtype is numeric and ``numeric_only=True`` is provided; previously this would raise a ``NotImplementedError`` (:issue:`47500`)
+- :meth:`DataFrame.compare` now accepts a ``suffixes`` to allow the user to specify the suffixes of both left and right DataFrame which are being compared. This is by default ``self`` and ``other`` (:issue:`44354`)
 -
 
 .. ---------------------------------------------------------------------------
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index ead4ea744c647..ffa0b46896f98 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -7776,6 +7776,14 @@ def __rdivmod__(self, other) -> tuple[DataFrame, DataFrame]:
 0    a     c  NaN   NaN
 2  NaN   NaN  3.0   4.0
 
+Assign suffixes
+
+>>> df.compare(df2, suffixes=("left", "right"))
+  col1       col3
+  left right left right
+0    a     c  NaN   NaN
+2  NaN   NaN  3.0   4.0
+
 Stack the differences on rows
 
 >>> df.compare(df2, align_axis=0)
@@ -7823,12 +7831,14 @@ def compare(
         align_axis: Axis = 1,
         keep_shape: bool = False,
         keep_equal: bool = False,
+        suffixes: Suffixes = ("self", "other"),
     ) -> DataFrame:
         return super().compare(
             other=other,
             align_axis=align_axis,
             keep_shape=keep_shape,
             keep_equal=keep_equal,
+            suffixes=suffixes,
         )
 
     def combine(
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index ba3474a2513fb..a8a0913d386ee 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -58,6 +58,7 @@
     Renamer,
     SortKind,
     StorageOptions,
+    Suffixes,
     T,
     TimedeltaConvertibleTypes,
     TimestampConvertibleTypes,
@@ -8965,6 +8966,7 @@ def compare(
         align_axis: Axis = 1,
         keep_shape: bool_t = False,
         keep_equal: bool_t = False,
+        suffixes: Suffixes = ("self", "other"),
     ):
         from pandas.core.reshape.concat import concat
 
@@ -8975,7 +8977,6 @@ def compare(
             )
 
         mask = ~((self == other) | (self.isna() & other.isna()))
-        keys = ["self", "other"]
 
         if not keep_equal:
             self = self.where(mask)
@@ -8990,13 +8991,21 @@ def compare(
             else:
                 self = self[mask]
                 other = other[mask]
+        if not isinstance(suffixes, tuple):
+            warnings.warn(
+                f"Passing 'suffixes' as a {type(suffixes)}, is not supported "
+                "and may give unexpected results. Provide 'suffixes' as a tuple "
+                "instead. In the future a 'TypeError' will be raised.",
+                FutureWarning,
+                stacklevel=find_stack_level(),
+            )
 
         if align_axis in (1, "columns"):  # This is needed for Series
             axis = 1
         else:
             axis = self._get_axis_number(align_axis)
 
-        diff = concat([self, other], axis=axis, keys=keys)
+        diff = concat([self, other], axis=axis, keys=suffixes)
 
         if axis >= self.ndim:
             # No need to reorganize data if stacking on new axis
diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py
index 4b7a487e9472d..039d37d70dd45 100644
--- a/pandas/core/shared_docs.py
+++ b/pandas/core/shared_docs.py
@@ -53,7 +53,7 @@
 ] = """
 Compare to another {klass} and show the differences.
 
-.. versionadded:: 1.1.0
+.. versionadded:: 1.5.0
 
 Parameters
 ----------
@@ -75,6 +75,10 @@
 keep_equal : bool, default False
     If true, the result keeps values that are equal.
     Otherwise, equal values are shown as NaNs.
+
+suffixes : tuple, default ('self', 'other')
+    Set the dataframes names in the comparison.
+
 """
 
 _shared_docs[
diff --git a/pandas/tests/frame/methods/test_compare.py b/pandas/tests/frame/methods/test_compare.py
index 468811eba0d39..f51fbf11c1b30 100644
--- a/pandas/tests/frame/methods/test_compare.py
+++ b/pandas/tests/frame/methods/test_compare.py
@@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
+    df1 = pd.DataFrame(
+        {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
+        columns=["col1", "col2", "col3"],
+    )
+    df2 = df1.copy()
+    df2.loc[0, "col1"] = "c"
+    df2.loc[2, "col3"] = np.nan
+
+    suffixes = ("left", "right")
+    comp = df1.compare(df2, suffixes=suffixes)
+    expected = pd.DataFrame(
+        {
+            ("col1", "left"): {0: "a", 2: np.nan},
+            ("col1", "right"): {0: "c", 2: np.nan},
+            ("col3", "left"): {0: np.nan, 2: 3.0},
+            ("col3", "right"): {0: np.nan, 2: np.nan},
+        }
+    )
+    tm.assert_frame_equal(comp, expected)
+    result_suffixes = comp.columns.get_level_values(1).unique()
+    assert result_suffixes.isin(suffixes).all(), "suffixes not equal"


Code review comments:
diff_hunk: @@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
+    df1 = pd.DataFrame(
+        {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
+        columns=["col1", "col2", "col3"],
+    )
+    df2 = df1.copy()
+    df2.loc[0, "col1"] = "c"
+    df2.loc[2, "col3"] = np.nan
+
+    suffixes = ("left", "right")
path: pandas/tests/frame/methods/test_compare.py
text: @user1:
Don't have to define this as a variable.

@author:
Done

diff_hunk: @@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
+    df1 = pd.DataFrame(
+        {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
+        columns=["col1", "col2", "col3"],
+    )
+    df2 = df1.copy()
+    df2.loc[0, "col1"] = "c"
+    df2.loc[2, "col3"] = np.nan
+
+    suffixes = ("left", "right")
+    comp = df1.compare(df2, suffixes=suffixes)
path: pandas/tests/frame/methods/test_compare.py
text: @user1:
can you call result?

@author:
Done

diff_hunk: @@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
path: pandas/tests/frame/methods/test_compare.py
text: @user1:
reference missing

@author:
What do you mean? comment?

@user1:
We refernce the issue number there

@author:
Done

diff_hunk: @@ -8990,13 +8991,21 @@ def compare(
             else:
                 self = self[mask]
                 other = other[mask]
+        if not isinstance(suffixes, tuple):
path: pandas/core/generic.py
text: @user1:
Why not raising immediately? This is a new argument, so we can be as strict as we want

@author:
I edited it.
What do you think are other limitations on the input that can make it invalid?

@author:
@user1 I was thinking one test maybe the type of the elements inside the tuple. Do we only limit the type to string? float and other numeric values work too, do we include them?

@user1:
We do at other places, so would be ok I think

@author:
Ok cool. So is it done?

diff_hunk: @@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
+    df1 = pd.DataFrame(
+        {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
+        columns=["col1", "col2", "col3"],
+    )
+    df2 = df1.copy()
+    df2.loc[0, "col1"] = "c"
+    df2.loc[2, "col3"] = np.nan
+
+    suffixes = ("left", "right")
+    comp = df1.compare(df2, suffixes=suffixes)
+    expected = pd.DataFrame(
+        {
+            ("col1", "left"): {0: "a", 2: np.nan},
+            ("col1", "right"): {0: "c", 2: np.nan},
+            ("col3", "left"): {0: np.nan, 2: 3.0},
+            ("col3", "right"): {0: np.nan, 2: np.nan},
+        }
+    )
+    tm.assert_frame_equal(comp, expected)
+    result_suffixes = comp.columns.get_level_values(1).unique()
path: pandas/tests/frame/methods/test_compare.py
text: @user1:
This should not be necessary right? assert_frame_equal checks the column names too

@author:
makes sense

@author:
Done

diff_hunk: @@ -180,3 +180,28 @@ def test_compare_unaligned_objects():
         df1 = pd.DataFrame(np.ones((3, 3)))
         df2 = pd.DataFrame(np.zeros((2, 1)))
         df1.compare(df2)
+
+
+def test_compare_suffixes():
+    # GH
+    df1 = pd.DataFrame(
+        {"col1": ["a", "b", "c"], "col2": [1.0, 2.0, np.nan], "col3": [1.0, 2.0, 3.0]},
+        columns=["col1", "col2", "col3"],
+    )
+    df2 = df1.copy()
+    df2.loc[0, "col1"] = "c"
path: pandas/tests/frame/methods/test_compare.py
text: @user1:
Please define the df completley, this is hard to understand imo

@author:
Done