数据可视化实战¶
用 matplotlib + seaborn 把数据变成洞察——从基础图表到专业报告
🎯 图表选择速查¶
先想清楚你要表达什么,再选图表。
| 你要表达什么? | 推荐图表 | 示例 |
|---|---|---|
| 比较数值大小 | 柱状图 / 条形图 | 各渠道收入对比 |
| 展示变化趋势 | 折线图 | 月度用户增长 |
| 显示分布形态 | 直方图 / 箱线图 | 用户消费金额分布 |
| 展示比例构成 | 饼图 / 堆叠柱状图 | 流量来源占比 |
| 发现变量关系 | 散点图 / 热力图 | 年龄与消费的相关性 |
| 比较多组分布 | 小提琴图 / 箱线图 | 不同城市的收入分布 |
📊 基础图表¶
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
# 全局设置
plt.rcParams.update({
'figure.figsize': (10, 6),
'font.size': 12,
'axes.titlesize': 14,
'axes.labelsize': 12,
})
# 模拟数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=12, freq='ME')
categories = ['搜索', '社交', '直接访问', '邮件', '广告']
1. 折线图 — 展示趋势¶
fig, ax = plt.subplots(figsize=(10, 5))
revenue = [120, 135, 148, 162, 155, 178, 190, 205, 198, 215, 230, 250]
ax.plot(dates, revenue, marker='o', linewidth=2, markersize=8,
color='#2196F3', markerfacecolor='white', markeredgewidth=2)
ax.set_title('月度收入趋势', fontweight='bold', pad=15)
ax.set_xlabel('月份')
ax.set_ylabel('收入 (万元)')
ax.grid(True, alpha=0.3)
ax.fill_between(dates, revenue, alpha=0.1, color='#2196F3')
# 标注最高点
max_idx = np.argmax(revenue)
ax.annotate(f'最高: ¥{revenue[max_idx]}万',
xy=(dates[max_idx], revenue[max_idx]),
xytext=(dates[max_idx], revenue[max_idx] + 15),
arrowprops=dict(arrowstyle='->', color='gray'),
ha='center')
plt.tight_layout()
2. 柱状图 — 比较大小¶
fig, ax = plt.subplots(figsize=(10, 5))
values = [350, 280, 200, 150, 120]
colors = ['#2196F3', '#4CAF50', '#FF9800', '#9C27B0', '#F44336']
bars = ax.bar(categories, values, color=colors, edgecolor='white', linewidth=0.5)
# 数值标签
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
f'¥{val}万', ha='center', fontweight='bold')
ax.set_title('各渠道收入对比', fontweight='bold', pad=15)
ax.set_ylabel('收入 (万元)')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_ylim(0, max(values) * 1.15)
plt.tight_layout()
3. 直方图 — 看分布¶
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 生成偏态数据(消费金额)
spending = np.random.lognormal(mean=4.5, sigma=0.8, size=10000)
# 左:直方图
axes[0].hist(spending, bins=50, color='#2196F3', alpha=0.7, edgecolor='white')
axes[0].axvline(np.median(spending), color='red', linestyle='--',
linewidth=2, label=f'中位数: ¥{np.median(spending):.0f}')
axes[0].axvline(np.mean(spending), color='orange', linestyle='--',
linewidth=2, label=f'均值: ¥{np.mean(spending):.0f}')
axes[0].set_title('消费金额分布')
axes[0].legend()
# 右:箱线图
axes[1].boxplot(spending, vert=True, patch_artist=True,
boxprops=dict(facecolor='#2196F3', alpha=0.5))
axes[1].set_title('消费金额箱线图')
axes[1].set_ylabel('金额 (元)')
plt.tight_layout()
4. 散点图 — 看关系¶
fig, ax = plt.subplots(figsize=(8, 6))
# 模拟:年龄 vs 消费
age = np.random.normal(35, 12, 500)
spend = 500 + 20 * age + np.random.normal(0, 200, 500)
scatter = ax.scatter(age, spend, c=spend, cmap='Blues',
alpha=0.6, edgecolors='white', linewidth=0.3)
# 添加趋势线
z = np.polyfit(age, spend, 1)
p = np.poly1d(z)
x_line = np.linspace(age.min(), age.max(), 100)
ax.plot(x_line, p(x_line), color='red', linewidth=2, linestyle='--',
label=f'趋势线 (斜率={z[0]:.1f})')
ax.set_xlabel('年龄')
ax.set_ylabel('月消费 (元)')
ax.set_title('年龄与消费的关系', fontweight='bold')
ax.legend()
plt.colorbar(scatter, label='消费金额')
plt.tight_layout()
5. 热力图 — 相关性矩阵¶
fig, ax = plt.subplots(figsize=(10, 8))
# 模拟特征矩阵
np.random.seed(42)
data = pd.DataFrame({
'age': np.random.normal(35, 10, 1000),
'income': np.random.lognormal(4, 0.5, 1000),
'spending': np.random.lognormal(5, 0.8, 1000),
'frequency': np.random.poisson(5, 1000),
'tenure': np.random.exponential(12, 1000),
})
corr = data.corr()
# 热力图
mask = np.triu(np.ones_like(corr, dtype=bool))
sns.heatmap(corr, mask=mask, annot=True, fmt='.2f',
cmap='RdBu_r', center=0, square=True,
linewidths=1, cbar_kws={'shrink': 0.8},
ax=ax)
ax.set_title('特征相关性热力图', fontweight='bold', pad=15)
plt.tight_layout()
🎨 专业报告美化技巧¶
技巧 1:统一的配色方案¶
# 推荐配色
COLORS = {
'primary': '#2196F3',
'success': '#4CAF50',
'warning': '#FF9800',
'danger': '#F44336',
'purple': '#9C27B0',
}
# 色盲友好配色
CBB_PALETTE = ['#0072B2', '#009E73', '#F0E442',
'#D55E00', '#CC79A7', '#56B4E9']
技巧 2:多子图排版¶
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('用户行为分析仪表盘', fontsize=16, fontweight='bold', y=1.02)
# (0,0): 月度趋势
axes[0, 0].plot(dates, revenue, 'o-', color=COLORS['primary'])
axes[0, 0].set_title('月度收入')
# (0,1): 渠道占比
axes[0, 1].pie(values, labels=categories, autopct='%1.1f%%',
colors=[COLORS[k] for k in ['primary', 'success',
'warning', 'purple', 'danger']])
# (1,0): 分布
axes[1, 0].hist(spending, bins=40, color=COLORS['primary'], alpha=0.7)
# (1,1): Top N
top10_idx = np.argsort(values)[::-1]
axes[1, 1].barh(range(len(values)), [values[i] for i in top10_idx],
color=[COLORS['primary']])
axes[1, 1].set_yticks(range(len(values)))
plt.tight_layout()
技巧 3:保存高清图¶
# 保存为 PNG(适合网页/PPT)
plt.savefig('report.png', dpi=150, bbox_inches='tight',
facecolor='white', edgecolor='none')
# 保存为 SVG(适合进一步编辑)
plt.savefig('report.svg', format='svg', bbox_inches='tight')
📈 进阶:Seaborn 一行出图¶
Seaborn 是 matplotlib 的高级封装,适合快速探索而非精细定制。
# 分类对比
sns.boxplot(data=df, x='category', y='spending')
# 分布 + 分类
sns.violinplot(data=df, x='gender', y='spending', split=True)
# 多变量关系
sns.pairplot(df[['age', 'income', 'spending', 'frequency']],
diag_kind='kde', plot_kws={'alpha': 0.5})
# 分类散点
sns.stripplot(data=df, x='day_of_week', y='spending',
jitter=True, alpha=0.3)
# 回归 + 分布
sns.jointplot(data=df, x='age', y='spending', kind='reg')
⚠️ 常见错误¶
| 错误 | 为什么不好 | 正确做法 |
|---|---|---|
| 3D 饼图 | 视觉扭曲,比例难读 | 用条形图替代 |
| 双 Y 轴 | 容易误导(调整刻度=改变结论) | 两个并排图 |
| 颜色太多 | 分散注意力 | 2-3 种颜色足够 |
| 不标注坐标轴 | 信息不完整 | 始终标注 + 单位 |
| 堆叠面积图太多层 | 无法比较中间层的趋势 | 最多 3-4 层 |