当前位置: 代码网 > it编程>前端脚本>Python > Python Matplotlib统计图绘制全攻略(附完整代码)

Python Matplotlib统计图绘制全攻略(附完整代码)

2026年09月23日 Python 我要评论
上一期我们把数据预处理那一套跑通以后,不少读者留言说想看绘图这块的内容——确实,数据洗得再干净,最终还是要靠统计图把结论讲清楚。这期接着系列第七篇,我们把matplotlib这

上一期我们把数据预处理那一套跑通以后,不少读者留言说想看绘图这块的内容——确实,数据洗得再干净,最终还是要靠统计图把结论讲清楚。这期接着系列第七篇,我们把matplotlib这套核心库的常见统计图一个个画一遍,代码直接贴,参数逐个解释,画完以后你手里的数据基本就啥图都能出了。

这套内容我自己平时做数据分析报表、写技术方案、给业务方做汇报,用的全是一个套路。matplotlib看着简单,但坑不少,尤其是中文字体、子图布局、数据标签这几个地方,新手经常卡住。这篇我就按实际使用频率从高到低把图讲一遍,顺便把那些文档里不会写的经验一起放进来。

1. 绘图核心库选型与基础认知

1.1 为什么把matplotlib当作核心绘图库

先聊一个很多人纠结过的问题:绘图库这么多,seaborn、plotly、pyecharts、ggplot2,为什么还是绕不开matplotlib?

我的判断很简单:matplotlib是python生态里几乎所有绘图工具的底层依赖。seaborn底层是matplotlib,pandas的plot方法底层是matplotlib,你去看很多高级库的源码,最后调用的还是matplotlib的api。把matplotlib搞明白,等于掌握了绘图的基础语言,后面迁移到任何封装库都不慌。

另外,matplotlib在学术论文、技术报告里的出图质量是经过几十年检验的。它的默认样式虽然丑,但胜在严谨:坐标轴、刻度、图例、标注,每个细节都可以精确控制。这对要发表论文或者做正式报告的朋友来说,是plotly那种交互式图表替代不了的。

当然,如果你做的是dashboard或者需要交互效果,plotly和pyecharts是更好的选择。但在做探索性分析、快速出图、批量生成报表的场景里,matplotlib依然是最顺手的那把刀。

1.2 绘图前的环境准备与最小可运行示例

先保证环境里装了matplotlib,推荐用pip安装:

pip install matplotlib numpy

numpy通常也建议一起装,因为绘图过程中生成数据、算统计量都离不开它。导入模块的方式我建议固定成下面这套:

import matplotlib.pyplot as plt
import numpy as np

然后是最小可运行的绘图脚本,我称之为“三行出图法”:

plt.figure(figsize=(8, 5))
plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
plt.show()

这个脚本虽然简单,但把绘图的核心流程跑通了。我在教学的时候经常用这个例子说明一个概念:matplotlib绘图就像画画,先铺画布(figure),再动笔(plot),最后展示或保存(show/savefig)。

1.3 必须要搞懂的figure和axes概念

很多新手画图只会用 plt.plot() 这种pyplot接口,一到复杂布局就懵了。这里必须搞清楚一个底层概念: figure是整个画布,axes是画布上的一块绘图区域

打个比方,figure就像一张书桌,axes是铺在书桌上的纸。你可以在同一张书桌上铺好几张纸(subplots),每一张纸上画不同的内容,各画各的。纸张也可以大小不一、位置不同(gridspec)。

我推荐从一开始就习惯用面向对象的方式写绘图代码:

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([1, 2, 3, 4], [1, 4, 9, 16])
ax.set_title("折线图示例")
ax.set_xlabel("x轴")
ax.set_ylabel("y轴")
plt.show()

plt.subplots() 返回两个对象: fig 是整个figure画布, ax 是axes绘图区域。后续所有操作都通过 ax 来调用,这样代码的可读性和可扩展性都远高于全局接口。特别是画子图的时候,面向对象接口的优势会非常明显。

2. 常用统计图逐个拆解与代码复现

2.1 折线图:趋势类数据的首选

折线图主要用来展示数据随时间或其他连续变量的变化趋势。在业务分析里,最常见的场景就是看每天的访问量、每周的销售额、每月的用户增长。

基础版折线图:

import matplotlib.pyplot as plt
import numpy as np

# 模拟一周的日访问量数据
days = np.arange(1, 8)
visits = [320, 450, 390, 510, 680, 720, 850]

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(days, visits, marker='o', linewidth=2, color='#2e86ab')
ax.set_title("一周访问量趋势", fontsize=14)
ax.set_xlabel("日期(周一~周日)")
ax.set_ylabel("访问量")
ax.grid(true, linestyle='--', alpha=0.6)
plt.show()

这里的 marker='o' 给每个数据点加上圆点标记, linewidth=2 控制线条粗细, color 可以接受十六进制颜色码。需要注意的是 ax.grid(true) 这行,很多初学者会忽略网格线,但实际报表里网格线能极大提升数据的可读性。

实际工作中,折线图经常需要叠加多条线做对比。比如对比今年和去年的月度销售额:

months = np.arange(1, 13)
sales_2024 = [8.2, 9.1, 8.8, 10.2, 11.5, 12.1, 11.8, 13.2, 14.5, 15.1, 14.8, 16.2]
sales_2025 = [9.0, 9.8, 10.5, 11.2, 12.8, 13.5, 14.2, 15.8, 16.5, 17.2, 18.1, 19.5]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(months, sales_2024, marker='s', label='2024年', linewidth=2)
ax.plot(months, sales_2025, marker='^', label='2025年', linewidth=2)
ax.set_title("近两年月度销售额对比")
ax.set_xlabel("月份")
ax.set_ylabel("销售额(万元)")
ax.legend()
ax.grid(true, linestyle='--', alpha=0.5)
plt.show()

多条折线对比时,图例 legend() 是必须的,否则看的人分不清哪条线是哪组数据。另外,如果两条线数值范围差距大,可以考虑双y轴,但能用单y轴尽量用单y轴,双y轴容易误导读者,这个是后话。

折线图的一个实用技巧是标注最大值点:

max_idx = np.argmax(sales_2025)
ax.annotate(f'峰值: {sales_2025[max_idx]}万', 
            xy=(months[max_idx], sales_2025[max_idx]),
            xytext=(months[max_idx]+0.5, sales_2025[max_idx]-2),
            arrowprops=dict(arrowstyle='->', color='gray'))

annotate 是标注利器,核心参数是 xy (要标注的点)和 xytext (标注文字位置), arrowprops 控制箭头样式。这个操作在做汇报ppt时特别加分,一眼就能让领导看到关键结论。

2.2 柱状图:分类对比的核心武器

柱状图适用于展示分类数据的数值对比。比如不同产品的销量、不同部门的预算、不同渠道的转化率。它和折线图的本质区别在于:折线图强调连续变化,柱状图强调离散对比。

基础垂直柱状图:

categories = ['产品a', '产品b', '产品c', '产品d', '产品e']
sales = [120, 95, 150, 80, 135]

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(categories, sales, color='#3d8b7b', edgecolor='white')
ax.set_title("各产品销售额对比")
ax.set_xlabel("产品类别")
ax.set_ylabel("销售额(万元)")

# 添加数值标签
for bar in bars:
    height = bar.get_height()
    ax.text(bar.get_x() + bar.get_width()/2., height + 2,
            f'{height}', ha='center', va='bottom', fontsize=11)
plt.show()

这里有个高频需求: 在柱状图顶部显示数值标签 。实现方式是用 ax.text() ,关键是定位: bar.get_x() + bar.get_width()/2 得到柱子的水平中心位置, height + 2 略高于柱子顶部。 ha='center' 水平居中, va='bottom' 垂直底部对齐。

业务场景里更常见的其实是分组柱状图,也就是多个系列并排对比:

# 三个产品线在四个季度的销售额
quarters = ['q1', 'q2', 'q3', 'q4']
product_a = [30, 35, 40, 45]
product_b = [25, 30, 35, 40]
product_c = [20, 25, 30, 35]

x = np.arange(len(quarters))
width = 0.25  # 柱子宽度

fig, ax = plt.subplots(figsize=(10, 6))
bars1 = ax.bar(x - width, product_a, width, label='产品线a')
bars2 = ax.bar(x, product_b, width, label='产品线b')
bars3 = ax.bar(x + width, product_c, width, label='产品线c')

ax.set_xticks(x)
ax.set_xticklabels(quarters)
ax.set_title("各产品线季度销售额对比")
ax.set_ylabel("销售额(万元)")
ax.legend()
ax.grid(axis='y', linestyle='--', alpha=0.5)
plt.show()

分组柱状图的关键是位置计算:三组柱子分别放在 x - width x x + width 的位置,每根柱子宽度为 width 。三根柱子宽度加起来(0.75)不能超过组间距(1),否则会重叠。设置刻度位置用 ax.set_xticks(x) ,再通过 set_xticklabels 替换为分类名称。

我踩过的坑是: grid(axis='y') 可以只显示横向网格线,避免纵向网格线把柱状图搞得很乱。另外,柱状图的柱子太多时(超过10根),横向柱状图 ax.barh 往往比垂直柱状图更合适,因为分类名称有更多空间水平展示。

堆积柱状图是另一个常用变体,适合展示“总量+构成”的场景:

fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(quarters, product_a, label='产品线a')
ax.bar(quarters, product_b, bottom=product_a, label='产品线b')
ax.bar(quarters, product_c, bottom=np.array(product_a)+np.array(product_b), label='产品线c')
ax.set_title("各季度销售额堆积图")
ax.legend()
plt.show()

堆积图的核心参数是 bottom ,它决定当前系列从哪个高度开始画。第二个系列的 bottom 是第一个系列的数据,第三个系列的 bottom 是前两个系列的数据之和。注意 bottom 接收的是数组,所以要做数组加法。

2.3 饼图与环形图:占比表达的注意事项

饼图适合展示整体中各个部分的占比,但有一个严格限制: 类别不要超过5-6个 。类别一多,饼图就变成灾难,完全没法看。这一点在做图之前就要想清楚。

基础饼图:

channels = ['自然搜索', '直接访问', '社交媒体', '付费广告', '外部链接']
traffic = [3500, 2000, 1500, 1000, 500]

fig, ax = plt.subplots(figsize=(7, 7))
wedges, texts, autotexts = ax.pie(
    traffic, labels=channels, autopct='%1.1f%%',
    explode=[0.05, 0, 0, 0, 0],
    shadow=false, startangle=90,
    colors=['#4c72b0', '#55a868', '#c44e52', '#8172b2', '#ccb974']
)
ax.set_title("各渠道流量占比")
plt.show()

参数逐个说: autopct='%1.1f%%' 控制在扇区内部显示百分比,保留一位小数; explode=[0.05, 0, 0, 0, 0] 控制扇区的分离程度,数值越大离圆心越远,适合突出某一个重点类别; startangle=90 把起始角度设为从12点钟方向开始。

如果想把关键部分做得更突出,把 explode 那个类别的值设大一点就行。不过 shadow 参数我建议保持 false ,默认的阴影效果在正式文档里显得不够干净。

还需要个进阶技巧:把占比很小的类别合并成“其他”类。比如上面外部链接只有500,占比不到7%,单独展示意义不大。处理方式是:

# 合并过小的类别
channels_small = [c for c in channels if traffic[channels.index(c)] < 600]
traffic_merged = [t for t in traffic if t >= 600]
labels_merged = [c for c in channels if traffic[channels.index(c)] >= 600]
traffic_merged.append(sum(traffic[t < 600]))
labels_merged.append('其他')

这个操作在实际工作中经常遇到,不然饼图被一堆5%以下的小扇区填满,图例密密麻麻,根本没法看。

环形图其实更好用。所谓环形图就是中间挖掉一块的饼图,留白区域可以放总数、标题或者关键指标。matplotlib里实现环形图的技巧是设置 wedgeprops 参数:

fig, ax = plt.subplots(figsize=(7, 7))
wedges, texts, autotexts = ax.pie(
    traffic, labels=channels, autopct='%1.1f%%',
    startangle=90,
    wedgeprops=dict(width=0.4, edgecolor='white')
)
ax.text(0, 0, f'总流量\n{sum(traffic)}', ha='center', va='center', fontsize=16)
plt.show()

wedgeprops=dict(width=0.4) 是关键,它把饼图的扇区宽度限制为半径的40%,中间就空了。然后通过 ax.text(0, 0, ...) 把总流量写在圆心位置。这在做汇报时比普通饼图好看得多,又能额外传递一个关键数字。

2.4 散点图:变量关系的探索窗口

散点图是探索两个连续变量关系时的首选图形。它的价值在于:一眼看出相关性、聚类趋势、异常点。比如广告投入和销售额的关系、用户年龄和消费金额的关系。

基础散点图:

# 模拟广告投入和销售额数据
np.random.seed(42)
ad_spend = np.random.uniform(10, 50, 60)
sales = 3.5 * ad_spend + np.random.normal(0, 10, 60)

fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(ad_spend, sales, alpha=0.7, edgecolors='white', linewidth=0.5)
ax.set_title("广告投入与销售额关系")
ax.set_xlabel("广告投入(千元)")
ax.set_ylabel("销售额(万元)")
plt.show()

alpha=0.7 控制点的透明度,数据点很多时一定要加透明度,不然重叠区域黑压压一片什么都看不出来。 edgecolors='white' 给点加白色描边,让重叠的点之间能区分边界。

进阶操作是给散点加第三个维度的信息:点的大小映射一个变量,颜色映射另一个变量。这就是俗称的气泡图:

# 模拟城市数据:广告投入、销售额、目标人群规模
population = np.random.uniform(5, 30, 60)  # 人群规模

fig, ax = plt.subplots(figsize=(9, 7))
scatter = ax.scatter(
    ad_spend, sales, 
    s=population * 20,     # 点大小映射人群规模
    c=population,           # 颜色也映射人群规模
    cmap='viridis',
    alpha=0.6, edgecolors='white', linewidth=0.5
)
cbar = plt.colorbar(scatter, ax=ax)
cbar.set_label('人群规模(万人)')
ax.set_title("广告投入、销售额与人群规模气泡图")
ax.set_xlabel("广告投入(千元)")
ax.set_ylabel("销售额(万元)")
plt.show()

c=population 控制点的颜色值, cmap='viridis' 指定色带, plt.colorbar(scatter, ax=ax) 添加颜色条。这张图里x轴、y轴、点大小、点颜色,四个维度同时展示,信息量一下就上来了。

观察散点图时常需要叠加趋势线。最简单的方式是用numpy做线性拟合:

coef = np.polyfit(ad_spend, sales, 1)
poly = np.poly1d(coef)
x_line = np.linspace(ad_spend.min(), ad_spend.max(), 50)
ax.plot(x_line, poly(x_line), color='red', linewidth=2, linestyle='--', label=f'趋势线 y={coef[0]:.2f}x+{coef[1]:.1f}')
ax.legend()

np.polyfit(x, y, 1) 做一阶多项式拟合,返回斜率和截距。 np.poly1d(coef) 生成拟合函数。这条趋势线能直观反映两个变量之间的整体关系方向和相关强度。

2.5 直方图:看数据的分布形态

直方图不是柱状图,这一点很多人容易搞混。直方图是用来展示 单个连续变量的分布情况 的,横轴是变量的取值范围分成的区间(bin),纵轴是这个区间内的样本数量(频率)。

基础直方图:

# 模拟用户年龄数据
np.random.seed(10)
ages = np.random.normal(35, 10, 1000)
ages = np.clip(ages, 18, 68)

fig, ax = plt.subplots(figsize=(9, 6))
ax.hist(ages, bins=30, edgecolor='white', color='#6b8ead', alpha=0.8)
ax.set_title("用户年龄分布")
ax.set_xlabel("年龄")
ax.set_ylabel("人数")
plt.show()

这里 bins=30 是把年龄范围分成30个区间。bins的选择是直方图最容易出问题的地方:太小,分布细节被抹平;太大,噪声太大看不出形状。我的经验是,数据量在1000左右时bins取20-30比较合适;数据量更大时可以适当增加。如果不确定,可以先不传bins参数,让matplotlib自动计算,然后微调。

直方图变成分布密度图,只需要加一个参数 density=true

ax.hist(ages, bins=30, density=true, edgecolor='white', color='#6b8ead', alpha=0.7)

这样纵轴就从“人数”变成了“概率密度”,整个直方图的面积之和为1。这在对比两组不同数量级的数据时特别有用。比如要对比男性和女性的年龄分布,两组样本量不一样,直接用频数直方图没法比,用密度直方图就可以:

male_ages = np.random.normal(32, 8, 800)
female_ages = np.random.normal(38, 9, 500)

fig, ax = plt.subplots(figsize=(9, 6))
ax.hist(male_ages, bins=30, density=true, alpha=0.6, label='男性', color='#4c72b0')
ax.hist(female_ages, bins=30, density=true, alpha=0.6, label='女性', color='#c44e52')
ax.legend()
ax.set_title("男女年龄分布密度对比")
plt.show()

两个分布叠加对比时,必定要加 alpha 透明度,否则后面的分布完全被遮住。

在实际分析中,为了更平滑地展示分布形态,还可以叠加核密度估计曲线。需要使用scipy:

from scipy.stats import gaussian_kde

kde_male = gaussian_kde(male_ages)
kde_female = gaussian_kde(female_ages)

x_range = np.linspace(min(male_ages.min(), female_ages.min()), max(male_ages.max(), female_ages.max()), 200)
ax.plot(x_range, kde_male(x_range), color='#4c72b0', linewidth=2, label='男性密度曲线')
ax.plot(x_range, kde_female(x_range), color='#c44e52', linewidth=2, label='女性密度曲线')

gaussian_kde 是scipy提供的核密度估计工具,它能基于样本数据拟合出平滑的概率密度函数。和直方图相比,密度曲线更平滑、更稳定,不依赖于bins的选择。

2.6 箱线图:分布特征的浓缩表达

箱线图(box plot)用一种非常紧凑的形式展示数据的分布特征,一张图包含五个关键统计量:最小值(下须)、第一四分位数(q1)、中位数(q2)、第三四分位数(q3)、最大值(上须),以及异常值。

基础的箱线图:

# 三个区域的门店订单金额
np.random.seed(5)
region_a = np.random.normal(200, 40, 100)
region_b = np.random.normal(180, 60, 100)
region_c = np.random.normal(220, 30, 100)

data = [region_a, region_b, region_c]

fig, ax = plt.subplots(figsize=(8, 6))
bp = ax.boxplot(data, labels=['华东区', '华南区', '华北区'], patch_artist=true)
ax.set_title("不同区域门店订单金额分布")
ax.set_ylabel("订单金额(元)")

# 设置箱体填充颜色
colors = ['#4c72b0', '#55a868', '#c44e52']
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)

plt.show()

箱线图解读是基本功:箱子底部是q1,顶部是q3,箱子里的横线是中位数。箱子的高度(iqr = q3-q1)代表数据的集中程度,箱子越矮越集中。箱子上下的须线延伸到数据的边界,超出须线范围的数据点(通常是 q3+1.5*iqr 以上的点或 q1-1.5*iqr 以下的点)作为异常值单独画点显示。

patch_artist=true 这个参数很多人不知道,matplotlib默认的箱线图是不填充颜色的,只有白色背景。想要彩色填充必须设置这个参数为true,然后通过 bp['boxes'] 拿到箱体对象逐一设置颜色。

我实际项目里为什么推荐箱线图?因为它是 多组数据分布对比的最佳选择 。比如上面案例里三个区域的门店订单金额,如果用直方图对比,三个分布叠加在一起会乱;但箱线图并排摆放,m型和异常值一目了然,没有任何视觉负担。

箱线图的一种扩展叫小提琴图(violin plot),它把箱线图和密度图结合,既能看出分布形状,又能看到箱体统计量。seaborn里有现成的 violinplot ,但如果只用matplotlib,可以用 ax.violinplot

fig, ax = plt.subplots(figsize=(8, 6))
vp = ax.violinplot(data, positions=[1, 2, 3], showmeans=true, showmedians=true)
ax.set_xticks([1, 2, 3])
ax.set_xticklabels(['华东区', '华南区', '华北区'])
ax.vlines([1, 2, 3], min(np.min(d) for d in data), max(np.max(d) for d in data), 
          linestyle='--', alpha=0.3)

小提琴图的“身子”越胖的地方代表该数值区间的样本越多,能看出分布是单峰还是双峰,比箱线图信息更丰富。

2.7 热力图:矩阵数据的可视化表达

热力图最适合展示矩阵类数据,比如变量间的相关性矩阵、地域数据的分布、时间序列热度表。matplotlib里实现热力图的方案有两种: imshow pcolormesh 。我推荐用 imshow 配合色带,简单高效。

模拟一组变量的相关性矩阵热力图:

# 生成4个变量的相关性矩阵
cov = np.array([[1, 0.8, -0.3, 0.5],
                [0.8, 1, -0.5, 0.2],
                [-0.3, -0.5, 1, -0.7],
                [0.5, 0.2, -0.7, 1]])
data = np.random.multivariate_normal(np.zeros(4), cov, 200)
corr_matrix = np.corrcoef(data.t)

fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(corr_matrix, cmap='coolwarm', vmin=-1, vmax=1)
ax.set_xticks(range(4))
ax.set_yticks(range(4))
ax.set_xticklabels(['收入', '年龄', '评分', '消费频次'])
ax.set_yticklabels(['收入', '年龄', '评分', '消费频次'])
plt.colorbar(im, ax=ax, shrink=0.8)
plt.show()

imshow 接收二维数组,数组每个元素对应一个格子,颜色由 cmap 色带映射。 vmin=-1 vmax=1 把色带范围固定为[-1, 1],相关性矩阵的取值正好在这个范围内,不然色带会自动缩放,导致颜色和数值对不上。

热力图搭配 plt.colorbar(im) 必须有颜色条,否则读者只能看颜色深浅,不知道具体数值。 shrink=0.8 控制颜色条长度,避免太长影响版式。

我还想提一个非常实用的技巧:在热力图上直接标注数值。

for i in range(corr_matrix.shape[0]):
    for j in range(corr_matrix.shape[1]):
        ax.text(j, i, f'{corr_matrix[i, j]:.2f}',
                ha='center', va='center', fontsize=11,
                color='white' if abs(corr_matrix[i, j]) > 0.5 else 'black')

双重循环遍历矩阵,用 ax.text 在每个格子中心写入数值。文字颜色根据背景色的深浅动态选择:背景颜色深(相关性绝对值大)时用白色文字,背景浅时用黑色文字。这个小细节能让热力图的数值可读性大幅提升。

3. 组合布局与整图视觉优化

3.1 子图布局:一张图里摆多张图的正确姿势

实际工作中,很少只画一张孤零零的图。常见场景是:一个报表页里左边放折线图看趋势,右边放柱状图看对比;或者上面是散点图,下面是直方图。子图布局就是解决这个问题的。

最基础的方法是 plt.subplots() 直接指定行列数:

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# axes是一个2x2的数组,通过索引访问每个子图
axes[0, 0].plot(months, sales_2025, color='#2e86ab')
axes[0, 0].set_title("2025年销售趋势")

axes[0, 1].bar(categories, sales, color='#3d8b7b')
axes[0, 1].set_title("产品销售额对比")

axes[1, 0].scatter(ad_spend, sales_data, alpha=0.6)
axes[1, 0].set_title("广告与销售关系")

axes[1, 1].hist(ages, bins=25, alpha=0.7)
axes[1, 1].set_title("用户年龄分布")

plt.tight_layout()
plt.show()

plt.subplots(2, 2) 返回一个2行2列的axes数组,索引方式 axes[row, col] 。每个子图的用法和单图完全一样。最后一定要加 plt.tight_layout() ,不然子图之间会挤在一起,标题和刻度标签互相重叠。这句是子图布局的灵魂。

如果希望子图之间共享坐标轴,比如两行图共享y轴刻度,可以在 subplots 里指定 sharey=true sharex=true 。这在对比同一指标的不同分组时非常有用,保证了图形之间的可比性。

更复杂的布局,比如第一行一个大图、第二行两个小图,用 gridspec 实现:

from matplotlib.gridspec import gridspec

fig = plt.figure(figsize=(10, 9))
gs = gridspec(2, 2, height_ratios=[1.5, 1], width_ratios=[2, 1])

ax1 = fig.add_subplot(gs[0, :])   # 第一行,跨两列
ax2 = fig.add_subplot(gs[1, 0])   # 第二行,左列
ax3 = fig.add_subplot(gs[1, 1])   # 第二行,右列

ax1.plot(months, sales_2025, color='#2e86ab')
ax1.set_title("全年度销售趋势")

ax2.bar(categories, sales, color='#3d8b7b')
ax2.set_title("产品销售对比")

ax3.scatter(ad_spend, sales_data, alpha=0.6)
ax3.set_title("广告与销售关系")

plt.tight_layout()
plt.show()

gridspec(2, 2) 创建2行2列的网格, height_ratios width_ratios 控制行高和列宽比例。 gs[0, :] 表示第一行跨所有列。这种布局在做数据分析报告时非常常见,一页纸就能完整展示三个维度的信息。

3.2 字体的全局配置与中文乱码解决

matplotlib默认字体不支持中文,直接写中文标签会显示成一个个方框(豆腐块)。这是中文用户最常见的坑,解决办法是设置中文字体。

我的做法是在每个绘图脚本的开头写上全局配置:

import matplotlib as mpl

plt.rcparams['font.sans-serif'] = ['simhei', 'microsoft yahei', 'pingfang sc', 'noto sans cjk sc']
plt.rcparams['axes.unicode_minus'] = false

第一行指定字体列表,matplotlib会按顺序查找可用的中文字体。“simhei”对应windows系统的黑体,“microsoft yahei”是微软雅黑,“pingfang sc”是mac的苹方,“noto sans cjk sc”是linux下的开源思源黑体。为什么要写多个?因为你不知道代码会在哪个系统上跑,多写几个能保证兼容性,哪个存在用哪个。

第二行 axes.unicode_minus = false 是另一个隐藏的坑:如果不设置,坐标轴的负号会显示成方块。因为matplotlib默认把负号当unicode字符处理,而中文字体里没有对应的字形。

检查系统里有哪些可用字体,用以下代码:

import matplotlib.font_manager as fm

fonts = [f.name for f in fm.fontmanager.ttflist]
cjk_fonts = [f for f in fonts if any(kw in f for kw in ['hei', 'song', 'yahei', 'pingfang', 'noto', 'wenquanyi'])]
print(cjk_fonts)

如果知道自己系统里实际装了哪种字体,直接单独设置更清爽:

plt.rcparams['font.sans-serif'] = ['pingfang sc']

3.3 grid、图例和坐标轴的微调技巧

很多图的问题不在主体图形,而在细节没调好。这里分享几个高频微调技巧。

网格线: ax.grid(true, linestyle='--', alpha=0.6) 基本是标配。 linestyle='--' 用虚线防止网格过度干扰数据展示。做柱状图时建议 ax.grid(axis='y', ...) 只保留横向网格线,竖向的会和新柱子边缘重叠。

图例位置: ax.legend() 默认放在右上角,经常挡住数据。推荐用 loc='best' 让其自动避让,或者手动指定位置:

ax.legend(loc='upper left', frameon=true, fancybox=true, shadow=false, fontsize=11)

loc 的常用取值有:'upper right'、'upper left'、'lower left'、'lower right'、'center left'、'center right',还有对应数字编码0-10。 frameon=true 给图例加边框, fancybox=true 让边框圆角化,视觉上更柔和。

坐标轴范围: ax.set_xlim(min_val, max_val) ax.set_ylim(min_val, max_val) 可以自定义坐标轴范围。在直方图等场景里,数据有少量离群点导致图形被压扁时,手动调整y轴范围可以让主体部分更清晰。但要小心:不要为了好看而把范围调得过窄,导致真实分布被隐藏。

坐标轴刻度密度也是常见问题。日期数据做x轴时,如果数据跨几个月,matplotlib默认会每隔几天标一个刻度,标签重叠成一团。解决办法是:

import matplotlib.dates as mdates

ax.xaxis.set_major_locator(mdates.monthlocator())   # 每月显示一个主刻度
ax.xaxis.set_major_formatter(mdates.dateformatter('%y-%m'))
plt.xticks(rotation=45)

monthlocator 控制一个月一个刻度, dateformatter 控制刻度标签的格式, rotation=45 让日期文字倾斜45度避免重叠。

3.4 保存图片的参数选择与格式对比

画完图以后,保存是关键一步。 plt.show() 只是预览,最终要通过 plt.savefig() 输出文件:

fig.savefig('output.png', dpi=200, bbox_inches='tight', facecolor='white')

dpi=200 是常用设置。默认保存的dpi是100,放大看会有锯齿;学术投稿或ppt用建议300,普通报表150-200足够。 bbox_inches='tight' 会自动裁掉图片周围的空白区域,让图片更紧凑,强烈建议每次都加。

facecolor='white' 指定背景色。如果你在jupyter notebook里画的图主题是深色的,直接保存背景也会是深色,打印出来没法看,需要手动设为白色。

不同格式的选择上,我建议:

格式适用场景特点
png通用、网页、ppt有损压缩,透明背景支持好,文件小
jpg照片类、插图有损压缩,文件更小,但不支持透明
pdf论文、正式文档矢量图,无限放大不糊,推荐
svg网页端、可编辑矢量,可后续编辑,文件稍大

论文投稿我强烈推荐pdf格式,矢量图放大多少都不怕糊。ppt里则建议png,兼容性最好。如果在一个窗口里同时显示和保存,注意 plt.savefig() 要在 plt.show() 之前调用,因为 show() 执行后大部分后端会清空画布。

4. 常见问题与排查经验实录

4.1 图表中文显示乱码或方框

这是中文用户遇到最多的问题,没有之一。现象是图表标题、坐标轴标签显示成一个个小方框。原因很简单:matplotlib默认字体(dejavu sans)里没有中文字形。

定位问题先执行:

import matplotlib.pyplot as plt
plt.rcparams['font.sans-serif'] = ['simhei']  # 按实际系统改

再看效果。还不行的话,确认系统中文字体路径是否正确,用 fm.fontmanager.ttflist 列出来检查。

如果公司环境不允许随便安装字体,还有一个备选方案:给特定的text对象单独指定字体字号:

ax.set_title("销售趋势", fontproperties='pingfang sc', fontsize=16)

但这个方法不适合多文本批量设置,全局 rcparams 还是更省事。

4.2 刻度标签重叠与图像布局混乱

图一多,布局崩是常态。最常见的是三个问题:子图挤在一起、x轴刻度标签重叠、图例遮挡数据主体。

子图挤在一起:在 plt.show() savefig 前加 plt.tight_layout() 基本能解决。如果tight_layout还不行(复杂gridspec布局下偶尔会失效),使用 plt.subplots_adjust(left=0.1, right=0.95, top=0.9, bottom=0.1, wspace=0.3, hspace=0.4) 手动调整边距和子图间距。 wspace hspace 分别控制子图的水平、垂直间距,范围0-1,数值越大间距越大。

刻度标签重叠:最快捷的办法是旋转角度。

plt.xticks(rotation=45, ha='right')

ha='right' 让文字右下对齐,旋转后更好看。时间序列的日期标签重叠问题,用上一节讲的 monthlocator 思路解决,配合 rotation=45 基本一劳永逸。

图例遮挡数据:优先 loc='best' 自动避让,如果自动避让的效果还是遮挡,就用 bbox_to_anchor 手动定位:

ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1))

bbox_to_anchor=(1.02, 1) 把图例放到坐标轴右侧外边,完全不遮挡绘图区域。这个方法在数据曲线较多的时候特别管用。

4.3 保存的图片模糊或者背景不对

保存的图片模糊,首先查的是 dpi savefig(dpi=300) 后图片会清晰很多。如果dpi已经调到300还模糊,检查 figsize 设置。 plt.subplots(figsize=(8, 5)) 表示8英寸×5英寸,乘以dpi就是输出像素。 figsize=(8, 5), dpi=100 得到800×500像素图; figsize=(8, 5), dpi=300 得到2400×1500像素,差距明显。

保存背景不对,多半是主题或画布背景色影响。jupyter notebook或者某个深色主题下画的图,savefig出来的背景是深灰色。处理方式在 savefig 里加 facecolor='white' ,或者画布创建时就指定:

fig, ax = plt.subplots(figsize=(8, 5), facecolor='white')

4.4 数据量大时绘图卡顿

几万个点的散点图或者高分辨率的直方图,matplotlib画起来会明显卡顿。处理思路有两种:降采样和改变绘制方式。

散点图数据量上万后,渲染压力主要来自每个点的描边和透明度计算。可以把 edgecolors 去掉,减少抗锯齿计算:

ax.scatter(x_data, y_data, s=5, alpha=0.5, linewidths=0)

数据量超过10万,建议随机采样后再画:

sample_idx = np.random.choice(len(x_data), size=20000, replace=false)
ax.scatter(x_data[sample_idx], y_data[sample_idx], s=3, alpha=0.3)

做探索性分析时,20万随机样本的分布形态已经非常接近全量数据了,完全够用。

imshow 处理超大矩阵时,可以设置 interpolation='nearest' 减少插值计算,分块渲染可以设置 rasterized=true

ax.imshow(large_matrix, cmap='viridis', interpolation='nearest', rasterized=true)

rasterized=true 的意思是该对象在保存矢量图(pdf/svg)时以栅格形式嵌入,这样文件大小能压缩到原来的十分之一。

4.5 读取数据时类型不对导致的绘图失败

还有一个很高频的坑,新手经常忽略。比如pandas读取excel后,某一列日期被当成字符串,直接拿去画折线图,x轴就乱了。

建议读取数据时先做类型转换:

import pandas as pd

df = pd.read_excel('sales.xlsx')
df['日期'] = pd.to_datetime(df['日期'])
df['销售额'] = pd.to_numeric(df['销售额'], errors='coerce')

pd.to_datetime 把日期字符串转为时间类型, pd.to_numeric 把数值字符串转为浮点数,出错的值设为nan。转换之后再用 df.plot(x='日期', y='销售额') 或者 ax.plot(df['日期'], df['销售额']) 就不会有问题了。

判断列类型的快速方式: print(df.dtypes) ,object类型大概率是字符串,需要转换。这个排查思路在数据导入类问题里能帮你省大量时间。

5. 图表的编排与输出实践

5.1 把图表输出成一个标准的分析报表页

工作中一个常见的需求是:把多个图表组合成一个完整的报表页面,自动输出。我通常的做法是写一个脚本,一次性生成所有图表并保存为图片文件,再用pil或reportlab拼接输出。

用matplotlib直接拼接图表的话,就用前面gridspec的技巧,把所有图集中到一张画布上。如果图太多,建议分开保存,再用pillow拼接:

from pil import image

img_list = []
for f in ['chart1.png', 'chart2.png', 'chart3.png', 'chart4.png']:
    img = image.open(f)
    img_list.append(img)

# 假设四张图,2x2拼接
new_img = image.new('rgb', (img_list[0].width * 2, img_list[0].height * 2), 'white')
new_img.paste(img_list[0], (0, 0))
new_img.paste(img_list[1], (img_list[0].width, 0))
new_img.paste(img_list[2], (0, img_list[0].height))
new_img.paste(img_list[3], (img_list[0].width, img_list[0].height))
new_img.save('combined_report.png')

注意拼接前确保所有图片尺寸一致,或者用 img.resize() 统一尺寸。这种方案的好处是每张子图都可以单独调整,对单张图的报错排查更方便。

如果是定期生成报表,建议把绘图脚本封装成函数。核心思路是把数据和参数传进去,返回 fig 对象。比如:

def plot_line_chart(x, y, title, xlabel, ylabel, save_path=none):
    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(x, y, marker='o', linewidth=2, color='#2e86ab')
    ax.set_title(title)
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.grid(true, linestyle='--', alpha=0.6)
    if save_path:
        fig.savefig(save_path, dpi=200, bbox_inches='tight')
    plt.close(fig)
    return fig

plt.close(fig) 这句很重要。如果不关闭figure对象,循环生成几十张图,内存会一直涨,最终卡死notebook。这是一个很隐蔽的性能问题,我早期就吃过这个亏。

5.2 画图过程中的调试思路

画图问题千奇百怪,但排查思路是固定的: 从默认参数入手,逐步添加自定义设置 。出现奇怪效果时,先把自定义参数删掉,画最基础的图,确认数据没问题后,再一个一个加回参数,定位是哪个参数导致的问题。

还有一个实用技巧:jupyter notebook里用 %matplotlib inline 画图时,建议在关键步骤后加 plt.show() 及时查看中间效果,而不是等所有代码跑完一次性输出。这样问题在哪一步出现,立刻就能定位。

如果图表内容比较多,一个一个参数试太慢,就用二分法:把绘图相关代码隔离到一个独立单元格,每次注释掉一半参数,看结果是否恢复正常。这个过程通常能在几分钟内锁定问题根源。

5.3 从这张代码图到复用的绘图模板

常用的样式配置,我会整理成一个固定的配置块放在脚本开头,作为自己的“绘图模板”:

# 绘图全局配置
import matplotlib as mpl
mpl.rcparams.update({
    'font.sans-serif': ['pingfang sc', 'simhei', 'microsoft yahei'],
    'axes.unicode_minus': false,
    'figure.dpi': 120,
    'axes.grid': true,
    'grid.linestyle': '--',
    'grid.alpha': 0.5,
    'axes.spines.top': false,
    'axes.spines.right': false,
    'legend.frameon': false,
})

上面几个配置的作用: axes.spines.top/right 去掉顶部和右侧的坐标轴线,视觉上更简洁; legend.frameon 去掉图例边框,让图例与图形融合。这些是默认主题不够好看的核心原因,一改完整个观感立刻提升。

这套配置我基本每台新电脑都会先写好,然后所有绘图脚本都基于它运行。颜色方案方面,我有几个习惯用色: #2e86ab (主题蓝)、 #c44e52 (红)、 #55a868 (绿)、 #8172b2 (紫)、 #ccb974 (土黄)。这些颜色来自五色原则,即使黑白打印也有区分度。

写在最后

绘图这活儿,说到底不是记住多少个参数,而是培养一种“拿到数据,脑海里先浮现出最合适的图”的直觉。我自己的经验是:先想清楚要表达什么结论——趋势看折线、对比看柱状、占比看饼环、相关看散点、分布看直方箱线、矩阵看热力——然后再动手写代码,效率高得多。

这期把matplotlib最核心的几类统计图完整过了一遍,代码和参数都是平时项目里直接复制就能用的。如果你在实际绘图时遇到奇怪的问题,别急着翻文档,先把参数一个个还原成默认值,往往马上就找到原因了。下一篇我打算讲讲统计图的配色和排版进阶,把图表从“能看”做到“好看”,我们到时候见。

以上就是python matplotlib统计图绘制全攻略(附完整代码)的详细内容,更多关于python matplotlib统计图的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com