数据可视化技能

图表选择指南、Python 可视化代码模式、设计原则和可访问性考虑,用于创建有效的数据可视化。

图表选择指南

按数据关系选择

你要展示的内容 最佳图表 替代方案
随时间变化的趋势 折线图 面积图(展示累积或构成时)
类别间比较 垂直柱状图 水平柱状图(类别多时)、棒棒糖图
排名 水平柱状图 点图、斜率图(比较两个时期)
部分与整体的构成 堆叠柱状图 矩形树图(层级结构)、华夫饼图
随时间变化的构成 堆叠面积图 100% 堆叠柱状图(关注比例)
分布 直方图 箱线图(比较组)、小提琴图、条带图
相关性(2 个变量) 散点图 气泡图(添加第 3 个变量作为大小)
相关性(多变量) 热力图(相关矩阵) 配对图
地理模式 等值线图 气泡地图、六边形地图
流程 桑基图 漏斗图(顺序阶段)
关系网络 网络图 弦图
绩效 vs 目标 子弹图 仪表盘(仅单个 KPI)
同时展示多个 KPI 小多组图 带独立图表的仪表盘

何时避免使用某些图表

Python 可视化代码模式

设置与样式

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
import pandas as pd
import numpy as np

# 专业样式设置
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams.update({
    'figure.figsize': (10, 6),
    'figure.dpi': 150,
    'font.size': 11,
    'axes.titlesize': 14,
    'axes.titleweight': 'bold',
    'axes.labelsize': 11,
    'xtick.labelsize': 10,
    'ytick.labelsize': 10,
    'legend.fontsize': 10,
    'figure.titlesize': 16,
})

# 色盲友好调色板
PALETTE_CATEGORICAL = ['#4C72B0', '#DD8452', '#55A868', '#C44E52', '#8172B3', '#937860']
PALETTE_SEQUENTIAL = 'YlOrRd'
PALETTE_DIVERGING = 'RdBu_r'

折线图(时间序列)

fig, ax = plt.subplots(figsize=(10, 6))

for label, group in df.groupby('category'):
    ax.plot(group['date'], group['value'], label=label, linewidth=2)

ax.set_title('按类别划分的指标趋势', fontweight='bold')
ax.set_xlabel('日期')
ax.set_ylabel('值')
ax.legend(loc='upper left', frameon=True)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# 格式化 x 轴日期
fig.autofmt_xdate()

plt.tight_layout()
plt.savefig('trend_chart.png', dpi=150, bbox_inches='tight')

柱状图(比较)

fig, ax = plt.subplots(figsize=(10, 6))

# 按值排序便于阅读
df_sorted = df.sort_values('metric', ascending=True)

bars = ax.barh(df_sorted['category'], df_sorted['metric'], color=PALETTE_CATEGORICAL[0])

# 添加数值标签
for bar in bars:
    width = bar.get_width()
    ax.text(width + 0.5, bar.get_y() + bar.get_height()/2,
            f'{width:,.0f}', ha='left', va='center', fontsize=10)

ax.set_title('按类别排名指标', fontweight='bold')
ax.set_xlabel('指标值')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.savefig('bar_chart.png', dpi=150, bbox_inches='tight')

直方图(分布)

fig, ax = plt.subplots(figsize=(10, 6))

ax.hist(df['value'], bins=30, color=PALETTE_CATEGORICAL[0], edgecolor='white', alpha=0.8)

# 添加均值和中间值线
mean_val = df['value'].mean()
median_val = df['value'].median()
ax.axvline(mean_val, color='red', linestyle='--', linewidth=1.5, label=f'均值: {mean_val:,.1f}')
ax.axvline(median_val, color='green', linestyle='--', linewidth=1.5, label=f'中位数: {median_val:,.1f}')

ax.set_title('值分布', fontweight='bold')
ax.set_xlabel('值')
ax.set_ylabel('频率')
ax.legend()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.savefig('histogram.png', dpi=150, bbox_inches='tight')

热力图

fig, ax = plt.subplots(figsize=(10, 8))

# 透视数据为热力图格式
pivot = df.pivot_table(index='row_dim', columns='col_dim', values='metric', aggfunc='sum')

sns.heatmap(pivot, annot=True, fmt=',.0f', cmap='YlOrRd',
            linewidths=0.5, ax=ax, cbar_kws={'label': '指标值'})

ax.set_title('按行维度和列维度的指标', fontweight='bold')
ax.set_xlabel('列维度')
ax.set_ylabel('行维度')

plt.tight_layout()
plt.savefig('heatmap.png', dpi=150, bbox_inches='tight')

小多组图

categories = df['category'].unique()
n_cats = len(categories)
n_cols = min(3, n_cats)
n_rows = (n_cats + n_cols - 1) // n_cols

fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 4*n_rows), sharex=True, sharey=True)
axes = axes.flatten() if n_cats > 1 else [axes]

for i, cat in enumerate(categories):
    ax = axes[i]
    subset = df[df['category'] == cat]
    ax.plot(subset['date'], subset['value'], color=PALETTE_CATEGORICAL[i % len(PALETTE_CATEGORICAL)])
    ax.set_title(cat, fontsize=12)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

# 隐藏空子图
for j in range(i+1, len(axes)):
    axes[j].set_visible(False)

fig.suptitle('按类别划分的趋势', fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('small_multiples.png', dpi=150, bbox_inches='tight')

数字格式化辅助函数

def format_number(val, format_type='number'):
    """格式化图表标签中的数字。"""
    if format_type == 'currency':
        if abs(val) >= 1e9:
            return f'${val/1e9:.1f}B'
        elif abs(val) >= 1e6:
            return f'${val/1e6:.1f}M'
        elif abs(val) >= 1e3:
            return f'${val/1e3:.1f}K'
        else:
            return f'${val:,.0f}'
    elif format_type == 'percent':
        return f'{val:.1f}%'
    elif format_type == 'number':
        if abs(val) >= 1e9:
            return f'{val/1e9:.1f}B'
        elif abs(val) >= 1e6:
            return f'{val/1e6:.1f}M'
        elif abs(val) >= 1e3:
            return f'{val/1e3:.1f}K'
        else:
            return f'{val:,.0f}'
    return str(val)

# 与坐标轴格式化器配合使用
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: format_number(x, 'currency')))

使用 Plotly 的交互式图表

import plotly.express as px
import plotly.graph_objects as go

# 简单的交互式折线图
fig = px.line(df, x='date', y='value', color='category',
              title='交互式指标趋势',
              labels={'value': '指标值', 'date': '日期'})
fig.update_layout(hovermode='x unified')
fig.write_html('interactive_chart.html')
fig.show()

# 带悬停数据的交互式散点图
fig = px.scatter(df, x='metric_a', y='metric_b', color='category',
                 size='size_metric', hover_data=['name', 'detail_field'],
                 title='相关性分析')
fig.show()

设计原则

颜色

排版

布局

准确性

可访问性考虑

色盲

屏幕阅读器

通用可访问性

可访问性检查清单

分享可视化之前: