Skip to content

Commit 22d1187

Browse files
rahulrathnavelmeeseeksmachine
authored andcommitted
Backport PR matplotlib#31707: Fix violinplot crash on empty datasets (matplotlib#31700)
1 parent fc2b567 commit 22d1187

4 files changed

Lines changed: 44 additions & 24 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Axes.violinplot and cbook.violin_stats ignore non-finite values
2+
---------------------------------------------------------------
3+
4+
`~matplotlib.axes.Axes.violinplot` and `matplotlib.cbook.violin_stats` now ignore masked and non-finite (NaN and inf) values.

lib/matplotlib/axes/_axes.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8893,6 +8893,8 @@ def violinplot(self, dataset, positions=None, vert=None,
88938893
- sequence of 1D arrays: A violin is drawn for each array in the sequence.
88948894
- 2D array: A violin is drawn for each column in the array.
88958895
8896+
Non-finite and masked values are ignored.
8897+
88968898
positions : array-like, default: [1, 2, ..., n]
88978899
The positions of the violins; i.e. coordinates on the x-axis for
88988900
vertical violins (or y-axis for horizontal violins).
@@ -9264,7 +9266,8 @@ def cycle_color(color, alpha=None):
92649266
for stats, pos, width, facecolor in bodies_zip:
92659267
# The 0.5 factor reflects the fact that we plot from v-p to v+p.
92669268
vals = np.array(stats['vals'])
9267-
vals = 0.5 * width * vals / vals.max()
9269+
if len(vals) > 0:
9270+
vals = 0.5 * width * vals / vals.max()
92689271
bodies += [fill(stats['coords'],
92699272
-vals + pos if side in ['both', 'low'] else pos,
92709273
vals + pos if side in ['both', 'high'] else pos,

lib/matplotlib/cbook.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,8 @@ def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None)
14991499
----------
15001500
X : 1D array or sequence of 1D arrays or 2D array
15011501
Sample data that will be used to produce the gaussian kernel density
1502-
estimates. Possible values:
1502+
estimates. Non-finite and masked values are ignored.
1503+
Possible values:
15031504
15041505
- 1D array: Statistics are computed for that array.
15051506
- sequence of 1D arrays: Statistics are computed for each array in the sequence.
@@ -1586,29 +1587,34 @@ def _kde_method(x, coords):
15861587
" must have the same length")
15871588

15881589
# Zip x and quantiles
1589-
for (x, q) in zip(X, quantiles):
1590-
# Dictionary of results for this distribution
1591-
stats = {}
1592-
1593-
# Calculate basic stats for the distribution
1594-
min_val = np.min(x)
1595-
max_val = np.max(x)
1596-
quantile_val = np.percentile(x, 100 * q)
1590+
for (x, quantile) in zip(X, quantiles):
1591+
x = np.asarray(x)
1592+
x, = delete_masked_points(x)
15971593

1598-
# Evaluate the kernel density estimate
1599-
coords = np.linspace(min_val, max_val, points)
1600-
stats['vals'] = method(x, coords)
1601-
stats['coords'] = coords
1602-
1603-
# Store additional statistics for this distribution
1604-
stats['mean'] = np.mean(x)
1605-
stats['median'] = np.median(x)
1606-
stats['min'] = min_val
1607-
stats['max'] = max_val
1608-
stats['quantiles'] = np.atleast_1d(quantile_val)
1609-
1610-
# Append to output
1611-
vpstats.append(stats)
1594+
if len(x) == 0:
1595+
vpstats.append({
1596+
'vals': np.array([]),
1597+
'coords': np.array([]),
1598+
'mean': np.nan,
1599+
'median': np.nan,
1600+
'min': np.nan,
1601+
'max': np.nan,
1602+
'quantiles': np.array([]),
1603+
})
1604+
else:
1605+
min_val = np.min(x)
1606+
max_val = np.max(x)
1607+
coords = np.linspace(min_val, max_val, points)
1608+
1609+
vpstats.append({
1610+
'vals': method(x, coords),
1611+
'coords': coords,
1612+
'mean': np.mean(x),
1613+
'median': np.median(x),
1614+
'min': min_val,
1615+
'max': max_val,
1616+
'quantiles': np.atleast_1d(np.percentile(x, 100 * quantile))
1617+
})
16121618

16131619
return vpstats
16141620

lib/matplotlib/tests/test_axes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10360,3 +10360,10 @@ def test_errorbar_uses_rcparams():
1036010360
assert_allclose([cap.get_markeredgewidth() for cap in caplines], 2.5)
1036110361
for barcol in barlinecols:
1036210362
assert_allclose(barcol.get_linewidths(), 1.75)
10363+
10364+
10365+
def test_violinplot_empty_dataset():
10366+
fig, ax = plt.subplots()
10367+
# This should not raise an exception
10368+
parts = ax.violinplot([np.random.randn(100), [], [np.nan, np.nan]])
10369+
assert len(parts["bodies"]) == 3

0 commit comments

Comments
 (0)