Skip to main content
MahiDev — Learn. Build. Become AI-ready.

Lesson 10

Violin Plots

Combine a box plot with a density curve in sns.violinplot, split each violin by a second group, and choose what is drawn inside with the inner argument.

Intermediate25 min
Violin Plots hero Module 26 Lesson 10Violin Plots hero Module 26 Lesson 10

A box plot hides the shape inside the box: two very different distributions can have the same five numbers. A violin plot keeps the shape. Each side is a density curve, mirrored, and a thin box plot sits inside.

Three violin shapes of score by track with a thin inner box and median dotThree violin shapes of score by track with a thin inner box and median dot

A first violin

Where the violin is wide, many rows have that value; where it is thin, few do. The dark bar inside is the interquartile range and the white dot is the median, just like a box plot.

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(26)
study = pd.DataFrame({
    "track": rng.choice(["Python", "Data", "Web"], 120),
    "mode": rng.choice(["online", "classroom"], 120),
    "week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()

sns.violinplot(data=study, x="track", y="score", order=["Python", "Data", "Web"])
plt.show()

print(study.groupby("track")["score"].median())

Output:

text
track
Data      68.0
Python    65.0
Web       63.0
Name: score, dtype: float64

What goes inside

inner chooses the middle mark: "box" is the default, "quart" draws three quartile lines, "point" and "stick" draw every row, and None leaves the violin empty.

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(26)
study = pd.DataFrame({
    "track": rng.choice(["Python", "Data", "Web"], 120),
    "mode": rng.choice(["online", "classroom"], 120),
    "week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()

fig, axes = plt.subplots(1, 4, figsize=(14, 3.5), sharey=True)
for ax, inner in zip(axes, ["box", "quart", "stick", None]):
    sns.violinplot(data=study, x="mode", y="score", inner=inner, ax=ax)
    ax.set_title(f"inner={inner!r}")
plt.show()

print(study.groupby("mode")["score"].quantile([0.25, 0.5, 0.75]).unstack())

Output:

text
            0.25  0.50  0.75
mode                        
classroom  59.00  66.0  74.0
online     55.25  65.0  70.0

Split violins

With a two-level hue and split=True, each violin shows one group on the left half and the other on the right, which makes the comparison direct. inner="quart" keeps the quartiles on both halves.

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(26)
study = pd.DataFrame({
    "track": rng.choice(["Python", "Data", "Web"], 120),
    "mode": rng.choice(["online", "classroom"], 120),
    "week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()

sns.violinplot(data=study, x="track", y="score", hue="mode", split=True, inner="quart")
plt.show()

print(study.pivot_table(index="track", columns="mode", values="score", aggfunc="median"))

Output:

text
mode    classroom  online
track                    
Data         68.0    68.0
Python       63.0    66.5
Web          66.5    63.0

Tame the edges and widths

Like kdeplot, violins can reach past the data; cut=0 stops them at the smallest and largest values. density_norm="count" makes bigger groups wider, so the Web track, with the most rows, stands out.

python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(26)
study = pd.DataFrame({
    "track": rng.choice(["Python", "Data", "Web"], 120),
    "mode": rng.choice(["online", "classroom"], 120),
    "week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()

sns.violinplot(data=study, x="track", y="score", cut=0, density_norm="count")
plt.show()

print(study["track"].value_counts())

Output:

text
track
Web       51
Python    35
Data      34
Name: count, dtype: int64

With fewer than about twenty rows per group, the curve is mostly guesswork. Use a box plot or draw the points instead.

Key takeaways

  • A violin is a mirrored density curve with a small box plot inside

  • Wide means common, thin means rare

  • inner= picks the middle mark; split=True halves a two-level hue

  • cut=0 stops at the data; density_norm="count" widens bigger groups

  • Next: Strip, Swarm, and catplot

  • Previous: Box Plots

Loading playground…

Check your understanding

Violin Plots Quiz

What does a violin plot add to a box plot?

1 / 10