Repository: modin-project/modin

Issue:
BUG: argmin returns incorrect results
### 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-main-branch).)


### Reproducible Example

```python
In [1]: import modin.pandas as mpd

In [2]: s = mpd.Series([1,2,3], index=[1,0,2])

In [3]: s.argmin()
Out[3]: np.int64(1)

In [4]: import pandas as pd

In [5]: s = pd.Series([1,2,3], index=[1,0,2])

In [6]: s.argmin()
Out[6]: np.int64(0)
```


### Issue Description

incorrect results

Spotted in [Narwhals](https://github.com/narwhals-dev/narwhals)

### Expected Behavior

same as pandas

### Error Logs

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


### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : 3e951a63084a9cbfd5e73f6f36653ee12d2a2bfa
python                : 3.12.5
python-bits           : 64
OS                    : Linux
OS-release            : 5.15.167.4-microsoft-standard-WSL2
Version               : #1 SMP Tue Nov 5 00:21:55 UTC 2024
machine               : x86_64
processor             : x86_64
byteorder             : little
LC_ALL                : None
LANG                  : C.UTF-8
LOCALE                : C.UTF-8

Modin dependencies
------------------
modin                 : 0.32.0
ray                   : None
dask                  : 2024.9.0
distributed           : 2024.9.0

pandas dependencies
-------------------
pandas                : 2.2.3
numpy                 : 2.2.0
pytz                  : 2024.2
dateutil              : 2.9.0.post0
pip                   : 24.3.1
Cython                : None
sphinx                : None
IPython               : 8.30.0
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : 4.12.3
blosc                 : None
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : None
fsspec                : 2024.10.0
html5lib              : None
hypothesis            : None
gcsfs                 : None
jinja2                : 3.1.4
lxml.etree            : None
matplotlib            : 3.9.2
numba                 : None
numexpr               : None
odfpy                 : None
openpyxl              : None
pandas_gbq            : None
psycopg2              : None
pymysql               : None
pyarrow               : 18.1.0
pyreadstat            : None
pytest                : 8.3.3
python-calamine       : None
pyxlsb                : None
s3fs                  : None
scipy                 : 1.14.1
sqlalchemy            : None
tables                : None
tabulate              : None
xarray                : None
xlrd                  : None
xlsxwriter            : None
zstandard             : None
tzdata                : 2024.2
qtpy                  : None
pyqt5                 : None

</details>


Discussion:

Patch:
diff --git a/docs/usage_guide/advanced_usage/index.rst b/docs/usage_guide/advanced_usage/index.rst
index 985a3e9e..f6e5c6f2 100644
--- a/docs/usage_guide/advanced_usage/index.rst
+++ b/docs/usage_guide/advanced_usage/index.rst
@@ -39,6 +39,9 @@ Additional APIs
 Modin also supports these additional APIs on top of pandas to improve user experience.
 
 - :py:meth:`~modin.pandas.DataFrame.modin.to_pandas` -- convert a Modin DataFrame/Series to a pandas DataFrame/Series.
+- :py:meth:`~modin.pandas.DataFrame.get_backend` -- Get the ``Backend`` :doc:`configuration variable </flow/modin/config>` of this ``DataFrame``.
+- :py:meth:`~modin.pandas.DataFrame.move_to` -- Move data and execution for this ``DataFrame`` to the given ``Backend`` :doc:`configuration variable </flow/modin/config>`. This method is an alias for ``DataFrame.set_backend``.
+- :py:meth:`~modin.pandas.DataFrame.set_backend` -- Move data and execution for this ``DataFrame`` to the given ``Backend`` :doc:`configuration variable </flow/modin/config>`. This method is an alias for ``DatFrame.move_to``.
 - :py:func:`~modin.pandas.io.from_pandas` -- convert a pandas DataFrame to a Modin DataFrame.
 - :py:meth:`~modin.pandas.DataFrame.modin.to_ray` -- convert a Modin DataFrame/Series to a Ray Dataset.
 - :py:func:`~modin.pandas.io.from_ray` -- convert a Ray Dataset to a Modin DataFrame.
diff --git a/docs/usage_guide/optimization_notes/index.rst b/docs/usage_guide/optimization_notes/index.rst
index 38e9771f..74df437e 100644
--- a/docs/usage_guide/optimization_notes/index.rst
+++ b/docs/usage_guide/optimization_notes/index.rst
@@ -314,7 +314,7 @@ Copy-pastable example, showing how mixing pandas and Modin DataFrames in a singl
   # Possible output: TypeError
 
 
-Using pandas to execute queries in Modin's ``"native"`` execution mode
+Using pandas to execute queries with Modin's ``"Pandas"`` backend
 """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
 
 By default, Modin distributes the data in a dataframe (or series) and attempts
@@ -323,39 +323,62 @@ to process data for different partitions in parallel.
 However, for certain scenarios, such as handling small datasets, Modin's
 parallel execution may introduce unnecessary overhead. In such cases, it's more
 efficient to use serial execution with a single, unpartitioned pandas dataframe.
-You can enable this kind of "native" execution by setting Modin's
-``StorageFormat`` and ``Engine``
-:doc:`configuration variables </flow/modin/config>` to ``"Native"``.
+You can enable this kind of local pandas execution by setting Modin's
+``Backend``
+:doc:`configuration variable </flow/modin/config>` to ``"Pandas"``.
 
-DataFrames created while Modin's global execution mode is set to ``"Native"``
-will continue to use native execution even if you switch the execution mode
+DataFrames created while Modin's global backend is set to ``"Pandas"``
+will continue to use native execution even if you switch the global backend
 later. Modin supports interoperability between distributed Modin DataFrames
-and those using native execution.
+and those using the pandas backend.
 
-Here is an example of using native execution:
+Here is an example of using the pandas backend.
 
 .. code-block:: python
 
   import modin.pandas as pd
   from modin import set_execution
-  from modin.config import StorageFormat, Engine
+  from modin.config import Backend
 
   # This dataframe will use Modin's default, distributed execution.
-  df_distributed_1 = pd.DataFrame([0])
-  assert df_distributed_1._query_compiler.engine != "Native"
+  original_backend = Backend.get()
+  assert original_backend != "Pandas"
+  distributed_df_1 = pd.DataFrame([0])
 
-  # Set execution to "Native" for native execution.
-  original_engine, original_storage_format = set_execution(
-    engine="Native",
-    storage_format="Native"
-  )
-  native_df = pd.DataFrame([1])
-  assert native_df._query_compiler.engine == "Native"
+  # Set backend to "Pandas" for local pandas execution.
+  Backend.put("Pandas")
+  modin_on_pandas_df = pd.DataFrame([1])
+  assert modin_on_pandas_df.get_backend() == "Pandas"
 
   # Revert to default settings for distributed execution
-  set_execution(engine=original_engine, storage_format=original_storage_format)
-  df_distributed_2 = pd.DataFrame([2])
-  assert df_distributed_2._query_compiler.engine == original_engine
+  Backend.put(original_backend)
+  distributed_df_2 = pd.DataFrame([2])
+  assert distributed_df_2.get_backend() == original_backend
+
+You can also use the pandas backend for some dataframes while using different
+backends for other dataframes. You can switch the backend of an individual
+dataframe or series with ``set_backend()`` or its synonym ``move_to()``.
+Here's an example of switching the backend for an individual dataframe.
+
+.. code-block:: python
+
+  import modin.pandas as pd
+
+  # This dataframe will use Modin's default, distributed execution.
+  original_backend = Backend.get()
+  assert original_backend != "Pandas"
+  distributed_df_1 = pd.DataFrame([0])
+
+  pandas_df_1 = distributed_df_1.move_to("Pandas")
+  assert pandas_df_1.get_backend() == "Pandas"
+  pandas_df_1 = pandas_df_1.sort_values(0)
+  assert pandas_df_1.get_backend() == "Pandas"
+
+  new_df = pandas_df_1.move_to(original_backend)
+  assert new_df.get_backend() == original_backend
+
+  new_df.set_backend("Pandas", inplace=True)
+  assert new_df.get_backend() == "Pandas"
 
 Operation-specific optimizations
 """"""""""""""""""""""""""""""""
diff --git a/environment-dev.yml b/environment-dev.yml
index 049b3e39..a8c806d3 100644
--- a/environment-dev.yml
+++ b/environment-dev.yml
@@ -12,6 +12,8 @@ dependencies:
   - psutil>=5.8.0
 
   # optional dependencies
+  # NOTE Keep the ray and dask dependencies in sync with the Linux and Windows
+  # Unidist environment dependencies.
   # ray==2.5.0 broken: https://github.com/conda-forge/ray-packages-feedstock/issues/100
   - ray-core>=2.1.0,!=2.5.0
   - pyarrow>=10.0.1
diff --git a/modin/config/envvars.py b/modin/config/envvars.py
index a48fbd0f..b78fc6a2 100644
--- a/modin/config/envvars.py
+++ b/modin/config/envvars.py
@@ -41,25 +41,25 @@ class EnvironmentVariable(Parameter, type=str, abstract=True):
     varname: Optional[str] = None
 
     @classmethod
-    def _get_raw_from_config(cls) -> str:
+    def _get_value_from_config(cls) -> Any:
         """
         Read the value from environment variable.
 
         Returns
         -------
-        str
-            Config raw value.
-
-        Raises
-        ------
-        TypeError
-            If `varname` is None.
-        KeyError
-            If value is absent.
+        Any
+            Config raw value if it's set, otherwise `_UNSET`.
         """
         if cls.varname is None:
             raise TypeError("varname should not be None")
-        return os.environ[cls.varname]
+        if cls.varname not in os.environ:
+            return _UNSET
+        raw = os.environ[cls.varname]
+        if not _TYPE_PARAMS[cls.type].verify(raw):
+            # TODO: use and test a better error message, like "Invalid value
+            # for {cls.varname}: {raw}"
+            raise ValueError(f"Unsupported raw value: {raw}")
+        return _TYPE_PARAMS[cls.type].decode(raw)
 
     @classmethod
     def get_help(cls) -> str:
@@ -166,13 +166,36 @@ class EnvWithSibilings(
                 cls._update_sibling = True
 
 
+class EnvironmentVariableDisallowingExecutionAndBackendBothSet(
+    EnvironmentVariable,
+    type=EnvironmentVariable.type,
+    abstract=True,
+):
+    """Subclass to disallow getting this variable from the environment when both execution and backend are set in the environment."""
+
+    @classmethod
+    @doc(EnvironmentVariable._get_value_from_config.__doc__)
+    def _get_value_from_config(cls) -> str:
+        if Backend.varname in os.environ and (
+            Engine.varname in os.environ or StorageFormat.varname in os.environ
+        ):
+            # Handling this case is tricky, in part because the combination of
+            # Backend and Engine/StorageFormat may be invalid. For now just
+            # disallow it.
+            raise ValueError("Can't specify both execution and backend in environment")
+        return super()._get_value_from_config()
+
+
 class IsDebug(EnvironmentVariable, type=bool):
     """Force Modin engine to be "Python" unless specified by $MODIN_ENGINE."""
 
     varname = "MODIN_DEBUG"
 
 
-class Engine(EnvironmentVariable, type=str):
+class Engine(
+    EnvironmentVariableDisallowingExecutionAndBackendBothSet,
+    type=str,
+):
     """Distribution engine to run queries by."""
 
     varname = "MODIN_ENGINE"
@@ -271,8 +294,45 @@ class Engine(EnvironmentVariable, type=str):
             )
         )
 
+    @classmethod
+    def get(cls) -> str:
+        """
+        Get the engine value.
 
-class StorageFormat(EnvironmentVariable, type=str):
+        Returns
+        -------
+        str
+            Engine value.
+        """
+        # We have to override get() because Engine may need to get its value
+        # from the OS's environment variables for Backend or Engine.
+
+        cls._warn_if_deprecated()
+
+        # First, check if we've already set the engine value.
+        if cls._value is not _UNSET:
+            return cls._value
+
+        engine_config_value = cls._get_value_from_config()
+        backend_config_value = Backend._get_value_from_config()
+
+        # If Engine is in the OS's configuration, use the configured Engine value.
+        # Otherwise, use the Backend config value if that exists. If it doesn't,
+        # fall back to the default Engine value.
+        cls._value = (
+            engine_config_value
+            if engine_config_value is not _UNSET
+            else (
+                Backend.get_execution_for_backend(backend_config_value).engine
+                if backend_config_value is not _UNSET
+                else cls._get_default()
+            )
+        )
+
+        return cls._value
+
+
+class StorageFormat(EnvironmentVariableDisallowingExecutionAndBackendBothSet, type=str):
     """Engine to run on a single node of distribution."""
 
     @classmethod
@@ -293,6 +353,43 @@ class StorageFormat(EnvironmentVariable, type=str):
             )
         )
 
+    @classmethod
+    def get(cls) -> str:
+        """
+        Get the storage format value.
+
+        Returns
+        -------
+        str
+            Storage format value.
+        """
+        # We have to override get() because StorageFormat may need to get its
+        # value from the OS's environment variables for Backend or StorageFormat.
+
+        cls._warn_if_deprecated()
+
+        # First, check if we've already set the engine value.
+        if cls._value is not _UNSET:
+            return cls._value
+
+        storage_format_config_value = cls._get_value_from_config()
+        backend_config_value = Backend._get_value_from_config()
+
+        # If StorageFormat is in the OS's configuration, use the configured
+        # StorageFormat value. Otherwise, use the Backend config value if that
+        # exists. If it doesn't, fall back to the default StorageFormat value.
+        cls._value = (
+            storage_format_config_value
+            if storage_format_config_value is not _UNSET
+            else (
+                Backend.get_execution_for_backend(backend_config_value).storage_format
+                if backend_config_value is not _UNSET
+                else cls._get_default()
+            )
+        )
+
+        return cls._value
+
     varname = "MODIN_STORAGE_FORMAT"
     default = "Pandas"
     choices = ("Pandas", "Native")
@@ -301,7 +398,7 @@ class StorageFormat(EnvironmentVariable, type=str):
 Execution = namedtuple("Execution", ["storage_format", "engine"])
 
 
-class Backend(EnvironmentVariable, type=str):
+class Backend(EnvironmentVariableDisallowingExecutionAndBackendBothSet, type=str):
     """
     An alias for execution, i.e. the combination of StorageFormat and Engine.
 
@@ -332,12 +429,7 @@ class Backend(EnvironmentVariable, type=str):
         value : str
             Backend value to set.
         """
-        value = cls.normalize(value)
-        if value not in cls.choices:
-            raise ValueError(
-                f"Unknown backend '{value}'. Please register the backend with Backend.register_backend()"
-            )
-        execution = cls._BACKEND_TO_EXECUTION[value]
+        execution = cls.get_execution_for_backend(value)
         set_execution(execution.engine, execution.storage_format)
 
     @classmethod
@@ -420,6 +512,74 @@ class Backend(EnvironmentVariable, type=str):
             )
         return cls._EXECUTION_TO_BACKEND[execution]
 
+    @classmethod
+    def get_execution_for_backend(cls, backend: str) -> Execution:
+        """
+        Get the execution for the given backend.
+
+        Parameters
+        ----------
+        backend : str
+            Backend to get the execution for.
+
+        Returns
+        -------
+        execution : Execution
+            The execution for the given backend
+        """
+        if not isinstance(backend, str):
+            raise TypeError(
+                "Backend value should be a string, but instead it is "
+                + f"{repr(backend)} of type {type(backend)}."
+            )
+        normalized_value = cls.normalize(backend)
+        if normalized_value not in cls.choices:
+            backend_choice_string = ", ".join(f"'{choice}'" for choice in cls.choices)
+            raise ValueError(
+                f"Unknown backend '{backend}'. Available backends are: "
+                + backend_choice_string
+            )
+        if normalized_value not in cls._BACKEND_TO_EXECUTION:
+            raise ValueError(
+                f"Backend '{backend}' has no known execution. Please "
+                + "register an execution for it with Backend.register_backend()."
+            )
+        return cls._BACKEND_TO_EXECUTION[normalized_value]
+
+    @classmethod
+    def get(cls) -> str:
+        """
+        Get the backend.
+
+        Returns
+        -------
+        str
+            Backend.
+        """
+        # We have to override get() because Backend may need to get its value
+        # from the OS's environment variables for Backend or Engine.
+
+        cls._warn_if_deprecated()
+
+        # First, check if we've already set the Backend value.
+        if cls._value is not _UNSET:
+            return cls._value
+
+        backend_config_value = Backend._get_value_from_config()
+
+        # If Backend is in the OS's configuration, use the configured Backend
+        # value. Otherwise, we need to figure out the Backend value based on
+        # the Engine and StorageFormat values.
+        cls._value = (
+            backend_config_value
+            if backend_config_value is not _UNSET
+            else cls.get_backend_for_execution(
+                Execution(storage_format=StorageFormat.get(), engine=Engine.get())
+            )
+        )
+
+        return cls._value
+
 
 Backend.register_backend("Ray", Execution("Pandas", "Ray"))
 Backend.register_backend("Dask", Execution("Pandas", "Dask"))
diff --git a/modin/config/pubsub.py b/modin/config/pubsub.py
index f3af1700..4942470b 100644
--- a/modin/config/pubsub.py
+++ b/modin/config/pubsub.py
@@ -224,19 +224,22 @@ class Parameter(object):
     _deprecation_descriptor: Optional[DeprecationDescriptor] = None
 
     @classmethod
-    def _get_raw_from_config(cls) -> str:
+    def _warn_if_deprecated(cls) -> None:
+        """Warn that the variable is deprecated if it has a deprecation descriptor."""
+        if cls._deprecation_descriptor is not None:
+            warnings.warn(
+                cls._deprecation_descriptor.deprecation_message(), FutureWarning
+            )
+
+    @classmethod
+    def _get_value_from_config(cls) -> Any:
         """
         Read the value from config storage.
 
         Returns
         -------
-        str
-            Config raw value.
-
-        Raises
-        ------
-        KeyError
-            If value is absent.
+        Any
+            Config raw value if it's set, otherwise `_UNSET`.
 
         Notes
         -----
@@ -332,21 +335,15 @@ class Parameter(object):
         Any
             Decoded and verified config value.
         """
-        if cls._deprecation_descriptor is not None:
-            warnings.warn(
-                cls._deprecation_descriptor.deprecation_message(), FutureWarning
-            )
+        cls._warn_if_deprecated()
         if cls._value is _UNSET:
             # get the value from env
-            try:
-                raw = cls._get_raw_from_config()
-            except KeyError:
+            config_value = cls._get_value_from_config()
+            if config_value is _UNSET:
                 cls._value = cls._get_default()
                 cls._value_source = ValueSource.DEFAULT
             else:
-                if not _TYPE_PARAMS[cls.type].verify(raw):
-                    raise ValueError(f"Unsupported raw value: {raw}")
-                cls._value = _TYPE_PARAMS[cls.type].decode(raw)
+                cls._value = config_value
                 cls._value_source = ValueSource.GOT_FROM_CFG_SOURCE
         return cls._value
 
@@ -360,10 +357,7 @@ class Parameter(object):
         value : Any
             Config value to set.
         """
-        if cls._deprecation_descriptor is not None:
-            warnings.warn(
-                cls._deprecation_descriptor.deprecation_message(), FutureWarning
-            )
+        cls._warn_if_deprecated()
         cls._check_callbacks(cls._put_nocallback(value))
         cls._value_source = ValueSource.SET_BY_USER
 
diff --git a/modin/core/execution/dispatching/factories/dispatcher.py b/modin/core/execution/dispatching/factories/dispatcher.py
index b7a0bb4b..d20e0dad 100644
--- a/modin/core/execution/dispatching/factories/dispatcher.py
+++ b/modin/core/execution/dispatching/factories/dispatcher.py
@@ -17,9 +17,14 @@ Contain IO dispatcher class.
 Dispatcher routes the work to execution-specific functions.
 """
 
-from modin.config import Engine, IsExperimental, StorageFormat
+from typing import Union
+
+from pandas._libs.lib import NoDefault, no_default
+
+from modin.config import Backend, Engine, IsExperimental, StorageFormat
 from modin.core.execution.dispatching.factories import factories
-from modin.utils import _inherit_docstrings, get_current_execution
+from modin.core.storage_formats.base import BaseQueryCompiler
+from modin.utils import _inherit_docstrings
 
 
 class FactoryNotFoundError(AttributeError):
@@ -110,28 +115,39 @@ class FactoryDispatcher(object):
     def get_factory(cls) -> factories.BaseFactory:
         """Get current factory."""
         if cls.__factory is None:
-            from modin.pandas import _update_engine
 
-            Engine.subscribe(_update_engine)
-            Engine.subscribe(cls._update_factory)
-            StorageFormat.subscribe(cls._update_factory)
-        return cls.__factory
+            from modin.pandas import _initialize_engine
+
+            Engine.subscribe(
+                lambda engine_parameter: _initialize_engine(engine_parameter.get())
+            )
+            Backend.subscribe(cls._update_factory)
+        return_value = cls.__factory
+        return return_value
 
     @classmethod
-    def _update_factory(cls, *args):
+    def _get_prepared_factory_for_backend(cls, backend) -> factories.BaseFactory:
         """
-        Update and prepare factory with a new one specified via Modin config.
+        Get factory for the specified backend.
 
         Parameters
         ----------
-        *args : iterable
-            This parameters serves the compatibility purpose.
-            Does not affect the result.
+        backend : str
+            Backend name.
+
+        Returns
+        -------
+        factories.BaseFactory
+            Factory for the specified backend.
         """
-        factory_name = get_current_execution() + "Factory"
+        execution = Backend.get_execution_for_backend(backend)
+        from modin.pandas import _initialize_engine
+
+        _initialize_engine(execution.engine)
+        factory_name = f"{execution.storage_format}On{execution.engine}Factory"
         experimental_factory_name = "Experimental" + factory_name
         try:
-            cls.__factory = getattr(factories, factory_name, None) or getattr(
+            factory = getattr(factories, factory_name, None) or getattr(
                 factories, experimental_factory_name
             )
         except AttributeError:
@@ -145,26 +161,54 @@ class FactoryDispatcher(object):
                 raise FactoryNotFoundError(
                     msg.format(factory_name, experimental_factory_name)
                 )
-            cls.__factory = StubFactory.set_failing_name(factory_name)
+            factory = StubFactory.set_failing_name(factory_name)
         else:
             try:
-                cls.__factory.prepare()
+                factory.prepare()
             except ModuleNotFoundError as err:
-                # incorrectly initialized, should be reset to None again
-                # so that an unobvious error does not appear in the following code:
-                # "AttributeError: 'NoneType' object has no attribute 'from_non_pandas'"
-                cls.__factory = None
                 raise ModuleNotFoundError(
                     f"Make sure all required packages are installed: {str(err)}"
                 ) from err
-            except BaseException:
-                cls.__factory = None
-                raise
+        return factory
 
     @classmethod
-    @_inherit_docstrings(factories.BaseFactory._from_pandas)
-    def from_pandas(cls, df):
-        return cls.get_factory()._from_pandas(df)
+    def _update_factory(cls, *args):
+        """
+        Update and prepare factory with a new one specified via Modin config.
+
+        Parameters
+        ----------
+        *args : iterable
+            This parameters serves the compatibility purpose.
+            Does not affect the result.
+        """
+        cls.__factory = cls._get_prepared_factory_for_backend(Backend.get())
+
+    @classmethod
+    def from_pandas(
+        cls, df, backend: Union[str, NoDefault] = no_default
+    ) -> BaseQueryCompiler:
+        """
+        Create a Modin query compiler from a pandas DataFrame.
+
+        Parameters
+        ----------
+        df : pandas.DataFrame
+            The pandas DataFrame to convert.
+        backend : str or NoDefault, default: NoDefault
+            The backend to use for the resulting query compiler. If NoDefault,
+            use the current global default ``Backend`` from the Modin config.
+
+        Returns
+        -------
+        BaseQueryCompiler
+            A Modin query compiler that wraps the input pandas DataFrame.
+        """
+        return (
+            cls.get_factory()
+            if backend is no_default
+            else cls._get_prepared_factory_for_backend(backend)
+        )._from_pandas(df)
 
     @classmethod
     @_inherit_docstrings(factories.BaseFactory._from_arrow)
diff --git a/modin/pandas/__init__.py b/modin/pandas/__init__.py
index 01f83ce9..b7eee337 100644
--- a/modin/pandas/__init__.py
+++ b/modin/pandas/__init__.py
@@ -101,10 +101,10 @@ import os
 
 from modin.config import Parameter
 
-_is_first_update = {}
+_engine_initialized = {}
 
 
-def _update_engine(publisher: Parameter):
+def _initialize_engine(engine_string: str):
     from modin.config import (
         CpuCount,
         Engine,
@@ -116,25 +116,25 @@ def _update_engine(publisher: Parameter):
     # Set this so that Pandas doesn't try to multithread by itself
     os.environ["OMP_NUM_THREADS"] = "1"
 
-    if publisher.get() == "Ray":
-        if _is_first_update.get("Ray", True):
+    if engine_string == "Ray":
+        if not _engine_initialized.get("Ray", False):
             from modin.core.execution.ray.common import initialize_ray
 
             initialize_ray()
-    elif publisher.get() == "Dask":
-        if _is_first_update.get("Dask", True):
+    elif engine_string == "Dask":
+        if not _engine_initialized.get("Dask", False):
             from modin.core.execution.dask.common import initialize_dask
 
             initialize_dask()
-    elif publisher.get() == "Unidist":
-        if _is_first_update.get("Unidist", True):
+    elif engine_string == "Unidist":
+        if not _engine_initialized.get("Unidist", False):
             from modin.core.execution.unidist.common import initialize_unidist
 
             initialize_unidist()
-    elif publisher.get() not in Engine.NOINIT_ENGINES:
-        raise ImportError("Unrecognized execution engine: {}.".format(publisher.get()))
+    elif engine_string not in Engine.NOINIT_ENGINES:
+        raise ImportError("Unrecognized execution engine: {}.".format(engine_string))
 
-    _is_first_update[publisher.get()] = False
+    _engine_initialized[engine_string] = True
 
 
 from modin.pandas import arrays, errors
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
index 298a376f..18a64cd7 100644
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -67,6 +67,7 @@ from pandas.core.dtypes.common import (
 )
 from pandas.core.indexes.api import ensure_index
 from pandas.core.methods.describe import _refine_percentiles
+from pandas.util._decorators import doc
 from pandas.util._validators import (
     validate_ascending,
     validate_bool_kwarg,
@@ -74,10 +75,11 @@ from pandas.util._validators import (
 )
 
 from modin import pandas as pd
+from modin.config import Backend, Execution
 from modin.error_message import ErrorMessage
 from modin.logging import ClassLogger, disable_logging
 from modin.pandas.accessor import CachedAccessor, ModinAPI
-from modin.pandas.utils import is_scalar
+from modin.pandas.utils import GET_BACKEND_DOC, SET_BACKEND_DOC, is_scalar
 from modin.utils import _inherit_docstrings, expanduser_path_arg, try_cast_to_pandas
 
 from .utils import _doc_binary_op, is_full_grab_slice
@@ -4388,3 +4390,31 @@ class BasePandasDataset(ClassLogger):
 
     # namespace for additional Modin functions that are not available in Pandas
     modin: ModinAPI = CachedAccessor("modin", ModinAPI)
+
+    @doc(SET_BACKEND_DOC, class_name=__qualname__)
+    def set_backend(self, backend: str, inplace: bool = False) -> Optional[Self]:
+        # TODO(https://github.com/modin-project/modin/issues/7467): refactor
+        # to avoid this cyclic import in most places we do I/O, e.g. in
+        # modin/pandas/io.py
+        from modin.core.execution.dispatching.factories.dispatcher import (
+            FactoryDispatcher,
+        )
+
+        pandas_self = self._query_compiler.to_pandas()
+        query_compiler = FactoryDispatcher.from_pandas(df=pandas_self, backend=backend)
+        if inplace:
+            self._update_inplace(query_compiler)
+            return None
+        else:
+            return self.__constructor__(query_compiler=query_compiler)
+
+    move_to = set_backend
+
+    @doc(GET_BACKEND_DOC, class_name=__qualname__)
+    def get_backend(self) -> str:
+        return Backend.get_backend_for_execution(
+            Execution(
+                engine=self._query_compiler.engine,
+                storage_format=self._query_compiler.storage_format,
+            )
+        )
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
index b85a2f6c..853d2486 100644
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -55,6 +55,7 @@ from pandas.core.dtypes.common import (
 )
 from pandas.core.indexes.frozen import FrozenList
 from pandas.io.formats.info import DataFrameInfo
+from pandas.util._decorators import doc
 from pandas.util._validators import validate_bool_kwarg
 
 from modin.config import PersistentPickle
@@ -77,12 +78,16 @@ from .groupby import DataFrameGroupBy
 from .iterator import PartitionIterator
 from .series import Series
 from .utils import (
+    GET_BACKEND_DOC,
+    SET_BACKEND_DOC,
     SET_DATAFRAME_ATTRIBUTE_WARNING,
     _doc_binary_op,
     cast_function_modin2pandas,
 )
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from modin.core.storage_formats import BaseQueryCompiler
 
 # Dictionary of extensions assigned to this class
@@ -3314,3 +3319,13 @@ class DataFrame(BasePandasDataset):
         return self._inflate_light, (self._query_compiler, pid)
 
     # Persistance support methods - END
+
+    @doc(SET_BACKEND_DOC, class_name=__qualname__)
+    def set_backend(self, backend: str, inplace: bool = False) -> Optional[Self]:
+        return super().set_backend(backend=backend, inplace=inplace)
+
+    move_to = set_backend
+
+    @doc(GET_BACKEND_DOC, class_name=__qualname__)
+    def get_backend(self) -> str:
+        return super().get_backend()
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index 11188e85..7245f0e6 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -29,6 +29,7 @@ from pandas.core.common import apply_if_callable, is_bool_indexer
 from pandas.core.dtypes.common import is_dict_like, is_list_like
 from pandas.core.series import _coerce_method
 from pandas.io.formats.info import SeriesInfo
+from pandas.util._decorators import doc
 from pandas.util._validators import validate_bool_kwarg
 
 from modin.config import PersistentPickle
@@ -50,10 +51,17 @@ from .series_utils import (
     StringMethods,
     StructAccessor,
 )
-from .utils import _doc_binary_op, cast_function_modin2pandas, is_scalar
+from .utils import (
+    GET_BACKEND_DOC,
+    SET_BACKEND_DOC,
+    _doc_binary_op,
+    cast_function_modin2pandas,
+    is_scalar,
+)
 
 if TYPE_CHECKING:
     import numpy.typing as npt
+    from typing_extensions import Self
 
     from modin.core.storage_formats import BaseQueryCompiler
 
@@ -746,7 +754,9 @@ class Series(BasePandasDataset):
         """
         Return int position of the largest value in the Series.
         """
-        result = self.idxmax(axis=axis, skipna=skipna, *args, **kwargs)
+        result = self.reset_index(drop=True).idxmax(
+            axis=axis, skipna=skipna, *args, **kwargs
+        )
         if np.isnan(result) or result is pandas.NA:
             result = -1
         return result
@@ -757,7 +767,9 @@ class Series(BasePandasDataset):
         """
         Return int position of the smallest value in the Series.
         """
-        result = self.idxmin(axis=axis, skipna=skipna, *args, **kwargs)
+        result = self.reset_index(drop=True).idxmin(
+            axis=axis, skipna=skipna, *args, **kwargs
+        )
         if np.isnan(result) or result is pandas.NA:
             result = -1
         return result
@@ -2760,3 +2772,13 @@ class Series(BasePandasDataset):
         return self._inflate_light, (self._query_compiler, self.name, pid)
 
     # Persistance support methods - END
+
+    @doc(SET_BACKEND_DOC, class_name=__qualname__)
+    def set_backend(self, backend: str, inplace: bool = False) -> Optional[Self]:
+        return super().set_backend(backend=backend, inplace=inplace)
+
+    move_to = set_backend
+
+    @doc(GET_BACKEND_DOC, class_name=__qualname__)
+    def get_backend(self) -> str:
+        return super().get_backend()
diff --git a/modin/pandas/utils.py b/modin/pandas/utils.py
index 5cd061ca..3fd654fb 100644
--- a/modin/pandas/utils.py
+++ b/modin/pandas/utils.py
@@ -43,6 +43,47 @@ SET_DATAFRAME_ATTRIBUTE_WARNING = (
 )
 
 
+GET_BACKEND_DOC = """
+Get the backend for this ``{class_name}``.
+
+Returns
+-------
+str
+    The name of the backend.
+"""
+
+SET_BACKEND_DOC = """
+Move the data in this ``{class_name}`` from its current backend to the given one.
+
+Further operations on this ``{class_name}`` will use the new backend instead of
+the current one.
+
+Parameters
+----------
+backend : str
+    The name of the backend to set.
+inplace : bool, default: False
+    Whether to modify this ``{class_name}`` in place.
+
+Returns
+-------
+``{class_name}`` or None
+    If ``inplace`` is False, returns a new instance of the ``{class_name}``
+    with the given backend. If ``inplace`` is ``True``, returns None.
+
+Notes
+-----
+This method will
+    1) convert the data in this ``{class_name}`` to a pandas DataFrame in this
+       Python process
+    2) load the data from pandas to the new backend.
+
+Either step may be slow and/or memory-intensive, especially if this
+``{class_name}``'s data is large, or one or both of the backends do not store
+their data locally.
+"""
+
+
 def cast_function_modin2pandas(func):
     """
     Replace Modin functions with pandas functions if `func` is callable.
diff --git a/requirements/env_unidist_linux.yml b/requirements/env_unidist_linux.yml
index 155d8893..9f745424 100644
--- a/requirements/env_unidist_linux.yml
+++ b/requirements/env_unidist_linux.yml
@@ -14,6 +14,16 @@ dependencies:
   - psutil>=5.8.0
 
   # optional dependencies
+  # NOTE Keep the ray and dask dependencies in sync with the Windows Unidist
+  # environment and the general environment-dev.yml.
+  # We include the ray and dask dependencies here because we want to test
+  # switching dataframe backends to ray or dask.
+  # ray==2.5.0 broken: https://github.com/conda-forge/ray-packages-feedstock/issues/100
+  - ray-core>=2.1.0,!=2.5.0
+  # workaround for https://github.com/conda/conda/issues/11744
+  - grpcio!=1.45.*
+  - grpcio!=1.46.*
+  - dask>=2.22.0
   - pyarrow>=10.0.1
   - xarray>=2022.12.0
   - jinja2>=3.1.2
diff --git a/requirements/env_unidist_win.yml b/requirements/env_unidist_win.yml
index f4028288..149f7a98 100644
--- a/requirements/env_unidist_win.yml
+++ b/requirements/env_unidist_win.yml
@@ -14,6 +14,16 @@ dependencies:
   - psutil>=5.8.0
 
   # optional dependencies
+  # NOTE Keep the ray and dask dependencies in sync with the Linux Unidist
+  # environment and the general environment-dev.yml.
+  # We include the ray and dask dependencies here because we want to test
+  # switching dataframe backends to ray or dask.
+  # ray==2.5.0 broken: https://github.com/conda-forge/ray-packages-feedstock/issues/100
+  - ray-core>=2.1.0,!=2.5.0
+  # workaround for https://github.com/conda/conda/issues/11744
+  - grpcio!=1.45.*
+  - grpcio!=1.46.*
+  - dask>=2.22.0
   - pyarrow>=10.0.1
   - xarray>=2022.12.0
   - jinja2>=3.1.2


Test patch:
diff --git a/.github/actions/run-core-tests/group_2/action.yml b/.github/actions/run-core-tests/group_2/action.yml
index 0ce78a38..d77ffd17 100644
--- a/.github/actions/run-core-tests/group_2/action.yml
+++ b/.github/actions/run-core-tests/group_2/action.yml
@@ -18,6 +18,7 @@ runs:
                                                       modin/tests/pandas/dataframe/test_udf.py \
                                                       modin/tests/pandas/dataframe/test_window.py \
                                                       modin/tests/pandas/dataframe/test_pickle.py \
-                                                      modin/tests/pandas/test_repartition.py
+                                                      modin/tests/pandas/test_repartition.py \
+                                                      modin/tests/pandas/test_backend.py
           echo "::endgroup::"
         shell: bash -l {0}
diff --git a/modin/tests/config/test_envvars.py b/modin/tests/config/test_envvars.py
index c0c9c0d7..94d7cc3e 100644
--- a/modin/tests/config/test_envvars.py
+++ b/modin/tests/config/test_envvars.py
@@ -11,9 +11,11 @@
 # ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
+import itertools
 import os
 import re
 import sys
+import unittest.mock as mock
 from unittest.mock import Mock, patch
 
 import pandas
@@ -23,7 +25,7 @@ from pytest import param
 import modin.config as cfg
 import modin.pandas as pd
 from modin.config.envvars import _check_vars
-from modin.config.pubsub import _UNSET, ExactStr
+from modin.config.pubsub import _UNSET, ExactStr, ValueSource
 from modin.pandas.base import BasePandasDataset
 from modin.tests.pandas.utils import switch_execution
 
@@ -33,7 +35,7 @@ from modin.tests.pandas.utils import switch_execution
 # out of the order they are defined in. Be careful when running the test
 # locally or when adding new test cases. In particular, note:
 #   - test_ray_cluster_resources() causes us to permanently attach the
-#     `_update_engine` subscriber to Engine: https://github.com/modin-project/modin/blob/6252ebde19935bd1f6a6850209bf8a1f5e5ecfb7/modin/core/execution/dispatching/factories/dispatcher.py#L115
+#     `_initialize_engine` subscriber to Engine: https://github.com/modin-project/modin/blob/6252ebde19935bd1f6a6850209bf8a1f5e5ecfb7/modin/core/execution/dispatching/factories/dispatcher.py#L115
 #     Changing to any engine after that test runs will cause Modin to try to
 #     initialize the engine.
 #   - In CI, we only run these tests with Ray execution, in the
@@ -48,17 +50,19 @@ UNIDIST_SKIP_REASON = (
 )
 
 
-def reset_vars(*vars: tuple[cfg.Parameter]):
+@pytest.fixture
+def clear_backend_execution_and_storage_format(monkeypatch):
     """
-    Reset value for the passed parameters.
+    Reset environment variables and config classes for backend, execution, and storage format.
 
     Parameters
     ----------
     *vars : tuple[Parameter]
     """
-    for var in vars:
-        var._value = _UNSET
-        _ = os.environ.pop(var.varname, None)
+    for variable in (cfg.Backend, cfg.StorageFormat, cfg.Engine):
+        monkeypatch.setattr(variable, "_value", _UNSET)
+        monkeypatch.setattr(variable, "_value_source", ValueSource.DEFAULT)
+        monkeypatch.delitem(os.environ, variable.varname, raising=False)
 
 
 @pytest.fixture
@@ -81,6 +85,26 @@ def make_custom_envvar(request):
     return CustomVar
 
 
+@pytest.fixture(scope="session")
+def add_pandas_duplicate_on_ray_execution():
+    """
+    Add an execution mode with the storage format Test_Pandasduplicate and engine Ray.
+
+    This mode's execution is equivalent to PandasOnRay execution.
+    """
+    cfg.StorageFormat.add_option("Test_Pandasduplicate")
+    from modin.core.execution.dispatching.factories import factories
+
+    factories.Test_PandasduplicateOnRayFactory = factories.PandasOnRayFactory
+    cfg.Backend.register_backend(
+        "Test_Backend_1",
+        cfg.Execution(
+            storage_format="Test_Pandasduplicate",
+            engine="Ray",
+        ),
+    )
+
+
 @pytest.fixture
 def set_custom_envvar(make_custom_envvar):
     os.environ[make_custom_envvar.varname] = "  custom  "
@@ -493,6 +517,271 @@ class TestBackend:
         ):
             cfg.Backend.add_option("NewBackend")
 
+    @pytest.mark.parametrize(
+        "order_to_get_in",
+        itertools.permutations(
+            [
+                cfg.Backend,
+                cfg.Engine,
+                cfg.StorageFormat,
+            ]
+        ),
+        ids=lambda permutation: "_".join(x.__name__ for x in permutation),
+    )
+    @pytest.mark.parametrize(
+        "storage_environment_variable, engine_environment_variable, variable_to_expected_value",
+        [
+            (
+                "Native",
+                "Native",
+                {
+                    cfg.Backend: "Pandas",
+                    cfg.Engine: "Native",
+                    cfg.StorageFormat: "Native",
+                },
+            ),
+            (
+                "NATIVE",
+                "NATIVE",
+                {
+                    cfg.Backend: "Pandas",
+                    cfg.Engine: "Native",
+                    cfg.StorageFormat: "Native",
+                },
+            ),
+            (
+                "Pandas",
+                "Dask",
+                {
+                    cfg.Backend: "Dask",
+                    cfg.Engine: "Dask",
+                    cfg.StorageFormat: "Pandas",
+                },
+            ),
+        ],
+    )
+    def test_storage_format_and_engine_come_from_environment(
+        self,
+        monkeypatch,
+        clear_backend_execution_and_storage_format,
+        order_to_get_in,
+        storage_environment_variable,
+        engine_environment_variable,
+        variable_to_expected_value,
+    ):
+        with mock.patch.dict(
+            os.environ,
+            {
+                cfg.StorageFormat.varname: storage_environment_variable,
+                cfg.Engine.varname: engine_environment_variable,
+            },
+        ):
+            for variable in order_to_get_in:
+                expected_value = variable_to_expected_value[variable]
+                assert (
+                    variable.get() == expected_value
+                ), f"{variable.__name__} was {variable.get()} instead of {expected_value}"
+
+    @pytest.mark.parametrize(
+        "order_to_get_in",
+        itertools.permutations(
+            [
+                cfg.Backend,
+                cfg.Engine,
+                cfg.StorageFormat,
+            ]
+        ),
+        ids=lambda permutation: "_".join(x.__name__ for x in permutation),
+    )
+    @pytest.mark.parametrize(
+        "engine_environment_variable, variable_to_expected_value",
+        [
+            (
+                "Dask",
+                {cfg.Backend: "Dask", cfg.StorageFormat: "Pandas", cfg.Engine: "Dask"},
+            ),
+            (
+                "DASK",
+                {cfg.Backend: "Dask", cfg.StorageFormat: "Pandas", cfg.Engine: "Dask"},
+            ),
+            (
+                "python",
+                {
+                    cfg.Backend: "Python_Test",
+                    cfg.StorageFormat: "Pandas",
+                    cfg.Engine: "Python",
+                },
+            ),
+            (
+                "ray",
+                {cfg.Backend: "Ray", cfg.StorageFormat: "Pandas", cfg.Engine: "Ray"},
+            ),
+            # note that we can't test Native here because it's not valid to use
+            # "Native" engine with the default storage format of "Pandas."
+        ],
+    )
+    def test_only_engine_comes_from_environment(
+        self,
+        clear_backend_execution_and_storage_format,
+        order_to_get_in,
+        engine_environment_variable,
+        variable_to_expected_value,
+    ):
+        with mock.patch.dict(
+            os.environ,
+            {cfg.Engine.varname: engine_environment_variable},
+        ):
+            for var in order_to_get_in:
+                expected_value = variable_to_expected_value[var]
+                assert (
+                    var.get() == expected_value
+                ), f"{var.__name__} was {var.get()} instead of {expected_value}"
+
+    @pytest.mark.parametrize(
+        "order_to_get_in",
+        itertools.permutations(
+            [
+                cfg.Backend,
+                cfg.Engine,
+                cfg.StorageFormat,
+            ]
+        ),
+        ids=lambda permutation: "_".join(x.__name__ for x in permutation),
+    )
+    def test_only_storage_format_comes_from_environment(
+        self,
+        clear_backend_execution_and_storage_format,
+        order_to_get_in,
+        add_pandas_duplicate_on_ray_execution,
+    ):
+        # To test switching StorageFormat alone, we have to add a new backend
+        # that works with the default "Pandas" execution.
+        with mock.patch.dict(
+            os.environ,
+            {
+                cfg.StorageFormat.varname: "Test_Pandasduplicate",
+            },
+        ):
+            cfg.Engine.put("Ray")
+            for variable in order_to_get_in:
+                expected_value = {
+                    cfg.Backend: "Test_Backend_1",
+                    cfg.Engine: "Ray",
+                    cfg.StorageFormat: "Test_Pandasduplicate",
+                }[variable]
+                assert (
+                    variable.get() == expected_value
+                ), f"{variable.__name__} was {variable.get()} instead of {expected_value}"
+
+    @pytest.mark.parametrize(
+        "order_to_get_in",
+        itertools.permutations(
+            [
+                cfg.Backend,
+                cfg.Engine,
+                cfg.StorageFormat,
+            ]
+        ),
+        ids=lambda permutation: "_".join(x.__name__ for x in permutation),
+    )
+    @pytest.mark.parametrize(
+        "backend_environment_variable, variable_to_expected_value",
+        [
+            (
+                "Pandas",
+                {
+                    cfg.Backend: "Pandas",
+                    cfg.Engine: "Native",
+                    cfg.StorageFormat: "Native",
+                },
+            ),
+            (
+                "Ray",
+                {cfg.Backend: "Ray", cfg.Engine: "Ray", cfg.StorageFormat: "Pandas"},
+            ),
+            (
+                "Dask",
+                {cfg.Backend: "Dask", cfg.Engine: "Dask", cfg.StorageFormat: "Pandas"},
+            ),
+            (
+                "python_test",
+                {
+                    cfg.Backend: "Python_Test",
+                    cfg.Engine: "Python",
+                    cfg.StorageFormat: "Pandas",
+                },
+            ),
+        ],
+    )
+    def test_backend_comes_from_environment(
+        self,
+        monkeypatch,
+        clear_backend_execution_and_storage_format,
+        order_to_get_in,
+        backend_environment_variable,
+        variable_to_expected_value,
+    ):
+        with mock.patch.dict(
+            os.environ,
+            {
+                cfg.Backend.varname: backend_environment_variable,
+            },
+        ):
+            for variable in order_to_get_in:
+                expected_value = variable_to_expected_value[variable]
+                assert (
+                    variable.get() == expected_value
+                ), f"{variable.__name__} was {variable.get()} instead of {expected_value}"
+
+    @pytest.mark.parametrize(
+        "order_to_get_in",
+        itertools.permutations(
+            [cfg.Backend, cfg.Engine, cfg.StorageFormat],
+        ),
+        ids=lambda permutation: "_".join(x.__name__ for x in permutation),
+    )
+    def test_environment_not_set_and_pick_up_default_engine(
+        self, clear_backend_execution_and_storage_format, order_to_get_in
+    ):
+        for variable in order_to_get_in:
+            assert variable.get() == variable._get_default()
+
+    @pytest.mark.parametrize(
+        "execution_variable, value",
+        [(cfg.Engine, "Python"), (cfg.StorageFormat, "Pandas")],
+    )
+    @pytest.mark.parametrize(
+        "variable_to_get",
+        [cfg.Backend, cfg.Engine, cfg.StorageFormat],
+    )
+    def test_conflicting_execution_and_backend_in_environment(
+        self,
+        monkeypatch,
+        clear_backend_execution_and_storage_format,
+        execution_variable,
+        value,
+        variable_to_get,
+    ):
+        monkeypatch.setitem(os.environ, cfg.Backend.varname, "Ray")
+        monkeypatch.setitem(os.environ, execution_variable.varname, value)
+        with pytest.raises(
+            ValueError,
+            match=re.escape("Can't specify both execution and backend in environment"),
+        ):
+            variable_to_get.get()
+
+    def test_get_execution_for_unknown_backend(self):
+        backend_choice_string = ", ".join(
+            f"'{choice}'" for choice in cfg.Backend.choices
+        )
+        with pytest.raises(
+            ValueError,
+            match=re.escape(
+                f"Unknown backend 'Unknown'. Available backends are: {backend_choice_string}"
+            ),
+        ):
+            cfg.Backend.get_execution_for_backend("Unknown")
+
 
 @pytest.mark.parametrize(
     "config_name",
diff --git a/modin/tests/config/test_parameter.py b/modin/tests/config/test_parameter.py
index cc9bbb5f..82fd371c 100644
--- a/modin/tests/config/test_parameter.py
+++ b/modin/tests/config/test_parameter.py
@@ -16,13 +16,16 @@ from collections import defaultdict
 import pytest
 
 from modin.config import Parameter
+from modin.config.pubsub import _TYPE_PARAMS
 
 
 def make_prefilled(vartype, varinit):
     class Prefilled(Parameter, type=vartype):
         @classmethod
-        def _get_raw_from_config(cls):
-            return varinit
+        def _get_value_from_config(cls):
+            if not _TYPE_PARAMS[cls.type].verify(varinit):
+                raise ValueError(f"Unsupported raw value: {varinit}")
+            return _TYPE_PARAMS[cls.type].decode(varinit)
 
     return Prefilled
 
diff --git a/modin/tests/pandas/test_api.py b/modin/tests/pandas/test_api.py
index e6dc2151..cbc92c9c 100644
--- a/modin/tests/pandas/test_api.py
+++ b/modin/tests/pandas/test_api.py
@@ -19,6 +19,17 @@ import pytest
 
 import modin.pandas as pd
 
+_MODIN_EXTRA_ATTRIBUTES = (
+    # modin - namespace for accessing additional Modin functions that are not available in Pandas
+    "modin",
+    # get_backend - get storage and engine backend for the current DataFrame
+    "get_backend",
+    # set_backend - set storage and engine backend for the current DataFrame
+    "set_backend",
+    # move_to - set storage and engine backend for the current DataFrame
+    "move_to",
+)
+
 
 def test_top_level_api_equality():
     modin_dir = [obj for obj in dir(pd) if obj[0] != "_"]
@@ -150,7 +161,6 @@ def test_dataframe_api_equality():
 
     ignore_in_pandas = ["timetuple"]
     # modin - namespace for accessing additional Modin functions that are not available in Pandas
-    ignore_in_modin = ["modin"]
     missing_from_modin = set(pandas_dir) - set(modin_dir)
     assert not len(
         missing_from_modin - set(ignore_in_pandas)
@@ -158,13 +168,14 @@ def test_dataframe_api_equality():
         len(missing_from_modin - set(ignore_in_pandas))
     )
     assert not len(
-        set(modin_dir) - set(ignore_in_modin) - set(pandas_dir)
+        set(modin_dir) - set(_MODIN_EXTRA_ATTRIBUTES) - set(pandas_dir)
     ), "Differences found in API: {}".format(set(modin_dir) - set(pandas_dir))
 
-    # These have to be checked manually
-    allowed_different = ["modin"]
-
-    assert_parameters_eq((pandas.DataFrame, pd.DataFrame), modin_dir, allowed_different)
+    assert_parameters_eq(
+        (pandas.DataFrame, pd.DataFrame),
+        modin_dir,
+        allowed_different=_MODIN_EXTRA_ATTRIBUTES,
+    )
 
 
 def test_series_str_api_equality():
@@ -265,17 +276,15 @@ def test_series_api_equality():
     assert not len(missing_from_modin), "Differences found in API: {}".format(
         missing_from_modin
     )
-    # modin - namespace for accessing additional Modin functions that are not available in Pandas
-    ignore_in_modin = ["modin"]
-    extra_in_modin = set(modin_dir) - set(ignore_in_modin) - set(pandas_dir)
+
+    extra_in_modin = set(modin_dir) - set(_MODIN_EXTRA_ATTRIBUTES) - set(pandas_dir)
     assert not len(extra_in_modin), "Differences found in API: {}".format(
         extra_in_modin
     )
 
-    # These have to be checked manually
-    allowed_different = ["modin"]
-
-    assert_parameters_eq((pandas.Series, pd.Series), modin_dir, allowed_different)
+    assert_parameters_eq(
+        (pandas.Series, pd.Series), modin_dir, allowed_different=_MODIN_EXTRA_ATTRIBUTES
+    )
 
 
 def assert_parameters_eq(objects, attributes, allowed_different):
diff --git a/modin/tests/pandas/test_backend.py b/modin/tests/pandas/test_backend.py
new file mode 100644
index 00000000..37a2978e
--- /dev/null
+++ b/modin/tests/pandas/test_backend.py
@@ -0,0 +1,180 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team licenses this file to you under the
+# Apache License, Version 2.0 (the "License"); you may not use this file except in
+# compliance with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+import platform
+import re
+
+import pytest
+
+import modin.pandas as pd
+from modin.config import Backend
+from modin.config import context as config_context
+from modin.tests.pandas.utils import df_equals
+
+WINDOWS_RAY_SKIP_MARK = pytest.mark.skipif(
+    platform.system() == "Windows",
+    reason=(
+        "Some windows tests with engine != ray use 2 cores, but that "
+        + "doesn't work with ray due to "
+        + "https://github.com/modin-project/modin/issues/7387"
+    ),
+)
+
+
+def test_new_dataframe_uses_default_backend():
+    # We run this test with `Backend` set to just one value (instead of
+    # trying to look for every possible `Backend` value in the same pytest
+    # process) because switching to the MPI backend within a test process
+    # that's not set up to run MPI (i.e. because the test process has been
+    # started `mpiexec` instead of just `pytest`) would cause errors. We assume
+    # that CI runs this test file once with every possible `Backend`.
+    assert pd.DataFrame([1]).get_backend() == Backend.get()
+
+
+@pytest.mark.parametrize("setter_method", ["set_backend", "move_to"])
+@pytest.mark.parametrize(
+    "inplace_kwargs",
+    [
+        pytest.param({"inplace": True}, id="inplace"),
+        pytest.param({"inplace": False}, id="not_inplace"),
+        pytest.param({}, id="no_inplace_kwargs"),
+    ],
+)
+@pytest.mark.parametrize(
+    "starting_backend, new_backend, expected_result_backend",
+    [
+        pytest.param(Backend.get(), "pandas", "Pandas", id="current_to_pandas"),
+        pytest.param("pandas", Backend.get(), Backend.get(), id="pandas_to_current"),
+        pytest.param(
+            Backend.get(), "python_test", "Python_Test", id="current_to_python"
+        ),
+        pytest.param(
+            "python_test", Backend.get(), Backend.get(), id="python_to_current"
+        ),
+        pytest.param("python_test", "pandas", "Pandas", id="python_to_pandas1"),
+        pytest.param("PYTHON_test", "PANDAS", "Pandas", id="python_to_pandas2"),
+        pytest.param("pandas", "python_test", "Python_Test", id="pandas_to_python"),
+        pytest.param("pandas", "pandas", "Pandas", id="pandas_to_pandas"),
+        pytest.param(
+            "python_test", "python_test", "Python_Test", id="python_to_python"
+        ),
+        pytest.param(
+            "ray",
+            "dask",
+            "Dask",
+            id="ray_to_dask",
+            marks=WINDOWS_RAY_SKIP_MARK,
+        ),
+        pytest.param(
+            "dask",
+            "ray",
+            "Ray",
+            id="dask_to_ray",
+            marks=WINDOWS_RAY_SKIP_MARK,
+        ),
+        pytest.param(
+            "ray",
+            "python_test",
+            "Python_Test",
+            id="ray_to_python",
+            marks=WINDOWS_RAY_SKIP_MARK,
+        ),
+        pytest.param("dask", "python_test", "Python_Test", id="dask_to_python"),
+        pytest.param(
+            "python_test",
+            "ray",
+            "Ray",
+            id="python_to_ray",
+            marks=WINDOWS_RAY_SKIP_MARK,
+        ),
+        pytest.param("python_test", "dask", "Dask", id="python_to_dask"),
+        pytest.param("ray", "ray", "Ray", id="ray_to_ray", marks=WINDOWS_RAY_SKIP_MARK),
+        pytest.param("dask", "dask", "Dask", id="dask_to_dask"),
+    ],
+)
+@pytest.mark.parametrize(
+    "data_class",
+    [
+        pytest.param(pd.DataFrame, id="dataframe"),
+        pytest.param(pd.Series, id="series"),
+    ],
+)
+def test_set_valid_backend(
+    setter_method,
+    inplace_kwargs,
+    starting_backend,
+    new_backend,
+    data_class,
+    expected_result_backend,
+):
+    with config_context(Backend=starting_backend):
+        original_df = data_class([1])
+        # convert to pandas for comparison while still on the `starting_backend`.
+        original_df_as_pandas = original_df.modin.to_pandas()
+        method_result = getattr(original_df, setter_method)(
+            new_backend, **inplace_kwargs
+        )
+        if inplace_kwargs.get("inplace", False):
+            assert method_result is None
+            result_df = original_df
+        else:
+            assert method_result is not None
+            result_df = method_result
+        assert result_df.get_backend() == expected_result_backend
+        df_equals(result_df, original_df_as_pandas)
+        # The global Backend should remain the same even if we change the
+        # backend for a single dataframe.
+        assert Backend.get() == Backend.normalize(starting_backend)
+
+
+def test_set_nonexistent_backend():
+    backend_choice_string = ", ".join(f"'{choice}'" for choice in Backend.choices)
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Unknown backend 'does_not_exist'. "
+            + f"Available backends are: {backend_choice_string}"
+        ),
+    ):
+        pd.DataFrame([1]).set_backend("does_not_exist")
+
+
+@pytest.mark.parametrize("backend", [None, 1, [], {}])
+def test_wrong_backend_type(backend):
+    with pytest.raises(
+        TypeError,
+        match=re.escape(
+            "Backend value should be a string, but instead it is "
+            + f"{repr(backend)} of type {type(backend)}"
+        ),
+    ):
+        pd.DataFrame([1]).set_backend(backend)
+
+
+def test_get_backend_docstrings():
+    dataframe_method = pd.DataFrame.get_backend
+    series_method = pd.Series.get_backend
+    assert dataframe_method.__doc__ != series_method.__doc__
+    assert dataframe_method.__doc__ == series_method.__doc__.replace(
+        "Series", "DataFrame"
+    )
+
+
+@pytest.mark.parametrize("setter_method", ["set_backend", "move_to"])
+def test_set_backend_docstrings(setter_method):
+    dataframe_method = getattr(pd.DataFrame, setter_method)
+    series_method = getattr(pd.Series, setter_method)
+    assert dataframe_method.__doc__ != series_method.__doc__
+    assert dataframe_method.__doc__ == series_method.__doc__.replace(
+        "Series", "DataFrame"
+    )
diff --git a/modin/tests/pandas/test_series.py b/modin/tests/pandas/test_series.py
index 706afeaf..f4821c93 100644
--- a/modin/tests/pandas/test_series.py
+++ b/modin/tests/pandas/test_series.py
@@ -5188,3 +5188,12 @@ def test_logical_binary_with_list(op):
     result_md = getattr(series_md, op)(rhs)
     result_pd = getattr(series_pd, op)(rhs)
     df_equals(result_md, result_pd)
+
+
+@pytest.mark.parametrize("op", ["argmax", "argmin"])
+def test_argmax_argmin_7413(op):
+    # Ensures that argmin/argmax use positional index, not the actual index value
+    series_md, series_pd = create_test_series([1, 2, 3], index=["b", "a", "c"])
+    result_md = getattr(series_md, op)()
+    result_pd = getattr(series_pd, op)()
+    assert result_md == result_pd