Lesson 07
KDE and ECDF Plots
Draw smooth density curves with sns.kdeplot, tune them with bw_adjust, compare groups, and use ecdfplot to read percentiles straight off a curve.
A histogram's shape depends on where the bins fall. A kernel density estimate, or KDE, avoids bins: it places a small smooth bump on every value and adds the bumps up. The result is a curve whose area is 1. An ECDF is the other view: for every value, what share of the rows is at or below it.
A first density curve
kdeplot draws one curve for the column. The y axis is density, not a count, and the area under the whole curve is 1. Integrating the drawn line confirms it.
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()
ax = sns.kdeplot(data=study, x="score")
plt.show()
x, y = ax.lines[0].get_data()
print(round(np.trapz(y, x), 3))Output:
1.0Smoothing with bw_adjust
The bandwidth is how wide each bump is. bw_adjust scales Seaborn's automatic choice: below 1 follows the data closely and gets wiggly, above 1 smooths more and can hide real bumps.
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()
for adjust in [0.3, 1, 2.5]:
sns.kdeplot(data=study, x="score", bw_adjust=adjust, label=f"bw_adjust={adjust}")
plt.legend()
plt.show()
print("same data, three smoothings")Output:
same data, three smoothingsCompare groups
With hue, each group gets a curve. By default the curves share one total area of 1, so a larger group gets a taller curve. common_norm=False gives each group its own area of 1 so you compare shapes, not sizes.
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.kdeplot(data=study, x="score", hue="track", fill=True, common_norm=False, alpha=0.3)
plt.show()
print(study["track"].value_counts())Output:
track
Web 51
Python 35
Data 34
Name: count, dtype: int64The curve runs past the data
Bumps near the edges spill beyond the smallest and largest values, so the curve suggests scores below 40 that nobody got. cut=0 stops the curve at the data's range, and clip=(0, 100) stops it at limits you know.
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()
ax = sns.kdeplot(data=study, x="score", label="default")
sns.kdeplot(data=study, x="score", cut=0, label="cut=0", ax=ax)
plt.legend()
plt.show()
print("data range:", study["score"].min(), study["score"].max())
print("curves start at:", [round(line.get_xdata()[0], 1) for line in ax.lines])Output:
data range: 40.0 85.0
curves start at: [27.9, 40.0]Read percentiles with ecdfplot
ecdfplot climbs from 0 to 1. Pick a score on the x axis and the height is the share of rows at or below it. It needs no bins and no smoothing, so it never invents values.
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.ecdfplot(data=study, x="score", hue="mode")
plt.axhline(0.5, color="grey", linestyle="--")
plt.show()
print(study.groupby("mode")["score"].median())
print((study["score"] <= 70).mean().round(3))Output:
mode
classroom 66.0
online 65.0
Name: score, dtype: float64
0.708Key takeaways
-
A KDE adds a smooth bump per value; its total area is 1
-
bw_adjust below 1 follows the data, above 1 smooths more
-
common_norm=False compares group shapes rather than sizes
-
ecdfplot shows the share of rows at or below each value
-
Previous: Histograms with histplot
Loading playground…
Check your understanding
KDE and ECDF Plots Quiz
What does a KDE place on every value?
1 / 10