diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index 6ba57d5b49..8c14a2f3bc 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -6,11 +6,13 @@ v0.12.1 (Unreleased)

 - |Feature| Added the :class:`objects.Perc` stat (:pr:`3063`).

-- |Feature| The :class:`Band` and :class:`Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).
+- |Feature| The :class:`objects.Band` and :class:`objects.Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).

-- |Enhancement| The :class:`Jitter` move now applies a small amount of jitter by default (:pr:`3066`).
+- |Enhancement| |Defaults| The :class:`objects.Jitter` move now applies a small amount of jitter by default (:pr:`3066`).

-- |Enhancement| Marks that sort along the orient axis (e.g. :class:`Line`) now use a stable algorithm (:pr:`3064`).
+- |Enhancement| |Defaults| Axes with a :class:`objects.Nominal` scale now appear like categorical axes in class seaborn, with fixed margins, no grid, and an inverted y axis (:pr:`3069`).
+
+- |Enhancement| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).

 - |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).

diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py
index 4f0290a494..bfe612a1f5 100644
--- a/seaborn/_core/plot.py
+++ b/seaborn/_core/plot.py
@@ -25,7 +25,7 @@
 from seaborn._stats.base import Stat
 from seaborn._core.data import PlotData
 from seaborn._core.moves import Move
-from seaborn._core.scales import Scale
+from seaborn._core.scales import Scale, Nominal
 from seaborn._core.subplots import Subplots
 from seaborn._core.groupby import GroupBy
 from seaborn._core.properties import PROPERTIES, Property
@@ -1238,7 +1238,6 @@ def _setup_scales(
             # This only affects us when sharing *paired* axes. This is a novel/niche
             # behavior, so we will raise rather than hack together a workaround.
             if axis is not None and Version(mpl.__version__) < Version("3.4.0"):
-                from seaborn._core.scales import Nominal
                 paired_axis = axis in p._pair_spec.get("structure", {})
                 cat_scale = isinstance(scale, Nominal)
                 ok_dim = {"x": "col", "y": "row"}[axis]
@@ -1631,6 +1630,7 @@ def _finalize_figure(self, p: Plot) -> None:
             ax = sub["ax"]
             for axis in "xy":
                 axis_key = sub[axis]
+                axis_obj = getattr(ax, f"{axis}axis")

                 # Axis limits
                 if axis_key in p._limits:
@@ -1644,6 +1644,17 @@ def _finalize_figure(self, p: Plot) -> None:
                         hi = cast(float, hi) + 0.5
                     ax.set(**{f"{axis}lim": (lo, hi)})

+                # Nominal scale special-casing
+                if isinstance(self._scales.get(axis_key), Nominal):
+                    axis_obj.grid(False, which="both")
+                    if axis_key not in p._limits:
+                        nticks = len(axis_obj.get_major_ticks())
+                        lo, hi = -.5, nticks - .5
+                        if axis == "y":
+                            lo, hi = hi, lo
+                        set_lim = getattr(ax, f"set_{axis}lim")
+                        set_lim(lo, hi, auto=None)
+
         engine_default = None if p._target is not None else "tight"
         layout_engine = p._layout_spec.get("engine", engine_default)
         set_layout_engine(self._figure, layout_engine)

diff --git a/tests/_core/test_plot.py b/tests/_core/test_plot.py
index 03a64917f3..e855aef9e6 100644
--- a/tests/_core/test_plot.py
+++ b/tests/_core/test_plot.py
@@ -645,6 +645,28 @@ def test_undefined_variable_raises(self):
         with pytest.raises(RuntimeError, match=err):
             p.plot()

+    def test_nominal_x_axis_tweaks(self):
+
+        p = Plot(x=["a", "b", "c"], y=[1, 2, 3])
+        ax1 = p.plot()._figure.axes[0]
+        assert ax1.get_xlim() == (-.5, 2.5)
+        assert not any(x.get_visible() for x in ax1.xaxis.get_gridlines())
+
+        lim = (-1, 2.1)
+        ax2 = p.limit(x=lim).plot()._figure.axes[0]
+        assert ax2.get_xlim() == lim
+
+    def test_nominal_y_axis_tweaks(self):
+
+        p = Plot(x=[1, 2, 3], y=["a", "b", "c"])
+        ax1 = p.plot()._figure.axes[0]
+        assert ax1.get_ylim() == (2.5, -.5)
+        assert not any(y.get_visible() for y in ax1.yaxis.get_gridlines())
+
+        lim = (-1, 2.1)
+        ax2 = p.limit(y=lim).plot()._figure.axes[0]
+        assert ax2.get_ylim() == lim
+

 class TestPlotting:

diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index 6ba57d5b..038de671 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -6,11 +6,13 @@ v0.12.1 (Unreleased)

 - |Feature| Added the :class:`objects.Perc` stat (:pr:`3063`).

-- |Feature| The :class:`Band` and :class:`Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).
+- |Feature| The :class:`objects.Band` and :class:`objects.Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).

-- |Enhancement| The :class:`Jitter` move now applies a small amount of jitter by default (:pr:`3066`).
+- |Enhancement| |Defaults| The :class:`objects.Jitter` move now applies a small amount of jitter by default (:pr:`3066`).

-- |Enhancement| Marks that sort along the orient axis (e.g. :class:`Line`) now use a stable algorithm (:pr:`3064`).
+- |Enhancement| |Defaults| Axes with a :class:`objects.Nominal` scale now appear like categorical axes in class seaborn, with fixed margins, no grid, and an inverted y axis.
+
+- |Enhancement| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).

 - |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:


diff --git a/doc/_docstrings/objects.Jitter.ipynb b/doc/_docstrings/objects.Jitter.ipynb
new file mode 100644
index 0000000000..6aa600cddd
--- /dev/null
+++ b/doc/_docstrings/objects.Jitter.ipynb
@@ -0,0 +1,178 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f2e5a85d-c710-492b-a4fc-09b45ae26471",
+   "metadata": {
+    "tags": [
+     "hide"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "import seaborn.objects as so\n",
+    "from seaborn import load_dataset\n",
+    "penguins = load_dataset(\"penguins\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "14b5927c-42f1-4934-adee-3d380b8b3228",
+   "metadata": {},
+   "source": [
+    "When used without any arguments, a small amount of jitter will be applied along the orientation axis:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bc1b4941-bbe6-4afc-b51a-0ac67cbe417d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"species\", \"body_mass_g\")\n",
+    "    .add(so.Dots(), so.Jitter())\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "1101690e-6c19-4219-aa4e-180798454df1",
+   "metadata": {},
+   "source": [
+    "The `width` parameter controls the amount of jitter relative to the spacing between the marks:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c4251b9d-8b11-4c2c-905c-2f3b523dee70",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"species\", \"body_mass_g\")\n",
+    "    .add(so.Dots(), so.Jitter(.5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "38aa639a-356e-4674-970b-53d55379b2b7",
+   "metadata": {},
+   "source": [
+    "The `width` parameter always applies to the orientation axis, so the direction of jitter will adapt along with the orientation:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1cfe1c07-7e81-45a0-a989-240503046133",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"body_mass_g\", \"species\")\n",
+    "    .add(so.Dots(), so.Jitter(.5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "0f5de4cc-3383-4503-8b59-9c48230a12a5",
+   "metadata": {},
+   "source": [
+    "Because the `width` jitter is relative, it can be used when the orientation axis is numeric without further tweaking:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c94c41e8-29c4-4439-a5d1-0b8ffb244890",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins[\"body_mass_g\"].round(-3), penguins[\"flipper_length_mm\"])\n",
+    "    .add(so.Dots(), so.Jitter())\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "dd982dfa-fd9f-4edc-8190-18f0e101ae1a",
+   "metadata": {},
+   "source": [
+    "In contrast to `width`, the `x` and `y` parameters always refer to specific axes and control the jitter in data units:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b0f2e5ca-68ad-4439-a4ee-f32f65682e95",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins[\"body_mass_g\"].round(-3), penguins[\"flipper_length_mm\"])\n",
+    "    .add(so.Dots(), so.Jitter(x=100))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "a90ba526-8043-42ed-8f57-36445c163c0d",
+   "metadata": {},
+   "source": [
+    "Both `x` and `y` can be used in a single transform:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6c07ed1d-ac77-4b30-90a8-e1b8760f9fad",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(\n",
+    "        penguins[\"body_mass_g\"].round(-3),\n",
+    "        penguins[\"flipper_length_mm\"].round(-1),\n",
+    "    )\n",
+    "    .add(so.Dots(), so.Jitter(x=200, y=5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bb04c7a2-93f0-44cf-aacf-0eb436d0f14b",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "py310",
+   "language": "python",
+   "name": "py310"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.0"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index ee779cee17..6ba57d5b49 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -8,6 +8,8 @@ v0.12.1 (Unreleased)

 - |Feature| The :class:`Band` and :class:`Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).

+- |Enhancement| The :class:`Jitter` move now applies a small amount of jitter by default (:pr:`3066`).
+
 - |Enhancement| Marks that sort along the orient axis (e.g. :class:`Line`) now use a stable algorithm (:pr:`3064`).

 - |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).
diff --git a/seaborn/_core/moves.py b/seaborn/_core/moves.py
index 2c814be87d..1efba41e63 100644
--- a/seaborn/_core/moves.py
+++ b/seaborn/_core/moves.py
@@ -1,12 +1,15 @@
 from __future__ import annotations
 from dataclasses import dataclass
-from typing import ClassVar, Callable, Optional, Union
+from typing import ClassVar, Callable, Optional, Union, cast

 import numpy as np
 from pandas import DataFrame

 from seaborn._core.groupby import GroupBy
 from seaborn._core.scales import Scale
+from seaborn._core.typing import Default
+
+default = Default()

 @dataclass
@@ -24,26 +27,34 @@ def __call__(
 @dataclass
 class Jitter(Move):
     """
-    Random displacement of marks along either or both axes to reduce overplotting.
+    Random displacement along one or both axes to reduce overplotting.
+
+    Parameters
+    ----------
+    width : float
+        Magnitude of jitter, relative to mark width, along the orientation axis.
+        If not provided, the default value will be 0 when `x` or `y` are set, otherwise
+        there will be a small amount of jitter applied by default.
+    x : float
+        Magnitude of jitter, in data units, along the x axis.
+    y : float
+        Magnitude of jitter, in data units, along the y axis.
+
+    Examples
+    --------
+    .. include:: ../docstrings/objects.Jitter.rst
+
     """
-    width: float = 0
+    width: float | Default = default
     x: float = 0
     y: float = 0
-
-    seed: Optional[int] = None
-
-    # TODO what is the best way to have a reasonable default?
-    # The problem is that "reasonable" seems dependent on the mark
+    seed: int | None = None

     def __call__(
         self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
     ) -> DataFrame:

-        # TODO is it a problem that GroupBy is not used for anything here?
-        # Should we type it as optional?
-
         data = data.copy()
-
         rng = np.random.default_rng(self.seed)

         def jitter(data, col, scale):
@@ -51,8 +62,13 @@ def jitter(data, col, scale):
             offsets = noise * scale
             return data[col] + offsets

+        if self.width is default:
+            width = 0.0 if self.x or self.y else 0.2
+        else:
+            width = cast(float, self.width)
+
         if self.width:
-            data[orient] = jitter(data, orient, self.width * data["width"])
+            data[orient] = jitter(data, orient, width * data["width"])
         if self.x:
             data["x"] = jitter(data, "x", self.x)
         if self.y:
diff --git a/seaborn/_core/plot.py b/seaborn/_core/plot.py
index 3e32f04ddc..4f0290a494 100644
--- a/seaborn/_core/plot.py
+++ b/seaborn/_core/plot.py
@@ -29,7 +29,13 @@
 from seaborn._core.subplots import Subplots
 from seaborn._core.groupby import GroupBy
 from seaborn._core.properties import PROPERTIES, Property
-from seaborn._core.typing import DataSource, VariableSpec, VariableSpecList, OrderSpec
+from seaborn._core.typing import (
+    DataSource,
+    VariableSpec,
+    VariableSpecList,
+    OrderSpec,
+    Default,
+)
 from seaborn._core.rules import categorical_order
 from seaborn._compat import set_scale_obj, set_layout_engine
 from seaborn.rcmod import axes_style, plotting_context
@@ -47,6 +53,9 @@
     from typing_extensions import TypedDict

+default = Default()
+
+
 # ---- Definitions for internal specs --------------------------------- #

@@ -79,13 +88,6 @@ class PairSpec(TypedDict, total=False):

 # --- Local helpers ----------------------------------------------------------------

-class Default:
-    def __repr__(self):
-        return "<default>"
-
-
-default = Default()
-

 @contextmanager
 def theme_context(params: dict[str, Any]) -> Generator:
diff --git a/seaborn/_core/typing.py b/seaborn/_core/typing.py
index 5c790c4cfa..5295b995e4 100644
--- a/seaborn/_core/typing.py
+++ b/seaborn/_core/typing.py
@@ -29,3 +29,11 @@
 ContinuousValueSpec = Union[
     Tuple[float, float], List[float], Dict[Any, float], None,
 ]
+
+
+class Default:
+    def __repr__(self):
+        return "<default>"
+
+
+default = Default()

diff --git a/tests/_core/test_moves.py b/tests/_core/test_moves.py
index bac86d12e2..6fd88bb5fc 100644
--- a/tests/_core/test_moves.py
+++ b/tests/_core/test_moves.py
@@ -78,6 +78,15 @@ def check_pos(self, res, df, var, limit):
         assert (res[var] < df[var] + limit / 2).all()
         assert (res[var] > df[var] - limit / 2).all()

+    def test_default(self, df):
+
+        orient = "x"
+        groupby = self.get_groupby(df, orient)
+        res = Jitter()(df, groupby, orient, {})
+        self.check_same(res, df, "y", "grp2", "width")
+        self.check_pos(res, df, "x", 0.2 * df["width"])
+        assert (res["x"] - df["x"]).abs().min() > 0
+
     def test_width(self, df):

         width = .4

--- /dev/null
+++ b/doc/_docstrings/objects.Jitter.ipynb
@@ -0,0 +1,59 @@
+import seaborn.objects as so
+
+from seaborn import load_dataset
+
+penguins = load_dataset("penguins")
+When used without any arguments, a small amount of jitter will be applied along the orientation axis:
+(
+
+    so.Plot(penguins, "species", "body_mass_g")
+
+    .add(so.Dots(), so.Jitter())
+
+)
+The `width` parameter controls the amount of jitter relative to the spacing between the marks:
+(
+
+    so.Plot(penguins, "species", "body_mass_g")
+
+    .add(so.Dots(), so.Jitter(.5))
+
+)
+The `width` parameter always applies to the orientation axis, so the direction of jitter will adapt along with the orientation:
+(
+
+    so.Plot(penguins, "body_mass_g", "species")
+
+    .add(so.Dots(), so.Jitter(.5))
+
+)
+Because the `width` jitter is relative, it can be used when the orientation axis is numeric without further tweaking:
+(
+
+    so.Plot(penguins["body_mass_g"].round(-3), penguins["flipper_length_mm"])
+
+    .add(so.Dots(), so.Jitter())
+
+)
+In contrast to `width`, the `x` and `y` parameters always refer to specific axes and control the jitter in data units:
+(
+
+    so.Plot(penguins["body_mass_g"].round(-3), penguins["flipper_length_mm"])
+
+    .add(so.Dots(), so.Jitter(x=100))
+
+)
+Both `x` and `y` can be used in a single transform:
+(
+
+    so.Plot(
+
+        penguins["body_mass_g"].round(-3),
+
+        penguins["flipper_length_mm"].round(-1),
+
+    )
+
+    .add(so.Dots(), so.Jitter(x=200, y=5))
+
+)

diff --git a/doc/_docstrings/objects.Jitter.ipynb b/doc/_docstrings/objects.Jitter.ipynb
new file mode 100644
index 00000000..6aa600cd
--- /dev/null
+++ b/doc/_docstrings/objects.Jitter.ipynb
@@ -0,0 +1,178 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f2e5a85d-c710-492b-a4fc-09b45ae26471",
+   "metadata": {
+    "tags": [
+     "hide"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "import seaborn.objects as so\n",
+    "from seaborn import load_dataset\n",
+    "penguins = load_dataset(\"penguins\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "14b5927c-42f1-4934-adee-3d380b8b3228",
+   "metadata": {},
+   "source": [
+    "When used without any arguments, a small amount of jitter will be applied along the orientation axis:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bc1b4941-bbe6-4afc-b51a-0ac67cbe417d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"species\", \"body_mass_g\")\n",
+    "    .add(so.Dots(), so.Jitter())\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "1101690e-6c19-4219-aa4e-180798454df1",
+   "metadata": {},
+   "source": [
+    "The `width` parameter controls the amount of jitter relative to the spacing between the marks:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c4251b9d-8b11-4c2c-905c-2f3b523dee70",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"species\", \"body_mass_g\")\n",
+    "    .add(so.Dots(), so.Jitter(.5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "38aa639a-356e-4674-970b-53d55379b2b7",
+   "metadata": {},
+   "source": [
+    "The `width` parameter always applies to the orientation axis, so the direction of jitter will adapt along with the orientation:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1cfe1c07-7e81-45a0-a989-240503046133",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins, \"body_mass_g\", \"species\")\n",
+    "    .add(so.Dots(), so.Jitter(.5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "0f5de4cc-3383-4503-8b59-9c48230a12a5",
+   "metadata": {},
+   "source": [
+    "Because the `width` jitter is relative, it can be used when the orientation axis is numeric without further tweaking:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c94c41e8-29c4-4439-a5d1-0b8ffb244890",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins[\"body_mass_g\"].round(-3), penguins[\"flipper_length_mm\"])\n",
+    "    .add(so.Dots(), so.Jitter())\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "dd982dfa-fd9f-4edc-8190-18f0e101ae1a",
+   "metadata": {},
+   "source": [
+    "In contrast to `width`, the `x` and `y` parameters always refer to specific axes and control the jitter in data units:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b0f2e5ca-68ad-4439-a4ee-f32f65682e95",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(penguins[\"body_mass_g\"].round(-3), penguins[\"flipper_length_mm\"])\n",
+    "    .add(so.Dots(), so.Jitter(x=100))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "a90ba526-8043-42ed-8f57-36445c163c0d",
+   "metadata": {},
+   "source": [
+    "Both `x` and `y` can be used in a single transform:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6c07ed1d-ac77-4b30-90a8-e1b8760f9fad",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "(\n",
+    "    so.Plot(\n",
+    "        penguins[\"body_mass_g\"].round(-3),\n",
+    "        penguins[\"flipper_length_mm\"].round(-1),\n",
+    "    )\n",
+    "    .add(so.Dots(), so.Jitter(x=200, y=5))\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bb04c7a2-93f0-44cf-aacf-0eb436d0f14b",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "py310",
+   "language": "python",
+   "name": "py310"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.0"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index ee779cee..3cbc5831 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -8,6 +8,8 @@ v0.12.1 (Unreleased)

 - |Feature| The :class:`Band` and :class:`Range` marks will now cover the full extent of the data if `min` / `max` variables are not explicitly assigned or added in a transform (:pr:`3056`).

+- |Enhancement| The :class:`Jitter` move now applies a small amount of jitter by default.
+
 - |Enhancement| Marks that sort along the orient axis (e.g. :class:`Line`) now use a stable algorithm (:pr:`3064`).

 - |Fix| Make :class:`objects.PolyFit` robust to missing data (:pr:`3010`).


diff --git a/doc/whatsnew/v0.12.0.rst b/doc/whatsnew/v0.12.0.rst
index 013200b90f..09d63ce3f7 100644
--- a/doc/whatsnew/v0.12.0.rst
+++ b/doc/whatsnew/v0.12.0.rst
@@ -60,10 +60,12 @@ Other updates

 - |Feature| It is now possible to aggregate / sort a :func:`lineplot` along the y axis using `orient="y"` (:pr:`2854`).

-- |Feature| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).
+- |Feature| Made it easier to customize :class:`FacetGrid` / :class:`PairGrid` / :class:`JointGrid` with a fluent (method-chained) style by adding `apply`/ `pipe` methods. Additionally, fixed the `tight_layout` and `refline` methods so that they return `self` (:pr:`2926`).

 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

+- |Enhancement| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).
+
 - |Enhancement| Error bars in :func:`regplot` now inherit the alpha value of the points they correspond to (:pr:`2540`).

 - |Enhancement| When using :func:`pairplot` with `corner=True` and `diag_kind=None`, the top left y axis label is no longer hidden (:pr:`2850`).
diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py
index 3bb74f07fd..db03a29e75 100644
--- a/seaborn/axisgrid.py
+++ b/seaborn/axisgrid.py
@@ -50,6 +50,35 @@ def figure(self):
         """Access the :class:`matplotlib.figure.Figure` object underlying the grid."""
         return self._figure

+    def apply(self, func, *args, **kwargs):
+        """
+        Pass the grid to a user-supplied function and return self.
+
+        The `func` must accept an object of this type for its first
+        positional argument. Additional arguments are passed through.
+        The return value of `func` is ignored; this method returns self.
+        See the `pipe` method if you want the return value.
+
+        Added in v0.12.0.
+
+        """
+        func(self, *args, **kwargs)
+        return self
+
+    def pipe(self, func, *args, **kwargs):
+        """
+        Pass the grid to a user-supplied function and return its value.
+
+        The `func` must accept an object of this type for its first
+        positional argument. Additional arguments are passed through.
+        The return value of `func` becomes the return value of this method.
+        See the `apply` method if you want to return self instead.
+
+        Added in v0.12.0.
+
+        """
+        return func(self, *args, **kwargs)
+
     def savefig(self, *args, **kwargs):
         """
         Save an image of the plot.
@@ -84,6 +113,7 @@ def tight_layout(self, *args, **kwargs):
         if self._tight_layout_pad is not None:
             kwargs.setdefault("pad", self._tight_layout_pad)
         self._figure.tight_layout(*args, **kwargs)
+        return self

     def add_legend(self, legend_data=None, title=None, label_order=None,
                    adjust_subtitles=False, **kwargs):
@@ -1006,6 +1036,8 @@ def refline(self, *, x=None, y=None, color='.5', linestyle='--', **line_kws):
         if y is not None:
             self.map(plt.axhline, y=y, **line_kws)

+        return self
+
     # ------ Properties that are part of the public API and documented by Sphinx

     @property

diff --git a/tests/test_axisgrid.py b/tests/test_axisgrid.py
index 4a33da94a0..3ade4ebac3 100644
--- a/tests/test_axisgrid.py
+++ b/tests/test_axisgrid.py
@@ -673,6 +673,29 @@ def test_refline(self):
         assert g.axes[0, 0].lines[-1].get_color() == color
         assert g.axes[0, 0].lines[-1].get_linestyle() == linestyle

+    def test_apply(self, long_df):
+
+        def f(grid, color):
+            grid.figure.set_facecolor(color)
+
+        color = (.1, .6, .3, .9)
+        g = ag.FacetGrid(long_df)
+        res = g.apply(f, color)
+        assert res is g
+        assert g.figure.get_facecolor() == color
+
+    def test_pipe(self, long_df):
+
+        def f(grid, color):
+            grid.figure.set_facecolor(color)
+            return color
+
+        color = (.1, .6, .3, .9)
+        g = ag.FacetGrid(long_df)
+        res = g.pipe(f, color)
+        assert res == color
+        assert g.figure.get_facecolor() == color
+

 class TestPairGrid:

diff --git a/doc/whatsnew/v0.12.0.rst b/doc/whatsnew/v0.12.0.rst
index 013200b9..af8f35c0 100644
--- a/doc/whatsnew/v0.12.0.rst
+++ b/doc/whatsnew/v0.12.0.rst
@@ -60,10 +60,12 @@ Other updates

 - |Feature| It is now possible to aggregate / sort a :func:`lineplot` along the y axis using `orient="y"` (:pr:`2854`).

-- |Feature| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).
+- |Feature| Made it easier to customize :class:`FacetGrid` / :class:`PairGrid` / :class:`JointGrid` with a fluent (method-chained) style by adding `apply`/ `pipe` methods. Additionally, fixed the `tight_layout` and `refline` methods so that they return `self`.

 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

+- |Enhancement| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).
+
 - |Enhancement| Error bars in :func:`regplot` now inherit the alpha value of the points they correspond to (:pr:`2540`).

 - |Enhancement| When using :func:`pairplot` with `corner=True` and `diag_kind=None`, the top left y axis label is no longer hidden (:pr:`2850`).


diff --git a/doc/whatsnew/v0.12.0.rst b/doc/whatsnew/v0.12.0.rst
index 07c98fa907..b1435a6cbd 100644
--- a/doc/whatsnew/v0.12.0.rst
+++ b/doc/whatsnew/v0.12.0.rst
@@ -62,6 +62,8 @@ Other updates

 - |Feature| Made it easier to customize :class:`FacetGrid` / :class:`PairGrid` / :class:`JointGrid` with a fluent (method-chained) style by adding `apply`/ `pipe` methods. Additionally, fixed the `tight_layout` and `refline` methods so that they return `self` (:pr:`2926`).

+- |Feature| Added :meth:`FacetGrid.tick_params` and :meth:`PairGrid.tick_params` to customize the appearance of the ticks, tick labels, and gridlines of all subplots at once (:pr:`2944`).
+
 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

 - |Enhancement| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).
diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py
index 4a7dafbd68..12e5bdeb72 100644
--- a/seaborn/axisgrid.py
+++ b/seaborn/axisgrid.py
@@ -278,6 +278,27 @@ def legend(self):
         except AttributeError:
             return None

+    def tick_params(self, axis='both', **kwargs):
+        """Modify the ticks, tick labels, and gridlines.
+
+        Parameters
+        ----------
+        axis : {'x', 'y', 'both'}
+            The axis on which to apply the formatting.
+        kwargs : keyword arguments
+            Additional keyword arguments to pass to
+            :meth:`matplotlib.axes.Axes.tick_params`.
+
+        Returns
+        -------
+        self : Grid instance
+            Returns self for easy chaining.
+
+        """
+        for ax in self.figure.axes:
+            ax.tick_params(axis=axis, **kwargs)
+        return self
+

 _facet_docs = dict(

diff --git a/tests/test_axisgrid.py b/tests/test_axisgrid.py
index 3ade4ebac3..ed1a1cd721 100644
--- a/tests/test_axisgrid.py
+++ b/tests/test_axisgrid.py
@@ -653,14 +653,14 @@ def test_refline(self):

         g = ag.FacetGrid(self.df, row="a", col="b")
         g.refline()
-        for ax in g.axes.ravel():
+        for ax in g.axes.flat:
             assert not ax.lines

         refx = refy = 0.5
         hline = np.array([[0, refy], [1, refy]])
         vline = np.array([[refx, 0], [refx, 1]])
         g.refline(x=refx, y=refy)
-        for ax in g.axes.ravel():
+        for ax in g.axes.flat:
             assert ax.lines[0].get_color() == '.5'
             assert ax.lines[0].get_linestyle() == '--'
             assert len(ax.lines) == 2
@@ -696,6 +696,19 @@ def f(grid, color):
         assert res == color
         assert g.figure.get_facecolor() == color

+    def test_tick_params(self):
+
+        g = ag.FacetGrid(self.df, row="a", col="b")
+        color = "blue"
+        pad = 3
+        g.tick_params(pad=pad, color=color)
+        for ax in g.axes.flat:
+            for axis in ["xaxis", "yaxis"]:
+                for tick in getattr(ax, axis).get_major_ticks():
+                    assert mpl.colors.same_color(tick.tick1line.get_color(), color)
+                    assert mpl.colors.same_color(tick.tick2line.get_color(), color)
+                    assert tick.get_pad() == pad
+

 class TestPairGrid:

@@ -1431,6 +1444,19 @@ def test_legend(self):
         g2 = ag.pairplot(self.df)
         assert g2.legend is None

+    def test_tick_params(self):
+
+        g = ag.PairGrid(self.df)
+        color = "red"
+        pad = 3
+        g.tick_params(pad=pad, color=color)
+        for ax in g.axes.flat:
+            for axis in ["xaxis", "yaxis"]:
+                for tick in getattr(ax, axis).get_major_ticks():
+                    assert mpl.colors.same_color(tick.tick1line.get_color(), color)
+                    assert mpl.colors.same_color(tick.tick2line.get_color(), color)
+                    assert tick.get_pad() == pad
+

 class TestJointGrid:

diff --git a/doc/whatsnew/v0.12.0.rst b/doc/whatsnew/v0.12.0.rst
index 07c98fa9..07bc073f 100644
--- a/doc/whatsnew/v0.12.0.rst
+++ b/doc/whatsnew/v0.12.0.rst
@@ -62,6 +62,8 @@ Other updates

 - |Feature| Made it easier to customize :class:`FacetGrid` / :class:`PairGrid` / :class:`JointGrid` with a fluent (method-chained) style by adding `apply`/ `pipe` methods. Additionally, fixed the `tight_layout` and `refline` methods so that they return `self` (:pr:`2926`).

+- |Feature| Added :meth:`FacetGrid.tick_params` and :meth:`PairGrid.tick_params` to customize the appearance of the ticks, tick labels, and gridlines of all subplots at once.
+
 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

 - |Enhancement| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).


diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index 5db8f8644a..ca11a2db9c 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -14,7 +14,9 @@ v0.12.1 (Unreleased)

 - |Enhancement| |Defaults| Axes with a :class:`objects.Nominal` scale now appear like categorical axes in class seaborn, with fixed margins, no grid, and an inverted y axis (:pr:`3069`).

-- |Enhancement| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).
+- |Enhancement| |API| The :meth:`objects.Continuous.label` method now accepts `base=None` to override the default formatter with a log transform (:pr:`3087`).
+
+- |Enhancement| |Fix| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).

 - |Enhancement| |Fix| Added a `label` parameter to :func:`pointplot`, which addresses a regression in 0.12.0 when :func:`pointplot` is passed to :class:`FacetGrid` (:pr:`3016`).

diff --git a/seaborn/_core/scales.py b/seaborn/_core/scales.py
index 98f3f00363..8579f8b065 100644
--- a/seaborn/_core/scales.py
+++ b/seaborn/_core/scales.py
@@ -35,6 +35,7 @@
 from pandas import Series

 from seaborn._core.rules import categorical_order
+from seaborn._core.typing import Default, default

 from typing import TYPE_CHECKING
 if TYPE_CHECKING:
@@ -494,7 +495,7 @@ def label(
         self,
         formatter: Formatter | None = None, *,
         like: str | Callable | None = None,
-        base: int | None = None,
+        base: int | None | Default = default,
         unit: str | None = None,
     ) -> Continuous:
         """
@@ -510,6 +511,7 @@ def label(
             and returns a string.
         base : number
             Use log formatter (with scientific notation) having this value as the base.
+            Set to `None` to override the default formatter with a log transform.
         unit : str or (str, str) tuple
             Use  SI prefixes with these units (e.g., with `unit="g"`, a tick value
             of 5000 will appear as `5 kg`). When a tuple, the first element gives the
@@ -613,7 +615,7 @@ def _get_locators(self, locator, at, upto, count, every, between, minor):
     def _get_formatter(self, locator, formatter, like, base, unit):

         log_base, symlog_thresh = self._parse_for_log_params(self.trans)
-        if base is None:
+        if base is default:
             if symlog_thresh:
                 log_base = 10
             base = log_base

diff --git a/tests/_core/test_scales.py b/tests/_core/test_scales.py
index 1bceed9244..c4b39f5d34 100644
--- a/tests/_core/test_scales.py
+++ b/tests/_core/test_scales.py
@@ -213,6 +213,15 @@ def test_log_tick_count(self, x):
         a.set_view_interval(.5, 1050)
         assert_array_equal(a.major.locator(), [1, 10, 100, 1000])

+    def test_log_tick_format_disabled(self, x):
+
+        s = Continuous(trans="log").label(base=None)._setup(x, Coordinate())
+        a = PseudoAxis(s._matplotlib_scale)
+        a.set_view_interval(20, 20000)
+        labels = a.major.formatter.format_ticks(a.major.locator())
+        for text in labels:
+            assert re.match(r"^\d+$", text)
+
     def test_log_tick_every(self, x):

         with pytest.raises(RuntimeError, match="`every` not supported"):

diff --git a/doc/whatsnew/v0.12.1.rst b/doc/whatsnew/v0.12.1.rst
index 5db8f864..d4ec5fc2 100644
--- a/doc/whatsnew/v0.12.1.rst
+++ b/doc/whatsnew/v0.12.1.rst
@@ -14,7 +14,9 @@ v0.12.1 (Unreleased)

 - |Enhancement| |Defaults| Axes with a :class:`objects.Nominal` scale now appear like categorical axes in class seaborn, with fixed margins, no grid, and an inverted y axis (:pr:`3069`).

-- |Enhancement| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).
+- |Enhancement| |API| The :meth:`objects.Continuous.label` method now accepts `base=None` to override the default formatter with a log transform.
+
+- |Enhancement| |Fix| Marks that sort along the orient axis (e.g. :class:`objects.Line`) now use a stable algorithm (:pr:`3064`).

 - |Enhancement| |Fix| Added a `label` parameter to :func:`pointplot`, which addresses a regression in 0.12.0 when :func:`pointplot` is passed to :class:`FacetGrid` (:pr:`3016`).


diff --git a/doc/whats_new/v0.22.rst b/doc/whats_new/v0.22.rst
index d5d7fddf7417d..bbd35676defcf 100644
--- a/doc/whats_new/v0.22.rst
+++ b/doc/whats_new/v0.22.rst
@@ -104,6 +104,14 @@ Changelog
   deprecated. This change is made to restore some backward compatibility with
   the behavior of this utility in version 0.21. :pr:`15947` by `Thomas Fan`_.

+:mod:`sklearn.multioutput`
+..........................
+
+- |Feature| :func:`multioutput.MultiOutputRegressor.fit` and
+  :func:`multioutput.MultiOutputClassifier.fit` now can accept `fit_params`
+  to pass to the `estimator.fit` method of each step. :issue:`15953`
+  :pr:`15959` by :user:`Ke Huang <huangk10>`.
+
 .. _changes_0_22:

 Version 0.22.0
diff --git a/sklearn/multioutput.py b/sklearn/multioutput.py
index a6e8fc3c5dc16..945957cdc12a7 100644
--- a/sklearn/multioutput.py
+++ b/sklearn/multioutput.py
@@ -24,7 +24,8 @@
 from .model_selection import cross_val_predict
 from .utils import check_array, check_X_y, check_random_state
 from .utils.metaestimators import if_delegate_has_method
-from .utils.validation import check_is_fitted, has_fit_parameter
+from .utils.validation import (check_is_fitted, has_fit_parameter,
+                               _check_fit_params)
 from .utils.multiclass import check_classification_targets
 from .utils import deprecated

@@ -32,12 +33,12 @@
            "ClassifierChain", "RegressorChain"]

-def _fit_estimator(estimator, X, y, sample_weight=None):
+def _fit_estimator(estimator, X, y, sample_weight=None, **fit_params):
     estimator = clone(estimator)
     if sample_weight is not None:
-        estimator.fit(X, y, sample_weight=sample_weight)
+        estimator.fit(X, y, sample_weight=sample_weight, **fit_params)
     else:
-        estimator.fit(X, y)
+        estimator.fit(X, y, **fit_params)
     return estimator

@@ -121,7 +122,7 @@ def partial_fit(self, X, y, classes=None, sample_weight=None):
                 sample_weight, first_time) for i in range(y.shape[1]))
         return self

-    def fit(self, X, y, sample_weight=None):
+    def fit(self, X, y, sample_weight=None, **fit_params):
         """ Fit the model to data.
         Fit a separate model for each output variable.

@@ -139,6 +140,9 @@ def fit(self, X, y, sample_weight=None):
             Only supported if the underlying regressor supports sample
             weights.

+        **fit_params : dict of string -> object
+            Parameters passed to the ``estimator.fit`` method of each step.
+
         Returns
         -------
         self : object
@@ -164,9 +168,12 @@ def fit(self, X, y, sample_weight=None):
             raise ValueError("Underlying estimator does not support"
                              " sample weights.")

+        fit_params_validated = _check_fit_params(X, fit_params)
+
         self.estimators_ = Parallel(n_jobs=self.n_jobs)(
             delayed(_fit_estimator)(
-                self.estimator, X, y[:, i], sample_weight)
+                self.estimator, X, y[:, i], sample_weight,
+                **fit_params_validated)
             for i in range(y.shape[1]))
         return self

@@ -338,7 +345,7 @@ class MultiOutputClassifier(ClassifierMixin, _MultiOutputEstimator):
     def __init__(self, estimator, n_jobs=None):
         super().__init__(estimator, n_jobs)

-    def fit(self, X, Y, sample_weight=None):
+    def fit(self, X, Y, sample_weight=None, **fit_params):
         """Fit the model to data matrix X and targets Y.

         Parameters
@@ -351,12 +358,14 @@ def fit(self, X, Y, sample_weight=None):
             Sample weights. If None, then samples are equally weighted.
             Only supported if the underlying classifier supports sample
             weights.
+        **fit_params : dict of string -> object
+            Parameters passed to the ``estimator.fit`` method of each step.

         Returns
         -------
         self : object
         """
-        super().fit(X, Y, sample_weight)
+        super().fit(X, Y, sample_weight, **fit_params)
         self.classes_ = [estimator.classes_ for estimator in self.estimators_]
         return self

diff --git a/sklearn/tests/test_multioutput.py b/sklearn/tests/test_multioutput.py
index 6256f72a4b0b3..4a528716d181f 100644
--- a/sklearn/tests/test_multioutput.py
+++ b/sklearn/tests/test_multioutput.py
@@ -30,6 +30,7 @@
 from sklearn.base import ClassifierMixin
 from sklearn.utils import shuffle
 from sklearn.model_selection import GridSearchCV
+from sklearn.dummy import DummyRegressor, DummyClassifier

 def test_multi_target_regression():
@@ -561,3 +562,29 @@ class A(MultiOutputEstimator, MultiOutputRegressor):

     with pytest.warns(FutureWarning, match="is deprecated in version 0.22"):
         A(SGDRegressor(random_state=0, max_iter=5))
+
+
+class DummyRegressorWithFitParams(DummyRegressor):
+    def fit(self, X, y, sample_weight=None, **fit_params):
+        self._fit_params = fit_params
+        return super().fit(X, y, sample_weight)
+
+
+class DummyClassifierWithFitParams(DummyClassifier):
+    def fit(self, X, y, sample_weight=None, **fit_params):
+        self._fit_params = fit_params
+        return super().fit(X, y, sample_weight)
+
+
+@pytest.mark.parametrize(
+    "estimator, dataset",
+    [(MultiOutputClassifier(DummyClassifierWithFitParams(strategy="prior")),
+      datasets.make_multilabel_classification()),
+     (MultiOutputRegressor(DummyRegressorWithFitParams()),
+      datasets.make_regression(n_targets=3))])
+def test_multioutput_estimator_with_fit_params(estimator, dataset):
+    X, y = dataset
+    some_param = np.zeros_like(X)
+    estimator.fit(X, y, some_param=some_param)
+    for dummy_estimator in estimator.estimators_:
+        assert 'some_param' in dummy_estimator._fit_params

diff --git a/doc/whats_new/v0.22.rst b/doc/whats_new/v0.22.rst
index d5d7fddf7..b1aa4d7e8 100644
--- a/doc/whats_new/v0.22.rst
+++ b/doc/whats_new/v0.22.rst
@@ -104,6 +104,13 @@ Changelog
   deprecated. This change is made to restore some backward compatibility with
   the behavior of this utility in version 0.21. :pr:`15947` by `Thomas Fan`_.

+:mod:`sklearn.multioutput`
+..........................
+
+- |Feature| :func:`multioutput.MultiOutputRegressor.fit` and
+  :func:`multioutput.MultiOutputClassifier.fit` now can accept `fit_params`
+  to pass to the `estimator.fit` method of each step.
+
 .. _changes_0_22:

 Version 0.22.0


diff --git a/doc/whats_new/v0.23.rst b/doc/whats_new/v0.23.rst
index 6d398ceef3cbd..1430535c25393 100644
--- a/doc/whats_new/v0.23.rst
+++ b/doc/whats_new/v0.23.rst
@@ -54,6 +54,12 @@ Changelog
 :mod:`sklearn.datasets`
 .......................

+- |Enhancement| Added ``return_centers`` parameter  in
+  :func:`datasets.make_blobs`, which can be used to return
+  centers for each cluster.
+  :pr:`15709` by :user:`<shivamgargsya>` and
+  :user:`Venkatachalam N <venkyyuvy>`.
+
 - |Enhancement| Functions :func:`datasets.make_circles` and
   :func:`datasets.make_moons` now accept two-element tuple.
   :pr:`15707` by :user:`Maciej J Mikulski <mjmikulski>`.
diff --git a/sklearn/datasets/_samples_generator.py b/sklearn/datasets/_samples_generator.py
index d57dfc9bda999..62ef492f42f5e 100644
--- a/sklearn/datasets/_samples_generator.py
+++ b/sklearn/datasets/_samples_generator.py
@@ -731,7 +731,8 @@ def make_moons(n_samples=100, shuffle=True, noise=None, random_state=None):

 def make_blobs(n_samples=100, n_features=2, centers=None, cluster_std=1.0,
-               center_box=(-10.0, 10.0), shuffle=True, random_state=None):
+               center_box=(-10.0, 10.0), shuffle=True, random_state=None,
+               return_centers=False):
     """Generate isotropic Gaussian blobs for clustering.

     Read more in the :ref:`User Guide <sample_generators>`.
@@ -769,6 +770,11 @@ def make_blobs(n_samples=100, n_features=2, centers=None, cluster_std=1.0,
         for reproducible output across multiple function calls.
         See :term:`Glossary <random_state>`.

+    return_centers : bool, optional (default=False)
+        If True, then return the centers of each cluster
+
+        .. versionadded:: 0.23
+
     Returns
     -------
     X : array of shape [n_samples, n_features]
@@ -777,6 +783,10 @@ def make_blobs(n_samples=100, n_features=2, centers=None, cluster_std=1.0,
     y : array of shape [n_samples]
         The integer labels for cluster membership of each sample.

+    centers : array, shape [n_centers, n_features]
+        The centers of each cluster. Only returned if
+        ``return_centers=True``.
+
     Examples
     --------
     >>> from sklearn.datasets import make_blobs
@@ -869,7 +879,10 @@ def make_blobs(n_samples=100, n_features=2, centers=None, cluster_std=1.0,
         X = X[indices]
         y = y[indices]

-    return X, y
+    if return_centers:
+        return X, y, centers
+    else:
+        return X, y

 def make_friedman1(n_samples=100, n_features=10, noise=0.0, random_state=None):

diff --git a/sklearn/datasets/tests/test_samples_generator.py b/sklearn/datasets/tests/test_samples_generator.py
index c683e277c705a..ab712d8c235a6 100644
--- a/sklearn/datasets/tests/test_samples_generator.py
+++ b/sklearn/datasets/tests/test_samples_generator.py
@@ -324,6 +324,15 @@ def test_make_blobs_n_samples_centers_none(n_samples):
         "Incorrect number of samples per blob"

+def test_make_blobs_return_centers():
+    n_samples = [10, 20]
+    n_features = 3
+    X, y, centers = make_blobs(n_samples=n_samples, n_features=n_features,
+                               return_centers=True, random_state=0)
+
+    assert centers.shape == (len(n_samples), n_features)
+
+
 def test_make_blobs_error():
     n_samples = [20, 20, 20]
     centers = np.array([[0.0, 0.0], [1.0, 1.0], [0.0, 1.0]])

sklearn/datasets/tests/test_samples_generator.py::test_make_multilabel_classification_valid_arguments[params0-'n_classes' should be an integer]

sklearn/datasets/tests/test_samples_generator.py::test_make_classification_weights_type[weights4-ValueError-Weights specified but incompatible with number of classes.]

sklearn/datasets/tests/test_samples_generator.py::test_make_multilabel_classification_valid_arguments[params1-'length' should be an integer]

sklearn/datasets/tests/test_samples_generator.py::test_make_classification_weights_type[weights0-ValueError-Weights specified but incompatible with number of classes.]

sklearn/datasets/tests/test_samples_generator.py::test_make_classification_weights_type[weights1-ValueError-Weights specified but incompatible with number of classes.]

sklearn/datasets/tests/test_samples_generator.py::test_make_classification_weights_type[weights2-ValueError-Weights specified but incompatible with number of classes.]

sklearn/datasets/tests/test_samples_generator.py::test_make_classification_weights_type[weights3-ValueError-Weights specified but incompatible with number of classes.]

diff --git a/doc/whats_new/v0.23.rst b/doc/whats_new/v0.23.rst
index 6d398ceef..2964bb676 100644
--- a/doc/whats_new/v0.23.rst
+++ b/doc/whats_new/v0.23.rst
@@ -54,6 +54,10 @@ Changelog
 :mod:`sklearn.datasets`
 .......................

+- |Enhancement| Added ``return_centers`` parameter  in
+  :func:`datasets.make_blobs`, which can be used to return
+  centers for each cluster.
+
 - |Enhancement| Functions :func:`datasets.make_circles` and
   :func:`datasets.make_moons` now accept two-element tuple.
   :pr:`15707` by :user:`Maciej J Mikulski <mjmikulski>`.


diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index b7f53eb3198c6..aca7d47703e73 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -296,6 +296,10 @@ Changelog
   :class:`preprocessing.KBinsDiscretizer`.
   :pr:`16335` by :user:`Arthur Imbert <Henley13>`.

+- |Enhancement| Add ``clip`` parameter to :class:`preprocessing.MinMaxScaler`,
+  which clips the transformed values of test data to ``feature_range``.
+  :pr:`17833` by :user:`Yashika Sharma <yashika51>`.
+
 :mod:`sklearn.svm`
 ..................

diff --git a/sklearn/preprocessing/_data.py b/sklearn/preprocessing/_data.py
index 2b92bd3d09467..2659240863285 100644
--- a/sklearn/preprocessing/_data.py
+++ b/sklearn/preprocessing/_data.py
@@ -243,6 +243,10 @@ class MinMaxScaler(TransformerMixin, BaseEstimator):
         Set to False to perform inplace row normalization and avoid a
         copy (if the input is already a numpy array).

+    clip: bool, default=False
+        Set to True to clip transformed values of held-out data to
+        provided `feature range`.
+
     Attributes
     ----------
     min_ : ndarray of shape (n_features,)
@@ -311,9 +315,10 @@ class MinMaxScaler(TransformerMixin, BaseEstimator):
     """

     @_deprecate_positional_args
-    def __init__(self, feature_range=(0, 1), *, copy=True):
+    def __init__(self, feature_range=(0, 1), *, copy=True, clip=False):
         self.feature_range = feature_range
         self.copy = copy
+        self.clip = clip

     def _reset(self):
         """Reset internal data-dependent state of the scaler, if necessary.
@@ -427,6 +432,8 @@ def transform(self, X):

         X *= self.scale_
         X += self.min_
+        if self.clip:
+            np.clip(X, self.feature_range[0], self.feature_range[1], out=X)
         return X

     def inverse_transform(self, X):

diff --git a/sklearn/preprocessing/tests/test_common.py b/sklearn/preprocessing/tests/test_common.py
index 802329fc5ce32..a00dd2b6cb025 100644
--- a/sklearn/preprocessing/tests/test_common.py
+++ b/sklearn/preprocessing/tests/test_common.py
@@ -36,18 +36,20 @@ def _get_valid_samples_by_column(X, col):

 @pytest.mark.parametrize(
-    "est, func, support_sparse, strictly_positive",
-    [(MaxAbsScaler(), maxabs_scale, True, False),
-     (MinMaxScaler(), minmax_scale, False, False),
-     (StandardScaler(), scale, False, False),
-     (StandardScaler(with_mean=False), scale, True, False),
-     (PowerTransformer('yeo-johnson'), power_transform, False, False),
-     (PowerTransformer('box-cox'), power_transform, False, True),
-     (QuantileTransformer(n_quantiles=10), quantile_transform, True, False),
-     (RobustScaler(), robust_scale, False, False),
-     (RobustScaler(with_centering=False), robust_scale, True, False)]
+    "est, func, support_sparse, strictly_positive, omit_kwargs",
+    [(MaxAbsScaler(), maxabs_scale, True, False, []),
+     (MinMaxScaler(), minmax_scale, False, False, ['clip']),
+     (StandardScaler(), scale, False, False, []),
+     (StandardScaler(with_mean=False), scale, True, False, []),
+     (PowerTransformer('yeo-johnson'), power_transform, False, False, []),
+     (PowerTransformer('box-cox'), power_transform, False, True, []),
+     (QuantileTransformer(n_quantiles=10), quantile_transform, True, False,
+     []),
+     (RobustScaler(), robust_scale, False, False, []),
+     (RobustScaler(with_centering=False), robust_scale, True, False, [])]
 )
-def test_missing_value_handling(est, func, support_sparse, strictly_positive):
+def test_missing_value_handling(est, func, support_sparse, strictly_positive,
+                                omit_kwargs):
     # check that the preprocessing method let pass nan
     rng = np.random.RandomState(42)
     X = iris.data.copy()
@@ -74,7 +76,12 @@ def test_missing_value_handling(est, func, support_sparse, strictly_positive):
     with pytest.warns(None) as records:
         Xt_class = est.transform(X_train)
     assert len(records) == 0
-    Xt_func = func(X_train, **est.get_params())
+    kwargs = est.get_params()
+    # remove the parameters which should be omitted because they
+    # are not defined in the sister function of the preprocessing class
+    for kwarg in omit_kwargs:
+        _ = kwargs.pop(kwarg)
+    Xt_func = func(X_train, **kwargs)
     assert_array_equal(np.isnan(Xt_func), np.isnan(Xt_class))
     assert_allclose(Xt_func[~np.isnan(Xt_func)], Xt_class[~np.isnan(Xt_class)])

diff --git a/sklearn/preprocessing/tests/test_data.py b/sklearn/preprocessing/tests/test_data.py
index 2241491da8fca..c09acf1591532 100644
--- a/sklearn/preprocessing/tests/test_data.py
+++ b/sklearn/preprocessing/tests/test_data.py
@@ -2526,3 +2526,19 @@ def test_standard_scaler_sparse_partial_fit_finite_variance(X_2):
     scaler = StandardScaler(with_mean=False)
     scaler.fit(X_1).partial_fit(X_2)
     assert np.isfinite(scaler.var_[0])
+
+
+@pytest.mark.parametrize(
+    "feature_range", [(0, 1), (-10, 10)]
+)
+def test_minmax_scaler_clip(feature_range):
+    # test behaviour of the paramter 'clip' in MinMaxScaler
+    X = iris.data
+    scaler = MinMaxScaler(feature_range=feature_range, clip=True).fit(X)
+    X_min, X_max = np.min(X, axis=0), np.max(X, axis=0)
+    X_test = [np.r_[X_min[:2] - 10, X_max[2:] + 10]]
+    X_transformed = scaler.transform(X_test)
+    assert_allclose(
+        X_transformed,
+        [[feature_range[0], feature_range[0],
+          feature_range[1], feature_range[1]]])

diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index b7f53eb31..4b4e9e8d7 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -296,6 +296,9 @@ Changelog
   :class:`preprocessing.KBinsDiscretizer`.
   :pr:`16335` by :user:`Arthur Imbert <Henley13>`.

+- |Enhancement| Add ``clip`` parameter to :class:`preprocessing.MinMaxScaler`,
+  which clips the transformed values of test data to ``feature_range``.
+
 :mod:`sklearn.svm`
 ..................


diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst
index 024dd074e2e41..79d57913a9565 100644
--- a/doc/modules/preprocessing.rst
+++ b/doc/modules/preprocessing.rst
@@ -590,6 +590,34 @@ In the transformed `X`, the first column is the encoding of the feature with
 categories "male"/"female", while the remaining 6 columns is the encoding of
 the 2 features with respectively 3 categories each.

+:class:`OneHotEncoder` supports categorical features with missing values by
+considering the missing values as an additional category::
+
+    >>> X = [['male', 'Safari'],
+    ...      ['female', None],
+    ...      [np.nan, 'Firefox']]
+    >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
+    >>> enc.categories_
+    [array(['female', 'male', nan], dtype=object),
+     array(['Firefox', 'Safari', None], dtype=object)]
+    >>> enc.transform(X).toarray()
+    array([[0., 1., 0., 0., 1., 0.],
+           [1., 0., 0., 0., 0., 1.],
+           [0., 0., 1., 1., 0., 0.]])
+
+If a feature contains both `np.nan` and `None`, they will be considered
+separate categories::
+
+    >>> X = [['Safari'], [None], [np.nan], ['Firefox']]
+    >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
+    >>> enc.categories_
+    [array(['Firefox', 'Safari', None, nan], dtype=object)]
+    >>> enc.transform(X).toarray()
+    array([[0., 1., 0., 0.],
+           [0., 0., 1., 0.],
+           [0., 0., 0., 1.],
+           [1., 0., 0., 0.]])
+
 See :ref:`dict_feature_extraction` for categorical features that are
 represented as a dict, not as scalars.

@@ -791,5 +819,5 @@ error with a ``filterwarnings``::
   ...                         category=UserWarning, append=False)

 For a full code example that demonstrates using a :class:`FunctionTransformer`
-to extract features from text data see
+to extract features from text data see
 :ref:`sphx_glr_auto_examples_compose_plot_column_transformer.py`
diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index f35dcc8f72ee4..b52fbfc14bd40 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -552,6 +552,9 @@ Changelog
 :mod:`sklearn.preprocessing`
 ............................

+- |Feature| :class:`preprocessing.OneHotEncoder` now supports missing
+  values by treating them as a category. :pr:`17317` by `Thomas Fan`_.
+
 - |Feature| Add a new ``handle_unknown`` parameter with a
   ``use_encoded_value`` option, along with a new ``unknown_value`` parameter,
   to :class:`preprocessing.OrdinalEncoder` to allow unknown categories during
diff --git a/examples/compose/plot_column_transformer_mixed_types.py b/examples/compose/plot_column_transformer_mixed_types.py
index 539f38a60bbe4..a2937e041f186 100644
--- a/examples/compose/plot_column_transformer_mixed_types.py
+++ b/examples/compose/plot_column_transformer_mixed_types.py
@@ -72,9 +72,7 @@
     ('scaler', StandardScaler())])

 categorical_features = ['embarked', 'sex', 'pclass']
-categorical_transformer = Pipeline(steps=[
-    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
-    ('onehot', OneHotEncoder(handle_unknown='ignore'))])
+categorical_transformer = OneHotEncoder(handle_unknown='ignore')

 preprocessor = ColumnTransformer(
     transformers=[
diff --git a/examples/inspection/plot_permutation_importance.py b/examples/inspection/plot_permutation_importance.py
index d708aa0fd6756..0751337ef69ff 100644
--- a/examples/inspection/plot_permutation_importance.py
+++ b/examples/inspection/plot_permutation_importance.py
@@ -63,16 +63,13 @@
 X_train, X_test, y_train, y_test = train_test_split(
     X, y, stratify=y, random_state=42)

-categorical_pipe = Pipeline([
-    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
-    ('onehot', OneHotEncoder(handle_unknown='ignore'))
-])
+categorical_encoder = OneHotEncoder(handle_unknown='ignore')
 numerical_pipe = Pipeline([
     ('imputer', SimpleImputer(strategy='mean'))
 ])

 preprocessing = ColumnTransformer(
-    [('cat', categorical_pipe, categorical_columns),
+    [('cat', categorical_encoder, categorical_columns),
      ('num', numerical_pipe, numerical_columns)])

 rf = Pipeline([
@@ -122,8 +119,7 @@
 #   predictions that generalize to the test set (when the model has enough
 #   capacity).
 ohe = (rf.named_steps['preprocess']
-         .named_transformers_['cat']
-         .named_steps['onehot'])
+         .named_transformers_['cat'])
 feature_names = ohe.get_feature_names(input_features=categorical_columns)
 feature_names = np.r_[feature_names, numerical_columns]

diff --git a/sklearn/preprocessing/_encoders.py b/sklearn/preprocessing/_encoders.py
index a621c9b5fb5dd..357a8f5b9a3d0 100644
--- a/sklearn/preprocessing/_encoders.py
+++ b/sklearn/preprocessing/_encoders.py
@@ -27,7 +27,7 @@ class _BaseEncoder(TransformerMixin, BaseEstimator):

     """

-    def _check_X(self, X):
+    def _check_X(self, X, force_all_finite=True):
         """
         Perform custom check_array:
         - convert list of strings to object dtype
@@ -41,17 +41,19 @@ def _check_X(self, X):
         """
         if not (hasattr(X, 'iloc') and getattr(X, 'ndim', 0) == 2):
             # if not a dataframe, do normal check_array validation
-            X_temp = check_array(X, dtype=None)
+            X_temp = check_array(X, dtype=None,
+                                 force_all_finite=force_all_finite)
             if (not hasattr(X, 'dtype')
                     and np.issubdtype(X_temp.dtype, np.str_)):
-                X = check_array(X, dtype=object)
+                X = check_array(X, dtype=object,
+                                force_all_finite=force_all_finite)
             else:
                 X = X_temp
             needs_validation = False
         else:
             # pandas dataframe, do validation later column by column, in order
             # to keep the dtype information to be used in the encoder.
-            needs_validation = True
+            needs_validation = force_all_finite

         n_samples, n_features = X.shape
         X_columns = []
@@ -71,8 +73,9 @@ def _get_feature(self, X, feature_idx):
         # numpy arrays, sparse arrays
         return X[:, feature_idx]

-    def _fit(self, X, handle_unknown='error'):
-        X_list, n_samples, n_features = self._check_X(X)
+    def _fit(self, X, handle_unknown='error', force_all_finite=True):
+        X_list, n_samples, n_features = self._check_X(
+            X, force_all_finite=force_all_finite)

         if self.categories != 'auto':
             if len(self.categories) != n_features:
@@ -88,9 +91,16 @@ def _fit(self, X, handle_unknown='error'):
             else:
                 cats = np.array(self.categories[i], dtype=Xi.dtype)
                 if Xi.dtype != object:
-                    if not np.all(np.sort(cats) == cats):
-                        raise ValueError("Unsorted categories are not "
-                                         "supported for numerical categories")
+                    sorted_cats = np.sort(cats)
+                    error_msg = ("Unsorted categories are not "
+                                 "supported for numerical categories")
+                    # if there are nans, nan should be the last element
+                    stop_idx = -1 if np.isnan(sorted_cats[-1]) else None
+                    if (np.any(sorted_cats[:stop_idx] != cats[:stop_idx]) or
+                        (np.isnan(sorted_cats[-1]) and
+                         not np.isnan(sorted_cats[-1]))):
+                        raise ValueError(error_msg)
+
                 if handle_unknown == 'error':
                     diff = _check_unknown(Xi, cats)
                     if diff:
@@ -99,8 +109,9 @@ def _fit(self, X, handle_unknown='error'):
                         raise ValueError(msg)
             self.categories_.append(cats)

-    def _transform(self, X, handle_unknown='error'):
-        X_list, n_samples, n_features = self._check_X(X)
+    def _transform(self, X, handle_unknown='error', force_all_finite=True):
+        X_list, n_samples, n_features = self._check_X(
+            X, force_all_finite=force_all_finite)

         X_int = np.zeros((n_samples, n_features), dtype=int)
         X_mask = np.ones((n_samples, n_features), dtype=bool)
@@ -355,8 +366,26 @@ def _compute_drop_idx(self):
                        "of features ({}), got {}")
                 raise ValueError(msg.format(len(self.categories_),
                                             len(self.drop)))
-            missing_drops = [(i, val) for i, val in enumerate(self.drop)
-                             if val not in self.categories_[i]]
+            missing_drops = []
+            drop_indices = []
+            for col_idx, (val, cat_list) in enumerate(zip(self.drop,
+                                                          self.categories_)):
+                if not is_scalar_nan(val):
+                    drop_idx = np.where(cat_list == val)[0]
+                    if drop_idx.size:  # found drop idx
+                        drop_indices.append(drop_idx[0])
+                    else:
+                        missing_drops.append((col_idx, val))
+                    continue
+
+                # val is nan, find nan in categories manually
+                for cat_idx, cat in enumerate(cat_list):
+                    if is_scalar_nan(cat):
+                        drop_indices.append(cat_idx)
+                        break
+                else:  # loop did not break thus drop is missing
+                    missing_drops.append((col_idx, val))
+
             if any(missing_drops):
                 msg = ("The following categories were supposed to be "
                        "dropped, but were not found in the training "
@@ -365,10 +394,7 @@ def _compute_drop_idx(self):
                                 ["Category: {}, Feature: {}".format(c, v)
                                     for c, v in missing_drops])))
                 raise ValueError(msg)
-            return np.array([np.where(cat_list == val)[0][0]
-                             for (val, cat_list) in
-                             zip(self.drop, self.categories_)],
-                            dtype=object)
+            return np.array(drop_indices, dtype=object)

     def fit(self, X, y=None):
         """
@@ -388,7 +414,8 @@ def fit(self, X, y=None):
         self
         """
         self._validate_keywords()
-        self._fit(X, handle_unknown=self.handle_unknown)
+        self._fit(X, handle_unknown=self.handle_unknown,
+                  force_all_finite='allow-nan')
         self.drop_idx_ = self._compute_drop_idx()
         return self

@@ -431,7 +458,8 @@ def transform(self, X):
         """
         check_is_fitted(self)
         # validation of X happens in _check_X called by _transform
-        X_int, X_mask = self._transform(X, handle_unknown=self.handle_unknown)
+        X_int, X_mask = self._transform(X, handle_unknown=self.handle_unknown,
+                                        force_all_finite='allow-nan')

         n_samples, n_features = X_int.shape

diff --git a/sklearn/utils/_encode.py b/sklearn/utils/_encode.py
index f830ff952ac1a..012fe7eb2fe0f 100644
--- a/sklearn/utils/_encode.py
+++ b/sklearn/utils/_encode.py
@@ -1,4 +1,7 @@
+from typing import NamedTuple
+
 import numpy as np
+from . import is_scalar_nan

 def _unique(values, *, return_inverse=False):
@@ -27,13 +30,107 @@ def _unique(values, *, return_inverse=False):
     if values.dtype == object:
         return _unique_python(values, return_inverse=return_inverse)
     # numerical
-    return np.unique(values, return_inverse=return_inverse)
+    out = np.unique(values, return_inverse=return_inverse)
+
+    if return_inverse:
+        uniques, inverse = out
+    else:
+        uniques = out
+
+    # np.unique will have duplicate missing values at the end of `uniques`
+    # here we clip the nans and remove it from uniques
+    if uniques.size and is_scalar_nan(uniques[-1]):
+        nan_idx = np.searchsorted(uniques, np.nan)
+        uniques = uniques[:nan_idx + 1]
+        if return_inverse:
+            inverse[inverse > nan_idx] = nan_idx
+
+    if return_inverse:
+        return uniques, inverse
+    return uniques
+
+
+class MissingValues(NamedTuple):
+    """Data class for missing data information"""
+    nan: bool
+    none: bool
+
+    def to_list(self):
+        """Convert tuple to a list where None is always first."""
+        output = []
+        if self.none:
+            output.append(None)
+        if self.nan:
+            output.append(np.nan)
+        return output
+
+
+def _extract_missing(values):
+    """Extract missing values from `values`.
+
+    Parameters
+    ----------
+    values: set
+        Set of values to extract missing from.
+
+    Returns
+    -------
+    output: set
+        Set with missing values extracted.
+
+    missing_values: MissingValues
+        Object with missing value information.
+    """
+    missing_values_set = {value for value in values
+                          if value is None or is_scalar_nan(value)}
+
+    if not missing_values_set:
+        return values, MissingValues(nan=False, none=False)
+
+    if None in missing_values_set:
+        if len(missing_values_set) == 1:
+            output_missing_values = MissingValues(nan=False, none=True)
+        else:
+            # If there is more than one missing value, then it has to be
+            # float('nan') or np.nan
+            output_missing_values = MissingValues(nan=True, none=True)
+    else:
+        output_missing_values = MissingValues(nan=True, none=False)
+
+    # create set without the missing values
+    output = values - missing_values_set
+    return output, output_missing_values
+
+
+class _nandict(dict):
+    """Dictionary with support for nans."""
+    def __init__(self, mapping):
+        super().__init__(mapping)
+        for key, value in mapping.items():
+            if is_scalar_nan(key):
+                self.nan_value = value
+                break
+
+    def __missing__(self, key):
+        if hasattr(self, 'nan_value') and is_scalar_nan(key):
+            return self.nan_value
+        raise KeyError(key)
+
+
+def _map_to_integer(values, uniques):
+    """Map values based on its position in uniques."""
+    table = _nandict({val: i for i, val in enumerate(uniques)})
+    return np.array([table[v] for v in values])

 def _unique_python(values, *, return_inverse):
     # Only used in `_uniques`, see docstring there for details
     try:
-        uniques = sorted(set(values))
+        uniques_set = set(values)
+        uniques_set, missing_values = _extract_missing(uniques_set)
+
+        uniques = sorted(uniques_set)
+        uniques.extend(missing_values.to_list())
         uniques = np.array(uniques, dtype=values.dtype)
     except TypeError:
         types = sorted(t.__qualname__
@@ -42,9 +139,7 @@ def _unique_python(values, *, return_inverse):
                         f"strings or numbers. Got {types}")

     if return_inverse:
-        table = {val: i for i, val in enumerate(uniques)}
-        inverse = np.array([table[v] for v in values])
-        return uniques, inverse
+        return uniques, _map_to_integer(values, uniques)

     return uniques

@@ -79,9 +174,8 @@ def _encode(values, *, uniques, check_unknown=True):
         Encoded values
     """
     if values.dtype == object:
-        table = {val: i for i, val in enumerate(uniques)}
         try:
-            return np.array([table[v] for v in values])
+            return _map_to_integer(values, uniques)
         except KeyError as e:
             raise ValueError(f"y contains previously unseen labels: {str(e)}")
     else:
@@ -118,26 +212,58 @@ def _check_unknown(values, known_values, return_mask=False):
         Additionally returned if ``return_mask=True``.

     """
-    if values.dtype == object:
+    valid_mask = None
+
+    if values.dtype.kind in 'UO':
+        values_set = set(values)
+        values_set, missing_in_values = _extract_missing(values_set)
+
         uniques_set = set(known_values)
-        diff = list(set(values) - uniques_set)
+        uniques_set, missing_in_uniques = _extract_missing(uniques_set)
+        diff = values_set - uniques_set
+
+        nan_in_diff = missing_in_values.nan and not missing_in_uniques.nan
+        none_in_diff = missing_in_values.none and not missing_in_uniques.none
+
+        def is_valid(value):
+            return (value in uniques_set or
+                    missing_in_uniques.none and value is None or
+                    missing_in_uniques.nan and is_scalar_nan(value))
+
         if return_mask:
-            if diff:
-                valid_mask = np.array([val in uniques_set for val in values])
+            if diff or nan_in_diff or none_in_diff:
+                valid_mask = np.array([is_valid(value) for value in values])
             else:
                 valid_mask = np.ones(len(values), dtype=bool)
-            return diff, valid_mask
-        else:
-            return diff
+
+        diff = list(diff)
+        if none_in_diff:
+            diff.append(None)
+        if nan_in_diff:
+            diff.append(np.nan)
     else:
         unique_values = np.unique(values)
-        diff = list(np.setdiff1d(unique_values, known_values,
-                                 assume_unique=True))
+        diff = np.setdiff1d(unique_values, known_values,
+                            assume_unique=True)
         if return_mask:
-            if diff:
+            if diff.size:
                 valid_mask = np.in1d(values, known_values)
             else:
                 valid_mask = np.ones(len(values), dtype=bool)
-            return diff, valid_mask
-        else:
-            return diff
+
+        # check for nans in the known_values
+        if np.isnan(known_values).any():
+            diff_is_nan = np.isnan(diff)
+            if diff_is_nan.any():
+                # removes nan from valid_mask
+                if diff.size and return_mask:
+                    is_nan = np.isnan(values)
+                    valid_mask[is_nan] = 1
+
+                # remove nan from diff
+                diff = diff[~diff_is_nan]
+        diff = list(diff)
+
+    if return_mask:
+        return diff, valid_mask
+    return diff

diff --git a/sklearn/preprocessing/tests/test_encoders.py b/sklearn/preprocessing/tests/test_encoders.py
index 239d388ebd9d1..aa169597976af 100644
--- a/sklearn/preprocessing/tests/test_encoders.py
+++ b/sklearn/preprocessing/tests/test_encoders.py
@@ -9,6 +9,7 @@
 from sklearn.exceptions import NotFittedError
 from sklearn.utils._testing import assert_array_equal
 from sklearn.utils._testing import assert_allclose
+from sklearn.utils import is_scalar_nan

 from sklearn.preprocessing import OneHotEncoder
 from sklearn.preprocessing import OrdinalEncoder
@@ -201,8 +202,14 @@ def check_categorical_onehot(X):
 @pytest.mark.parametrize("X", [
     [['def', 1, 55], ['abc', 2, 55]],
     np.array([[10, 1, 55], [5, 2, 55]]),
-    np.array([['b', 'A', 'cat'], ['a', 'B', 'cat']], dtype=object)
-    ], ids=['mixed', 'numeric', 'object'])
+    np.array([['b', 'A', 'cat'], ['a', 'B', 'cat']], dtype=object),
+    np.array([['b', 1, 'cat'], ['a', np.nan, 'cat']], dtype=object),
+    np.array([['b', 1, 'cat'], ['a', float('nan'), 'cat']], dtype=object),
+    np.array([[None, 1, 'cat'], ['a', 2, 'cat']], dtype=object),
+    np.array([[None, 1, None], ['a', np.nan, None]], dtype=object),
+    np.array([[None, 1, None], ['a', float('nan'), None]], dtype=object),
+    ], ids=['mixed', 'numeric', 'object', 'mixed-nan', 'mixed-float-nan',
+            'mixed-None', 'mixed-None-nan', 'mixed-None-float-nan'])
 def test_one_hot_encoder(X):
     Xtr = check_categorical_onehot(np.array(X)[:, [0]])
     assert_allclose(Xtr, [[0, 1], [1, 0]])
@@ -315,8 +322,14 @@ def test_X_is_not_1D_pandas(method):
     (np.array([['A', 'cat'], ['B', 'cat']], dtype=object),
      [['A', 'B'], ['cat']], np.object_),
     (np.array([['A', 'cat'], ['B', 'cat']]),
-     [['A', 'B'], ['cat']], np.str_)
-    ], ids=['mixed', 'numeric', 'object', 'string'])
+     [['A', 'B'], ['cat']], np.str_),
+    (np.array([[1, 2], [np.nan, 2]]), [[1, np.nan], [2]], np.float_),
+    (np.array([['A', np.nan], [None, np.nan]], dtype=object),
+     [['A', None], [np.nan]], np.object_),
+    (np.array([['A', float('nan')], [None, float('nan')]], dtype=object),
+     [['A', None], [float('nan')]], np.object_),
+    ], ids=['mixed', 'numeric', 'object', 'string', 'missing-float',
+            'missing-np.nan-object', 'missing-float-nan-object'])
 def test_one_hot_encoder_categories(X, cat_exp, cat_dtype):
     # order of categories should not depend on order of samples
     for Xi in [X, X[::-1]]:
@@ -325,7 +338,12 @@ def test_one_hot_encoder_categories(X, cat_exp, cat_dtype):
         # assert enc.categories == 'auto'
         assert isinstance(enc.categories_, list)
         for res, exp in zip(enc.categories_, cat_exp):
-            assert res.tolist() == exp
+            res_list = res.tolist()
+            if is_scalar_nan(exp[-1]):
+                assert is_scalar_nan(res_list[-1])
+                assert res_list[:-1] == exp[:-1]
+            else:
+                assert res.tolist() == exp
             assert np.issubdtype(res.dtype, cat_dtype)

@@ -339,7 +357,21 @@ def test_one_hot_encoder_categories(X, cat_exp, cat_dtype):
     (np.array([['a', 'b']], dtype=object).T,
      np.array([['a', 'd']], dtype=object).T,
      [np.array(['a', 'b', 'c'])], np.object_),
-    ], ids=['object', 'numeric', 'object-string-cat'])
+    (np.array([[None, 'a']], dtype=object).T,
+     np.array([[None, 'b']], dtype=object).T,
+     [[None, 'a', 'z']], object),
+    (np.array([['a', 'b']], dtype=object).T,
+     np.array([['a', np.nan]], dtype=object).T,
+     [['a', 'b', 'z']], object),
+    (np.array([['a', None]], dtype=object).T,
+     np.array([['a', np.nan]], dtype=object).T,
+     [['a', None, 'z']], object),
+    (np.array([['a', np.nan]], dtype=object).T,
+     np.array([['a', None]], dtype=object).T,
+     [['a', np.nan, 'z']], object),
+    ], ids=['object', 'numeric', 'object-string',
+            'object-string-none', 'object-string-nan',
+            'object-None-and-nan', 'object-nan-and-None'])
 def test_one_hot_encoder_specified_categories(X, X2, cats, cat_dtype):
     enc = OneHotEncoder(categories=cats)
     exp = np.array([[1., 0., 0.],
@@ -379,6 +411,12 @@ def test_one_hot_encoder_unsorted_categories():
     with pytest.raises(ValueError, match=msg):
         enc.fit_transform(X)

+    # np.nan must be the last category in categories[0] to be considered sorted
+    X = np.array([[1, 2, np.nan]]).T
+    enc = OneHotEncoder(categories=[[1, np.nan, 2]])
+    with pytest.raises(ValueError, match=msg):
+        enc.fit_transform(X)
+

 def test_one_hot_encoder_specified_categories_mixed_columns():
     # multiple columns
@@ -449,36 +487,6 @@ def test_one_hot_encoder_drop_equals_if_binary():
     assert_allclose(result, expected)

-@pytest.mark.parametrize("X", [np.array([[1, np.nan]]).T,
-                               np.array([['a', np.nan]], dtype=object).T],
-                         ids=['numeric', 'object'])
-@pytest.mark.parametrize("as_data_frame", [False, True],
-                         ids=['array', 'dataframe'])
-@pytest.mark.parametrize("handle_unknown", ['error', 'ignore'])
-def test_one_hot_encoder_raise_missing(X, as_data_frame, handle_unknown):
-    if as_data_frame:
-        pd = pytest.importorskip('pandas')
-        X = pd.DataFrame(X)
-
-    ohe = OneHotEncoder(categories='auto', handle_unknown=handle_unknown)
-
-    with pytest.raises(ValueError, match="Input contains NaN"):
-        ohe.fit(X)
-
-    with pytest.raises(ValueError, match="Input contains NaN"):
-        ohe.fit_transform(X)
-
-    if as_data_frame:
-        X_partial = X.iloc[:1, :]
-    else:
-        X_partial = X[:1, :]
-
-    ohe.fit(X_partial)
-
-    with pytest.raises(ValueError, match="Input contains NaN"):
-        ohe.transform(X)
-
-
 @pytest.mark.parametrize("X", [
     [['abc', 2, 55], ['def', 1, 55]],
     np.array([[10, 2, 55], [20, 1, 55]]),
@@ -700,23 +708,39 @@ def test_one_hot_encoder_warning():
     np.testing.assert_no_warnings(enc.fit_transform, X)

-def test_one_hot_encoder_drop_manual():
-    cats_to_drop = ['def', 12, 3, 56]
+@pytest.mark.parametrize("missing_value", [np.nan, None, float('nan')])
+def test_one_hot_encoder_drop_manual(missing_value):
+    cats_to_drop = ['def', 12, 3, 56, missing_value]
     enc = OneHotEncoder(drop=cats_to_drop)
-    X = [['abc', 12, 2, 55],
-         ['def', 12, 1, 55],
-         ['def', 12, 3, 56]]
+    X = [['abc', 12, 2, 55, 'a'],
+         ['def', 12, 1, 55, 'a'],
+         ['def', 12, 3, 56, missing_value]]
     trans = enc.fit_transform(X).toarray()
-    exp = [[1, 0, 1, 1],
-           [0, 1, 0, 1],
-           [0, 0, 0, 0]]
+    exp = [[1, 0, 1, 1, 1],
+           [0, 1, 0, 1, 1],
+           [0, 0, 0, 0, 0]]
     assert_array_equal(trans, exp)
     dropped_cats = [cat[feature]
                     for cat, feature in zip(enc.categories_,
                                             enc.drop_idx_)]
-    assert_array_equal(dropped_cats, cats_to_drop)
-    assert_array_equal(np.array(X, dtype=object),
-                       enc.inverse_transform(trans))
+    X_inv_trans = enc.inverse_transform(trans)
+    X_array = np.array(X, dtype=object)
+
+    # last value is np.nan
+    if is_scalar_nan(cats_to_drop[-1]):
+        assert_array_equal(dropped_cats[:-1], cats_to_drop[:-1])
+        assert is_scalar_nan(dropped_cats[-1])
+        assert is_scalar_nan(cats_to_drop[-1])
+        # do not include the last column which includes missing values
+        assert_array_equal(X_array[:, :-1], X_inv_trans[:, :-1])
+
+        # check last column is the missing value
+        assert_array_equal(X_array[-1, :-1], X_inv_trans[-1, :-1])
+        assert is_scalar_nan(X_array[-1, -1])
+        assert is_scalar_nan(X_inv_trans[-1, -1])
+    else:
+        assert_array_equal(dropped_cats, cats_to_drop)
+        assert_array_equal(X_array, X_inv_trans)

 @pytest.mark.parametrize(
@@ -775,9 +799,61 @@ def test_encoders_has_categorical_tags(Encoder):
     assert 'categorical' in Encoder()._get_tags()['X_types']

-@pytest.mark.parametrize('Encoder', [OneHotEncoder, OrdinalEncoder])
-def test_encoders_does_not_support_none_values(Encoder):
-    values = [["a"], [None]]
-    with pytest.raises(TypeError, match="Encoders require their input to be "
-                                        "uniformly strings or numbers."):
-        Encoder().fit(values)
+@pytest.mark.parametrize("missing_value", [np.nan, None])
+def test_ohe_missing_values_get_feature_names(missing_value):
+    # encoder with missing values with object dtypes
+    X = np.array([['a', 'b', missing_value, 'a', missing_value]],
+                 dtype=object).T
+    ohe = OneHotEncoder(sparse=False, handle_unknown='ignore').fit(X)
+    names = ohe.get_feature_names()
+    assert_array_equal(names, ['x0_a', 'x0_b', f'x0_{missing_value}'])
+
+
+def test_ohe_missing_value_support_pandas():
+    # check support for pandas with mixed dtypes and missing values
+    pd = pytest.importorskip('pandas')
+    df = pd.DataFrame({
+        'col1': ['dog', 'cat', None, 'cat'],
+        'col2': np.array([3, 0, 4, np.nan], dtype=float)
+    }, columns=['col1', 'col2'])
+    expected_df_trans = np.array([
+        [0, 1, 0, 0, 1, 0, 0],
+        [1, 0, 0, 1, 0, 0, 0],
+        [0, 0, 1, 0, 0, 1, 0],
+        [1, 0, 0, 0, 0, 0, 1],
+    ])
+
+    Xtr = check_categorical_onehot(df)
+    assert_allclose(Xtr, expected_df_trans)
+
+
+@pytest.mark.parametrize('pd_nan_type', ['pd.NA', 'np.nan'])
+def test_ohe_missing_value_support_pandas_categorical(pd_nan_type):
+    # checks pandas dataframe with categorical features
+    if pd_nan_type == 'pd.NA':
+        # pd.NA is in pandas 1.0
+        pd = pytest.importorskip('pandas', minversion="1.0")
+        pd_missing_value = pd.NA
+    else:  # np.nan
+        pd = pytest.importorskip('pandas')
+        pd_missing_value = np.nan
+
+    df = pd.DataFrame({
+        'col1': pd.Series(['c', 'a', pd_missing_value, 'b', 'a'],
+                          dtype='category'),
+    })
+    expected_df_trans = np.array([
+        [0, 0, 1, 0],
+        [1, 0, 0, 0],
+        [0, 0, 0, 1],
+        [0, 1, 0, 0],
+        [1, 0, 0, 0],
+    ])
+
+    ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
+    df_trans = ohe.fit_transform(df)
+    assert_allclose(expected_df_trans, df_trans)
+
+    assert len(ohe.categories_) == 1
+    assert_array_equal(ohe.categories_[0][:-1], ['a', 'b', 'c'])
+    assert np.isnan(ohe.categories_[0][-1])
diff --git a/sklearn/utils/tests/test_encode.py b/sklearn/utils/tests/test_encode.py
index 9371fa6e88e3e..53c380e192341 100644
--- a/sklearn/utils/tests/test_encode.py
+++ b/sklearn/utils/tests/test_encode.py
@@ -1,3 +1,5 @@
+import pickle
+
 import numpy as np
 import pytest
 from numpy.testing import assert_array_equal
@@ -44,6 +46,15 @@ def test_encode_with_check_unknown():
         _encode(values, uniques=uniques, check_unknown=False)

+def _assert_check_unknown(values, uniques, expected_diff, expected_mask):
+    diff = _check_unknown(values, uniques)
+    assert_array_equal(diff, expected_diff)
+
+    diff, valid_mask = _check_unknown(values, uniques, return_mask=True)
+    assert_array_equal(diff, expected_diff)
+    assert_array_equal(valid_mask, expected_mask)
+
+
 @pytest.mark.parametrize("values, uniques, expected_diff, expected_mask", [
   (np.array([1, 2, 3, 4]),
    np.array([1, 2, 3]),
@@ -53,6 +64,22 @@ def test_encode_with_check_unknown():
    np.array([2, 5, 1]),
    [4],
    [True, True, False, True]),
+  (np.array([2, 1, np.nan]),
+   np.array([2, 5, 1]),
+   [np.nan],
+   [True, True, False]),
+  (np.array([2, 1, 4, np.nan]),
+   np.array([2, 5, 1, np.nan]),
+   [4],
+   [True, True, False, True]),
+  (np.array([2, 1, 4, np.nan]),
+   np.array([2, 5, 1]),
+   [4, np.nan],
+   [True, True, False, False]),
+  (np.array([2, 1, 4, 5]),
+   np.array([2, 5, 1, np.nan]),
+   [4],
+   [True, True, False, True]),
   (np.array(['a', 'b', 'c', 'd'], dtype=object),
    np.array(['a', 'b', 'c'], dtype=object),
    np.array(['d'], dtype=object),
@@ -60,14 +87,122 @@ def test_encode_with_check_unknown():
   (np.array(['d', 'c', 'a', 'b'], dtype=object),
    np.array(['a', 'c', 'b'], dtype=object),
    np.array(['d'], dtype=object),
-   [False, True, True, True])
+   [False, True, True, True]),
+  (np.array(['a', 'b', 'c', 'd']),
+   np.array(['a', 'b', 'c']),
+   np.array(['d']),
+   [True, True, True, False]),
+  (np.array(['d', 'c', 'a', 'b']),
+   np.array(['a', 'c', 'b']),
+   np.array(['d']),
+   [False, True, True, True]),
 ])
 def test_check_unknown(values, uniques, expected_diff, expected_mask):
-    diff = _check_unknown(values, uniques)
+    _assert_check_unknown(values, uniques, expected_diff, expected_mask)

-    assert_array_equal(diff, expected_diff)

-    diff, valid_mask = _check_unknown(values, uniques, return_mask=True)
+@pytest.mark.parametrize("missing_value", [None, np.nan, float('nan')])
+@pytest.mark.parametrize('pickle_uniques', [True, False])
+def test_check_unknown_missing_values(missing_value, pickle_uniques):
+    # check for check_unknown with missing values with object dtypes
+    values = np.array(['d', 'c', 'a', 'b', missing_value], dtype=object)
+    uniques = np.array(['c', 'a', 'b', missing_value], dtype=object)
+    if pickle_uniques:
+        uniques = pickle.loads(pickle.dumps(uniques))

-    assert_array_equal(diff, expected_diff)
-    assert_array_equal(valid_mask, expected_mask)
+    expected_diff = ['d']
+    expected_mask = [False, True, True, True, True]
+    _assert_check_unknown(values, uniques, expected_diff, expected_mask)
+
+    values = np.array(['d', 'c', 'a', 'b', missing_value], dtype=object)
+    uniques = np.array(['c', 'a', 'b'], dtype=object)
+    if pickle_uniques:
+        uniques = pickle.loads(pickle.dumps(uniques))
+
+    expected_diff = ['d', missing_value]
+
+    expected_mask = [False, True, True, True, False]
+    _assert_check_unknown(values, uniques, expected_diff, expected_mask)
+
+    values = np.array(['a', missing_value], dtype=object)
+    uniques = np.array(['a', 'b', 'z'], dtype=object)
+    if pickle_uniques:
+        uniques = pickle.loads(pickle.dumps(uniques))
+
+    expected_diff = [missing_value]
+    expected_mask = [True, False]
+    _assert_check_unknown(values, uniques, expected_diff, expected_mask)
+
+
+@pytest.mark.parametrize('missing_value', [np.nan, None, float('nan')])
+@pytest.mark.parametrize('pickle_uniques', [True, False])
+def test_unique_util_missing_values_objects(missing_value, pickle_uniques):
+    # check for _unique and _encode with missing values with object dtypes
+    values = np.array(['a', 'c', 'c', missing_value, 'b'], dtype=object)
+    expected_uniques = np.array(['a', 'b', 'c', missing_value], dtype=object)
+
+    uniques = _unique(values)
+
+    if missing_value is None:
+        assert_array_equal(uniques, expected_uniques)
+    else:  # missing_value == np.nan
+        assert_array_equal(uniques[:-1], expected_uniques[:-1])
+        assert np.isnan(uniques[-1])
+
+    if pickle_uniques:
+        uniques = pickle.loads(pickle.dumps(uniques))
+
+    encoded = _encode(values, uniques=uniques)
+    assert_array_equal(encoded, np.array([0, 2, 2, 3, 1]))
+
+
+def test_unique_util_missing_values_numeric():
+    # Check missing values in numerical values
+    values = np.array([3, 1, np.nan, 5, 3, np.nan], dtype=float)
+    expected_uniques = np.array([1, 3, 5, np.nan], dtype=float)
+    expected_inverse = np.array([1, 0, 3, 2, 1, 3])
+
+    uniques = _unique(values)
+    assert_array_equal(uniques, expected_uniques)
+
+    uniques, inverse = _unique(values, return_inverse=True)
+    assert_array_equal(uniques, expected_uniques)
+    assert_array_equal(inverse, expected_inverse)
+
+    encoded = _encode(values, uniques=uniques)
+    assert_array_equal(encoded, expected_inverse)
+
+
+def test_unique_util_with_all_missing_values():
+    # test for all types of missing values for object dtype
+    values = np.array([np.nan, 'a', 'c', 'c', None, float('nan'),
+                       None], dtype=object)
+
+    uniques = _unique(values)
+    assert_array_equal(uniques[:-1], ['a', 'c', None])
+    # last value is nan
+    assert np.isnan(uniques[-1])
+
+    expected_inverse = [3, 0, 1, 1, 2, 3, 2]
+    _, inverse = _unique(values, return_inverse=True)
+    assert_array_equal(inverse, expected_inverse)
+
+
+def test_check_unknown_with_both_missing_values():
+    # test for both types of missing values for object dtype
+    values = np.array([np.nan, 'a', 'c', 'c', None, np.nan,
+                       None], dtype=object)
+
+    diff = _check_unknown(values,
+                          known_values=np.array(['a', 'c'], dtype=object))
+    assert diff[0] is None
+    assert np.isnan(diff[1])
+
+    diff, valid_mask = _check_unknown(
+        values, known_values=np.array(['a', 'c'], dtype=object),
+        return_mask=True)
+
+    assert diff[0] is None
+    assert np.isnan(diff[1])
+    assert_array_equal(valid_mask,
+                       [False, True, True, True, False, False, False])

sklearn/preprocessing/tests/test_encoders.py::test_one_hot_encoder_invalid_params[X_fit2-params2-Wrong input for parameter `drop`]

sklearn/preprocessing/tests/test_encoders.py::test_one_hot_encoder_invalid_params[X_fit3-params3-The following categories were supposed]

sklearn/preprocessing/tests/test_encoders.py::test_one_hot_encoder_invalid_params[X_fit0-params0-Wrong input for parameter `drop`]

diff --git a/doc/modules/preprocessing.rst b/doc/modules/preprocessing.rst
index 024dd074e..79d57913a 100644
--- a/doc/modules/preprocessing.rst
+++ b/doc/modules/preprocessing.rst
@@ -590,6 +590,34 @@ In the transformed `X`, the first column is the encoding of the feature with
 categories "male"/"female", while the remaining 6 columns is the encoding of
 the 2 features with respectively 3 categories each.

+:class:`OneHotEncoder` supports categorical features with missing values by
+considering the missing values as an additional category::
+
+    >>> X = [['male', 'Safari'],
+    ...      ['female', None],
+    ...      [np.nan, 'Firefox']]
+    >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
+    >>> enc.categories_
+    [array(['female', 'male', nan], dtype=object),
+     array(['Firefox', 'Safari', None], dtype=object)]
+    >>> enc.transform(X).toarray()
+    array([[0., 1., 0., 0., 1., 0.],
+           [1., 0., 0., 0., 0., 1.],
+           [0., 0., 1., 1., 0., 0.]])
+
+If a feature contains both `np.nan` and `None`, they will be considered
+separate categories::
+
+    >>> X = [['Safari'], [None], [np.nan], ['Firefox']]
+    >>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
+    >>> enc.categories_
+    [array(['Firefox', 'Safari', None, nan], dtype=object)]
+    >>> enc.transform(X).toarray()
+    array([[0., 1., 0., 0.],
+           [0., 0., 1., 0.],
+           [0., 0., 0., 1.],
+           [1., 0., 0., 0.]])
+
 See :ref:`dict_feature_extraction` for categorical features that are
 represented as a dict, not as scalars.

@@ -791,5 +819,5 @@ error with a ``filterwarnings``::
   ...                         category=UserWarning, append=False)

 For a full code example that demonstrates using a :class:`FunctionTransformer`
-to extract features from text data see
+to extract features from text data see
 :ref:`sphx_glr_auto_examples_compose_plot_column_transformer.py`

diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index f35dcc8f7..a0733ffa0 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -552,6 +552,9 @@ Changelog
 :mod:`sklearn.preprocessing`
 ............................

+- |Feature| :class:`preprocessing.OneHotEncoder` now supports missing
+  values by treating them as a category.
+
 - |Feature| Add a new ``handle_unknown`` parameter with a
   ``use_encoded_value`` option, along with a new ``unknown_value`` parameter,
   to :class:`preprocessing.OrdinalEncoder` to allow unknown categories during


diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index 9e31762d62c29..b5d0145af4822 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -158,6 +158,9 @@ Changelog
   :pr:`16289` by :user:`Masashi Kishimoto <kishimoto-banana>` and
   :user:`Olivier Grisel <ogrisel>`.

+- |Enhancement| :class:`isotonic.IsotonicRegression` now accepts 2darray with 1 feature as
+  input array. :pr:`17379` by :user:`Jiaxiang <fujiaxiang>`.
+
 :mod:`sklearn.metrics`
 ......................

diff --git a/sklearn/isotonic.py b/sklearn/isotonic.py
index 50fa0654f7585..905528d3b25f0 100644
--- a/sklearn/isotonic.py
+++ b/sklearn/isotonic.py
@@ -211,7 +211,7 @@ class IsotonicRegression(RegressorMixin, TransformerMixin, BaseEstimator):
     >>> from sklearn.datasets import make_regression
     >>> from sklearn.isotonic import IsotonicRegression
     >>> X, y = make_regression(n_samples=10, n_features=1, random_state=41)
-    >>> iso_reg = IsotonicRegression().fit(X.flatten(), y)
+    >>> iso_reg = IsotonicRegression().fit(X, y)
     >>> iso_reg.predict([.1, .2])
     array([1.8628..., 3.7256...])
     """
@@ -223,9 +223,11 @@ def __init__(self, *, y_min=None, y_max=None, increasing=True,
         self.increasing = increasing
         self.out_of_bounds = out_of_bounds

-    def _check_fit_data(self, X, y, sample_weight=None):
-        if len(X.shape) != 1:
-            raise ValueError("X should be a 1d array")
+    def _check_input_data_shape(self, X):
+        if not (X.ndim == 1 or (X.ndim == 2 and X.shape[1] == 1)):
+            msg = "Isotonic regression input X should be a 1d array or " \
+                  "2d array with 1 feature"
+            raise ValueError(msg)

     def _build_f(self, X, y):
         """Build the f_ interp1d function."""
@@ -246,7 +248,8 @@ def _build_f(self, X, y):

     def _build_y(self, X, y, sample_weight, trim_duplicates=True):
         """Build the y_ IsotonicRegression."""
-        self._check_fit_data(X, y, sample_weight)
+        self._check_input_data_shape(X)
+        X = X.reshape(-1)  # use 1d view

         # Determine increasing if auto-determination requested
         if self.increasing == 'auto':
@@ -295,7 +298,7 @@ def fit(self, X, y, sample_weight=None):

         Parameters
         ----------
-        X : array-like of shape (n_samples,)
+        X : array-like of shape (n_samples,) or (n_samples, 1)
             Training data.

         y : array-like of shape (n_samples,)
@@ -339,7 +342,7 @@ def transform(self, T):

         Parameters
         ----------
-        T : array-like of shape (n_samples,)
+        T : array-like of shape (n_samples,) or (n_samples, 1)
             Data to transform.

         Returns
@@ -355,8 +358,8 @@ def transform(self, T):

         T = check_array(T, dtype=dtype, ensure_2d=False)

-        if len(T.shape) != 1:
-            raise ValueError("Isotonic regression input should be a 1d array")
+        self._check_input_data_shape(T)
+        T = T.reshape(-1)  # use 1d view

         # Handle the out_of_bounds argument by clipping if needed
         if self.out_of_bounds not in ["raise", "nan", "clip"]:
@@ -379,7 +382,7 @@ def predict(self, T):

         Parameters
         ----------
-        T : array-like of shape (n_samples,)
+        T : array-like of shape (n_samples,) or (n_samples, 1)
             Data to transform.

         Returns

diff --git a/sklearn/tests/test_isotonic.py b/sklearn/tests/test_isotonic.py
index 3da76c1f0bb88..66892370f06f0 100644
--- a/sklearn/tests/test_isotonic.py
+++ b/sklearn/tests/test_isotonic.py
@@ -9,7 +9,8 @@
                               IsotonicRegression, _make_unique)

 from sklearn.utils.validation import check_array
-from sklearn.utils._testing import (assert_raises, assert_array_equal,
+from sklearn.utils._testing import (assert_raises, assert_allclose,
+                                    assert_array_equal,
                                     assert_array_almost_equal,
                                     assert_warns_message, assert_no_warnings)
 from sklearn.utils import shuffle
@@ -535,3 +536,43 @@ def test_isotonic_thresholds(increasing):
         assert all(np.diff(y_thresholds) >= 0)
     else:
         assert all(np.diff(y_thresholds) <= 0)
+
+
+def test_input_shape_validation():
+    # Test from #15012
+    # Check that IsotonicRegression can handle 2darray with only 1 feature
+    X = np.arange(10)
+    X_2d = X.reshape(-1, 1)
+    y = np.arange(10)
+
+    iso_reg = IsotonicRegression().fit(X, y)
+    iso_reg_2d = IsotonicRegression().fit(X_2d, y)
+
+    assert iso_reg.X_max_ == iso_reg_2d.X_max_
+    assert iso_reg.X_min_ == iso_reg_2d.X_min_
+    assert iso_reg.y_max == iso_reg_2d.y_max
+    assert iso_reg.y_min == iso_reg_2d.y_min
+    assert_array_equal(iso_reg.X_thresholds_, iso_reg_2d.X_thresholds_)
+    assert_array_equal(iso_reg.y_thresholds_, iso_reg_2d.y_thresholds_)
+
+    y_pred1 = iso_reg.predict(X)
+    y_pred2 = iso_reg_2d.predict(X_2d)
+    assert_allclose(y_pred1, y_pred2)
+
+
+def test_isotonic_2darray_more_than_1_feature():
+    # Ensure IsotonicRegression raises error if input has more than 1 feature
+    X = np.arange(10)
+    X_2d = np.c_[X, X]
+    y = np.arange(10)
+
+    msg = "should be a 1d array or 2d array with 1 feature"
+    with pytest.raises(ValueError, match=msg):
+        IsotonicRegression().fit(X_2d, y)
+
+    iso_reg = IsotonicRegression().fit(X, y)
+    with pytest.raises(ValueError, match=msg):
+        iso_reg.predict(X_2d)
+
+    with pytest.raises(ValueError, match=msg):
+        iso_reg.transform(X_2d)

diff --git a/doc/whats_new/v0.24.rst b/doc/whats_new/v0.24.rst
index 9e31762d6..a30006b15 100644
--- a/doc/whats_new/v0.24.rst
+++ b/doc/whats_new/v0.24.rst
@@ -158,6 +158,9 @@ Changelog
   :pr:`16289` by :user:`Masashi Kishimoto <kishimoto-banana>` and
   :user:`Olivier Grisel <ogrisel>`.

+- |Enhancement| :class:`isotonic.IsotonicRegression` now accepts 2darray with 1 feature as
+  input array.
+
 :mod:`sklearn.metrics`
 ......................