图表选择指南、Python 可视化代码模式、设计原则和可访问性考虑,用于创建有效的数据可视化。
| 你要展示的内容 | 最佳图表 | 替代方案 |
|---|---|---|
| 随时间变化的趋势 | 折线图 | 面积图(展示累积或构成时) |
| 类别间比较 | 垂直柱状图 | 水平柱状图(类别多时)、棒棒糖图 |
| 排名 | 水平柱状图 | 点图、斜率图(比较两个时期) |
| 部分与整体的构成 | 堆叠柱状图 | 矩形树图(层级结构)、华夫饼图 |
| 随时间变化的构成 | 堆叠面积图 | 100% 堆叠柱状图(关注比例) |
| 分布 | 直方图 | 箱线图(比较组)、小提琴图、条带图 |
| 相关性(2 个变量) | 散点图 | 气泡图(添加第 3 个变量作为大小) |
| 相关性(多变量) | 热力图(相关矩阵) | 配对图 |
| 地理模式 | 等值线图 | 气泡地图、六边形地图 |
| 流程 | 桑基图 | 漏斗图(顺序阶段) |
| 关系网络 | 网络图 | 弦图 |
| 绩效 vs 目标 | 子弹图 | 仪表盘(仅单个 KPI) |
| 同时展示多个 KPI | 小多组图 | 带独立图表的仪表盘 |
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')))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()sns.color_palette("colorblind")分享可视化之前: