Seaborn 直方图:在 Python 中创建分布图
Updated on
理解数据的分布对于统计分析和决策制定至关重要。然而,许多数据专业人士在创建清晰、信息丰富的分布图以揭示模式和异常值方面遇到困难。通用绘图工具通常需要大量自定义并产生视觉上不吸引人的结果。
Seaborn 的直方图函数通过提供高级接口来解决这个问题,只需最少的代码即可创建美观的分布图。该库自动为箱体大小、颜色和样式选择适当的默认值,同时在需要时为您提供细粒度控制。
本指南涵盖了掌握 seaborn 直方图所需的一切,从基本图到高级自定义技术。您将学习如何使用 sns.histplot() 和 sns.displot()、控制分箱策略、叠加 KDE 曲线、比较多个分布以及避免常见陷阱。
什么是直方图?
直方图通过将数据范围划分为箱并计算每个箱中的观测数来显示连续变量的分布。每个条的高度表示该箱范围内数据点的频率或密度。
直方图帮助您识别:
- 集中趋势(大多数值聚集的位置)
- 数据的分散和变异性
- 偏度和异常值
- 多峰分布(多个峰值)
Seaborn 提供两个主要函数来创建直方图:histplot() 用于轴级图,displot() 用于带有自动分面支持的图形级图。
使用 sns.histplot() 创建基本直方图
histplot() 函数是在 seaborn 中创建直方图的主要工具。它提供了一个简单的接口和强大的自定义选项。
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
# 生成样本数据
np.random.seed(42)
data = np.random.normal(100, 15, 1000)
# 创建基本直方图
sns.histplot(data=data)
plt.title('基本 Seaborn 直方图')
plt.xlabel('值')
plt.ylabel('计数')
plt.show()这将创建一个直方图,其箱大小基于 Freedman-Diaconis 规则自动确定,该规则平衡了细节和噪声。
对于 DataFrame 中的数据:
import pandas as pd
# 创建 DataFrame
df = pd.DataFrame({
'values': np.random.normal(100, 15, 1000),
'category': np.random.choice(['A', 'B', 'C'], 1000)
})
# 从 DataFrame 绘制直方图
sns.histplot(data=df, x='values')
plt.show()控制箱:大小、数量和范围
箱的选择极大地影响直方图揭示模式的方式。箱太少会过度简化分布,而箱太多会产生噪声。
指定箱数
# 创建具有 30 个箱的直方图
sns.histplot(data=data, bins=30)
plt.title('具有 30 个箱的直方图')
plt.show()设置箱宽度
# 设置特定箱宽度
sns.histplot(data=data, binwidth=5)
plt.title('箱宽度 = 5 的直方图')
plt.show()定义箱边缘
# 自定义箱边缘
bin_edges = [70, 80, 90, 100, 110, 120, 130]
sns.histplot(data=data, bins=bin_edges)
plt.title('具有自定义箱边缘的直方图')
plt.show()控制箱范围
# 限制直方图范围
sns.histplot(data=data, binrange=(80, 120))
plt.title('限制范围的直方图(80-120)')
plt.show()添加 KDE 叠加
核密度估计 (KDE) 提供了概率密度函数的平滑估计,帮助您看到分布的整体形状。
# 带 KDE 叠加的直方图
sns.histplot(data=data, kde=True)
plt.title('带 KDE 叠加的直方图')
plt.show()您还可以仅显示 KDE 曲线:
# 仅 KDE(无直方图条)
sns.kdeplot(data=data)
plt.title('仅 KDE 曲线')
plt.show()或组合多个可视化:
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 仅直方图
sns.histplot(data=data, ax=axes[0])
axes[0].set_title('仅直方图')
# 仅 KDE
sns.kdeplot(data=data, ax=axes[1])
axes[1].set_title('仅 KDE')
# 两者组合
sns.histplot(data=data, kde=True, ax=axes[2])
axes[2].set_title('直方图 + KDE')
plt.tight_layout()
plt.show()理解 stat 参数
stat 参数控制为每个箱计算的统计量,影响 y 轴的解释。
| stat 值 | 描述 | 使用场景 |
|---|---|---|
count | 观测数(默认) | 显示绝对频率 |
frequency | 与 count 相同 | count 的替代名称 |
density | 归一化使面积等于 1 | 比较不同样本大小的分布 |
probability | 归一化使高度和为 1 | 显示箱的概率 |
percent | 百分比形式的概率 | 比概率更直观 |
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
stats = ['count', 'frequency', 'density', 'probability', 'percent']
for idx, stat_type in enumerate(stats):
ax = axes[idx // 3, idx % 3]
sns.histplot(data=data, stat=stat_type, kde=True, ax=ax)
ax.set_title(f'stat="{stat_type}"')
# 删除额外的子图
fig.delaxes(axes[1, 2])
plt.tight_layout()
plt.show()比较不同样本大小的分布时使用 density,因为它将直方图归一化使总面积等于 1。
使用 hue 比较多个分布
hue 参数允许您在单个图中比较不同类别的分布。
# 创建多类别数据
df = pd.DataFrame({
'values': np.concatenate([
np.random.normal(90, 10, 500),
np.random.normal(105, 12, 500),
np.random.normal(100, 8, 500)
]),
'group': ['A'] * 500 + ['B'] * 500 + ['C'] * 500
})
# 使用 hue 绘图
sns.histplot(data=df, x='values', hue='group', kde=True)
plt.title('按组划分的多个分布')
plt.show()使用 multiple 控制叠加行为
multiple 参数确定如何显示多个分布:
| multiple 值 | 描述 | 何时使用 |
|---|---|---|
layer | 使用透明度叠加(默认) | 比较形状和重叠 |
dodge | 并排放置条 | 强调每个箱的差异 |
stack | 垂直堆叠条 | 显示总数和比例 |
fill | 归一化高度堆叠 | 关注比例 |
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
multiple_types = ['layer', 'dodge', 'stack', 'fill']
for idx, mult_type in enumerate(multiple_types):
ax = axes[idx // 2, idx % 2]
sns.histplot(data=df, x='values', hue='group',
multiple=mult_type, ax=ax)
ax.set_title(f'multiple="{mult_type}"')
plt.tight_layout()
plt.show()使用 sns.displot() 创建图形级图
虽然 histplot() 是轴级函数,但 displot() 是图形级函数,提供了自动分面等附加功能。
# 基本 displot(创建整个图形)
sns.displot(data=df, x='values', hue='group', kde=True)
plt.show()displot() 的优势
- 使用
col和row参数的自动分面 - 子图之间的一致大小
- 默认图外图例
- 在直方图、KDE 和 ECDF 之间轻松切换
# 添加分面维度
df['dataset'] = np.random.choice(['Train', 'Test'], len(df))
# 创建分面图
sns.displot(data=df, x='values', hue='group',
col='dataset', kde=True, height=4, aspect=1.2)
plt.show()使用 kind 切换图类型
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 直方图
sns.displot(data=df, x='values', kind='hist', kde=True)
plt.title('kind="hist"')
# KDE
sns.displot(data=df, x='values', kind='kde')
plt.title('kind="kde"')
# ECDF(经验累积分布函数)
sns.displot(data=df, x='values', kind='ecdf')
plt.title('kind="ecdf"')
plt.tight_layout()
plt.show()函数比较表
| 功能 | histplot() | displot() | distplot() (已弃用) | matplotlib hist() |
|---|---|---|---|---|
| 级别 | 轴级 | 图形级 | 轴级 | 轴级 |
| 分面 | 否 | 是(col/row) | 否 | 否 |
| KDE 支持 | 是 | 是 | 是 | 否(手动) |
| Hue 支持 | 是 | 是 | 有限 | 否 |
| 多重分布 | layer/dodge/stack/fill | layer/dodge/stack/fill | 否 | 否 |
| Stat 选项 | 5 个选项 | 5 个选项 | 有限 | 有限 |
| 默认样式 | 现代 | 现代 | 现代 | 基本 |
| 状态 | 当前 | 当前 | 已弃用 | 标准 |
| 最适合 | 子图、集成 | 独立图、分面 | 遗留代码 | 基本图 |
建议:使用 plt.subplots() 创建多个子图时使用 histplot()。需要独立可视化或分面时使用 displot()。
histplot() 参数参考
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
data | DataFrame | None | 输入数据结构 |
x, y | 向量/字符串 | None | x 和 y 轴的变量 |
hue | 向量/字符串 | None | 颜色分组变量 |
weights | 向量/字符串 | None | 观测权重 |
stat | 字符串 | "count" | 要计算的统计量(count/frequency/density/probability/percent) |
bins | int/向量 | "auto" | 箱数或箱边缘 |
binwidth | float | None | 箱宽度 |
binrange | 元组 | None | 箱范围(min, max) |
discrete | bool | None | 将变量视为离散 |
cumulative | bool | False | 计算累积分布 |
common_bins | bool | True | 对所有 hue 级别使用相同的箱 |
common_norm | bool | True | 对所有 hue 级别使用相同的归一化 |
multiple | 字符串 | "layer" | 如何绘制多个分布(layer/dodge/stack/fill) |
element | 字符串 | "bars" | 视觉表示(bars/step/poly) |
fill | bool | True | 填充条/多边形 |
shrink | float | 1 | 缩放条宽度 |
kde | bool | False | 添加 KDE 曲线 |
kde_kws | dict | None | KDE 的附加参数 |
line_kws | dict | None | KDE 线的参数 |
thresh | float | 0 | 删除箱的阈值 |
pthresh | float | None | 作为比例的阈值 |
pmax | float | None | 要显示的最大比例 |
cbar | bool | False | 添加颜色条(用于双变量) |
cbar_ax | Axes | None | 颜色条的轴 |
cbar_kws | dict | None | 颜色条参数 |
palette | 字符串/列表 | None | 调色板 |
hue_order | 列表 | None | hue 级别的顺序 |
hue_norm | 元组 | None | hue 的归一化 |
color | 颜色 | None | 所有元素的单一颜色 |
log_scale | bool/元组 | False | 对轴使用对数刻度 |
legend | bool | True | 显示图例 |
ax | Axes | None | 要绘制的 matplotlib 轴 |
高级自定义
自定义颜色和样式
# 每个类别的自定义颜色
custom_palette = {'A': '#FF6B6B', 'B': '#4ECDC4', 'C': '#45B7D1'}
sns.histplot(data=df, x='values', hue='group',
palette=custom_palette, alpha=0.6,
edgecolor='black', linewidth=1.5)
plt.title('自定义颜色直方图')
plt.show()边缘颜色和透明度
# 强调箱边缘
sns.histplot(data=data, bins=20, edgecolor='black',
linewidth=2, alpha=0.7, color='skyblue')
plt.title('具有突出边缘的直方图')
plt.show()对数刻度
对于跨越多个数量级的数据,使用对数刻度:
# 生成对数正态数据
log_data = np.random.lognormal(3, 1, 1000)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 线性刻度
sns.histplot(data=log_data, ax=axes[0])
axes[0].set_title('线性刻度')
# 对数刻度
sns.histplot(data=log_data, log_scale=True, ax=axes[1])
axes[1].set_title('对数刻度')
plt.tight_layout()
plt.show()累积分布
累积直方图显示到每个箱的观测累计总数:
# 累积直方图
sns.histplot(data=data, cumulative=True, stat='density',
element='step', fill=False)
plt.title('累积分布')
plt.ylabel('累积密度')
plt.show()这对于确定百分位数和比较分布很有用。
更改视觉元素
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
elements = ['bars', 'step', 'poly']
for ax, elem in zip(axes, elements):
sns.histplot(data=data, element=elem, kde=True, ax=ax)
ax.set_title(f'element="{elem}"')
plt.tight_layout()
plt.show()双变量直方图(2D 直方图)
Seaborn 可以创建 2D 直方图来可视化两个变量的联合分布:
# 生成相关的 2D 数据
np.random.seed(42)
x = np.random.normal(100, 15, 1000)
y = x + np.random.normal(0, 10, 1000)
# 2D 直方图
sns.histplot(x=x, y=y, bins=30, cbar=True)
plt.title('2D 直方图(双变量分布)')
plt.show()结合 KDE 进行双变量分析
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 2D 直方图
sns.histplot(x=x, y=y, bins=30, cbar=True, ax=axes[0])
axes[0].set_title('2D 直方图')
# KDE 等高线图
sns.kdeplot(x=x, y=y, fill=True, cmap='viridis', ax=axes[1])
axes[1].set_title('2D KDE 图')
plt.tight_layout()
plt.show()使用 Hue 的双变量
# 添加类别
categories = np.random.choice(['组 1', '组 2'], 1000)
sns.histplot(x=x, y=y, hue=categories, bins=20)
plt.title('带 Hue 的 2D 直方图')
plt.show()常见错误及避免方法
错误 1:使用太多或太少的箱
问题:过度平滑隐藏模式,而太多箱产生噪声。
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 箱太少
sns.histplot(data=data, bins=5, ax=axes[0])
axes[0].set_title('箱太少(5) - 过度平滑')
# 适当数量
sns.histplot(data=data, bins=30, ax=axes[1])
axes[1].set_title('适当的箱(30)')
# 箱太多
sns.histplot(data=data, bins=100, ax=axes[2])
axes[2].set_title('箱太多(100) - 噪声多')
plt.tight_layout()
plt.show()解决方案:从自动箱选择开始,然后根据数据特征进行调整。对于正态分布使用 Sturges 规则(bins = log2(n) + 1),对于偏斜数据使用 Freedman-Diaconis 规则。
错误 2:比较不同样本大小的分布
问题:原始计数使得比较具有不同总观测数的分布变得困难。
# 不同的样本大小
small_sample = np.random.normal(100, 15, 200)
large_sample = np.random.normal(100, 15, 2000)
df_samples = pd.DataFrame({
'value': np.concatenate([small_sample, large_sample]),
'sample': ['小(n=200)'] * 200 + ['大(n=2000)'] * 2000
})
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 错误:使用 count
sns.histplot(data=df_samples, x='value', hue='sample',
stat='count', ax=axes[0])
axes[0].set_title('错误:Count(难以比较)')
# 正确:使用 density
sns.histplot(data=df_samples, x='value', hue='sample',
stat='density', common_norm=False, ax=axes[1])
axes[1].set_title('正确:Density(易于比较)')
plt.tight_layout()
plt.show()解决方案:使用 stat='density' 或 stat='probability' 并设置 common_norm=False。
错误 3:忽略重叠分布
问题:高不透明度的默认层模式使重叠不可见。
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 不好:不透明条隐藏重叠
sns.histplot(data=df, x='values', hue='group',
alpha=1.0, ax=axes[0])
axes[0].set_title('不好:不透明条')
# 好:透明度显示重叠
sns.histplot(data=df, x='values', hue='group',
alpha=0.5, ax=axes[1])
axes[1].set_title('好:透明条')
plt.tight_layout()
plt.show()解决方案:使用 alpha=0.5 实现透明度,或使用 multiple='dodge' 并排放置条。
错误 4:未正确标记轴
问题:不清楚 y 轴代表什么,特别是在使用不同 stat 值时。
# 良好实践:清晰的标签
sns.histplot(data=data, stat='density', kde=True)
plt.title('值的分布')
plt.xlabel('值')
plt.ylabel('密度')
plt.show()解决方案:始终清晰地标记轴,特别是在使用 stat='density' 或 stat='probability' 时标记 y 轴。
错误 5:使用已弃用的 distplot()
问题:旧代码使用 distplot(),它已被弃用且灵活性较差。
# 旧方法(已弃用)
# sns.distplot(data) # 不要使用这个
# 新方法
sns.histplot(data=data, kde=True)
plt.show()解决方案:迁移到直方图的 histplot() 或 KDE 曲线的 kdeplot()。
使用 PyGWalker 交互式可视化数据
虽然 seaborn 提供了出色的静态直方图可视化,但 PyGWalker 提供了一个交互式替代方案,让您可以动态探索分布。PyGWalker 将您的 pandas DataFrame 转换为类似 Tableau 的交互式界面,您可以在其中创建直方图、调整分箱并在可视化类型之间切换,而无需编写代码。
import pygwalker as pyg
import pandas as pd
import numpy as np
# 创建样本数据
df = pd.DataFrame({
'values': np.random.normal(100, 15, 1000),
'category': np.random.choice(['A', 'B', 'C'], 1000),
'score': np.random.uniform(0, 100, 1000)
})
# 启动交互式浏览器
pyg.walk(df)PyGWalker 用于直方图分析的优势:
- 拖放界面:通过拖动变量到搁架来创建直方图
- 动态分箱:使用滑块交互式调整箱计数
- 多变量探索:快速切换变量以比较分布
- 导出功能:将洞察保存为图像或共享交互式报告
- 无需编码:非技术团队成员可以独立探索数据
访问 github.com/Kanaries/pygwalker (opens in a new tab) 开始交互式数据可视化。
真实世界示例:分析考试成绩
让我们应用直方图技术来分析不同班级的考试成绩分布:
# 生成真实的考试数据
np.random.seed(42)
n_students = 500
exam_data = pd.DataFrame({
'score': np.concatenate([
np.random.normal(75, 10, 200), # A 班
np.random.normal(68, 15, 150), # B 班
np.random.normal(82, 8, 150) # C 班
]),
'class': ['A 班'] * 200 + ['B 班'] * 150 + ['C 班'] * 150,
'study_hours': np.random.uniform(0, 40, n_students)
})
# 将分数限制在有效范围内
exam_data['score'] = exam_data['score'].clip(0, 100)
# 创建综合可视化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 带 KDE 的整体分布
sns.histplot(data=exam_data, x='score', kde=True,
bins=30, ax=axes[0, 0])
axes[0, 0].set_title('整体成绩分布')
axes[0, 0].axvline(exam_data['score'].mean(), color='red',
linestyle='--', label=f'平均值: {exam_data["score"].mean():.1f}')
axes[0, 0].legend()
# 比较班级
sns.histplot(data=exam_data, x='score', hue='class',
stat='density', common_norm=False,
alpha=0.5, bins=25, ax=axes[0, 1])
axes[0, 1].set_title('按班级划分的成绩分布')
# 用于比例的堆叠视图
sns.histplot(data=exam_data, x='score', hue='class',
multiple='stack', bins=25, ax=axes[1, 0])
axes[1, 0].set_title('堆叠分布(总计数)')
# 累积分布
sns.histplot(data=exam_data, x='score', hue='class',
stat='density', element='step', fill=False,
cumulative=True, common_norm=False, ax=axes[1, 1])
axes[1, 1].set_title('按班级划分的累积分布')
plt.tight_layout()
plt.show()
# 打印摘要统计
print(exam_data.groupby('class')['score'].describe())此示例演示:
- 带平均参考线的整体分布分析
- 针对不同班级大小归一化的密度比较
- 显示每个班级贡献的堆叠可视化
- 用于百分位数分析的累积分布
常见问题
如何为直方图选择正确的箱数?
最佳箱数取决于您的数据大小和分布。Seaborn 的自动箱选择(基于 Freedman-Diaconis 规则)在大多数情况下效果很好。对于手动选择,对于正态分布使用 Sturges 规则:bins = log2(n) + 1(1000 个样本通常为 10-20 个箱),或对于一般数据使用平方根规则:bins = sqrt(n)。尝试不同的值并选择一个在不产生过多噪声的情况下揭示模式的值。当您需要在多个图中使用一致的箱大小进行比较时,使用 binwidth 而不是 bins。
seaborn 中 histplot 和 displot 的区别是什么?
histplot() 是一个轴级函数,在特定的 matplotlib 轴对象上绘图,适合使用 plt.subplots() 创建复杂的多图图形。displot() 是一个图形级函数,创建整个图形并支持使用 col 和 row 参数的自动分面。在将直方图集成到具有多个子图的现有 matplotlib 图形时使用 histplot()。需要独立可视化或需要跨多个分类变量创建分面图时使用 displot()。两个函数都支持用于控制箱、KDE、hue 和统计的相同核心参数。
我应该对直方图使用 stat='density' 还是 stat='probability'?
当您需要直方图面积等于 1 时使用 stat='density',使其与概率密度函数可比较,并且非常适合叠加理论分布。当您想将 y 轴解释为每个箱中数据的比例时使用 stat='probability'(或 stat='percent'),所有箱高度总和为 1(或 100%)。在比较不同样本大小的分布或进行统计分析时选择 stat='density'。在演示文稿中需要更直观的解释或向非技术受众解释结果时选择 stat='probability'。
如何创建具有多个重叠分布的直方图?
使用 hue 参数指定一个将数据分成组的分类变量。使用 multiple 参数控制叠加行为:使用 'layer'(默认)表示透明重叠条,使用 'dodge' 表示并排条,使用 'stack' 表示显示总数的垂直堆叠条,或使用 'fill' 表示显示比例的归一化堆叠条。设置 alpha=0.5 使重叠区域可见。比较不同样本大小的分布时,始终使用 stat='density' 或 stat='probability' 并结合 common_norm=False 以确保公平比较。使用 kde=True 添加 KDE 曲线以突出每个分布的整体形状。
为什么 seaborn 的 distplot 被弃用,我应该使用什么替代?
Seaborn 在 0.11.0 版本中弃用了 distplot(),因为它将多个功能组合在一个函数中,参数命名不一致。替代函数提供了更清晰的接口和更多的灵活性:对于直方图使用 histplot(),对于核密度估计使用 kdeplot(),对于经验累积分布函数使用 ecdfplot(),对于地毯图使用 rugplot()。这些新函数提供了更好的参数名称、更多的自定义选项、对基于 hue 的分组的原生支持以及与其他 seaborn 函数的一致行为。要迁移旧代码,对于最常见的用例,将 sns.distplot(data) 替换为 sns.histplot(data, kde=True)。
结论
Seaborn 直方图提供了一种强大而灵活的方式,用最少的代码可视化数据分布。histplot() 函数提供了对分箱、统计和分组的细粒度控制,而 displot() 简化了分面可视化。通过掌握 bins、stat、hue 和 multiple 等参数,您可以创建出版质量的分布图,揭示模式、异常值和组间差异。
关键要点:
- 使用自动箱选择进行初步探索,然后调整以提高清晰度
- 比较不同样本大小的分布时应用
stat='density' - 利用
hue和multiple参数有效比较多个分布 - 添加 KDE 叠加以显示平滑的分布形状
- 选择
histplot()与 matplotlib 子图集成,选择displot()用于独立分面图
无论您是分析考试成绩、科学测量还是业务指标,seaborn 直方图都可以帮助您理解数据分布并有效传达见解。将这些技术与 PyGWalker 等交互式工具结合起来,进行全面的探索性数据分析。