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

曲线越高 = 抽到该值的概率越大;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:轴标签和坐标范围(固定范围方便后面对比)。

rug plot

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 箱直方图做参照。

kdeplot 目标曲线

这条平滑曲线就是我们要”手工复现”的目标。

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

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[布尔掩码, '新列'] = 值按条件填充新列