2026-08-06-Note · Data 100: Visualization I(下):箱线图、小提琴图与 KDE
讲义: 接 2026-08-05-Note(前半:柱状图、直方图、偏度、离群值)。
7. 箱线图与并排箱线图
Cell 33:单个箱线图
sns.boxplot(data=wb, y='Gross domestic product: % growth : 2016');sns.boxplot(data=wb, y='...'):画 GDP 增长率的箱线图。箱体下边 Q1、上边 Q3、中间线是 median,须线(whiskers)延伸到正常范围边界。

箱线图各元素说明(来自讲义配图):

Cell 35:并排箱线图
sns.boxplot(data=wb, x="Continent", y='Gross domestic product: % growth : 2016');- 同时指定
x(分类变量 Continent)和y(定量变量 GDP 增长率),seaborn 就画出跨大洲的并排箱线图,方便比较分布。

8. 小提琴图(Violin Plot)
Cell 37
sns.violinplot(data=wb, y="Gross national income per capita, Atlas method: $: 2016");sns.violinplot(data=wb, y=gni):小提琴图 = 箱线图 + 两侧平滑密度曲线。宽度表示该取值附近的数据密度,所以它能显示多峰(modality),而箱线图不能。

9. KDE(核密度估计)
Cell 39:重新初始化并重命名两列
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore", "use_inf_as_na")
wb = pd.read_csv("data/world_bank.csv", index_col=0)
wb = wb.rename(columns={'Antiretroviral therapy coverage: % of people living with HIV: 2015':"HIV rate",
'Gross national income per capita, Atlas method: $: 2016':'gni'})
wb.head()- 前五行:把需要的库全部导入一遍(相当于新开 kernel 的初始化);
warnings.filterwarnings(...):同上,静音弃用警告;wb = pd.read_csv(...):重新读数据;wb.rename(columns={...}):一次把两个长列名改成HIV rate和gni;wb.head():确认数据可用。
Cell 40:displot = 直方图 + KDE 一步画
sns.displot(data = wb, x = 'HIV rate', \
kde = True, stat = "density")
plt.title("Distribution of HIV rates");sns.displot(data=wb, x='HIV rate', kde=True, stat="density"):displot是”distribution plot”;kde=True表示在直方图上叠加 KDE 曲线;stat="density"让直方图面积=比例;- 末尾
\:Python 行继续符(这行代码在 notebook 里跨两行显示); plt.title(...):加标题。

曲线越高 = 抽到该值的概率越大;KDE 曲线下面积恒为 1。
Cell 42:toy 数据的 rug plot
data = [2.2, 2.8, 3.7, 5.3, 5.7]
sns.rugplot(data, height=0.3)
plt.xlabel("Data")
plt.ylabel("Density")
plt.xlim(-3, 10)
plt.ylim(0, 0.5);data = [2.2, 2.8, 3.7, 5.3, 5.7]:手工构造 5 个数据点,方便演示 KDE 构造过程;sns.rugplot(data, height=0.3):rug plot 在每个数据点位置画一条竖线(地毯图);plt.xlabel/ylabel/xlim/ylim:轴标签和坐标范围(固定范围方便后面对比)。

Cell 44:kdeplot + histplot 叠加
plt.xlabel("Data")
plt.xlim(-3, 10)
plt.ylim(0, 0.5)
sns.kdeplot(data, bw_method=0.65)
sns.histplot(data, stat='density', bins=2);sns.kdeplot(data, bw_method=0.65):画 KDE 曲线;bw_method=0.65控制带宽(bandwidth),这里相当于 α;sns.histplot(data, stat='density', bins=2):叠加一个 2 箱直方图做参照。

这条平滑曲线就是我们要”手工复现”的目标。
Cell 46:一步版 histplot + kde
plt.xlabel("Data")
plt.xlim(-3, 10)
plt.ylim(0, 0.5)
sns.histplot(data, bins=2, kde=True, stat="density", kde_kws=dict(cut=3, bw_method=0.65));kde=True:在直方图里直接附带 KDE;kde_kws=dict(cut=3, bw_method=0.65):传给底层kdeplot的关键字——cut=3让曲线在数据范围外再延伸 3 倍带宽,bw_method=0.65指定带宽。

Cell 48:高斯核函数 + 单个核
def gaussian_kernel(x, z, a):
return (1/np.sqrt(2*np.pi*a**2)) * np.exp((-(x - z)**2 / (2 * a**2)))
# Plot our datapoint
sns.rugplot([2.2], height=0.3)
# Plot the kernel
x = np.linspace(-3, 10, 1000)
plt.plot(x, gaussian_kernel(x, 2.2, 1))
plt.xlabel("Data")
plt.ylabel("Density")
plt.xlim(-3, 10)
plt.ylim(0, 0.5);def gaussian_kernel(x, z, a):定义高斯核函数——x是曲线上任意位置,z是核的中心(某个数据点),a是带宽 α(高斯的标准差);return (1/np.sqrt(2*np.pi*a**2)) * np.exp(-(x - z)**2 / (2*a**2)):正态分布概率密度公式,np.sqrt开方、np.exp指数,完全向量化(x 可以是数组);sns.rugplot([2.2], height=0.3):标记数据点 2.2;x = np.linspace(-3, 10, 1000):在 [-3, 10] 上生成 1000 个等距点,作为曲线的横坐标;plt.plot(x, gaussian_kernel(x, 2.2, 1)):画出以 2.2 为中心、α=1 的高斯核;- 后面四行:轴标签和范围。

核函数就是”描述这个数据点附近随机采样概率”的曲线:在 2.2 处最高,两侧衰减。
Cell 50:三个 KDE 辅助函数 + 未归一化核
def create_kde(kernel, pts, a):
def f(x):
output = 0
for pt in pts:
output += kernel(x, pt, a)
return output / len(pts) # Normalization factor
return f
def plot_kde(kernel, pts, a):
f = create_kde(kernel, pts, a)
x = np.linspace(min(pts) - 5, max(pts) + 5, 1000)
y = [f(xi) for xi in x]
plt.plot(x, y);
def plot_separate_kernels(kernel, pts, a, norm=False):
x = np.linspace(min(pts) - 5, max(pts) + 5, 1000)
for pt in pts:
y = kernel(x, pt, a)
if norm:
y /= len(pts)
plt.plot(x, y)
plt.show();
plt.xlim(-3, 10)
plt.ylim(0, 0.5)
plt.xlabel("Data")
plt.ylabel("Density")
plot_separate_kernels(gaussian_kernel, data, a = 1)create_kde(kernel, pts, a):返回一个闭包函数f(x)——- 内部
for pt in pts: output += kernel(x, pt, a):把所有核在 x 处的值相加; return output / len(pts):除以数据点个数 n,即归一化(否则总面积 = n)。
- 内部
plot_kde(kernel, pts, a):调用create_kde得到 f,在[min(pts)-5, max(pts)+5]上采样 1000 个点,逐点求值后画出最终 KDE 曲线;plot_separate_kernels(kernel, pts, a, norm=False):单独画出每个核(每个数据点一条曲线);norm=True时把每条曲线除以 n;- 末尾几行设置坐标范围后调用
plot_separate_kernels(gaussian_kernel, data, a=1):画出 5 个未归一化的高斯核。

注意:这些核直接相加的话,总面积是 5(每个核面积 1),不是合法的密度曲线。
Cell 52:归一化后的核
plt.xlim(-3, 10)
plt.ylim(0, 0.5)
plt.xlabel("Data")
plt.ylabel("Density")
# The `norm` argument specifies whether or not to normalize the kernels
plot_separate_kernels(gaussian_kernel, data, a = 1, norm = True)- 前三行设置坐标范围与标签;
plot_separate_kernels(gaussian_kernel, data, a=1, norm=True):与 Cell 50 相同,只是norm=True——每个核乘以 1/5,曲线整体变矮、变”胖”。

Cell 54:最终 KDE = 归一化核之和
plt.xlim(-3, 10)
plt.ylim(0, 0.5)
plt.xlabel("Data")
plt.ylabel("Density")
plot_kde(gaussian_kernel, data, a=1)- 坐标设置同上;
plot_kde(gaussian_kernel, data, a=1):把 5 个归一化核相加,得到最终 KDE 曲线——与 Cell 44 里sns.kdeplot画出的曲线一致。

KDE 三步:每个点放核 → 归一化(×1/n)→ 相加。数学形式:
Cell 56:箱形核(Boxcar Kernel)
def boxcar_kernel(alpha, x, z):
return (((x-z)>=-alpha/2)&((x-z)<=alpha/2))/alpha
xs = np.linspace(-5, 5, 200)
alpha=1
kde_curve = [boxcar_kernel(alpha, x, 0) for x in xs]
plt.plot(xs, kde_curve);def boxcar_kernel(alpha, x, z):定义箱形核——中心 z、带宽 α;return (((x-z)>=-alpha/2)&((x-z)<=alpha/2))/alpha:|x-z| ≤ α/2时布尔值为 True(数值 1),除以 α 得1/α;窗口外为 False(数值 0)。整个表达式自动把布尔转成数值;xs = np.linspace(-5, 5, 200):横坐标网格;alpha = 1:带宽设为 1;kde_curve = [boxcar_kernel(alpha, x, 0) for x in xs]:用列表推导逐点计算以 0 为中心的箱形核;plt.plot(xs, kde_curve):画出来就是矩形窗。

只要函数非负且积分为 1,就能当核——高斯核和箱形核只是两种选择。
10. 踩坑记录
- 定量变量不能直接画 countplot:会对每个唯一值画一根柱,造成 overplotting,完全无法解读;
- 直方图看面积、不看高度:
stat="density"(或density=True)之后,y 轴是 density,柱子面积才等于数据比例; - matplotlib 不自动加轴标签:
plt.xlabel/plt.ylabel必须自己写;seaborn 会自动处理; - 多条件布尔选择要加括号:
(df['a'] < q1) | (df['a'] > q3),且用&/|而不是and/or; - bins 数量影响结论:同一分布 5 bins 单峰、10 bins 双峰、20 bins 看不清——判断峰数要小心;
df.rename()返回新对象:不重新赋值wb = wb.rename(...)就不会生效;- KDE 不归一化总面积 = n:
create_kde里必须output / len(pts); - hue 用了颜色就必须有图例:seaborn 自动生成,自己用 matplotlib 时要手动加;
- 小提琴图的宽度有意义、箱线图的宽度无意义:选哪个取决于”密度”是否是你想强调的信息。
11. 函数速查
| 函数 / 方法 | 作用 |
|---|---|
wb['col'].value_counts() | 统计唯一值频数 |
.plot(kind='bar') | pandas 内置柱状图(功能有限) |
plt.bar(x, y) | matplotlib 柱状图 |
sns.countplot(data=..., x=...) | seaborn 分类频数柱状图 |
plt.hist(x, density=True, bins=...) | matplotlib 直方图,返回 (densities, bins, patches) |
sns.histplot(data, x=..., stat="density", bins=..., hue=...) | seaborn 直方图(可分组叠加) |
sns.boxplot(data, x=..., y=...) | 箱线图(x 缺省时单变量) |
sns.violinplot(data, x=..., y=...) | 小提琴图 |
sns.kdeplot(x, bw_method=...) | KDE 曲线 |
sns.rugplot(x, height=...) | 数据点地毯图 |
sns.displot(data, x=..., kde=True, stat="density") | 直方图 + KDE 一步画 |
np.percentile(x, [25, 50, 75]) | 计算四分位数 |
df.rename(columns={旧: 新}) | 重命名列 |
df.loc[布尔掩码, '新列'] = 值 | 按条件填充新列 |