前言
“一图胜千言”——数据可视化是数据分析的最后一步,也是最关键的一步。无论多么复杂的模型、多么精妙的数据,处理结果最终都需要以直观的方式呈现给用户。
Python 生态拥有丰富的数据可视化库,从基础的 Matplotlib 到高级的 Plotly,从静态图表到交互式仪表盘,总有一款适合你的需求。
本文将系统覆盖:
- Matplotlib:Python 可视化的基石
- Seaborn:统计图表的高级封装
- Plotly:交互式图表的首选
- 实战场景:从数据到图表的完整流程
- 最佳实践:配色、布局、排版
可视化库对比
主流库一览
| 库 | 定位 | 图表类型 | 交互性 | 学习曲线 | 适用场景 |
|---|---|---|---|---|---|
| Matplotlib | 基础层 | 几乎所有 | ❌ 弱 | ⭐⭐⭐ | 定制化图表、论文出版 |
| Seaborn | 统计层 | 统计图表 | ❌ 弱 | ⭐⭐ | 统计分析、快速探索 |
| Plotly | 交互层 | 多种类型 | ✅ 强 | ⭐⭐ | 仪表盘、Web 应用 |
| Altair | 声明层 | Vega-Lite | ✅ 强 | ⭐⭐ | 快速探索、JSON 驱动 |
| Bokeh | 交互层 | Web 图表 | ✅ 强 | ⭐⭐⭐ | Web 应用、大数据 |
| PyEcharts | 交互层 | ECharts | ✅ 强 | ⭐⭐ | 中文图表、Dashboards |
选型指南
# 快速生成统计图表 → Seaborn
import seaborn as sns
sns.boxplot(data=df, x='category', y='value')
# 高度定制化图表 → Matplotlib
import matplotlib.pyplot as plt
plt.plot(x, y, color='red', linewidth=2)
# 交互式仪表盘 → Plotly Dash
import dash
import plotly.express as px
px.scatter(df, x='x', y='y', color='category')
# Web 应用嵌入 → Bokeh / PyEcharts
# 中文场景 → PyEcharts
Matplotlib 基础
安装与导入
# 安装
pip install matplotlib numpy pandas
# Jupyter 魔法命令(自动显示图表)
%matplotlib inline
# 或交互模式
%matplotlib widget
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# 设置中文字体(重要!)
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
图表结构详解
fig, ax = plt.subplots(figsize=(10, 6))
# fig = Figure(画布,整个图形)
# ax = Axes(坐标系,一个 fig 可以有多个 ax)
# 图表各部分名称
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
# 图表组成
# - Figure: 整个画布
# - Axes: 坐标系(包含 X/Y 轴)
# - Axis: X 轴和 Y 轴
# - Title: 标题
# - Labels: 轴标签
# - Tick: 刻度
# - Legend: 图例
# - Grid: 网格线
plt.show() # 显示图表
fig.savefig('chart.png', dpi=300, bbox_inches='tight') # 保存图表
基本图表类型
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
y = np.sin(x)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 1. 折线图
axes[0, 0].plot(x, y, 'b-', linewidth=2, label='sin(x)')
axes[0, 0].set_title('折线图')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# 2. 散点图
np.random.seed(42)
x_scatter = np.random.randn(50)
y_scatter = np.random.randn(50)
colors = np.random.choice(['red', 'blue', 'green'], 50)
sizes = np.random.randint(50, 200, 50)
axes[0, 1].scatter(x_scatter, y_scatter, c=colors, s=sizes, alpha=0.6)
axes[0, 1].set_title('散点图')
# 3. 柱状图
categories = ['A', 'B', 'C', 'D']
values = [23, 45, 56, 78]
bars = axes[1, 0].bar(categories, values, color=['#3498db', '#2ecc71', '#e74c3c', '#f39c12'])
axes[1, 0].set_title('柱状图')
# 添加数值标签
for bar, val in zip(bars, values):
axes[1, 0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
str(val), ha='center', va='bottom')
# 4. 直方图
data_normal = np.random.normal(0, 1, 1000)
axes[1, 1].hist(data_normal, bins=30, color='#9b59b6', edgecolor='white', alpha=0.7)
axes[1, 1].set_title('直方图(正态分布)')
axes[1, 1].set_xlabel('值')
axes[1, 1].set_ylabel('频数')
plt.tight_layout()
plt.show()
颜色与样式
# 颜色指定方式
plt.plot(x, y, color='red') # 颜色名称
plt.plot(x, y, color='#FF5733') # HEX
plt.plot(x, y, color=(1, 0.34, 0.2)) # RGB 元组 (0-1)
plt.plot(x, y, color='C0') # Tableau 颜色 C0-C9
# 颜色映射
plt.imshow(data, cmap='viridis') # viridis, plasma, coolwarm, RdYlGn
plt.colorbar() # 显示颜色条
# 线型
plt.plot(x, y, linestyle='--') # --, :, -., solid
plt.plot(x, y, linewidth=2) # 线宽
# 标记
plt.plot(x, y, marker='o') # o, s, D, ^, v, <, >
plt.plot(x, y, markersize=10)
plt.plot(x, y, markerfacecolor='red')
# 样式简写
# 'b-' 蓝色实线
# 'ro' 红色圆点
# 'g--' 绿色虚线
# 'ks:' 黑色方形点线
Matplotlib 进阶
多子图布局
# 方法1:subplots
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
axes = axes.flatten() # 展平便于索引
# 方法2:subplot2grid(更灵活的布局)
fig = plt.figure(figsize=(12, 6))
ax1 = plt.subplot2grid((2, 3), (0, 0), colspan=2) # 第1行,占2列
ax2 = plt.subplot2grid((2, 3), (0, 2)) # 第1行,第3列
ax3 = plt.subplot2grid((2, 3), (1, 0), colspan=3) # 第2行,占3列
# 方法3:GridSpec(更精细的控制)
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 3, figure=fig, hspace=0.3, wspace=0.3)
ax1 = fig.add_subplot(gs[0, :]) # 第1行,占整行
ax2 = fig.add_subplot(gs[1, :-1]) # 第2行,前2列
ax3 = fig.add_subplot(gs[1:, -1]) # 第2-3行,最后1列
ax4 = fig.add_subplot(gs[2, 0]) # 第3行,第1列
双 Y 轴与共享轴
# 双 Y 轴
fig, ax1 = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.exp(x)
ax1.plot(x, y1, 'b-', label='sin(x)')
ax1.set_xlabel('X')
ax1.set_ylabel('sin(x)', color='blue')
ax2 = ax1.twinx() # 创建共享 X 轴的双 Y 轴
ax2.plot(x, y2, 'r-', label='exp(x)')
ax2.set_ylabel('exp(x)', color='red')
# 添加图例
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.title('双 Y 轴图表')
plt.show()
# 共享 X 轴
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(10, 8))
ax1.plot(x, y1)
ax2.plot(x, y2)
plt.show()
注释与标注
fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
y = x ** 2
ax.plot(x, y, 'b-', linewidth=2)
# 文本标注
ax.text(5, 50, 'y = x²', fontsize=14, ha='center')
# 带箭头的标注
ax.annotate(
'最大值点',
xy=(10, 100), # 箭头指向点
xytext=(7, 70), # 文本位置
fontsize=12,
arrowprops=dict(
arrowstyle='->',
color='red',
lw=2
),
color='red'
)
# 水平/垂直线
ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5)
ax.axvline(x=5, color='gray', linestyle='--', alpha=0.5)
# 填充区域
ax.fill_between(x, 0, y, alpha=0.3, color='blue')
ax.fill_between(x, y, y + 20, alpha=0.3, color='green')
plt.show()
保存与导出
# 保存各种格式
fig.savefig('chart.png', dpi=300, bbox_inches='tight')
fig.savefig('chart.pdf', bbox_inches='tight') # 矢量图,适合出版
fig.savefig('chart.svg', bbox_inches='tight') # SVG,可编辑
fig.savefig('chart.jpg', dpi=150, quality=95) # JPEG
# 透明背景
fig.savefig('chart_transparent.png', transparent=True)
# 不含白边
plt.savefig('chart.png', dpi=300, bbox_inches='tight', pad_inches=0)
# 保存数据
np.savetxt('data.csv', np.column_stack([x, y]), delimiter=',', header='x,y')
Seaborn 统计可视化
Seaborn 基础
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# 设置样式
sns.set_theme(style='whitegrid') # darkgrid, whitegrid, dark, white, ticks
sns.set_palette('husl') # 配色方案
# 内置数据集
tips = sns.load_dataset('tips') # 小费数据集
iris = sns.load_dataset('iris') # 鸢尾花数据集
flights = sns.load_dataset('flights') # 航班数据
print(tips.head())
# total_bill tip sex smoker day time size
# 0 16.99 1.01 Female No Sun Dinner 2
# 1 10.34 1.66 Male No Sun Dinner 3
关系图
# 散点图 + 回归线
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左图:简单散点图
sns.scatterplot(data=tips, x='total_bill', y='tip', ax=axes[0])
axes[0].set_title('小费 vs 账单金额')
# 右图:带回归线的散点图
sns.regplot(data=tips, x='total_bill', y='tip', ax=axes[1])
axes[1].set_title('带回归线的小费分析')
# 增强散点图(带分类)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 左图:按性别分类
sns.scatterplot(data=tips, x='total_bill', y='tip',
hue='sex', style='smoker', ax=axes[0])
axes[0].set_title('按性别和吸烟状态分类')
# 右图:带边缘分布
sns.jointplot(data=tips, x='total_bill', y='tip',
kind='reg', marginal_kws=dict(bins=30))
plt.suptitle('联合分布图', y=1.02)
plt.tight_layout()
plt.show()
分布图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. 直方图 + KDE
sns.histplot(data=tips, x='total_bill', kde=True, ax=axes[0, 0])
axes[0, 0].set_title('账单金额分布')
# 2. 分类直方图
sns.histplot(data=tips, x='total_bill', hue='sex',
multiple='stack', kde=True, ax=axes[0, 1])
axes[0, 1].set_title('按性别分类的账单分布')
# 3. KDE 图
sns.kdeplot(data=tips, x='total_bill', hue='day',
multiple='layer', ax=axes[1, 0])
axes[1, 0].set_title('各天账单金额密度')
# 4. 箱线图
sns.boxplot(data=tips, x='day', y='total_bill',
hue='sex', palette='Set2', ax=axes[1, 1])
axes[1, 1].set_title('各天账单金额箱线图')
plt.tight_layout()
plt.show()
分类图
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
# 1. 柱状图
sns.barplot(data=tips, x='day', y='total_bill',
hue='sex', estimator='mean', capsize=0.1, ax=axes[0, 0])
axes[0, 0].set_title('各天平均账单(带误差棒)')
# 2. 计数图
sns.countplot(data=tips, x='day', hue='sex', ax=axes[0, 1])
axes[0, 1].set_title('各天顾客数量')
# 3. 箱线图
sns.boxplot(data=tips, x='day', y='total_bill',
hue='smoker', palette='Set3', ax=axes[0, 2])
axes[0, 2].set_title('吸烟者与非吸烟者账单对比')
# 4. 小提琴图
sns.violinplot(data=tips, x='day', y='total_bill',
hue='sex', split=True, palette='Set2', ax=axes[1, 0])
axes[1, 0].set_title('小提琴图(显示分布形态)')
# 5. 点图
sns.pointplot(data=tips, x='day', y='total_bill',
hue='sex', markers=['o', 's'], errorbar='sd', ax=axes[1, 1])
axes[1, 1].set_title('点图(显示趋势)')
# 6. 热力图(相关性)
numeric_cols = tips.select_dtypes(include=[np.number]).columns
corr = tips[numeric_cols].corr()
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0,
fmt='.2f', ax=axes[1, 2])
axes[1, 2].set_title('账单数据相关性热力图')
plt.tight_layout()
plt.show()
高级:FacetGrid
# 多维度分类可视化
g = sns.FacetGrid(tips, col='time', row='sex', margin_titles=True, height=4)
g.map_dataframe(sns.scatterplot, x='total_bill', y='tip', hue='smoker')
g.add_legend()
g.set_axis_labels('账单金额', '小费')
g.set_titles(col_template='{col_name}', row_template='{row_name}')
plt.suptitle('多维度分类散点图', y=1.02)
plt.show()
Plotly 交互式图表
Plotly 基础
pip install plotly kaleido # kaleido 用于导出静态图
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
# 内置数据集
df = px.data.iris() # 鸢尾花数据集
print(df.head())
散点图
# 简单散点图
fig = px.scatter(df, x='sepal_width', y='sepal_length',
color='species', title='鸢尾花散点图')
# 添加趋势线
fig = px.scatter(df, x='sepal_width', y='sepal_length',
color='species', trendline='ols') # OLS 回归
# 3D 散点图
fig = px.scatter_3d(df, x='sepal_width', y='sepal_length',
z='petal_width', color='species',
title='3D 散点图')
# 气泡图(大小映射到数值)
fig = px.scatter(df, x='sepal_width', y='sepal_length',
size='petal_length', color='species',
hover_name='species_id',
title='气泡图')
fig.show()
其他常见图表
# 折线图
df_line = pd.DataFrame({
'x': range(10),
'y': [1, 3, 2, 5, 4, 6, 5, 7, 6, 8],
'category': ['A']*5 + ['B']*5
})
fig = px.line(df_line, x='x', y='y', color='category',
markers=True, title='折线图')
# 柱状图
fig = px.bar(df, x='species', y='sepal_width',
color='species', title='柱状图',
barmode='group') # group, stack, relative
# 饼图
df_pie = df.groupby('species').size().reset_index(name='count')
fig = px.pie(df_pie, values='count', names='species',
title='物种占比', hole=0.4) # hole 变为环形图
# 箱线图
fig = px.box(df, x='species', y='sepal_length',
color='species', title='箱线图',
points='all') # points: all, outliers, False
# 热力图
fig = px.density_heatmap(df, x='sepal_width', y='sepal_length',
title='密度热力图')
fig.show()
地图可视化
# 地图示例
df_map = pd.DataFrame({
'city': ['北京', '上海', '广州', '深圳'],
'lat': [39.9042, 31.2304, 23.1291, 22.5431],
'lon': [116.4074, 121.4737, 113.2644, 114.0579],
'value': [100, 120, 80, 90]
})
fig = px.scatter_geo(df_map, lat='lat', lon='lon',
size='value', color='value',
hover_name='city',
projection='natural earth',
title='城市数据地图')
# 美国地图
df_usa = px.data.election()
fig = px.scatter_geo(df_usa, locations='county', locationmode='USA-states',
color='winner', scope='usa')
fig.show()
子图与布局
# 创建子图
from plotly.subplots import make_subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('散点图', '柱状图', '折线图', '饼图'),
specs=[[{'type': 'scatter'}, {'type': 'bar'}],
[{'type': 'scatter'}, {'type': 'pie'}]]
)
# 添加图表
fig.add_trace(
go.Scatter(x=[1, 2, 3], y=[4, 5, 6], name='散点'),
row=1, col=1
)
fig.add_trace(
go.Bar(x=['A', 'B', 'C'], y=[3, 5, 2], name='柱状'),
row=1, col=2
)
# 更新布局
fig.update_layout(
title='多子图布局',
showlegend=True,
height=600,
width=900
)
fig.show()
导出与保存
# 保存为 HTML(交互式,可嵌入网页)
fig.write_html('interactive_chart.html')
# 保存为静态图片
fig.write_image('chart.png', width=1200, height=800, scale=2)
fig.write_image('chart.pdf')
# 保存为 JSON
fig.write_json('chart.json')
常见图表类型
选型指南
| 目的 | 推荐图表 | 示例场景 |
|---|---|---|
| 比较 | 柱状图、条形图 | 产品销量对比 |
| 趋势 | 折线图、面积图 | 股价走势、用户增长 |
| 分布 | 直方图、KDE、箱线图 | 用户年龄分布 |
| 构成 | 饼图、堆叠柱状图 | 市场份额占比 |
| 关系 | 散点图、热力图 | 身高体重相关性 |
| 地理 | 地图、 Choropleth | 各省销售额 |
对比图表
# 分组柱状图
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
categories = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = [100, 120, 110, 130]
product_b = [80, 90, 100, 95]
# 方案1:分组柱状图
x = np.arange(len(categories))
width = 0.35
axes[0].bar(x - width/2, product_a, width, label='产品 A')
axes[0].bar(x + width/2, product_b, width, label='产品 B')
axes[0].set_xlabel('季度')
axes[0].set_ylabel('销量')
axes[0].set_title('分组柱状图')
axes[0].set_xticks(x)
axes[0].set_xticklabels(categories)
axes[0].legend()
# 方案2:堆叠柱状图
axes[1].bar(categories, product_a, label='产品 A')
axes[1].bar(categories, product_b, bottom=product_a, label='产品 B')
axes[1].set_xlabel('季度')
axes[1].set_ylabel('销量')
axes[1].set_title('堆叠柱状图')
axes[1].legend()
plt.tight_layout()
plt.show()
趋势图表
# 面积图
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
# 普通面积图
axes[0].fill_between(x, y1, alpha=0.3, label='sin')
axes[0].fill_between(x, y2, alpha=0.3, label='cos')
axes[0].set_title('面积图')
# 堆叠面积图
df_area = pd.DataFrame({
'x': x,
'A': y1 * 10 + 50,
'B': y2 * 10 + 30,
'C': np.random.randint(10, 30, 100)
})
axes[1].stackplot(df_area['x'], df_area['A'], df_area['B'], df_area['C'],
labels=['A', 'B', 'C'], alpha=0.8)
axes[1].set_title('堆叠面积图')
axes[1].legend(loc='upper left')
plt.tight_layout()
plt.show()
实战案例
案例1:股票数据可视化
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import mplfinance as mpf
# 下载股票数据
stock = yf.download('AAPL', start='2025-01-01', end='2026-05-15')
print(stock.head())
# K 线图(需要 mplfinance)
mpf.plot(stock, type='candle', style='charles',
title='Apple Inc. K线图',
ylabel='价格',
volume=True,
mav=(5, 10, 20), # 移动平均线
savefig='candlestick.png')
# 普通折线图
fig, axes = plt.subplots(2, 1, figsize=(14, 10),
gridspec_kw={'height_ratios': [3, 1]})
# 价格走势
axes[0].plot(stock.index, stock['Close'], label='收盘价')
axes[0].plot(stock.index, stock['MA5'], label='MA5', alpha=0.7)
axes[0].plot(stock.index, stock['MA20'], label='MA20', alpha=0.7)
axes[0].fill_between(stock.index, stock['Low'], stock['High'], alpha=0.3)
axes[0].set_title('Apple Inc. 价格走势')
axes[0].set_ylabel('价格 (USD)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 成交量
colors = ['green' if stock['Close'].iloc[i] >= stock['Open'].iloc[i]
else 'red' for i in range(len(stock))]
axes[1].bar(stock.index, stock['Volume'], color=colors, alpha=0.7)
axes[1].set_ylabel('成交量')
axes[1].set_xlabel('日期')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stock_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
案例2:用户行为分析仪表盘
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
# 模拟用户行为数据
np.random.seed(42)
dates = pd.date_range('2026-01-01', '2026-05-15', freq='D')
df = pd.DataFrame({
'date': dates,
'DAU': np.random.randint(1000, 5000, len(dates)), # 日活用户
'new_users': np.random.randint(100, 500, len(dates)),
'retention_rate': np.random.uniform(0.3, 0.8, len(dates)),
'avg_session_time': np.random.uniform(5, 30, len(dates)), # 分钟
})
# 创建仪表盘
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('日活用户趋势', '新增用户趋势', '留存率分布', '平均使用时长'),
specs=[[{'type': 'scatter'}, {'type': 'scatter'}],
[{'type': 'histogram'}, {'type': 'box'}]]
)
# 日活趋势
fig.add_trace(
go.Scatter(x=df['date'], y=df['DAU'],
mode='lines', name='DAU', fill='tozeroy'),
row=1, col=1
)
# 新增用户趋势
fig.add_trace(
go.Bar(x=df['date'], y=df['new_users'], name='新增用户'),
row=1, col=2
)
# 留存率直方图
fig.add_trace(
go.Histogram(x=df['retention_rate'], nbinsx=20, name='留存率'),
row=2, col=1
)
# 使用时长箱线图
fig.add_trace(
go.Box(y=df['avg_session_time'], name='使用时长'),
row=2, col=2
)
fig.update_layout(
title='用户行为分析仪表盘',
height=800,
showlegend=False
)
fig.show()
fig.write_html('user_dashboard.html')
案例3:电商数据分析
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
# 模拟电商数据
np.random.seed(42)
df = pd.DataFrame({
'category': np.random.choice(['电子产品', '服装', '食品', '图书', '家居'], 500),
'price': np.random.lognormal(4.5, 1, 500), # 价格分布
'rating': np.random.uniform(1, 5, 500), # 评分
'sales': np.random.randint(10, 1000, 500), # 销量
'discount': np.random.uniform(0, 0.5, 500) # 折扣率
})
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# 1. 各类别销售额占比(饼图)
sales_by_cat = df.groupby('category')['sales'].sum().sort_values(ascending=False)
colors = sns.color_palette('husl', len(sales_by_cat))
axes[0, 0].pie(sales_by_cat, labels=sales_by_cat.index, autopct='%1.1f%%',
colors=colors, explode=[0.05] * len(sales_by_cat))
axes[0, 0].set_title('各类别销售额占比')
# 2. 价格 vs 评分(带回归)
sns.regplot(data=df, x='price', y='rating', ax=axes[0, 1],
scatter_kws={'alpha': 0.3}, line_kws={'color': 'red'})
axes[0, 1].set_title('价格与评分关系')
# 3. 各类别评分箱线图
sns.boxplot(data=df, x='category', y='rating', ax=axes[1, 0],
palette='Set2')
axes[1, 0].set_title('各类别评分分布')
axes[1, 0].tick_params(axis='x', rotation=30)
# 4. 销售额 TOP10 商品(条形图)
top10 = df.nlargest(10, 'sales')[['category', 'sales']]
colors = [colors[sales_by_cat.index.get_loc(c)] for c in top10['category']]
axes[1, 1].barh(range(10), top10['sales'].values, color=colors)
axes[1, 1].set_yticks(range(10))
axes[1, 1].set_yticklabels(top10['category'].values)
axes[1, 1].set_xlabel('销量')
axes[1, 1].set_title('销量 TOP10 商品')
axes[1, 1].invert_yaxis()
plt.tight_layout()
plt.savefig('ecommerce_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
图表美化与主题
配色方案
import matplotlib.pyplot as plt
import seaborn as sns
# Seaborn 调色板
sns.palettes.SEABORN_PALETTES
# ['deep', 'muted', 'pastel', 'bright', 'dark', 'colorblind']
# 使用调色板
plt.figure(figsize=(10, 6))
current_palette = sns.color_palette('pastel')
sns.barplot(x=['A', 'B', 'C', 'D'], y=[3, 5, 2, 4], palette=current_palette)
# 自定义颜色列表
custom_colors = ['#E74C3C', '#3498DB', '#2ECC71', '#F39C12', '#9B59B6']
sns.barplot(x=['A', 'B', 'C', 'D', 'E'], y=[3, 5, 2, 4, 6], palette=custom_colors)
# 渐变色
from matplotlib.colors import LinearSegmentedColormap
colors = ['#FF6B6B', '#FFE66D', '#4ECDC4'] # 红 -> 黄 -> 青
cmap = LinearSegmentedColormap.from_list('my_cmap', colors)
Seaborn 主题
# 可用主题
sns.set_theme(style='darkgrid') # 深色网格
sns.set_theme(style='whitegrid') # 白色网格(推荐)
sns.set_theme(style='dark') # 深色背景
sns.set_theme(style='white') # 纯白背景
sns.set_theme(style='ticks') # 带刻度线
# 全局设置
plt.rcParams.update({
'figure.facecolor': 'white',
'axes.facecolor': 'white',
'axes.edgecolor': '.9',
'axes.grid': True,
'grid.color': '.9',
'axes.spines.top': False,
'axes.spines.right': False,
'font.family': 'sans-serif',
'font.sans-serif': ['Arial', 'Helvetica'],
'axes.titlesize': 14,
'axes.labelsize': 12,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'legend.fontsize': 10,
})
自定义样式
# 创建自定义样式
plt.style.use('default') # 重置
# 自定义 Matplotlib 参数
import matplotlib as mpl
# 设置全局样式
mpl.rcParams['figure.figsize'] = (10, 6)
mpl.rcParams['figure.dpi'] = 100
mpl.rcParams['savefig.dpi'] = 300
mpl.rcParams['font.size'] = 12
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['lines.linewidth'] = 2
mpl.rcParams['axes.labelsize'] = 12
mpl.rcParams['axes.titlesize'] = 14
mpl.rcParams['xtick.labelsize'] = 10
mpl.rcParams['ytick.labelsize'] = 10
mpl.rcParams['legend.fontsize'] = 10
mpl.rcParams['axes.grid'] = True
mpl.rcParams['grid.alpha'] = 0.3
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['axes.spines.right'] = False
# 保存自定义样式
# matplotlib.rc_file('custom_style.mplstyle')
# matplotlib.rc_context({'font.size': 14})
性能优化
大数据可视化
# 问题:数据量过大时渲染慢
# 解决方案1:数据采样
def sample_data(df, n=1000):
if len(df) > n:
return df.sample(n=n)
return df
# 解决方案2:使用 Datashader(Bokeh生态)
# pip install datashader
import datashader as ds
import datashader.transfer_functions as tf
# 百万级散点图
cvs = ds.Canvas(plot_width=800, plot_height=600)
agg = cvs.points(df, 'x', 'y')
img = tf.shade(agg, cmap=['lightblue', 'darkblue'], how='log')
# 解决方案3:Plotly 的 WebGL 渲染
fig = px.scatter(df, x='x', y='y', render_mode='webgl')
# 解决方案4:Hexbin 代替散点图
plt.hexbin(x, y, gridsize=30, cmap='YlOrRd')
渲染优化
# 1. 避免创建过多对象
# ❌ 循环中频繁创建 Figure
for i in range(100):
fig, ax = plt.subplots()
ax.plot(x, y)
plt.savefig(f'frame_{i}.png')
plt.close(fig)
# ✅ 使用子图,分批保存
fig, axes = plt.subplots(10, 10, figsize=(50, 50))
# ... 绘制 ...
fig.savefig('grid.png', dpi=100)
plt.close(fig)
# 2. 缓存不变的图表
# 如果图表数据不变,可以保存为 SVG 后续修改
# 3. 使用面向对象的 API
# ❌ pyplot 方式(全局状态)
plt.figure()
plt.plot(x, y)
plt.savefig('chart.png')
# ✅ 面向对象方式(更高效)
fig, ax = plt.subplots()
ax.plot(x, y)
fig.savefig('chart.png')
plt.close(fig)
排错指南
常见错误与解决方案
错误1:中文字体不显示
# 问题:中文显示为方块
# 解决1:设置中文字体
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
# 解决2:下载中文字体
# 下载 Noto Sans CJK 或 Source Han Sans
# 放到 ~/.matplotlib/fontdir/
import matplotlib.font_manager as fm
fm.fontManager.addfont('/path/to/NotoSansCJK-Regular.ttc')
# 解决3:使用英文标签
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title('Distribution Chart')
错误2:负号显示为方块
# 解决
plt.rcParams['axes.unicode_minus'] = False # 使用 ASCII 负号
# 或
plt.rcParams['axes.unicode_minus'] = True
# 并确保字体支持
错误3:图例遮挡数据
# 问题:图例位置不当
# 解决1:调整位置
plt.legend(loc='upper right', bbox_to_anchor=(1.15, 1))
# 解决2:放在图外
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
# 解决3:放在图表下方
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.1), ncol=3)
# 解决4:放在空白区域
fig.tight_layout(rect=[0, 0, 0.85, 1])
错误4:刻度标签重叠
# 问题:X 轴标签太挤
# 解决1:旋转
plt.xticks(rotation=45, ha='right')
# 解决2:每隔一个显示
plt.xticks(x[::2], labels[::2])
# 解决3:自动调整
plt.gcf().autofmt_xdate()
# 解决4:调整间距
plt.subplots_adjust(bottom=0.2)
错误5:子图间距不当
# 问题:子图太挤
# 解决1:自动调整
plt.tight_layout() # 推荐
# 解决2:手动调整
plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1,
wspace=0.3, hspace=0.3)
# 解决3:GridSpec 精确控制
from matplotlib.gridspec import GridSpec
gs = GridSpec(2, 2, hspace=0.3, wspace=0.3)
错误6:图例颜色与实际不符
# 问题:多条线共用同一颜色
# 解决1:指定颜色
colors = plt.cm.viridis(np.linspace(0, 1, len(lines)))
for i, line in enumerate(lines):
ax.plot(x, y[i], color=colors[i])
# 解决2:确保颜色一致
sns.lineplot(data=df, x='x', y='y', hue='category', palette='tab10')
# 解决3:刷新绑定
plt.gca().set_prop_cycle(None) # 重置颜色循环
Jupyter 特定问题
# 问题1:图表不显示
%matplotlib inline # 在 notebook 开头添加
# 问题2:交互模式
%matplotlib widget # 交互式图表(需要 ipympl)
# 问题3:图表太大
plt.figure(figsize=(10, 6), dpi=80)
# 问题4:清除缓存
%reset -f
import matplotlib.pyplot as plt
plt.close('all')
总结
核心要点
- 选对工具:Matplotlib 定制化、Seaborn 统计分析、Plotly 交互展示
- 理解图表类型:比较用柱状图、趋势用折线图、分布用直方图
- 配色要专业:使用专业配色方案,避免彩虹色滥用
- 标注要清晰:标题、轴标签、图例、单位缺一不可
- 避免过度设计:简洁是美,数据说话
速查表
┌─────────────────────────────────────────────────────────────────┐
│ 图表类型速查表 │
├──────────────────┬──────────────────────────────────────────────┤
│ 目的 │ 图表 │
├──────────────────┼──────────────────────────────────────────────┤
│ 单一变量分布 │ 直方图、KDE、箱线图 │
│ 多变量关系 │ 散点图、热力图 │
│ 类别比较 │ 柱状图、条形图、饼图 │
│ 时间趋势 │ 折线图、面积图 │
│ 占比构成 │ 堆叠柱状图、环形图 │
│ 地理分布 │ 地图、Choropleth │
│ 相关性分析 │ 相关热力图、PairPlot │
└──────────────────┴──────────────────────────────────────────────┘
常用代码模板
# 基础图表模板
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, y, label='数据')
ax.set_xlabel('X 轴')
ax.set_ylabel('Y 轴')
ax.set_title('标题')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Seaborn 快速图表
sns.set_theme(style='whitegrid')
sns.relplot(data=df, x='x', y='y', col='category', kind='scatter')
plt.show()
# Plotly 交互图表
fig = px.scatter(df, x='x', y='y', color='category', title='标题')
fig.update_layout(template='plotly_white')
fig.show()
💡 提示: 好的数据可视化应该让读者一眼就能理解数据的故事。选择合适的图表类型比花哨的样式更重要。 📚 扩展阅读:
原创内容,版权所有。未经授权,禁止转载。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END













暂无评论内容