Repository: modin-project/modin
Pull request: FIX-#4907: Implement `radd` for Series and DataFrame
Signed-off-by: Karthik Velayutham <vkarthik@ponder.io>

<!--
Thank you for your contribution!
Please review the contributing docs: https://modin.readthedocs.io/en/latest/development/contributing.html
if you have questions about contributing.
-->

## What do these changes do?

This PR implements a proper implementation for `radd` which is needed for string concatenation on DataFrames and Series objects.

- [x] commit message follows format outlined [here](https://modin.readthedocs.io/en/latest/development/contributing.html#commit-message-formatting)
- [x] passes `flake8 modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] passes `black --check modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] signed commit with `git commit -s` <!-- you can amend your commit with a signature via `git commit -amend -s` -->
- [x] Resolves #4907 
- [x] tests added and passing
- [x] module layout described at `docs/development/architecture.rst` is up-to-date <!-- if you have added, renamed or removed files or directories please update the documentation accordingly -->
- [x] added (Issue Number: PR title (PR Number)) and github username to release notes for next major release <!-- e.g. DOCS-#4077: Add release notes template to docs folder (#4078) -->


Problem statement:
BUG: Appending string to column returns in different order
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: macOS 12.2.1
- **Modin version** (`modin.__version__`): 0.15.0+101.g14edd1ca
- **Python version**: 3.9.12
- **Code we can use to reproduce**:
```python
import modin.pandas as pd
import pandas
from modin.pandas.test.utils import df_equals

pdf = pandas.DataFrame({'a' : ['a','b','c','d']})
mdf = pd.DataFrame({'a' : ['a','b','c','d']})

pdf['a'] = 'random_string' + pdf['a']
mdf['a'] = 'random_string' + mdf['a']

df_equals(pdf, mdf)
```
### Describe the problem
<!-- Describe the problem clearly here. -->
Appending a string to a `str` column in a Modin DataFrame returns values in a different order.

### Source code / logs
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->

<details>
<summary> Error logs </summary>

```python
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Input In [18], in <cell line: 11>()
      8 pdf['a'] = 'random_string' + pdf['a']
      9 mdf['a'] = 'random_string' + mdf['a']
---> 11 df_equals(pdf, mdf)

File ~/Documents/modin/modin/pandas/test/utils.py:589, in df_equals(df1, df2)
    586     assert_empty_frame_equal(df1, df2)
    588 if isinstance(df1, pandas.DataFrame) and isinstance(df2, pandas.DataFrame):
--> 589     assert_frame_equal(
    590         df1,
    591         df2,
    592         check_dtype=False,
    593         check_datetimelike_compat=True,
    594         check_index_type=False,
    595         check_column_type=False,
    596         check_categorical=False,
    597     )
    598     df_categories_equals(df1, df2)
    599 elif isinstance(df1, pandas.Index) and isinstance(df2, pandas.Index):

    [... skipping hidden 2 frame]

File ~/opt/anaconda3/envs/modin/lib/python3.9/site-packages/pandas/_libs/testing.pyx:52, in pandas._libs.testing.assert_almost_equal()

File ~/opt/anaconda3/envs/modin/lib/python3.9/site-packages/pandas/_libs/testing.pyx:167, in pandas._libs.testing.assert_almost_equal()

File ~/opt/anaconda3/envs/modin/lib/python3.9/site-packages/pandas/_testing/asserters.py:682, in raise_assert_detail(obj, message, left, right, diff, index_values)
    679 if diff is not None:
    680     msg += f"\n[diff]: {diff}"
--> 682 raise AssertionError(msg)

AssertionError: DataFrame.iloc[:, 0] (column name="a") are different

DataFrame.iloc[:, 0] (column name="a") values are different (100.0 %)
[index]: [0, 1, 2, 3]
[left]:  [random_stringa, random_stringb, random_stringc, random_stringd]
[right]: [arandom_string, brandom_string, crandom_string, drandom_string]
```

</details>

Hints from issue discussion:
The problem here is that `radd` is just an alias for `add` so we don't actually consider the position of arguments. We'll have to make some changes to handle this case.

Commit message: Add release notes

Patch to review:
diff --git a/docs/release_notes/release_notes-0.16.0.rst b/docs/release_notes/release_notes-0.16.0.rst
index a62a476bacc..c09184a7d17 100644
--- a/docs/release_notes/release_notes-0.16.0.rst
+++ b/docs/release_notes/release_notes-0.16.0.rst
@@ -40,6 +40,7 @@ Key Features and Updates
   * FIX-#4835: Handle Pathlike paths in `read_parquet` (#4837)
   * FIX-#4872: Stop checking the private ray mac memory limit (#4873)
   * FIX-#4848: Fix rebalancing partitions when NPartitions == 1 (#4874)
+  * FIX-#4907: Implement `radd` for Series and DataFrame (#4908)
 * Performance enhancements
   * PERF-#4182: Add cell-wise execution for binary ops, fix bin ops for empty dataframes (#4391)
   * PERF-#4288: Improve perf of `groupby.mean` for narrow data (#4591)
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
index ab7edec7962..0ed0bac3e1e 100644
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -381,6 +381,7 @@ def to_numpy(self, **kwargs):
     mul = Binary.register(pandas.DataFrame.mul)
     ne = Binary.register(pandas.DataFrame.ne)
     pow = Binary.register(pandas.DataFrame.pow)
+    radd = Binary.register(pandas.DataFrame.radd)
     rfloordiv = Binary.register(pandas.DataFrame.rfloordiv)
     rmod = Binary.register(pandas.DataFrame.rmod)
     rpow = Binary.register(pandas.DataFrame.rpow)
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
index c7d11c60147..d08fe0db3ba 100644
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -1953,8 +1953,6 @@ def pow(
             "pow", other, axis=axis, level=level, fill_value=fill_value
         )
 
-    radd = add
-
     def quantile(
         self, q=0.5, axis=0, numeric_only=True, interpolation="linear"
     ):  # noqa: PR01, RT01, D200
@@ -2262,6 +2260,16 @@ def reset_index(
             )
         return self._create_or_update_from_compiler(new_query_compiler, inplace)
 
+    def radd(
+        self, other, axis="columns", level=None, fill_value=None
+    ):  # noqa: PR01, RT01, D200
+        """
+        Return addition of `BasePandasDataset` and `other`, element-wise (binary operator `radd`).
+        """
+        return self._binary_op(
+            "radd", other, axis=axis, level=level, fill_value=fill_value
+        )
+
     def rfloordiv(
         self, other, axis="columns", level=None, fill_value=None
     ):  # noqa: PR01, RT01, D200
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
index 197befebaec..027363aee2a 100644
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -1697,7 +1697,6 @@ def _prod(
         )
 
     product = DataFrameCompat.prod
-    radd = add
 
     def query(self, expr, inplace=False, **kwargs):  # noqa: PR01, RT01, D200
         """
@@ -1796,6 +1795,21 @@ def rfloordiv(
             broadcast=isinstance(other, Series),
         )
 
+    def radd(
+        self, other, axis="columns", level=None, fill_value=None
+    ):  # noqa: PR01, RT01, D200
+        """
+        Get addition of ``DataFrame`` and `other`, element-wise (binary operator `radd`).
+        """
+        return self._binary_op(
+            "radd",
+            other,
+            axis=axis,
+            level=level,
+            fill_value=fill_value,
+            broadcast=isinstance(other, Series),
+        )
+
     def rmod(
         self, other, axis="columns", level=None, fill_value=None
     ):  # noqa: PR01, RT01, D200
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index 7508d8280d5..49f9c667d13 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -170,9 +170,9 @@ def _set_name(self, name):
     def __add__(self, right):
         return self.add(right)
 
-    @_doc_binary_op(operation="addition", bin_op="add", right="left")
+    @_doc_binary_op(operation="addition", bin_op="radd", right="left")
     def __radd__(self, left):
-        return self.add(left)
+        return self.radd(left)
 
     @_doc_binary_op(operation="union", bin_op="and", right="other")
     def __and__(self, other):
@@ -495,6 +495,17 @@ def add(self, other, level=None, fill_value=None, axis=0):  # noqa: PR01, RT01,
             new_other, level=level, fill_value=fill_value, axis=axis
         )
 
+    def radd(
+        self, other, level=None, fill_value=None, axis=0
+    ):  # noqa: PR01, RT01, D200
+        """
+        Return Addition of series and other, element-wise (binary operator radd).
+        """
+        new_self, new_other = self._prepare_inter_op(other)
+        return super(Series, new_self).radd(
+            new_other, level=level, fill_value=fill_value, axis=axis
+        )
+
     def add_prefix(self, prefix):  # noqa: PR01, RT01, D200
         """
         Prefix labels with string `prefix`.
@@ -1441,7 +1452,6 @@ def _prod(
         )
 
     product = SeriesCompat.prod
-    radd = add
 
     def ravel(self, order="C"):  # noqa: PR01, RT01, D200
         """
diff --git a/modin/pandas/test/dataframe/test_binary.py b/modin/pandas/test/dataframe/test_binary.py
index c338bdbd030..57ac1f908ca 100644
--- a/modin/pandas/test/dataframe/test_binary.py
+++ b/modin/pandas/test/dataframe/test_binary.py
@@ -286,3 +286,12 @@ def test_empty_df(empty_operand):
         pandas_res = pandas_df_empty + pandas_df_empty
 
     df_equals(modin_res, pandas_res)
+
+
+def test_concat_string_to_df():
+    modin_df, pandas_df = create_test_dfs({"a": ["a", "b", "c", "d"]})
+
+    modin_df = "random_string" + modin_df
+    pandas_df = "random_string" + pandas_df
+
+    df_equals(modin_df, pandas_df)
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
index f5a457e00ed..6fbfa6ec2fd 100644
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -4540,6 +4540,16 @@ def test_encode(data, encoding_type):
         df_equals(modin_result, pandas_result)
 
 
+@pytest.mark.parametrize("data", test_string_data_values, ids=test_string_data_keys)
+def test_concat_string_to_series(data):
+    modin_series, pandas_series = create_test_series(data)
+
+    modin_series = "random_string" + modin_series
+    pandas_series = "random_string" + pandas_series
+
+    df_equals(modin_series, pandas_series)
+
+
 @pytest.mark.parametrize(
     "is_sparse_data", [True, False], ids=["is_sparse", "is_not_sparse"]
 )


Code review comments:
diff_hunk: @@ -286,3 +286,12 @@ def test_empty_df(empty_operand):
         pandas_res = pandas_df_empty + pandas_df_empty
 
     df_equals(modin_res, pandas_res)
+
+
+def test_concat_string_to_df():
path: modin/pandas/test/dataframe/test_binary.py
text: @user1:
```suggestion
def test_add_string_to_df():
```

diff_hunk: @@ -286,3 +286,12 @@ def test_empty_df(empty_operand):
         pandas_res = pandas_df_empty + pandas_df_empty
 
     df_equals(modin_res, pandas_res)
+
+
+def test_concat_string_to_df():
+    modin_df, pandas_df = create_test_dfs({"a": ["a", "b", "c", "d"]})
+
+    modin_df = "random_string" + modin_df
+    pandas_df = "random_string" + pandas_df
+
+    df_equals(modin_df, pandas_df)
path: modin/pandas/test/dataframe/test_binary.py
text: @user1:
```suggestion
    modin_df, pandas_df = create_test_dfs(['a', 'b'])
    eval_general(modin_df, pandas_df, lambda df: 'string' + df)
```

If you accept this suggestion, please check that the new test fails without this change.

@author:
Checked, the test fails without my changes.