5 minutes
数据可视化进阶
基础篇里我们学会了用 Matplotlib 画简单的折线图和柱状图。但到了进阶阶段,你需要更多——更丰富的图表类型、更美观的视觉风格、更直观的交互效果。
本文会深入讲解 Seaborn 的高级用法,再带你进入 Plotly 的交互式可视化世界,最后总结一套可视化设计的实用原则。
环境准备
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# Seaborn 主题
sns.set_theme(style='whitegrid', palette='muted', font='SimHei')
我们主要使用 Seaborn 内置数据集和 Plotly Express 示例数据来演示。
Seaborn 深度探索
基础篇我们用了 sns.histplot 和 sns.boxplot。Seaborn 的真正威力在于它的关系型绘图函数族。
relplot——关系图的统一入口
relplot 是一个 figure-level 函数,同时覆盖散点图和线图,并且天然支持分面:
# 加载数据
tips = sns.load_dataset('tips')
# scatter 模式
sns.relplot(data=tips, x='total_bill', y='tip', hue='time',
style='smoker', size='size', sizes=(20, 200),
col='day', row='sex', height=3)
plt.show()
一行代码画出 性别×星期 的分面散点图。relplot 的参数体系很统一:hue 控制颜色,style 控制样式,size 控制大小,col 和 row 控制分面。
displot——分布图的统一入口
# 加载数据
penguins = sns.load_dataset('penguins')
# 多种分布图
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
# 直方图
sns.histplot(data=penguins, x='flipper_length_mm', ax=axes[0, 0])
axes[0, 0].set_title('Histogram')
# KDE 图
sns.kdeplot(data=penguins, x='flipper_length_mm', fill=True, ax=axes[0, 1])
axes[0, 1].set_title('KDE')
# ECDF 图
sns.ecdfplot(data=penguins, x='flipper_length_mm', ax=axes[0, 2])
axes[0, 2].set_title('ECDF')
# 分面直方图
sns.histplot(data=penguins, x='flipper_length_mm', hue='species',
multiple='stack', ax=axes[1, 0])
axes[1, 0].set_title('Stacked by Species')
# 阶梯密度
sns.kdeplot(data=penguins, x='flipper_length_mm', hue='species',
multiple='fill', ax=axes[1, 1])
axes[1, 1].set_title('Fill KDE')
# 二维 KDE
sns.kdeplot(data=penguins, x='bill_length_mm', y='bill_depth_mm',
hue='species', thresh=0.1, ax=axes[1, 2])
axes[1, 2].set_title('2D KDE')
plt.tight_layout()
plt.show()
catplot——分类图的统一入口
# 箱线图
sns.catplot(data=tips, x='day', y='total_bill', kind='box', height=4)
plt.show()
# 小提琴图
sns.catplot(data=tips, x='day', y='total_bill', kind='violin',
hue='sex', split=True, height=4)
plt.show()
# 蜂群图
sns.catplot(data=tips, x='day', y='total_bill', kind='swarm',
hue='sex', height=4)
plt.show()
jointplot——联合分布
# 散点 + 直方图
sns.jointplot(data=penguins, x='bill_length_mm', y='bill_depth_mm',
hue='species', kind='scatter')
plt.show()
# 密度图
sns.jointplot(data=penguins, x='bill_length_mm', y='bill_depth_mm',
kind='kde', fill=True)
plt.show()
# 六边形分箱(适合大数据量)
sns.jointplot(data=penguins, x='bill_length_mm', y='bill_depth_mm',
kind='hex')
plt.show()
pairplot——多维配对
# 基础配对
sns.pairplot(penguins, hue='species', diag_kind='kde', height=2.5)
plt.show()
# 定制化配对
g = sns.PairGrid(penguins, vars=['bill_length_mm', 'bill_depth_mm',
'flipper_length_mm', 'body_mass_g'],
hue='species', height=2.5)
g.map_diag(sns.histplot)
g.map_upper(sns.scatterplot)
g.map_lower(sns.kdeplot)
g.add_legend()
plt.show()
heatmap——热力图进阶
# 相关矩阵热力图
df_num = penguins.select_dtypes(include=[np.number]).dropna()
corr = df_num.corr()
mask = np.triu(np.ones_like(corr, dtype=bool)) # 只显示下三角
sns.heatmap(corr, mask=mask, annot=True, fmt='.2f',
cmap='RdBu_r', center=0, vmin=-1, vmax=1,
linewidths=0.5, square=True,
cbar_kws={'shrink': 0.8, 'label': 'Pearson r'})
plt.title('企鹅数据集相关矩阵', fontsize=14)
plt.show()
定制 Seaborn 主题
默认主题已经不错,但你可以做到更好:
# 查看可用主题
print(sns.axes_style()) # 当前风格参数
# 五种内置风格
styles = ['whitegrid', 'darkgrid', 'white', 'dark', 'ticks']
fig, axes = plt.subplots(1, 5, figsize=(20, 3))
for ax, style in zip(axes, styles):
with sns.axes_style(style):
ax = sns.histplot(penguins['flipper_length_mm'], ax=ax)
ax.set_title(style)
plt.show()
# 自定义调色板
palettes = ['deep', 'muted', 'bright', 'pastel', 'dark', 'colorblind']
fig, axes = plt.subplots(2, 3, figsize=(15, 6))
for ax, palette in zip(axes.flat, palettes):
sns.set_palette(palette)
species = penguins['species'].unique()
for i, s in enumerate(species):
subset = penguins[penguins['species'] == s]
ax.scatter(subset['bill_length_mm'], subset['bill_depth_mm'],
label=s, alpha=0.7)
ax.set_title(f'Palette: {palette}')
ax.legend()
plt.tight_layout()
plt.show()
# 精细调参
sns.set_theme(
style='whitegrid',
palette='Set2',
font='SimHei',
font_scale=1.1,
rc={
'axes.edgecolor': '#cccccc',
'axes.linewidth': 0.8,
'grid.alpha': 0.3,
'figure.figsize': (10, 6),
'axes.titlesize': 14,
'axes.labelsize': 12,
}
)
Plotly 交互式可视化
Seaborn 输出的静态图适合打印和发表,但交互式图在数据探索时更有优势——你可以缩放、悬停查看数值、切换维度。
Plotly Express——简洁的交互式 API
# 散点图
df = px.data.iris()
fig = px.scatter(df, x='sepal_width', y='sepal_length',
color='species', size='petal_length',
hover_data=['petal_width'],
title='Iris 数据集交互散点图')
fig.show()
# 折线图
df_line = px.data.gapminder()
fig = px.line(df_line[df_line['country'] == 'China'],
x='year', y='gdpPercap',
title='中国 GDP 变化')
fig.show()
# 条形图
df_bar = px.data.tips()
fig = px.bar(df_bar, x='day', y='total_bill',
color='sex', barmode='group',
title='每日消费对比')
fig.show()
交互式热力图
corr = df.corr(numeric_only=True)
fig = px.imshow(corr, text_auto=True, color_continuous_scale='RdBu_r',
zmin=-1, zmax=1,
title='交互式相关矩阵')
fig.show()
3D 散点图
fig = px.scatter_3d(df, x='sepal_length', y='sepal_width', z='petal_length',
color='species', size='petal_width',
title='3D 散点图')
fig.show()
交互式地图
gapminder = px.data.gapminder()
fig = px.scatter_geo(gapminder[gapminder['year'] == 2007],
locations='iso_alpha',
size='pop', color='gdpPercap',
hover_name='country',
projection='natural earth',
title='2007 年全球人口与 GDP')
fig.show()
多子图布局
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('散点图', '直方图', '箱线图', '折线图'),
specs=[[{'type': 'scatter'}, {'type': 'histogram'}],
[{'type': 'box'}, {'type': 'scatter'}]]
)
# 散点图
fig.add_trace(go.Scatter(x=tips['total_bill'], y=tips['tip'],
mode='markers', name='消费 vs 小费'),
row=1, col=1)
# 直方图
fig.add_trace(go.Histogram(x=tips['total_bill'], name='消费分布'),
row=1, col=2)
# 箱线图
fig.add_trace(go.Box(y=tips['tip'], name='小费箱线图'),
row=2, col=1)
# 折线图
import plotly.graph_objects as go
import plotly.data as data
gapminder = data.gapminder()
china = gapminder[gapminder['country'] == 'China']
fig.add_trace(go.Scatter(x=china['year'], y=china['gdpPercap'],
mode='lines+markers', name='中国 GDP'),
row=2, col=2)
fig.update_layout(height=600, width=900, title_text='多图组合布局')
fig.show()
制作交互式仪表盘
Plotly 配合 Dash 可以搭建完整的数据仪表盘。这里给出一个简单的单页面示例:
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
# 数据
df = px.data.iris()
# 创建 Dash 应用
app = dash.Dash(__name__)
app.layout = html.Div([
html.H1('Iris 数据集探索仪表盘', style={'textAlign': 'center'}),
html.Div([
html.Label('选择 X 轴:'),
dcc.Dropdown(
id='x-axis',
options=[{'label': c, 'value': c} for c in df.columns[:4]],
value='sepal_length'
),
], style={'width': '30%', 'display': 'inline-block'}),
html.Div([
html.Label('选择 Y 轴:'),
dcc.Dropdown(
id='y-axis',
options=[{'label': c, 'value': c} for c in df.columns[:4]],
value='sepal_width'
),
], style={'width': '30%', 'display': 'inline-block'}),
dcc.Graph(id='scatter-plot'),
])
@app.callback(
Output('scatter-plot', 'figure'),
[Input('x-axis', 'value'), Input('y-axis', 'value')]
)
def update_plot(x_col, y_col):
fig = px.scatter(df, x=x_col, y=y_col, color='species',
title=f'{x_col} vs {y_col}')
return fig
if __name__ == '__main__':
app.run_server(debug=True)
可视化最佳实践
工具的熟练度到了一定程度后,真正区分水平的是设计感。
选择合适的图表
| 分析目标 | 推荐图表类型 |
|---|---|
| 数值分布 | 直方图、KDE、箱线图、小提琴图 |
| 类别对比 | 柱状图、分组箱线图、点图 |
| 趋势变化 | 折线图、面积图 |
| 变量关系 | 散点图、配对图、热力图 |
| 占比组成 | 饼图(类别少时)、堆叠柱状图 |
| 地理分布 | 地图(散点地图、choropleth) |
| 流程层级 | 桑基图、树状图 |
减少 chart junk
Edward Tufte 提出的"图表垃圾"概念至今适用:
# 坏例子——装饰过度
fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(['A', 'B', 'C'], [10, 20, 15])
ax.set_title('销售额(不要这种阴影和浮夸背景)')
ax.set_facecolor('#f0f0f0')
ax.grid(True, alpha=0.8, linestyle='-', linewidth=2)
plt.show()
# 好例子——简洁有效
fig, ax = plt.subplots(figsize=(6, 4))
bars = ax.bar(['A', 'B', 'C'], [10, 20, 15], color='#4ECDC4', width=0.5)
ax.set_title('销售额(简洁)', fontsize=14, fontweight='bold')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:.0f}万'))
plt.show()
提高数据-墨水比
数据-墨水比 = 图表中用于展示数据的墨水 / 总墨水用量
# 高数据-墨水比的 10 条规则
rules = {
'1': '去掉不必要的网格线',
'2': '避免 3D 效果(除非必须)',
'3': '删除多余的坐标轴和边框',
'4': '减少颜色的种类',
'5': '标记数据点而不是全靠图例对照',
'6': '排序让模式显现(按值排序的柱状图)',
'7': '使用原色而不是花哨的背景渐变',
'8': '图例靠近数据',
'9': '避免双 y 轴(通常能用分面替代)',
'10': '标注重点数据点',
}
pd.DataFrame(list(rules.items()), columns=['#', '规则'])
颜色选择的心理学
颜色不是装饰,它是数据的编码维度:
# 定性数据(分类)——不同类别
qualitative = sns.color_palette('Set2', n_colors=8)
sns.palplot(qualitative)
plt.title('定性调色板——用于分类变量')
plt.show()
# 顺序数据——低到高
sequential = sns.color_palette('Blues', n_colors=9)
sns.palplot(sequential)
plt.title('顺序调色板——用于连续数值')
plt.show()
# 发散数据——负到正
diverging = sns.color_palette('RdBu_r', n_colors=9)
sns.palplot(diverging)
plt.title('发散调色板——用于有零点的数据')
plt.show()
色盲友好也很重要:避免红绿对比,改用蓝橙或 viridis 色系。
实战:从数据到故事
可视化不只是"画图",它是讲故事的工具。来看一个完整的案例:
# 加载航班数据集
flights = sns.load_dataset('flights')
# 年份趋势
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 年度趋势
yearly = flights.groupby('year')['passengers'].sum()
axes[0].plot(yearly.index, yearly.values, marker='o', linewidth=2, color='#2E86AB')
axes[0].set_title('年度乘客总量', fontsize=12)
axes[0].set_xlabel('年份')
axes[0].set_ylabel('乘客数')
# 月度模式
monthly = flights.groupby('month')['passengers'].mean()
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
axes[1].bar(months, monthly.values, color='#A23B72')
axes[1].set_title('月度平均乘客数', fontsize=12)
axes[1].tick_params(axis='x', rotation=45)
# 热力图(年月矩阵)
pivot = flights.pivot_table(index='year', columns='month', values='passengers')
sns.heatmap(pivot, annot=True, fmt='d', cmap='YlOrRd', ax=axes[2])
axes[2].set_title('乘客数热力图(年×月)', fontsize=12)
plt.tight_layout()
plt.show()
# 交互式版本(Plotly)
fig = px.density_heatmap(flights, x='month', y='year', z='passengers',
title='乘客量热力图(交互版)',
color_continuous_scale='Viridis')
fig.show()
这个例子展示了同一个数据集从三个不同角度讲述故事——长期趋势、季节模式、交叉视角。
小结
从 Seaborn 到 Plotly,从静态到交互,从画图到设计——可视化进阶之路的核心是选择:选择正确的图表类型、选择恰当的颜色、选择合适的信息密度。
记住三个原则:
- 先想清楚要传达什么信息,再选择图表类型
- 形式服从功能,美观不能牺牲可读性
- 交互是探索的工具,静态是沟通的工具
下一篇文章我们暂时离开画图,进入统计学的世界——用数学语言量化你的发现。
Summary: Seaborn 高级绘图、Plotly 交互可视化与设计原则。