7.1 多图组合与布局 Seaborn 高级应用领域:7.1 多图组合与布局详解 在数据可视化中,单张图表有时难以全面展现数据的复杂性和多维度信息。为了更有效地传达数据故事,我们经常需要将多个图表组合在一起,形成一个统一的视图。Seaborn 作为强大的 Python 数据可视化库,不仅提供了丰富的图表类型,还提供了灵活多样的多图组合与布局方案。本节将深入探讨 Seaborn 在多图组合与布局方面的应用,帮助你创建更具洞察力和表现力的数据可视化作品。 7.1.1 为什么要进行多图组合与布局? 单图的局限性在于其表达能力的单一性。当我们需要: 比较不同维度的数据: 例如,在不同类别下观察同一变量的分布,或者比较多个变量之间的关系。
在数据可视化中,单张图表有时难以全面展现数据的复杂性和多维度信息。为了更有效地传达数据故事,我们经常需要将多个图表组合在一起,形成一个统一的视图。Seaborn 作为强大的 Python 数据可视化库,不仅提供了丰富的图表类型,还提供了灵活多样的多图组合与布局方案。本节将深入探讨 Seaborn 在多图组合与布局方面的应用,帮助你创建更具洞察力和表现力的数据可视化作品。
单图的局限性在于其表达能力的单一性。当我们需要:
比较不同维度的数据: 例如,在不同类别下观察同一变量的分布,或者比较多个变量之间的关系。
展示数据的不同侧面: 例如,同时展示数据的总体分布和局部细节,或者从不同角度分析同一数据集。
构建数据故事: 通过一系列相关的图表,逐步引导读者理解数据的内在逻辑和趋势。
节省空间,提高信息密度: 在有限的空间内呈现更多的数据信息,提高可视化效率。
多图组合与布局能够有效地克服单图的局限性,通过合理的组织和排列,将多个图表整合为一个整体,从而更全面、深入地挖掘和展现数据价值。
Seaborn 提供了多种方法来实现多图组合与布局,主要可以归纳为以下几类:
使用 matplotlib.pyplot.subplots() 手动创建子图网格: 这是最基础和灵活的方法,Seaborn 图表可以绘制在 matplotlib 的子图之上。
利用 Seaborn 的 FacetGrid 对象: FacetGrid 专为条件关系可视化设计,能够基于一个或多个分类变量创建子图网格。
使用 Seaborn 的 PairGrid 对象: PairGrid 专注于变量对关系的可视化,可以绘制变量两两之间的散点图、直方图等,并形成矩阵式的子图网格。
运用 Seaborn 的 JointGrid 对象: JointGrid 用于可视化两个变量的联合分布以及边缘分布,通常包含一个中心联合分布图和两个边缘分布图。
结合 matplotlib.gridspec.GridSpec 实现更复杂的布局: GridSpec 提供了更高级的网格布局控制,允许创建不规则形状的子图网格,实现更自由的布局设计。
自定义布局与手动调整: 对于特殊的需求,可以结合 matplotlib 的底层功能进行更精细的布局控制和手动调整。
下面我们将逐一深入探讨这些方法,并通过代码实践和详细解释,帮助你掌握 Seaborn 多图组合与布局的技巧。
matplotlib.pyplot.subplots() 创建子图网格matplotlib.pyplot.subplots() 是 matplotlib 库中用于创建子图网格的基础函数。Seaborn 图表本质上构建于 matplotlib 之上,因此我们可以利用 subplots() 创建的 Axes 对象,并在其上绘制 Seaborn 图表。
代码实践:
import matplotlib.pyplot as plt import seaborn as sns # 加载示例数据集 iris = sns.load_dataset('iris') # 创建一个 2x2 的子图网格 fig, axes = plt.subplots(2, 2, figsize=(12, 8)) fig.suptitle('Iris Dataset Feature Distributions', fontsize=16) # 设置整个图表的标题 # 在第一个子图上绘制花萼长度的直方图 sns.histplot(iris['sepal_length'], ax=axes[0, 0], color='skyblue') axes[0, 0].set_title('Sepal Length Distribution') # 在第二个子图上绘制花萼宽度的箱线图 sns.boxplot(x='species', y='sepal_width', data=iris, ax=axes[0, 1], palette='pastel') axes[0, 1].set_title('Sepal Width by Species') # 在第三个子图上绘制花瓣长度的散点图,按物种着色 sns.scatterplot(x='petal_length', y='petal_width', hue='species', data=iris, ax=axes[1, 0], palette='viridis') axes[1, 0].set_title('Petal Length vs Petal Width') # 在第四个子图上绘制物种计数的条形图 sns.countplot(x='species', data=iris, ax=axes[1, 1], palette='magma') axes[1, 1].set_title('Species Count') plt.tight_layout(rect=[0, 0, 1, 0.96]) # 调整布局,防止标题重叠 plt.show()
代码详解:
plt.subplots(nrows=2, ncols=2, figsize=(12, 8)): subplots() 函数创建了一个 2 行 2 列的子图网格。figsize 参数设置整个图表的尺寸。函数返回两个对象:
fig: Figure 对象,代表整个图表(画布)。
axes: 一个 NumPy 数组,包含了 4 个 Axes 对象,每个 Axes 对象代表一个子图。对于 nrows=2, ncols=2 的情况,axes 是一个 2x2 的数组,可以通过 axes[row, col] 的方式访问每个子图。
fig.suptitle('Iris Dataset Feature Distributions', fontsize=16): 使用 fig.suptitle() 设置整个图表的总标题。
sns.histplot(iris['sepal_length'], ax=axes[0, 0], color='skyblue'): 在第一个子图 (axes[0, 0]) 上绘制花萼长度的直方图。关键在于 ax=axes[0, 0] 参数,它指定了图表要绘制在哪个 Axes 对象上。后续的 Seaborn 图表函数也都使用了 ax 参数,将图表绘制到指定的子图上。
axes[0, 0].set_title('Sepal Length Distribution'): 使用 axes[0, 0].set_title() 为第一个子图设置子标题。每个 Axes 对象都有自己的标题、轴标签等属性,可以独立设置。
plt.tight_layout(rect=[0, 0, 1, 0.96]): plt.tight_layout() 函数自动调整子图的布局,避免子图之间或子图与图表边缘重叠。 rect 参数用于调整整个布局的边界,这里是为了给总标题预留空间。
graph TD 图示:
内容详解:
使用 subplots() 方法的优点在于其基础性和灵活性。你可以完全掌控子图网格的结构,并在每个子图上绘制任何类型的 matplotlib 或 Seaborn 图表。这对于创建自定义布局和组合不同类型的图表非常有用。
FacetGrid 对象FacetGrid 是 Seaborn 中专门用于创建条件关系图的类。它可以根据一个或多个分类变量,将数据集分割成多个子集,并在每个子集上绘制相同的图表类型。这非常适合比较不同条件下的数据分布或关系。
代码实践:
import seaborn as sns import matplotlib.pyplot as plt # 加载示例数据集 tips = sns.load_dataset('tips') # 创建 FacetGrid 对象,按 'time' 和 'smoker' 分面 g = sns.FacetGrid(tips, col='time', row='smoker', hue='sex', aspect=1.2, height=3) # 在每个子图上绘制散点图,展示 'total_bill' 和 'tip' 的关系 g.map(sns.scatterplot, 'total_bill', 'tip') # 添加图例 g.add_legend() # 设置轴标签和标题 g.set_axis_labels('Total Bill ($)', 'Tip ($)') g.fig.suptitle('Tip Amount vs. Total Bill by Time, Smoker, and Sex', y=1.02) # 设置整个图表的标题,并调整 y 坐标避免重叠 plt.show()
代码详解:
g = sns.FacetGrid(tips, col='time', row='smoker', hue='sex', aspect=1.2, height=3): 创建 FacetGrid 对象。
data=tips: 指定数据集为 tips。
col='time': 按 'time' 列的值(Lunch, Dinner)进行列分面,即按列排列子图。
row='smoker': 按 'smoker' 列的值(Yes, No)进行行分面,即按行排列子图。
hue='sex': 使用 'sex' 列的值(Male, Female)进行颜色编码,在散点图中用不同颜色区分性别。
aspect=1.2: 设置子图的宽高比。
height=3: 设置每个子图的高度,宽度会根据 aspect 自动调整。
g.map(sns.scatterplot, 'total_bill', 'tip'): g.map() 方法将指定的绘图函数 (sns.scatterplot) 应用于每个子图。 'total_bill' 和 'tip' 是 sns.scatterplot 函数的位置参数,表示 x 轴和 y 轴的数据列。
g.add_legend(): 添加图例,用于解释颜色编码的含义(在本例中是性别)。
g.set_axis_labels('Total Bill ($)', 'Tip ($)'): 设置所有子图的 x 轴和 y 轴标签。
g.fig.suptitle('Tip Amount vs. Total Bill by Time, Smoker, and Sex', y=1.02): 设置整个 FacetGrid 图表的总标题。 y=1.02 参数稍微向上调整标题的位置,避免与子图标题重叠。
graph TD 图示:
内容详解:
FacetGrid 的核心优势在于其 分面 (facet) 功能,可以根据分类变量自动创建子图网格,并方便地在每个子图上绘制条件关系图。它简化了条件可视化的流程,使得比较不同组别的数据变得更加直观和高效。FacetGrid 适用于需要根据一个或多个分类变量进行分组比较的场景。
PairGrid 对象PairGrid 专注于变量对关系的可视化。它可以绘制数据集中所有数值变量两两之间的关系,形成一个矩阵式的子图网格。对角线上的子图通常用于展示单个变量的分布(例如直方图或密度图),非对角线上的子图则用于展示两个变量之间的关系(例如散点图或核密度估计图)。
代码实践:
import seaborn as sns import matplotlib.pyplot as plt # 加载示例数据集 iris = sns.load_dataset('iris') # 创建 PairGrid 对象,使用 'species' 进行颜色编码 g = sns.PairGrid(iris, hue='species', palette='husl', diag_sharey=False) # 对角线上绘制直方图 g.map_diag(sns.histplot) # 非对角线上绘制散点图 g.map_offdiag(sns.scatterplot) # 添加图例 g.add_legend() # 设置总标题 g.fig.suptitle('Pairwise Relationships of Iris Features', y=1.02) plt.show()
代码详解:
g = sns.PairGrid(iris, hue='species', palette='husl', diag_sharey=False): 创建 PairGrid 对象。
data=iris: 指定数据集为 iris。
hue='species': 使用 'species' 列进行颜色编码。
palette='husl': 指定颜色调色板。
diag_sharey=False: 设置对角线子图是否共享 y 轴。这里设置为 False,表示对角线上的直方图的 y 轴不共享,各自独立。
g.map_diag(sns.histplot): g.map_diag() 方法将指定的绘图函数 (sns.histplot) 应用于对角线上的子图。
g.map_offdiag(sns.scatterplot): g.map_offdiag() 方法将指定的绘图函数 (sns.scatterplot) 应用于非对角线上的子图。
g.add_legend(): 添加图例。
g.fig.suptitle('Pairwise Relationships of Iris Features', y=1.02): 设置总标题。
graph TD 图示:
内容详解:
PairGrid 特别适合于探索数据集中多个变量之间的关系。它可以快速生成变量两两关系的可视化矩阵,帮助我们发现变量之间的相关性、分布特征以及潜在的模式。PairGrid 常用于初步的数据探索和特征工程阶段,快速了解数据集的整体结构。
JointGrid 对象JointGrid 用于可视化两个变量的联合分布以及边缘分布。它通常由三个子图组成:中心的联合分布图,以及分别位于上方和右侧的两个边缘分布图。边缘分布图展示了两个变量各自的单变量分布,而联合分布图则展示了它们之间的关系。
代码实践:
import seaborn as sns import matplotlib.pyplot as plt # 加载示例数据集 iris = sns.load_dataset('iris') # 创建 JointGrid 对象,指定 x 和 y 变量 g = sns.JointGrid(data=iris, x='sepal_length', y='sepal_width', hue='species', palette='muted') # 绘制散点图作为联合分布图 g.plot_joint(sns.scatterplot) # 绘制直方图作为边缘分布图 g.plot_marginals(sns.histplot, kde=False) # kde=False 关闭核密度估计,只显示直方图 # 设置轴标签和总标题 g.ax_joint.set_xlabel('Sepal Length (cm)') g.ax_joint.set_ylabel('Sepal Width (cm)') g.fig.suptitle('Joint and Marginal Distributions of Sepal Length and Sepal Width', y=1.02) plt.show()
代码详解:
g = sns.JointGrid(data=iris, x='sepal_length', y='sepal_width', hue='species', palette='muted'): 创建 JointGrid 对象。
data=iris: 指定数据集。
x='sepal_length', y='sepal_width': 指定 x 轴和 y 轴的变量。
hue='species', palette='muted': 使用 'species' 进行颜色编码,并指定调色板。
g.plot_joint(sns.scatterplot): g.plot_joint() 方法用于绘制联合分布图。这里使用 sns.scatterplot 绘制散点图。
g.plot_marginals(sns.histplot, kde=False): g.plot_marginals() 方法用于绘制边缘分布图。这里使用 sns.histplot 绘制直方图,并设置 kde=False 关闭核密度估计曲线。
g.ax_joint.set_xlabel('Sepal Length (cm)'), g.ax_joint.set_ylabel('Sepal Width (cm)'): 设置联合分布图的 x 轴和 y 轴标签。 g.ax_joint 属性可以访问到中心的联合分布图的 Axes 对象。
g.fig.suptitle('Joint and Marginal Distributions of Sepal Length and Sepal Width', y=1.02): 设置总标题。
graph TD 图示:
内容详解:
JointGrid 能够清晰地展示两个变量的联合分布以及各自的边缘分布,帮助我们理解两个变量之间的关系类型(例如线性关系、非线性关系、相关性强弱)以及各自的分布形态。JointGrid 适用于需要深入分析两个变量之间关系,并同时关注它们各自分布情况的场景。
matplotlib.gridspec.GridSpec 实现复杂布局matplotlib.gridspec.GridSpec 提供了更高级的网格布局控制能力。它允许我们创建不规则形状的子图网格,例如,某些子图可以跨越多行或多列,从而实现更自由和复杂的布局设计。
代码实践:
import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import seaborn as sns # 加载示例数据集 flights = sns.load_dataset('flights') flights_data = flights.pivot_table(index='month', columns='year', values='passengers') # 创建 Figure 对象 fig = plt.figure(figsize=(14, 10)) # 使用 GridSpec 创建布局,3行 2列 gs = gridspec.GridSpec(3, 2) # 第一个子图:热力图,占据第 1 行和第 1 列 ax1 = fig.add_subplot(gs[0, 0]) sns.heatmap(flights_data, ax=ax1, cmap='YlGnBu', cbar_kws={'shrink': 0.8}) ax1.set_title('Monthly Passenger Heatmap') # 第二个子图:箱线图,占据第 1 行和第 2 列 ax2 = fig.add_subplot(gs[0, 1]) sns.boxplot(x='month', y='passengers', data=flights, ax=ax2, palette='viridis') ax2.set_title('Monthly Passenger Boxplot') # 第三个子图:折线图,跨越第 2 和第 3 行,占据第 1 列 ax3 = fig.add_subplot(gs[1:, 0]) # gs[1:, 0] 表示从第 1 行开始到最后一行,第 0 列 sns.lineplot(x='year', y='passengers', data=flights, hue='month', ax=ax3, palette='tab20') ax3.set_title('Yearly Passenger Trend by Month') ax3.legend(loc='upper left', bbox_to_anchor=(1, 1)) # 将图例放置在子图外部 # 第四个子图:小提琴图,跨越第 2 和第 3 行,占据第 2 列 ax4 = fig.add_subplot(gs[1:, 1]) # gs[1:, 1] 表示从第 1 行开始到最后一行,第 1 列 sns.violinplot(x='month', y='passengers', data=flights, ax=ax4, palette='plasma') ax4.set_title('Monthly Passenger Violinplot') plt.tight_layout(rect=[0, 0, 1, 0.96]) # 调整布局 fig.suptitle('Flight Passenger Analysis', fontsize=16, y=0.99) # 设置总标题 plt.show()
代码详解:
gs = gridspec.GridSpec(3, 2): 创建 GridSpec 对象,指定网格为 3 行 2 列。
ax1 = fig.add_subplot(gs[0, 0]): 使用 fig.add_subplot(gs[index]) 创建子图。 gs[0, 0] 表示第一个子图占据网格的第 1 行第 1 列。
ax3 = fig.add_subplot(gs[1:, 0]): gs[1:, 0] 使用切片表示法,表示第三个子图跨越网格的第 2 行到最后一行(这里是第 2 和第 3 行),占据第 1 列。
ax4 = fig.add_subplot(gs[1:, 1]): gs[1:, 1] 表示第四个子图跨越网格的第 2 行到最后一行,占据第 2 列。
ax3.legend(loc='upper left', bbox_to_anchor=(1, 1)): 调整折线图的图例位置,使用 bbox_to_anchor 将图例放置在子图的右上角外部。
graph TD 图示:
内容详解:
GridSpec 提供了更强大的布局控制,可以创建不规则的子图网格,例如,子图可以跨越多行或多列,或者占据特定的网格区域。这使得我们可以设计更灵活、更具表现力的多图布局,更好地适应数据的特点和可视化目标。GridSpec 适用于需要创建复杂布局、组合不同类型图表、以及突出显示某些重要信息的情况。
除了上述 Seaborn 提供的便捷方法外,我们还可以结合 matplotlib 的底层功能进行更精细的布局控制和手动调整。例如,可以使用 fig.add_axes([left, bottom, width, height]) 手动指定子图的位置和尺寸,其中 left, bottom, width, height 都是相对于 Figure 尺寸的比例值 (0 到 1)。
代码实践 (示例,概念演示):
import matplotlib.pyplot as plt import seaborn as sns # 加载示例数据集 iris = sns.load_dataset('iris') fig = plt.figure(figsize=(12, 6)) # 手动添加第一个子图,占据左侧大部分区域 ax1 = fig.add_axes([0.05, 0.1, 0.6, 0.8]) # [left, bottom, width, height] sns.scatterplot(x='petal_length', y='petal_width', hue='species', data=iris, ax=ax1) ax1.set_title('Petal Length vs Petal Width') # 手动添加第二个子图,位于右侧上方小区域 ax2 = fig.add_axes([0.7, 0.6, 0.25, 0.3]) sns.boxplot(x='species', y='sepal_width', data=iris, ax=ax2, palette='pastel') ax2.set_title('Sepal Width Boxplot') # 手动添加第三个子图,位于右侧下方小区域 ax3 = fig.add_axes([0.7, 0.1, 0.25, 0.3]) sns.histplot(iris['sepal_length'], ax=ax3, color='skyblue') ax3.set_title('Sepal Length Histogram') fig.suptitle('Custom Layout Example', fontsize=16) plt.show()
代码详解:
ax1 = fig.add_axes([0.05, 0.1, 0.6, 0.8]): fig.add_axes() 函数手动创建一个 Axes 对象,并将其添加到 Figure 中。参数 [0.05, 0.1, 0.6, 0.8] 指定了子图的位置和尺寸:
left=0.05: 子图左边界距离 Figure 左边界的比例为 0.05。
bottom=0.1: 子图下边界距离 Figure 下边界的比例为 0.1。
width=0.6: 子图宽度占 Figure 宽度的比例为 0.6。
height=0.8: 子图高度占 Figure 高度的比例为 0.8。
后续的 ax2 和 ax3 也使用 add_axes() 手动指定位置和尺寸,从而实现了自定义的非网格布局。
内容详解:
add_axes() 方法提供了最精细的布局控制,允许我们完全自定义子图的位置和尺寸。这在需要创建非常规布局,或者需要将图表元素与文本、图像等其他元素自由组合时非常有用。然而,手动调整布局通常需要更多的 trial-and-error,不如 Seaborn 的网格对象那样方便快捷。
明确可视化目标: 在进行多图组合之前,首先要明确你的可视化目标。你想通过多图组合传达什么信息?哪些维度需要比较?哪些关系需要突出?清晰的目标是布局设计的指南。
选择合适的布局方法: 根据可视化目标和数据特点,选择最合适的布局方法。对于条件比较,FacetGrid 是首选;对于变量对关系,PairGrid 和 JointGrid 很方便;对于复杂布局,GridSpec 提供了更大的灵活性;对于完全自定义的布局,add_axes() 可以实现精细控制。
保持布局的逻辑性和一致性: 多图布局应该具有清晰的逻辑结构,方便读者理解图表之间的关系。例如,使用 FacetGrid 时,分面变量的选择要合理;使用 GridSpec 时,子图的排列要符合阅读习惯。同时,保持子图之间风格和元素的统一性,例如颜色、字体、轴标签等,可以提高整体视觉效果。
合理利用空间,避免信息过载: 多图组合的目的是提高信息密度,但也要避免信息过载,导致图表过于拥挤和难以解读。合理设置子图的尺寸、间距、以及图表元素的密度,确保图表清晰易读。