Repository: mwaskom/seaborn

Issue:
jointplot doesn't warn for ax= argument in seaborn 11.2
In seaborn 11.2, figure-level functions such as `catplot`,  'relplot', `displot` and `pairplot` issue a warning when an `ax=` parameter is passed:
```
UserWarning: catplot is a figure-level function and does not accept target axes. You may wish to try stripplot
  warnings.warn(msg, UserWarning)
```
`jointplot` doesn't issue such warning
```python
import seaborn as sns
tips = sns.load_dataset('tips')
fig, axs = plt.subplots(1, 2)
sns.jointplot(data=tips, x='total_bill', y='tip', hue='day', ax=axs[0])
```


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2863?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) Report
> Merging [#2863](https://codecov.io/gh/mwaskom/seaborn/pull/2863?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) (e58c8f4) into [master](https://codecov.io/gh/mwaskom/seaborn/commit/a48dc8f4df8d81ff8b50bdf26b6c094333f6ea8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) (a48dc8f) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mwaskom/seaborn/pull/2863/graphs/tree.svg?width=650&height=150&src=pr&token=SLuzHnA9Bl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom)](https://codecov.io/gh/mwaskom/seaborn/pull/2863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom)

```diff
@@            Coverage Diff             @@
##           master    #2863      +/-   ##
==========================================
+ Coverage   98.11%   98.13%   +0.01%     
==========================================
  Files          69       69              
  Lines       22247    22252       +5     
==========================================
+ Hits        21828    21836       +8     
+ Misses        419      416       -3     
```


| [Impacted Files](https://codecov.io/gh/mwaskom/seaborn/pull/2863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom) | Coverage Δ | |
|---|---|---|
| [seaborn/axisgrid.py](https://codecov.io/gh/mwaskom/seaborn/pull/2863/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-c2VhYm9ybi9heGlzZ3JpZC5weQ==) | `97.17% <100.00%> (+0.29%)` | :arrow_up: |
| [tests/test\_axisgrid.py](https://codecov.io/gh/mwaskom/seaborn/pull/2863/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Michael+Waskom#diff-dGVzdHMvdGVzdF9heGlzZ3JpZC5weQ==) | `99.36% <100.00%> (+<0.01%)` | :arrow_up: |


Patch:
diff --git a/doc/releases/v0.12.0.txt b/doc/releases/v0.12.0.txt
index 77b3b5b6..6beda839 100644
--- a/doc/releases/v0.12.0.txt
+++ b/doc/releases/v0.12.0.txt
@@ -51,6 +51,8 @@ Other updates
 
 - |Enhancement| Example datasets are now stored in an OS-specific cache location (as determined by `appdirs`) rather than in the user's home directory. Users should feel free to remove `~/seaborn-data` if desired (:pr:`2773`).
 
+- |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).
+
 - |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`).
@@ -79,6 +81,8 @@ Other updates
 
 - |Fix| Subplot titles will no longer be reset when calling :meth:`FacetGrid.map` or :meth:`FacetGrid.map_dataframe` (:pr:`2705`).
 
+- |Fix| Improved robustness to numerical errors in :func:`kdeplot` (:pr:`2862`).
+
 - |Dependencies| Made `scipy` an optional dependency and added `pip install seaborn[all]` as a method for ensuring the availability of compatible `scipy` and `statsmodels` libraries at install time. This has a few minor implications for existing code, which are explained in the Github pull request (:pr:`2398`).
 
 - |Dependencies| Following `NEP29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_, dropped support for Python 3.6 and bumped the minimally-supported versions of the library dependencies.
diff --git a/seaborn/axisgrid.py b/seaborn/axisgrid.py
index 04de7605..ae6c9aac 100644
--- a/seaborn/axisgrid.py
+++ b/seaborn/axisgrid.py
@@ -2137,12 +2137,9 @@ def jointplot(
     from .regression import regplot, residplot
     from .distributions import histplot, kdeplot, _freedman_diaconis_bins
 
-    # Handle deprecations
-    if "size" in kwargs:
-        height = kwargs.pop("size")
-        msg = ("The `size` parameter has been renamed to `height`; "
-               "please update your code.")
-        warnings.warn(msg, UserWarning)
+    if kwargs.pop("ax", None) is not None:
+        msg = "Ignoring `ax`; jointplot is a figure-level function."
+        warnings.warn(msg, UserWarning, stacklevel=2)
 
     # Set up empty default kwarg dicts
     joint_kws = {} if joint_kws is None else joint_kws.copy()
@@ -2285,6 +2282,9 @@ def jointplot(
         histplot(x=x, hue=hue, ax=grid.ax_marg_x, **marginal_kws)
         histplot(y=y, hue=hue, ax=grid.ax_marg_y, **marginal_kws)
 
+    # Make the main axes active in the matplotlib state machine
+    plt.sca(grid.ax_joint)
+
     return grid
 
 
diff --git a/seaborn/categorical.py b/seaborn/categorical.py
index d8d2b581..3b69c855 100644
--- a/seaborn/categorical.py
+++ b/seaborn/categorical.py
@@ -1576,8 +1576,8 @@ class _BarPlotter(_CategoricalStatPlotter):
 
     def __init__(self, x, y, hue, data, order, hue_order,
                  estimator, ci, n_boot, units, seed,
-                 orient, color, palette, saturation, errcolor,
-                 errwidth, capsize, dodge):
+                 orient, color, palette, saturation, width,
+                 errcolor, errwidth, capsize, dodge):
         """Initialize the plotter."""
         self.establish_variables(x, y, hue, data, orient,
                                  order, hue_order, units)
@@ -1585,6 +1585,7 @@ class _BarPlotter(_CategoricalStatPlotter):
         self.estimate_statistic(estimator, ci, n_boot, seed)
 
         self.dodge = dodge
+        self.width = width
 
         self.errcolor = errcolor
         self.errwidth = errwidth
@@ -2743,7 +2744,7 @@ swarmplot.__doc__ = dedent("""\
 def barplot(
     data=None, *, x=None, y=None, hue=None, order=None, hue_order=None,
     estimator=np.mean, ci=95, n_boot=1000, units=None, seed=None,
-    orient=None, color=None, palette=None, saturation=.75,
+    orient=None, color=None, palette=None, saturation=.75, width=.8,
     errcolor=".26", errwidth=None, capsize=None, dodge=True,
     ax=None,
     **kwargs,
@@ -2752,7 +2753,7 @@ def barplot(
     plotter = _BarPlotter(x, y, hue, data, order, hue_order,
                           estimator, ci, n_boot, units, seed,
                           orient, color, palette, saturation,
-                          errcolor, errwidth, capsize, dodge)
+                          width, errcolor, errwidth, capsize, dodge)
 
     if ax is None:
         ax = plt.gca()
@@ -2793,6 +2794,7 @@ barplot.__doc__ = dedent("""\
     {color}
     {palette}
     {saturation}
+    {width}
     errcolor : matplotlib color
         Color used for the error bar lines.
     {errwidth}
@@ -2910,7 +2912,7 @@ pointplot.__doc__ = dedent("""\
 
 def countplot(
     data=None, *, x=None, y=None, hue=None, order=None, hue_order=None,
-    orient=None, color=None, palette=None, saturation=.75,
+    orient=None, color=None, palette=None, saturation=.75, width=.8,
     dodge=True, ax=None, **kwargs
 ):
 
@@ -2936,7 +2938,7 @@ def countplot(
         x, y, hue, data, order, hue_order,
         estimator, ci, n_boot, units, seed,
         orient, color, palette, saturation,
-        errcolor, errwidth, capsize, dodge
+        width, errcolor, errwidth, capsize, dodge
     )
 
     plotter.value_label = "count"
diff --git a/seaborn/distributions.py b/seaborn/distributions.py
index ec0fd801..f9e5b787 100644
--- a/seaborn/distributions.py
+++ b/seaborn/distributions.py
@@ -334,16 +334,6 @@ class _DistributionPlotter(VectorPlotter):
             # Extract the data points from this sub set and remove nulls
             observations = sub_data[data_variable]
 
-            observation_variance = observations.var()
-            if math.isclose(observation_variance, 0) or np.isnan(observation_variance):
-                msg = (
-                    "Dataset has 0 variance; skipping density estimate. "
-                    "Pass `warn_singular=False` to disable this warning."
-                )
-                if warn_singular:
-                    warnings.warn(msg, UserWarning)
-                continue
-
             # Extract the weights for this subset of observations
             if "weights" in self.variables:
                 weights = sub_data["weights"]
@@ -353,7 +343,24 @@ class _DistributionPlotter(VectorPlotter):
                 part_weight = len(sub_data)
 
             # Estimate the density of observations at this level
-            density, support = estimator(observations, weights=weights)
+            variance = np.nan_to_num(observations.var())
+            singular = len(observations) < 2 or math.isclose(variance, 0)
+            try:
+                if not singular:
+                    # Convoluted approach needed because numerical failures
+                    # can manifest in a few different ways.
+                    density, support = estimator(observations, weights=weights)
+            except np.linalg.LinAlgError:
+                singular = True
+
+            if singular:
+                msg = (
+                    "Dataset has 0 variance; skipping density estimate. "
+                    "Pass `warn_singular=False` to disable this warning."
+                )
+                if warn_singular:
+                    warnings.warn(msg, UserWarning, stacklevel=4)
+                continue
 
             if log_scale:
                 support = np.power(10, support)
@@ -1054,8 +1061,10 @@ class _DistributionPlotter(VectorPlotter):
 
         for sub_vars, sub_data in self.iter_data("hue", from_comp_data=True):
 
-            # Extract the data points from this sub set and remove nulls
+            # Extract the data points from this sub set
             observations = sub_data[["x", "y"]]
+            min_variance = observations.var().fillna(0).min()
+            observations = observations["x"], observations["y"]
 
             # Extract the weights for this subset of observations
             if "weights" in self.variables:
@@ -1063,21 +1072,25 @@ class _DistributionPlotter(VectorPlotter):
             else:
                 weights = None
 
-            # Check that KDE will not error out
-            variance = observations[["x", "y"]].var()
-            if any(math.isclose(x, 0) for x in variance) or variance.isna().any():
+            # Estimate the density of observations at this level
+            singular = math.isclose(min_variance, 0)
+            try:
+                if not singular:
+                    density, support = estimator(*observations, weights=weights)
+            except np.linalg.LinAlgError:
+                # Testing for 0 variance doesn't catch all cases where scipy raises,
+                # but we can also get a ValueError, so we need this convoluted approach
+                singular = True
+
+            if singular:
                 msg = (
-                    "Dataset has 0 variance; skipping density estimate. "
+                    "KDE cannot be estimated (0 variance or perfect covariance). "
                     "Pass `warn_singular=False` to disable this warning."
                 )
                 if warn_singular:
-                    warnings.warn(msg, UserWarning)
+                    warnings.warn(msg, UserWarning, stacklevel=3)
                 continue
 
-            # Estimate the density of observations at this level
-            observations = observations["x"], observations["y"]
-            density, support = estimator(*observations, weights=weights)
-
             # Transform the support grid back to the original scale
             xx, yy = support
             if self._log_scaled("x"):


Test patch:
diff --git a/tests/test_axisgrid.py b/tests/test_axisgrid.py
index d0ac2689..4a33da94 100644
--- a/tests/test_axisgrid.py
+++ b/tests/test_axisgrid.py
@@ -1788,4 +1788,11 @@ class TestJointPlot:
 
         with pytest.warns(UserWarning):
             g = ag.jointplot(data=long_df, x="x", y="y", marginal_kws=dict(rug=True))
-            assert g.ax_marg_x.patches
+        assert g.ax_marg_x.patches
+
+    def test_ax_warning(self, long_df):
+
+        ax = plt.gca()
+        with pytest.warns(UserWarning):
+            g = ag.jointplot(data=long_df, x="x", y="y", ax=ax)
+        assert g.ax_joint.collections
diff --git a/tests/test_categorical.py b/tests/test_categorical.py
index fac45672..70554b3c 100644
--- a/tests/test_categorical.py
+++ b/tests/test_categorical.py
@@ -2203,26 +2203,27 @@ class TestBarPlotter(CategoricalFixture):
         estimator=np.mean, ci=95, n_boot=100, units=None, seed=None,
         order=None, hue_order=None,
         orient=None, color=None, palette=None,
-        saturation=.75, errcolor=".26", errwidth=None,
+        saturation=.75, width=0.8,
+        errcolor=".26", errwidth=None,
         capsize=None, dodge=True
     )
 
     def test_nested_width(self):
 
-        kws = self.default_kws.copy()
+        ax = cat.barplot(data=self.df, x="g", y="y", hue="h")
+        for bar in ax.patches:
+            assert bar.get_width() == pytest.approx(.8 / 2)
+        ax.clear()
 
-        p = cat._BarPlotter(**kws)
-        p.establish_variables("g", "y", hue="h", data=self.df)
-        assert p.nested_width == .8 / 2
-
-        p = cat._BarPlotter(**kws)
-        p.establish_variables("h", "y", "g", data=self.df)
-        assert p.nested_width == .8 / 3
+        ax = cat.barplot(data=self.df, x="g", y="y", hue="g", width=.5)
+        for bar in ax.patches:
+            assert bar.get_width() == pytest.approx(.5 / 3)
+        ax.clear()
 
-        kws["dodge"] = False
-        p = cat._BarPlotter(**kws)
-        p.establish_variables("h", "y", "g", data=self.df)
-        assert p.nested_width == .8
+        ax = cat.barplot(data=self.df, x="g", y="y", hue="g", dodge=False)
+        for bar in ax.patches:
+            assert bar.get_width() == pytest.approx(.8)
+        ax.clear()
 
     def test_draw_vertical_bars(self):
 
diff --git a/tests/test_distributions.py b/tests/test_distributions.py
index ac9ee5f3..69f08b9c 100644
--- a/tests/test_distributions.py
+++ b/tests/test_distributions.py
@@ -1,4 +1,5 @@
 import itertools
+import warnings
 
 import numpy as np
 import matplotlib as mpl
@@ -411,9 +412,15 @@ class TestKDEPlotUnivariate(SharedAxesLevelTests):
             ax = kdeplot(x=[5])
         assert not ax.lines
 
-        with pytest.warns(None) as record:
+        with pytest.warns(UserWarning):
+            # https://github.com/mwaskom/seaborn/issues/2762
+            ax = kdeplot(x=[1929245168.06679] * 18)
+        assert not ax.lines
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", UserWarning)
             ax = kdeplot(x=[5], warn_singular=False)
-        assert not record
+        assert not ax.lines
 
     def test_variable_assignment(self, long_df):
 
@@ -930,9 +937,14 @@ class TestKDEPlotBivariate:
             ax = dist.kdeplot(x=[5], y=[6])
         assert not ax.lines
 
-        with pytest.warns(None) as record:
+        with pytest.warns(UserWarning):
+            ax = kdeplot(x=[1929245168.06679] * 18, y=np.arange(18))
+        assert not ax.lines
+
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", UserWarning)
             ax = kdeplot(x=[5], y=[7], warn_singular=False)
-        assert not record
+        assert not ax.lines
 
     def test_fill_artists(self, long_df):