Repository: modin-project/modin

Issue:
BUG: AttributeError: Can only use .cat accessor with a 'category' dtype
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import pandas
import modin.pandas as pd
import modin.config as cfg

cfg.MinPartitionSize.put(2)

data = {"name": ["abc", "def", "ghi", "jkl"]}
pandas_df = pandas.DataFrame(data)
pandas_df = pandas_df.astype("category")
modin_df = pd.DataFrame(data)
modin_df = modin_df.astype("category")
print(pandas_df["name"].cat.codes)
# 0    0
# 1    1
# 2    2
# 3    3
# dtype: int8
print(modin_df["name"].cat.codes)
# AttributeError: Can only use .cat accessor with a 'category' dtype
```


### Issue Description

We use pandas.concat to concatenate dfs and apply a function to them along a full axis. If dfs have columns of "category" dtype, then after dfs are concatenated the resultant dtype will not be "category". See details in https://github.com/pandas-dev/pandas/issues/51362. That's why we see the error when calling .cat.

### Expected Behavior

Should match pandas.

### Error Logs

<details>

```python-traceback

Replace this line with the error backtrace (if applicable).

```

</details>


### Installed Versions

<details>

Replace this line with the output of pd.show_versions()

</details>



Discussion:

Resolving patch:
diff --git a/modin/conftest.py b/modin/conftest.py
--- a/modin/conftest.py
+++ b/modin/conftest.py
@@ -45,7 +45,12 @@ def _saving_make_api_url(token, _make_api_url=modin.utils._make_api_url):
 
 import modin  # noqa: E402
 import modin.config  # noqa: E402
-from modin.config import IsExperimental, TestRayClient  # noqa: E402
+from modin.config import (  # noqa: E402
+    NPartitions,
+    MinPartitionSize,
+    IsExperimental,
+    TestRayClient,
+)
 import uuid  # noqa: E402
 
 from modin.core.storage_formats import (  # noqa: E402
@@ -535,6 +540,22 @@ def get_generated_doc_urls():
     return lambda: _generated_doc_urls
 
 
+@pytest.fixture
+def set_num_partitions(request):
+    old_num_partitions = NPartitions.get()
+    NPartitions.put(request.param)
+    yield
+    NPartitions.put(old_num_partitions)
+
+
+@pytest.fixture
+def set_min_partition_size(request):
+    old_min_partition_size = MinPartitionSize.get()
+    MinPartitionSize.put(request.param)
+    yield
+    MinPartitionSize.put(old_min_partition_size)
+
+
 ray_client_server = None
 
 
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -3385,7 +3385,15 @@ def sort_columns_by_row_values(self, rows, ascending=True, **kwargs):
     # Cat operations
     def cat_codes(self):
         def func(df) -> np.ndarray:
+            # `df` is supposed to be consisted of multiple partitions,
+            # which should be concatenated before applying a function.
+            # `pd.concat` doesn't preserve categorical dtype
+            # if the dfs have categorical columns
+            # so we intentionaly restore the right dtype.
+            # TODO: revert the change when https://github.com/pandas-dev/pandas/issues/51362 is fixed.
             ser = df.iloc[:, 0]
+            if ser.dtype != "category":
+                ser = ser.astype("category", copy=False)
             return ser.cat.codes
 
         res = self._modin_frame.apply_full_axis(axis=0, func=func)


Test patch:
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -4333,6 +4333,21 @@ def test_cat_codes(data):
     df_equals(modin_result, pandas_result)
 
 
+@pytest.mark.parametrize(
+    "set_min_partition_size",
+    [1, 2],
+    ids=["four_partitions", "two_partitions"],
+    indirect=True,
+)
+def test_cat_codes_issue5650(set_min_partition_size):
+    data = {"name": ["abc", "def", "ghi", "jkl"]}
+    pandas_df = pandas.DataFrame(data)
+    pandas_df = pandas_df.astype("category")
+    modin_df = pd.DataFrame(data)
+    modin_df = modin_df.astype("category")
+    eval_general(modin_df, pandas_df, lambda df: df["name"].cat.codes)
+
+
 @pytest.mark.parametrize(
     "data", test_data_categorical_values, ids=test_data_categorical_keys
 )
diff --git a/modin/test/storage_formats/pandas/test_internals.py b/modin/test/storage_formats/pandas/test_internals.py
--- a/modin/test/storage_formats/pandas/test_internals.py
+++ b/modin/test/storage_formats/pandas/test_internals.py
@@ -113,14 +113,6 @@ def validate_partitions_cache(df):
             assert df._partitions[i, j].width() == column_widths[j]
 
 
-@pytest.fixture
-def set_num_partitions(request):
-    old_num_partitions = NPartitions.get()
-    NPartitions.put(request.param)
-    yield
-    NPartitions.put(old_num_partitions)
-
-
 def test_aligning_blocks():
     # Test problem when modin frames have the same number of rows, but different
     # blocks (partition.list_of_blocks). See #2322 for details