数据预处理跑通之后,最终要把结论用图讲清楚。本文围绕 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([1, 2, 3, 4], [1, 4, 9, 16])
- plt.show()
复制代码
这段代码把绘图核心流程跑通了:先铺画布 figure,再动笔 plot,最后展示或保存 show/savefig。
但复杂布局时,pyplot 全局接口就不够用了。需要理解 Figure 和 Axes: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 和 ax 两个对象,后续操作都通过 ax 调用。画子图时,这种写法的可读性和可扩展性明显高于全局接口。
二、折线图:趋势数据
折线图用来展示数据随时间或其他连续变量的变化趋势,业务里常见于日访问量、周销售额、月用户增长。
基础版:
- 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 控制箭头样式。做汇报时这个操作很加分,关键结论一眼可见。
三、柱状图:分类对比
柱状图展示分类数据的数值对比,比如不同产品销量、不同部门预算、不同渠道转化率。它和折线图的区别在于:折线图强调连续变化,柱状图强调离散对比。
基础垂直柱状图:
- 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()
复制代码
柱顶显示数值是高频需求。关键是定位: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 接收数组,所以要做数组加法。
四、饼图与环形图
饼图展示整体中各部分占比,但有严格限制:类别不要超过 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 控制扇区分离程度,数值越大离圆心越远,适合突出重点类别;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[0]:.2f}x+{coef[1]:.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 = [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'] 拿到箱体对象逐一设置。
箱线图是多组数据分布对比的好选择。三个区域订单金额如果用直方图叠加会乱,箱线图并排摆放,中位数和异常值一目了然。
扩展形式是小提琴图,把箱线图和密度图结合:
- 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)
复制代码
小提琴图“身子”越胖的地方代表该数值区间样本越多,能看出分布是单峰还是双峰,信息比箱线图更丰富。
八、热力图:矩阵数据
热力图适合矩阵类数据,比如变量间相关性矩阵、地域数据分布、时间序列热度表。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 判定,常用统计图基本都能覆盖。后续把它封装成绘图模板,批量出报表时复用即可。 |