Matplotlib统计图绘制实战:中文乱码与子图布局排查
数据预处理跑通之后,最终要把结论用图讲清楚。本文围绕 Python 的 matplotlib,把常用统计图逐个画一遍,代码直接给,参数逐个解释,重点放在函数参数、调用方式和容易踩的坑上。这套内容适合做数据分析报表、技术方案和业务汇报时直接复用。一、选型与底层概念
绘图库不少,seaborn、plotly、pyecharts 都能出图,但 matplotlib 仍然绕不开。原因是 Python 生态里大量绘图工具底层依赖它:seaborn 底层是 matplotlib,pandas 的 plot 方法底层也是 matplotlib,很多高级封装库的源码最后调用的还是 matplotlib 的 API。把 matplotlib 搞明白,后面迁移到封装库不会慌。
matplotlib 默认样式不算好看,但坐标轴、刻度、图例、标注都能精确控制,学术论文和技术报告里的出图质量经过长期检验。如果做 Dashboard 或强交互图表,plotly 和 pyecharts 更合适;做探索性分析、快速出图、批量生成报表,matplotlib 依然最顺手。
环境准备:
pip install matplotlib numpy
numpy 建议一起装,生成数据、算统计量都离不开它。导入方式固定成:
import matplotlib.pyplot as plt
import numpy as np
最小可运行脚本,也叫“三行出图法”:
plt.figure(figsize=(8, 5))
plt.plot(, )
plt.show()
这段代码把绘图核心流程跑通了:先铺画布 figure,再动笔 plot,最后展示或保存 show/savefig。
但复杂布局时,pyplot 全局接口就不够用了。需要理解 Figure 和 Axes:Figure 是整张画布,Axes 是画布上的一块绘图区域。Figure 像书桌,Axes 像铺在书桌上的纸,一张书桌可以铺好几张纸(subplots),每张纸上画不同内容,纸张也可以大小不一、位置不同(GridSpec)。
推荐从一开始就用面向对象写法:
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(, )
ax.set_title('折线图示例')
ax.set_xlabel('X轴')
ax.set_ylabel('Y轴')
plt.show()
plt.subplots() 返回 fig 和 ax 两个对象,后续操作都通过 ax 调用。画子图时,这种写法的可读性和可扩展性明显高于全局接口。
二、折线图:趋势数据
折线图用来展示数据随时间或其他连续变量的变化趋势,业务里常见于日访问量、周销售额、月用户增长。
基础版:
import matplotlib.pyplot as plt
import numpy as np
# 模拟一周的日访问量数据
days = np.arange(1, 8)
visits =
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 =
sales_2025 =
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}万',
xy=(months, sales_2025),
xytext=(months+0.5, sales_2025-2),
arrowprops=dict(arrowstyle='->', color='gray'))
annotate 的核心参数是 xy(被标注的点)和 xytext(标注文字位置),arrowprops 控制箭头样式。做汇报时这个操作很加分,关键结论一眼可见。
三、柱状图:分类对比
柱状图展示分类数据的数值对比,比如不同产品销量、不同部门预算、不同渠道转化率。它和折线图的区别在于:折线图强调连续变化,柱状图强调离散对比。
基础垂直柱状图:
categories = ['产品A', '产品B', '产品C', '产品D', '产品E']
sales =
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()
柱顶显示数值是高频需求。关键是定位:bar.get_x() + bar.get_width()/2 得到柱子水平中心,height + 2 略高于柱子顶部,ha='center' 水平居中,va='bottom' 垂直底部对齐。
分组柱状图:
quarters = ['Q1', 'Q2', 'Q3', 'Q4']
product_a =
product_b =
product_c =
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 接收数组,所以要做数组加法。
四、饼图与环形图
饼图展示整体中各部分占比,但有严格限制:类别不要超过 5 到 6 个,类别一多就没法看。
基础饼图:
channels = ['自然搜索', '直接访问', '社交媒体', '付费广告', '外部链接']
traffic =
fig, ax = plt.subplots(figsize=(7, 7))
wedges, texts, autotexts = ax.pie(
traffic, labels=channels, autopct='%1.1f%%',
explode=,
shadow=False, startangle=90,
colors=['#4C72B0', '#55A868', '#C44E52', '#8172B2', '#CCB974']
)
ax.set_title('各渠道流量占比')
plt.show()
参数逐个说:autopct='%1.1f%%' 在扇区内部显示百分比并保留一位小数;explode 控制扇区分离程度,数值越大离圆心越远,适合突出重点类别;startangle=90 把起始角度设为从 12 点钟方向开始;shadow 建议保持 False,默认阴影在正式文档里不够干净。
占比很小的类别可以合并成“其他”类。原文的处理思路是按阈值遍历 channels 和 traffic,把低于阈值的类别数值累加,标签换成“其他”,再重新传给 ax.pie。外部链接只有 500,占比不到 7%,单独展示意义不大,合并后图例不会密密麻麻。
环形图更实用。环形图就是中间挖空的饼图,留白处可以放总数、标题或关键指标。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, ...) 把总流量写在圆心。汇报时比普通饼图好看,还能多传递一个关键数字。
五、散点图:变量关系
散点图用于探索两个连续变量的关系,一眼看出相关性、聚类趋势和异常点,比如广告投入与销售额、用户年龄与消费金额。
基础散点图:
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:.2f}x+{coef:.1f}')
ax.legend()
np.polyfit(x, y, 1) 做一阶多项式拟合,返回斜率和截距;np.poly1d(coef) 生成拟合函数。趋势线能直观反映两个变量之间的整体关系方向和相关强度。
六、直方图:分布形态
直方图不是柱状图,它展示单个连续变量的分布,横轴是变量取值范围分成的区间 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 基于样本数据拟合平滑的概率密度函数。和直方图相比,密度曲线更平滑、更稳定,不依赖 bins 的选择。
七、箱线图:分布特征浓缩
箱线图用紧凑形式展示分布特征,一张图包含五个关键统计量:最小值(下须)、第一四分位数 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 =
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'] 拿到箱体对象逐一设置。
箱线图是多组数据分布对比的好选择。三个区域订单金额如果用直方图叠加会乱,箱线图并排摆放,中位数和异常值一目了然。
扩展形式是小提琴图,把箱线图和密度图结合:
fig, ax = plt.subplots(figsize=(8, 6))
vp = ax.violinplot(data, positions=, showmeans=True, showmedians=True)
ax.set_xticks()
ax.set_xticklabels(['华东区', '华南区', '华北区'])
ax.vlines(, min(np.min(d) for d in data), max(np.max(d) for d in data),
linestyle='--', alpha=0.3)
小提琴图“身子”越胖的地方代表该数值区间样本越多,能看出分布是单峰还是双峰,信息比箱线图更丰富。
八、热力图:矩阵数据
热力图适合矩阵类数据,比如变量间相关性矩阵、地域数据分布、时间序列热度表。matplotlib 里实现热力图有两种方案:imshow 和 pcolormesh,原文推荐用 imshow 配合色带,写法更简单。
九、常见问题
原文在后续章节列出了几类高频问题,值得在写绘图代码时提前留意:
中文显示乱码或方框。matplotlib 默认字体不含中文字形,涉及中文标题、坐标轴标签、图例时需要做全局字体配置。
刻度标签重叠与布局混乱。分类名称长、子图数量多时容易出现,需要调整刻度显示方式或布局参数。
保存的图片模糊或背景不对。与 savefig 的参数选择、输出格式有关,原文在“保存图片的参数选择与格式对比”一节中展开。
数据量大时绘图卡顿。散点图、直方图样本量很大时性能下降明显。
读取数据时类型不对导致绘图失败。从文件读入的数据如果被识别成字符串,绘图时就会报错,需要在绘图前检查 dtype。
结语
matplotlib 的默认样式一般,但胜在坐标轴、刻度、图例、标注都能精确控制。把 Figure 和 Axes 的概念理顺,记住折线图的 marker/linewidth/grid、柱状图的数值标签定位和分组位置计算、饼图的 autopct/explode/wedgeprops、散点图的 alpha/colorbar/polyfit、直方图的 bins/density/kde、箱线图的 patch_artist 和 IQR 判定,常用统计图基本都能覆盖。后续把它封装成绘图模板,批量出报表时复用即可。
Re: Matplotlib统计图绘制实战:中文乱码与子图布局排查
楼主整理得挺实在的,尤其是从三行出图法切入,再转到 fig、ax 的面向对象写法,这个路径对新手很友好。Figure 和 Axes 用书桌和纸来类比,理解子图布局确实顺很多。折线图部分参数解释得很细,marker、linewidth、grid、legend 和 annotate 这些点都是实际做报表经常用到的,尤其是多线对比必须加图例、峰值标注能直接突出结论,这点很有共鸣。柱状图刚开了个头,期待后续把分类对比、中文乱码和子图布局排查继续写完。标题里提到的中文乱码和子图布局,确实是 matplotlib 最让人头疼的地方,如果能结合代码把常见报错和排查思路讲清楚,这篇就很适合收藏复用了。Re: Matplotlib统计图绘制实战:中文乱码与子图布局排查
内容很实用,尤其是从 pyplot 全局接口切到 fig、ax 面向对象的写法,这个建议很关键,后面子图一多确实不容易乱。三行出图法过渡到 Figure 和 Axes 的比喻也挺好懂,书桌和纸的关系一下就清楚了。折线图里 marker、linewidth、grid、legend 和 annotate 这些参数都点到了,峰值标注那段做汇报很加分;双 Y 轴能不用就不用的提醒也很实在。柱状图部分好像在这里断了,正看到 color='#3D8B 就没有了。标题里的中文乱码和子图布局排查还没展开,这两块是最容易卡人的,蹲一个后续。整体很适合拿来当速查笔记。Re: Matplotlib统计图绘制实战:中文乱码与子图布局排查
感谢楼主分享,内容很扎实,尤其是把 Figure 和 Axes 的关系用书桌和纸来比喻,对刚上手的朋友很友好。我补充几个自己踩过的坑。 中文乱码这块,最省事的方案是在脚本开头加两行配置: plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False 第二行很多人会漏掉,不加的话负号会变成方块。Linux 服务器上如果没有 SimHei,可以换成 WenQuanYi Micro Hei 或者 Noto Sans CJK SC,先确认系统里装了对应字体再配置,不然还是会静默回退到默认字体继续乱码。保存图片时如果用的是 savefig,最好把 dpi 设到 150 以上,不然中文边缘发虚。 子图布局方面,plt.subplots 的 figsize 和 tight_layout 经常打架。我的习惯是先 plt.subplots(2, 2, figsize=(12, 8)),画完统一点 fig.tight_layout(),比手动调 subplots_adjust 省心。如果子图之间横纵尺度差异大,可以考虑 constrained_layout=True,比 tight_layout 对新版 matplotlib 更友好,图例、色条这些也能自动避让。 还有一个容易忽略的点:面向
页:
[1]