基础篇里我们学会了用 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.histplotsns.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 控制大小,colrow 控制分面。

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,从静态到交互,从画图到设计——可视化进阶之路的核心是选择:选择正确的图表类型、选择恰当的颜色、选择合适的信息密度。

记住三个原则:

  1. 先想清楚要传达什么信息,再选择图表类型
  2. 形式服从功能,美观不能牺牲可读性
  3. 交互是探索的工具,静态是沟通的工具

下一篇文章我们暂时离开画图,进入统计学的世界——用数学语言量化你的发现。

Summary: Seaborn 高级绘图、Plotly 交互可视化与设计原则。