Repository: mwaskom/seaborn

Issue:
Lineplot CI results differ between seaborn v0.10.1 and v0.11.0 when data includes NaN values
I noticed that the lineplot CI results differ between seaborn `0.10.1` and `0.11.0` when data includes `NaN` values. Specifically, the CI results contain `NaN` values in the newer seaborn version.

Reading through the release notes, I have not noticed a particular entry that would explain this change in behavior.

## Reproducing the issue
Full example code at the very end.

This example is based on one of the examples in the `lineplot` API documentation. Most importantly, I set 1% of each `event` group to `NaN` before plotting.
```py
fmri = sns.load_dataset("fmri")
sns.lineplot(data=fmri, x="timepoint", y="signal", hue="event", ax=axs[0])

def set_nan(event):
    event.iloc[::100] = np.nan
    return event

fmri.signal = fmri.groupby("event").signal.transform(set_nan)

sns.lineplot(data=fmri, x="timepoint", y="signal", hue="event", ax=axs[1])
```
![ci_with_nan_values_sns_0 10 1](https://user-images.githubusercontent.com/168390/93002582-50c0ae80-f538-11ea-91ed-626776e2a38d.png)
![ci_with_nan_values_sns_0 11 0](https://user-images.githubusercontent.com/168390/93002084-6338e900-f534-11ea-9907-3b3594926e76.png)
![ci_with_nan_values_sns_0 12 0 dev0](https://user-images.githubusercontent.com/168390/93002086-63d17f80-f534-11ea-9fb5-ff92ea5c971d.png)

Full code to reproduce figures:
```py
import numpy as np

import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

sns.set()

fig, axs = plt.subplots(1, 2, figsize=(8, 4))
axs[0].set_title(
    f"seaborn version: {sns.__version__}\n"
    f"matplotlib version: {matplotlib.__version__}\n"
    f"matplotlib backend: {plt.get_backend()}\n\n"
    "Original data"
)
axs[1].set_title("Data including NaN values")

fmri = sns.load_dataset("fmri")
sns.lineplot(data=fmri, x="timepoint", y="signal", hue="event", ax=axs[0])

def set_nan(event):
    event.iloc[::100] = np.nan
    return event

fmri.signal = fmri.groupby("event").signal.transform(set_nan)

sns.lineplot(data=fmri, x="timepoint", y="signal", hue="event", ax=axs[1])

fig.tight_layout()
fig.savefig(f"ci_with_nan_values_sns_{sns.__version__}.png")
```

Discussion:

Patch:
diff --git a/seaborn/distributions.py b/seaborn/distributions.py
index d548c774..4fc79efc 100644
--- a/seaborn/distributions.py
+++ b/seaborn/distributions.py
@@ -1050,7 +1050,7 @@ class _DistributionPlotter(VectorPlotter):
 
         estimator = KDE(**estimate_kws)
 
-        if "hue" not in self.variables:
+        if not set(self.variables) - {"x", "y"}:
             common_norm = False
 
         all_data = self.plot_data.dropna()
diff --git a/seaborn/relational.py b/seaborn/relational.py
index 6ec11abc..b272abcd 100644
--- a/seaborn/relational.py
+++ b/seaborn/relational.py
@@ -475,6 +475,13 @@ class _LinePlotter(_RelationalPlotter):
                 sort_cols = [var for var in sort_vars if var in self.variables]
                 sub_data = sub_data.sort_values(sort_cols)
 
+            # TODO
+            # How to handle NA? We don't want NA to propagate through to the
+            # estimate/CI when some values are present, but we would also like
+            # matplotlib to show "gaps" in the line when all values are missing.
+            # This is straightforward absent aggregation, but complicated with it.
+            sub_data = sub_data.dropna()
+
             # Due to the original design, code below was written assuming that
             # sub_data always has x, y, and units columns, which may be empty.
             # Adding this here to avoid otherwise disruptive changes, but it
diff --git a/seaborn/utils.py b/seaborn/utils.py
index 1d181b35..67cd4703 100644
--- a/seaborn/utils.py
+++ b/seaborn/utils.py
@@ -371,7 +371,7 @@ def percentiles(a, pcts, axis=None):
 def ci(a, which=95, axis=None):
     """Return a percentile range from an array of values."""
     p = 50 - which / 2, 50 + which / 2
-    return np.percentile(a, p, axis)
+    return np.nanpercentile(a, p, axis)
 
 
 def sig_stars(p):


Test patch:
diff --git a/seaborn/tests/test_distributions.py b/seaborn/tests/test_distributions.py
index 3ce5ea21..a0d9e0fc 100644
--- a/seaborn/tests/test_distributions.py
+++ b/seaborn/tests/test_distributions.py
@@ -2115,3 +2115,18 @@ class TestDisPlot:
 
         with pytest.raises(NotImplementedError):
             displot(long_df, x="x", y="y", kind="ecdf")
+
+    def test_bivariate_kde_norm(self, rng):
+
+        x, y = rng.normal(0, 1, (2, 100))
+        z = [0] * 80 + [1] * 20
+
+        g = displot(x=x, y=y, col=z, kind="kde", levels=10)
+        l1 = sum(bool(c.get_segments()) for c in g.axes.flat[0].collections)
+        l2 = sum(bool(c.get_segments()) for c in g.axes.flat[1].collections)
+        assert l1 > l2
+
+        g = displot(x=x, y=y, col=z, kind="kde", levels=10, common_norm=False)
+        l1 = sum(bool(c.get_segments()) for c in g.axes.flat[0].collections)
+        l2 = sum(bool(c.get_segments()) for c in g.axes.flat[1].collections)
+        assert l1 == l2
diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py
index 0828eb78..f2df31d8 100644
--- a/seaborn/tests/test_relational.py
+++ b/seaborn/tests/test_relational.py
@@ -1090,6 +1090,23 @@ class TestLinePlotter(Helpers):
         assert np.allclose(line.get_ydata(), expected_data.values)
         assert len(ax.collections) == 1
 
+        # Test that nans do not propagate to means or CIs
+
+        p = _LinePlotter(
+            variables=dict(
+                x=[1, 1, 1, 2, 2, 2, 3, 3, 3],
+                y=[1, 2, 3, 3, np.nan, 5, 4, 5, 6],
+            ),
+            estimator="mean", err_style="band", ci=95, n_boot=100, sort=True,
+        )
+        ax.clear()
+        p.plot(ax, {})
+        line, = ax.lines
+        assert line.get_xdata().tolist() == [1, 2, 3]
+        err_band = ax.collections[0].get_paths()
+        assert len(err_band) == 1
+        assert len(err_band[0].vertices) == 9
+
         p = _LinePlotter(
             data=long_df,
             variables=dict(x="x", y="y", hue="a"),