Python 数据可视化实战——matplotlib/plotly 制作专业级技术报告图表
2026/4/30大约 8 分钟
Python 数据可视化实战——matplotlib/plotly 制作专业级技术报告图表
适读人群:需要制作数据可视化报告的工程师和数据分析师 | 阅读时长:约16分钟 | 核心价值:从"能看"到"好看"——掌握制作专业级图表的核心技巧和完整代码模板
小余在公司做数据分析,每个月要给产品和运营出一份数据报告。用 pandas 处理数据他很熟,但一到可视化就头疼——图表总是丑,颜色难看,字体太小,坐标轴没有标注。领导每次看报告都说"能不能做得好看一点",他每次都不知道从哪里改起。
有一次他把图表发给我看,我给他数了数问题:默认蓝色柱子没有任何配色主题、y 轴没有单位、图例挡住了数据、标题字体太小、背景网格密密麻麻……
他很沮丧:"老张,我就是想让图表看起来像那些技术报告里的那种,专业、清晰,但我不知道差在哪里。"
其实答案很简单:专业图表 = 好的数据 + 明确的主题 + 克制的配色 + 清晰的标注。matplotlib 能做到,只是默认设置太丑,需要你主动去调。plotly 更现代,交互更好,但需要了解它的设计体系。
今天我们从实战角度,教你做真正专业的图表。
一、matplotlib 专业图表基础设置
1.1 建立统一的图表风格
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
import pandas as pd
# 设置全局样式(一次配置,处处生效)
def setup_chart_style():
"""建立统一的专业图表风格"""
plt.rcParams.update({
# 字体
"font.family": ["PingFang SC", "Microsoft YaHei", "DejaVu Sans"],
"font.size": 12,
"axes.titlesize": 16,
"axes.titleweight": "bold",
"axes.labelsize": 13,
# 图形大小
"figure.figsize": (12, 6),
"figure.dpi": 150,
# 背景和网格
"axes.facecolor": "#F8F9FA", # 浅灰色背景
"axes.grid": True,
"grid.color": "white", # 白色网格(在灰色背景上很好看)
"grid.linewidth": 1.0,
"grid.alpha": 0.8,
# 坐标轴
"axes.spines.top": False, # 去掉上边框
"axes.spines.right": False, # 去掉右边框
"axes.linewidth": 1.2,
# 颜色循环(使用专业配色方案)
"axes.prop_cycle": plt.cycler(color=[
"#4527A0", "#AB47BC", "#7E57C2", "#26C6DA", "#42A5F5",
"#66BB6A", "#FFCA28", "#EF5350"
])
})
setup_chart_style()1.2 完整的柱状图模板
def plot_monthly_revenue(data: dict, title: str, save_path: str = None):
"""
制作月度收入对比柱状图
data: {"月份": [...], "收入": [...], "目标": [...]}
"""
months = data["月份"]
revenue = data["收入"]
target = data.get("目标", None)
fig, ax = plt.subplots(figsize=(14, 7))
x = np.arange(len(months))
width = 0.6 if target is None else 0.35
# 主柱子
bars = ax.bar(
x if target is None else x - width/2,
revenue,
width=width,
color="#4527A0",
alpha=0.85,
label="实际收入",
zorder=3 # 确保在网格上面
)
# 目标线(如果有)
if target:
ax.bar(
x + width/2,
target,
width=width,
color="#AB47BC",
alpha=0.6,
label="目标",
zorder=3
)
# 在柱子顶部添加数值标注
for bar in bars:
height = bar.get_height()
ax.annotate(
f"¥{height/10000:.0f}万", # 格式化为万元
xy=(bar.get_x() + bar.get_width() / 2, height),
xytext=(0, 5), # 向上偏移5个点
textcoords="offset points",
ha="center", va="bottom",
fontsize=11,
color="#333333",
fontweight="bold"
)
# 轴设置
ax.set_xticks(x)
ax.set_xticklabels(months, fontsize=12)
ax.yaxis.set_major_formatter(
mticker.FuncFormatter(lambda x, _: f"¥{x/10000:.0f}万")
)
# 标题和标签
ax.set_title(title, fontsize=18, fontweight="bold", pad=20, color="#1a1a2e")
ax.set_xlabel("月份", fontsize=13, color="#555555")
ax.set_ylabel("收入金额(万元)", fontsize=13, color="#555555")
# 图例
if target:
ax.legend(loc="upper left", framealpha=0.9, fontsize=12)
# 添加数据来源注释
fig.text(
0.99, 0.01,
"数据来源:内部销售系统 | 制作:老张聊AI转型",
ha="right", va="bottom",
fontsize=9, color="#999999"
)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=200, bbox_inches="tight", facecolor="white")
print(f"图表已保存:{save_path}")
plt.show()
# 使用
data = {
"月份": [f"{m}月" for m in range(1, 13)],
"收入": [1200000, 1350000, 1180000, 1420000, 1560000, 1480000,
1320000, 1650000, 1720000, 1580000, 1890000, 2100000],
"目标": [1300000, 1300000, 1300000, 1500000, 1500000, 1500000,
1500000, 1700000, 1700000, 1700000, 1900000, 1900000]
}
plot_monthly_revenue(data, "2024年度月度收入完成情况", "monthly_revenue.png")二、多子图组合——制作完整的数据报告页
def create_dashboard(df: pd.DataFrame, save_path: str = "dashboard.png"):
"""
制作包含4个子图的数据仪表盘
"""
fig = plt.figure(figsize=(18, 12))
fig.suptitle("业务数据周报 · 2024年第48周",
fontsize=22, fontweight="bold", y=0.98, color="#1a1a2e")
# 创建4个子图区域
gs = fig.add_gridspec(2, 2, hspace=0.4, wspace=0.35)
ax1 = fig.add_subplot(gs[0, 0]) # 左上:日活趋势
ax2 = fig.add_subplot(gs[0, 1]) # 右上:渠道分布
ax3 = fig.add_subplot(gs[1, 0]) # 左下:转化漏斗
ax4 = fig.add_subplot(gs[1, 1]) # 右下:地区分布
# --- 子图1:折线图(日活趋势)---
dates = pd.date_range("2024-11-25", periods=7, freq="D")
dau = [45200, 48600, 52100, 47800, 55300, 61200, 58900]
ax1.plot(dates, dau, color="#4527A0", linewidth=2.5, marker="o",
markersize=8, markerfacecolor="white", markeredgewidth=2)
ax1.fill_between(dates, dau, alpha=0.15, color="#4527A0")
ax1.set_title("日活跃用户数(DAU)", fontsize=14, fontweight="bold")
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{x/10000:.1f}万"))
ax1.xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter("%m/%d"))
# --- 子图2:饼图(渠道分布)---
channels = ["自然搜索", "付费推广", "社交媒体", "直接访问", "其他"]
values = [35, 28, 18, 15, 4]
colors = ["#4527A0", "#AB47BC", "#7E57C2", "#26C6DA", "#B0BEC5"]
explode = (0.05, 0, 0, 0, 0) # 突出最大份额
wedges, texts, autotexts = ax2.pie(
values, labels=channels, colors=colors,
explode=explode, autopct="%1.1f%%",
pctdistance=0.75, startangle=90
)
for autotext in autotexts:
autotext.set_fontsize(11)
autotext.set_color("white")
ax2.set_title("流量渠道分布", fontsize=14, fontweight="bold")
# --- 子图3:水平条形图(转化漏斗)---
funnel_stages = ["访问首页", "浏览商品", "加入购物车", "提交订单", "完成支付"]
funnel_values = [100000, 68000, 32000, 18000, 14500]
colors_funnel = ["#4527A0", "#5C35B8", "#7E57C2", "#9575CD", "#AB47BC"]
bars = ax3.barh(funnel_stages, funnel_values, color=colors_funnel, alpha=0.85)
ax3.set_title("用户转化漏斗", fontsize=14, fontweight="bold")
ax3.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{x/10000:.0f}万"))
# 在条形右侧显示转化率
for i, (bar, val) in enumerate(zip(bars, funnel_values)):
rate = val / funnel_values[0] * 100
ax3.text(val + 500, bar.get_y() + bar.get_height()/2,
f"{rate:.1f}%", va="center", fontsize=10, color="#666666")
# --- 子图4:热力图风格的地区数据 ---
regions = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "南京"]
orders = [28500, 32100, 24600, 27800, 19200, 15600, 12300, 10800]
bar_colors = plt.cm.get_cmap("Purples")(np.linspace(0.4, 0.9, len(regions)))
sorted_data = sorted(zip(orders, regions), reverse=True)
orders_sorted = [d[0] for d in sorted_data]
regions_sorted = [d[1] for d in sorted_data]
ax4.barh(regions_sorted, orders_sorted, color=bar_colors)
ax4.set_title("各城市订单量 TOP8", fontsize=14, fontweight="bold")
ax4.xaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"{x/10000:.1f}万"))
# 统一设置所有子图的轴颜色
for ax in [ax1, ax2, ax3, ax4]:
ax.tick_params(colors="#555555")
# 添加日期水印
fig.text(0.5, 0.01, f"报告生成时间:2024-12-02 09:00 | 老张聊AI转型",
ha="center", fontsize=10, color="#AAAAAA")
plt.savefig(save_path, dpi=200, bbox_inches="tight", facecolor="white")
print(f"仪表盘已保存:{save_path}")
plt.show()
create_dashboard(None, "weekly_dashboard.png")三、plotly 交互式图表
plotly 的优势是可以生成交互式图表,适合嵌入网页或生成 HTML 报告。
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
# 专业配色主题
BRAND_COLORS = {
"primary": "#4527A0",
"secondary": "#AB47BC",
"accent": "#7E57C2",
"positive": "#26A69A",
"negative": "#EF5350",
"neutral": "#78909C"
}
def create_interactive_dashboard(df: pd.DataFrame) -> go.Figure:
"""
创建交互式数据仪表盘(可导出为 HTML)
"""
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("月度收入趋势", "品类销售占比", "用户增长", "日均转化率"),
specs=[
[{"type": "scatter"}, {"type": "pie"}],
[{"type": "bar"}, {"type": "indicator"}]
],
vertical_spacing=0.15,
horizontal_spacing=0.1
)
# 子图1:折线图
months = list(range(1, 13))
revenue = [120, 135, 118, 142, 156, 148, 132, 165, 172, 158, 189, 210]
fig.add_trace(
go.Scatter(
x=months, y=revenue,
mode="lines+markers",
name="月度收入(万元)",
line=dict(color=BRAND_COLORS["primary"], width=3),
marker=dict(size=8, color=BRAND_COLORS["secondary"]),
hovertemplate="<b>%{x}月</b><br>收入:¥%{y}万<extra></extra>"
),
row=1, col=1
)
# 子图2:饼图
fig.add_trace(
go.Pie(
labels=["电子产品", "服装", "食品", "家居", "其他"],
values=[35, 25, 20, 15, 5],
hole=0.4, # 甜甜圈效果
marker_colors=[BRAND_COLORS["primary"], BRAND_COLORS["secondary"],
BRAND_COLORS["accent"], "#26C6DA", "#B0BEC5"],
hovertemplate="<b>%{label}</b><br>占比:%{percent}<extra></extra>"
),
row=1, col=2
)
# 子图3:柱状图(月度新增用户)
new_users = [5200, 6100, 4800, 7200, 8100, 7600, 6300, 9100, 10200, 8800, 12300, 15600]
bar_colors = [BRAND_COLORS["positive"] if u > 8000 else BRAND_COLORS["primary"] for u in new_users]
fig.add_trace(
go.Bar(
x=months, y=new_users,
name="新增用户数",
marker_color=bar_colors,
hovertemplate="<b>%{x}月</b><br>新增:%{y}人<extra></extra>"
),
row=2, col=1
)
# 子图4:指标卡(KPI)
fig.add_trace(
go.Indicator(
mode="number+delta+gauge",
value=3.85,
delta={"reference": 3.20, "valueformat": ".2f", "suffix": "%"},
gauge={
"axis": {"range": [0, 10]},
"bar": {"color": BRAND_COLORS["primary"]},
"steps": [
{"range": [0, 3], "color": "#FFCDD2"},
{"range": [3, 6], "color": "#C8E6C9"},
{"range": [6, 10], "color": "#B3E5FC"}
],
"threshold": {
"line": {"color": BRAND_COLORS["negative"], "width": 4},
"thickness": 0.75, "value": 4.0
}
},
title={"text": "平均转化率(%)"},
number={"suffix": "%", "valueformat": ".2f"}
),
row=2, col=2
)
# 更新整体布局
fig.update_layout(
title={
"text": "业务数据月报 · 2024年",
"x": 0.5, "font": {"size": 24, "color": BRAND_COLORS["primary"]}
},
height=800,
showlegend=False,
paper_bgcolor="white",
plot_bgcolor="#F8F9FA",
font=dict(family="PingFang SC, Microsoft YaHei, sans-serif", size=12)
)
return fig
# 生成并保存
fig = create_interactive_dashboard(None)
fig.write_html("dashboard.html")
fig.write_image("dashboard.png", width=1400, height=800)
print("交互式图表已保存")四、踩坑实录一:中文字体显示为方块
现象:matplotlib 图表里的中文全显示成方块(□□□),英文正常。
原因:matplotlib 默认字体不包含中文字体。
解法:
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
# 方法一:指定系统中文字体(推荐)
plt.rcParams["font.sans-serif"] = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
# 方法二:查看可用字体
font_list = [f.name for f in fm.fontManager.ttflist if "PingFang" in f.name or "YaHei" in f.name]
print("可用中文字体:", font_list[:5])
# 方法三:手动指定字体文件(最稳定)
font_path = "/System/Library/Fonts/PingFang.ttc" # macOS
custom_font = fm.FontProperties(fname=font_path)
ax.set_title("中文标题", fontproperties=custom_font, fontsize=16)五、踩坑实录二:保存图片时文字被截断
现象:plt.savefig("chart.png") 保存后,图例或 x 轴标签被截断,但在屏幕上显示正常。
原因:tight_layout() 只影响显示,保存时没有自动调整边距。
解法:
# 方法一:savefig 时指定 bbox_inches
plt.savefig("chart.png", bbox_inches="tight", dpi=150)
# 方法二:保存前显式调用 tight_layout
plt.tight_layout(pad=1.5) # pad 是边距倍数
plt.savefig("chart.png", dpi=150)
# 方法三:手动调整子图间距
plt.subplots_adjust(left=0.1, right=0.95, top=0.9, bottom=0.15)六、踩坑实录三:plotly 图表导出为图片需要额外包
现象:fig.write_image("chart.png") 报错 ValueError: You must install kaleido。
解法:
pip install kaleido# 如果 kaleido 安装后还报错(macOS ARM 芯片常见问题)
# 使用 orca 替代
# pip install plotly-orca psutil
import plotly.io as pio
pio.orca.config.executable = "/path/to/orca"
# 或者用 selenium 方案(更通用)
# fig.write_html("chart.html") # 先保存 HTML
# 再用浏览器截图七、图表设计原则总结
好的技术图表遵循这几个原则:
- 一图一主题:每张图只表达一个核心观点,在标题里直接写出来
- 配色克制:主色 + 辅色 + 功能色(成功/失败),最多4种颜色
- 去掉多余元素:去掉上边框和右边框、简化网格、去掉多余图例
- 数字直接标注:重要的数字直接标在图上,不让读者去猜
- 统一风格:一份报告里所有图表用同一个字体、同一套颜色
