Skip to content

Commit 5c55704

Browse files
rahulrathnaveltimhoffmscottshambaugh
authored
Fix violinplot crash on empty datasets (matplotlib#31700) (matplotlib#31707)
* Fix violinplot crash on empty datasets (matplotlib#31700) * Apply reviewer feedback: stricter len check and align vpstats append logic * Update lib/matplotlib/tests/test_axes.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Fix violin_stats bailout to handle all-NaN datasets * Refactor violin_stats empty dataset check into if/else block * Fix Ruff W293: Remove trailing whitespace on blank lines in cbook.py * Refactor violin_stats to use early exit continue pattern * Refactor violin_stats to use dict literals and document NaN stripping * Move NaN-stripping documentation to X parameter docstring * Update lib/matplotlib/cbook.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * MAINT: Use delete_masked_points and document NaN handling for violinplot * Update lib/matplotlib/axes/_axes.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Update doc/api/next_api_changes/behavior/violinplot_empty.rst Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Update lib/matplotlib/cbook.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Update doc/api/next_api_changes/behavior/violinplot_empty.rst Co-authored-by: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> --------- Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com>
1 parent 28cf237 commit 5c55704

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
@@ -8958,6 +8958,8 @@ def violinplot(self, dataset, positions=None, vert=None,
89588958
- sequence of 1D arrays: A violin is drawn for each array in the sequence.
89598959
- 2D array: A violin is drawn for each column in the array.
89608960
8961+
Non-finite and masked values are ignored.
8962+
89618963
positions : array-like, default: [1, 2, ..., n]
89628964
The positions of the violins; i.e. coordinates on the x-axis for
89638965
vertical violins (or y-axis for horizontal violins).
@@ -9329,7 +9331,8 @@ def cycle_color(color, alpha=None):
93299331
for stats, pos, width, facecolor in bodies_zip:
93309332
# The 0.5 factor reflects the fact that we plot from v-p to v+p.
93319333
vals = np.array(stats['vals'])
9332-
vals = 0.5 * width * vals / vals.max()
9334+
if len(vals) > 0:
9335+
vals = 0.5 * width * vals / vals.max()
93339336
bodies += [fill(stats['coords'],
93349337
-vals + pos if side in ['both', 'low'] else pos,
93359338
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
@@ -1498,7 +1498,8 @@ def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None)
14981498
----------
14991499
X : 1D array or sequence of 1D arrays or 2D array
15001500
Sample data that will be used to produce the gaussian kernel density
1501-
estimates. Possible values:
1501+
estimates. Non-finite and masked values are ignored.
1502+
Possible values:
15021503
15031504
- 1D array: Statistics are computed for that array.
15041505
- sequence of 1D arrays: Statistics are computed for each array in the sequence.
@@ -1585,29 +1586,34 @@ def _kde_method(x, coords):
15851586
" must have the same length")
15861587

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

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

16121618
return vpstats
16131619

lib/matplotlib/tests/test_axes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10404,3 +10404,10 @@ def test_errorbar_uses_rcparams():
1040410404
assert_allclose([cap.get_markeredgewidth() for cap in caplines], 2.5)
1040510405
for barcol in barlinecols:
1040610406
assert_allclose(barcol.get_linewidths(), 1.75)
10407+
10408+
10409+
def test_violinplot_empty_dataset():
10410+
fig, ax = plt.subplots()
10411+
# This should not raise an exception
10412+
parts = ax.violinplot([np.random.randn(100), [], [np.nan, np.nan]])
10413+
assert len(parts["bodies"]) == 3

0 commit comments

Comments
 (0)