☰
NumPy+Matplotlib:从数组计算到数据可视化的完整实战
2026/10/14 14:42:01 网站建设 项目流程

做数据分析的人,十有八九都绕不开这两个库:NumPy负责把数据变成能算的数组,Matplotlib负责把算完的结果变成能看的图。我最早接触Python数据分析时,最直观的感受就是——光会调函数没用,得先把“数据在NumPy里怎么流动、到Matplotlib里怎么映射”这条链路想清楚,后面画图才能又快又稳。这篇内容就是专门聊一聊“Numpy之matplotlib”这个组合,不讲虚的,只讲怎么用NumPy生成、清洗、统计出结果,再交给Matplotlib变成折线图、散点图、柱状图和直方图,同时把中文乱码、坐标轴负号、子图布局这些高频坑一起排掉。适合刚入门Python数据分析的初学者,也适合那些经常画图但总在细节上返工的同学。

1. 先把定位搞清楚:NumPy算数据,Matplotlib画数据

1.1 两个库的分工,别再混着用

NumPy的核心是ndarray对象,也就是高维数组。它负责的事情包括:生成数据(np.linspace、np.arange、np.random)、数学运算(np.sin、np.exp、np.mean、np.std)、索引切片、条件筛选、排序聚合。说白了,它是整个数据处理流程里的“计算层”。

Matplotlib的核心是Figure和Axes。Figure是一整张画布,Axes是画布上具体的坐标系区域。它负责的事情是把数据点映射成点、线、柱、面积,再配上坐标轴、图例、标题。也就是数据的“表达层”。

可以把这个组合理解成后厨的关系:NumPy像备菜工,负责洗菜、切菜、腌制;Matplotlib像掌勺厨师,负责把处理好的食材变成一盘能端上桌的菜。食材没处理好,厨师再厉害也白搭;菜摆盘再好看,食材不新鲜也露馅。

为什么这对组合在数据分析里如此常见?因为不管数据来自CSV文件、数据库还是接口返回,进入Python之后第一步基本都是想办法转成ndarray,然后做清洗和计算。计算完的结果天然是ndarray,而Matplotlib的绘图函数恰好直接接收这个结构,不需要你再转成纯Python列表,省掉的不仅是转换代码,还有大量不必要的循环损耗。

很多人觉得数据量小,用纯Python列表也能算,没必要上NumPy。这个观点在几百个点的时候确实没毛病,可一旦上了几万、几十万个点,纯Python的for循环能慢到让你怀疑人生。NumPy的向量化运算底层是C实现,一条代码把循环整个干掉。我在某次处理实验数据时,用纯Python循环处理十万条样本,一次跑了好几分钟,改成NumPy向量化之后秒出结果。这种差距在画图前的数据清洗阶段尤其明显。

任务纯Python写法NumPy写法体验
计算sin值循环逐个算np.sin(大数组)快几倍到几十倍
求均值sum/len或statisticsnp.mean(数组)一行搞定
条件筛选列表推导数组布尔索引不用写循环

1.2 为什么数据非要在NumPy里“过一遍”再画

我见过不少新手,拿到数据直接往plt.plot里塞,结果图是出来了,后面要加个统计指标、要筛个异常值、要按区间聚合,全得从头再来一遍。原因就是没在画图前利用NumPy把数据处理干净。

真实业务里的数据很少是干净的。缺了值、单位不统一、异常点横冲直撞,这些情况如果在绘图之前不处理,图上就会出现莫名其妙的长线和尖峰。NumPy的价值就在于,你可以在数组层面上快速完成:

  • 去缺失值:用布尔索引把包含空值的行筛掉
  • 修剪异常值:np.clip把超出合理区间的值压回边界
  • 归一化:数组减去均值再除以标准差,标准做法,代码就一行
  • 排序和去重:np.sort、np.unique配合return_counts参数

处理完之后,数据结构依然是ndarray,拿来画图刚刚好。所以我的习惯是:先花80%的精力用NumPy把数据整理到“可画”的程度,再用20%精力调Matplotlib样式,图的质量自然就上来了。反过来的话,图改十遍也救不了一组脏数据。

2. 从NumPy数组到第一张图:核心工作流拆解

2.1 用NumPy快速造出“能画图的数据”

实际项目里数据源五花八门,但学习阶段最方便的方式是先用NumPy生成模拟数据。画图本质上是把两个序列对应起来:一个放在x轴,一个放在y轴,所以第一步永远是确定x怎么来、y怎么来。

最常见的数据生成函数有三个:

  • np.linspace(start, stop, num):在区间内生成num个等差点,包含首尾,画曲线最常用。
  • np.arange(start, stop, step):按步长生成等差序列,类似range但返回数组。
  • np.random.randn(n) 和 np.random.rand(n):生成标准正态分布或[0,1)均匀分布的随机数组,适合模拟噪声和散点。

比如要画一条正弦曲线:

import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x)

这段代码的意思是在[0, 2π]之间均匀取100个点作为x轴坐标,再对每个x计算正弦值作为y轴坐标。为什么用100个点而不是10个?因为点太少画出来就是一段段折线,点足够密曲线才平滑。至于图上要多少个点,取决于你对平滑度的要求,一般曲线100到500个点就够用了。

2.2 最简单也最常用的两种图:折线图和散点图

折线图适合展示连续变化趋势,散点图适合展示离散分布和相关性。这两种图是后续所有复杂图表的地基。

推荐直接使用面向对象的写法:

# 折线图 x = np.linspace(0, 2 * np.pi, 100) y = np.sin(x) fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y, label='sin(x)', color='#2E86AB') ax.set_title('正弦曲线') ax.set_xlabel('x (rad)') ax.set_ylabel('sin(x)') ax.legend() ax.grid(True) plt.show()
# 散点图 np.random.seed(42) x = np.random.randn(200) y = 2 * x + np.random.randn(200) * 0.5 fig, ax = plt.subplots(figsize=(8, 6)) ax.scatter(x, y, s=20, alpha=0.7) ax.set_title('带噪声的线性关系') ax.set_xlabel('x') ax.set_ylabel('y=2x+noise') plt.show()

为什么不用plt.plot一路画到底?plt.plot是pyplot接口,底层隐藏了一个“当前axes”的概念,简单画一两次没问题,但一旦画多个子图、需要精细控制坐标轴范围或共用坐标轴时,面向对象接口更稳。我一开始也习惯plt.plot,后来做多子图画错了好几次内容,才彻底改成fig, ax = plt.subplots()的写法,后面几乎没有再犯过类似错误。

2.3 把NumPy的计算结果直接喂给Matplotlib

要理解的一点是:plot、scatter这些函数接收ndarray,不需要手动转成list。实际场景里画的不会只有sin和cos,往往是对数据做了一堆统计、归一化、排序之后,再把结果直接扔给绘图。

举个常见例子,模拟带噪声的信号,再叠加均值参考线:

np.random.seed(10) x = np.linspace(0, 4 * np.pi, 300) signal = np.sin(x) + np.random.normal(0, 0.2, size=x.shape) mean_val = np.mean(signal) fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(x, signal, label='带噪声信号', alpha=0.7) ax.axhline(y=mean_val, color='red', linestyle='--', label=f'均值={mean_val:.2f}') ax.legend() plt.show()

这里的axhline是Matplotlib画水平参考线的方法。先用np.mean算出信号的均值,再画成一条虚线当辅助线,一眼就能看出噪声信号的整体水平。这种“先用NumPy算一个统计值,再画到图上当参考线”的做法,在业务汇报图表里非常实用。

2.4 让图表“看得懂”:标题、轴标签、图例、网格

很多新人画完图就不管了,x轴、y轴代表什么都不写,拿给同事看别人满头问号。图表的意义是让别人在几秒内看懂,所以基础要素必须补齐:

  • title:图表标题,一句话说明这张图讲什么
  • xlabel/ylabel:坐标轴含义和单位
  • legend:区分多条曲线
  • grid:网格线,辅助读数
  • 颜色、线宽、透明度:控制视觉层次,但别堆太多颜色

我见过一个很有意思的对比:同一组数据,一个人画出来的图只有一条蓝线,看不出重点;另一个人加了标题、轴标签、均值参考线和重点区域着色,呈现效果完全不一样。后者并没有用多高深的技术,只是把图表要素补齐了。

3. 高频实战场景:NumPy预处理 + Matplotlib可视化

3.1 排序、筛选后绘图:数据清理的第一步

场景设计:模拟某城市一年365天的每日最高气温,把超过32度的“高温天”标出来。这个任务里NumPy负责生成和筛选,Matplotlib负责展示。

temps = np.random.normal(loc=25, scale=8, size=365) temps = np.clip(temps, 0, None) hot_indices = np.where(temps > 32)[0] hot_values = temps[hot_indices] fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(np.arange(1, 366), temps, label='每日最高气温', alpha=0.7) ax.scatter(hot_indices + 1, hot_values, color='red', s=12, label='高温天(>32°C)') ax.axhline(y=32, color='orange', linestyle='--', label='32°C警戒线') ax.legend() plt.show()

这里有个很多人踩过的坑:如果只写hot_values = temps[temps > 32],确实能拿到超过32度的温度值,但丢掉了它们在第几天出现的信息。散点图需要x轴和y轴一一对应,所以要用np.where取索引,再通过索引去数组里取值。np.clip的作用是把不可能的负值修剪到0,属于画图前的数据清洗动作。

热浪天数、低温天数、超过某阈值的数据点,这类“筛选后标点”的需求在气象、运维监控、设备告警里到处都是,套路都是同一个:先用np.where拿到位置,再用散点图把特殊点突出。

3.2 聚合统计:从一堆数字到柱状图、直方图

场景设计:模拟一批用户的年龄段数据,看年龄分布;再模拟一批订单的类别数据,看类别占比。

直方图适合看数值型数据的分布:

ages = np.random.randint(18, 60, size=500) fig, ax = plt.subplots(figsize=(8, 5)) ax.hist(ages, bins=15, edgecolor='white') ax.set_title('用户年龄分布直方图') ax.set_xlabel('年龄') ax.set_ylabel('人数') plt.show()

柱状图适合看类别型数据的频次:

categories = np.random.choice(['A', 'B', 'C', 'D'], size=200, p=[0.1, 0.3, 0.4, 0.2]) unique_cats, counts = np.unique(categories, return_counts=True) fig, ax = plt.subplots(figsize=(7, 5)) ax.bar(unique_cats, counts) ax.set_title('类别频次柱状图') ax.set_xlabel('类别') ax.set_ylabel('频次') plt.show()

柱状图和直方图长得像,但本质不一样:柱状图的x轴是离散类别,柱与柱之间有空隙;直方图的x轴是连续数值区间,柱之间紧密相连。hist内部其实就调用了np.histogram,如果你已经算好了频次,也可以用count, edges = np.histogram(ages, bins=15),然后ax.bar(edges[:-1], count, width=...)自己画。

bins参数值得多讲两句:bins太少看不到分布细节,bins太多会显得杂乱。我一般先取10到20之间试,观察形状稳定了再定。用np.histogram的好处是你还能拿到频次数据本身,方便画完后继续做百分比计算。

3.3 随机数模拟与蒙特卡洛思路:散点图展示相关性

场景设计:构造两个变量,一个带噪声的线性关系,用散点图观察它们的相关性。这个套路在做回归分析、特征筛选时经常用到。

np.random.seed(42) x = np.random.randn(300) y = 2 * x + np.random.randn(300) * 0.5 corr = np.corrcoef(x, y)[0, 1] fig, ax = plt.subplots(figsize=(7, 6)) ax.scatter(x, y, s=25, alpha=0.6) ax.set_title(f'x与y的散点图 (相关系数r={corr:.2f})') ax.set_xlabel('x') ax.set_ylabel('y=2x+noise') plt.show()

为什么一定要加np.random.seed(42)?它保证每次运行生成的随机数都相同。复现实验结果、排查问题时太重要了。你换一个随机数,图长得都不一样,问题就没法讨论。我习惯在脚本开头固定一个种子,之后反复调整绘图参数,图的内容始终一致。

相关系数可以用np.corrcoef直接算出来,然后写进标题里。r越接近1正相关越强,越接近-1负相关越强,接近0基本无关。这个值配合散点图,比单纯看点的形状更有说服力。在探索性数据分析阶段,我会一口气用散点图矩阵看多个变量两两之间的关系,先找方向,再深入建模。

3.4 移动平均线示例:金融数据的朴素可视化

场景设计:模拟一只股票60个交易日的收盘价,计算MA5和MA20并绘制。这里特别说明,用的是纯模拟数据,只是演示技术指标的可视化方法,不构成任何投资建议。

price = 100 + np.cumsum(np.random.randn(60)) * 2 def moving_average(data, window): return np.convolve(data, np.ones(window) / window, mode='valid') ma5 = moving_average(price, 5) ma20 = moving_average(price, 20) x = np.arange(1, 61) fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(x, price, label='模拟收盘价', alpha=0.7) ax.plot(x[4:], ma5, label='MA5') ax.plot(x[19:], ma20, label='MA20') ax.legend() plt.show()

画图时最麻烦的是x轴对齐:price有60个点,MA5只有56个点,MA20只有41个点。直接用ax.plot(ma5)的话,这条线会从x=0开始,跟价格曲线对不上。

np.convolve(data, np.ones(window)/window, mode='valid')的含义是用一个长度为window、所有值都为1/window的窗口去滑动平均。valid模式会丢弃窗口不满5个或20个数据的边界区域,所以返回结果更短。MA5对应当前第5个交易日开始,因此x轴要从x[4:]对齐;MA20要从x[19:]对齐。这种偏移对齐在以后所有滑动窗口计算里都会遇到,学会一次就能迁移使用。

均线这种东西,虽然现在很多人说技术分析不靠谱,但它作为趋势平滑工具,能把价格数据的短期波动抹平,让眼睛更容易看出方向。用NumPy的卷积操作两三行就写完均线计算,比手动循环快得多,也更容易复用。

4. 先避开这些坑:Matplotlib和NumPy联调高频问题

4.1 中文乱码:字体设置是第一步

中文乱码是初学者遇到最多的一个问题,表现就是图里的中文标题、标签全部变成一个个小方块。

原因很简单:Matplotlib默认字体不支持中文字符。解决方式是在绘图代码前面设置字体:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # Windows黑体 plt.rcParams['axes.unicode_minus'] = False

不同系统的可用字体不太一样。Windows下常用SimHei或Microsoft YaHei,macOS下用PingFang SC,Linux下用Noto Sans CJK SC。如果你的环境装了对字体,也可以直接用对应名字。

有一个细节容易忽略:字体设置必须放在所有绘图代码之前。图已经创建后再改字体,经常不会生效。我习惯把这两行rcParams放在脚本最顶部,导入模块之后立刻设置,后面再画图就全部正常了。

4.2 负号显示成方块:axes.unicode_minus

设置了中文字体之后,坐标轴上的负数可能会变成一个小方块或者乱码。原因是默认字体在渲染数学减号的时候跟普通文本减号不一致。

解决办法就是上面那句: plt.rcParams['axes.unicode_minus'] = False

这行代码的意思是让坐标轴使用普通ASCII减号来显示负数,绝大多数场景下都安全。我经常看到有人设置了中文字体,负号还是方块,到处问为什么,其实就是漏了这一行。这两个设置最好捆绑出现,一次写全,省得后面再排查。

4.3 x和y长度不一致:先查shape再谈画图

报错信息很典型:x and y must have same first dimension, but have shapes (100,) and (101,)。意思就是x轴100个点,y轴101个点,没法对应。

最快的排查办法是打印形状: print(x.shape, y.shape)

常见出错场景:

  • 筛选后没有重新检查数组长度
  • 在循环里append数据时少append一次
  • 两个数据源的行数本来就不一样,没有做对齐

还有一个隐藏比较深的场景:用np.where取索引时,忘了它返回的是一个元组,要取[0]才是数组。我在3.1节写的就是hot_indices = np.where(temps > 32)[0],这个[0]很容易漏。漏掉之后,拿一个包含数组的元组去索引数据,得到的形状就乱了。

4.4 多子图布局:subplots的索引别搞混

用plt.subplots(2, 2)会返回figure和一个2x2的axes数组,画图时必须用axes[0,0]、axes[0,1]这种索引明确指定画到哪个位置。

常见错误是:明明创建了4个子图,结果用plt.plot画图,全部画到最后一个子图上去了。原因是plt.plot默认操作的是“当前axes”,在多子图场景里,当前axes不一定是你想画的那个。

推荐写法:

fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes[0, 0].plot(x1, y1) axes[0, 1].plot(x2, y2) axes[1, 0].plot(x3, y3) axes[1, 1].plot(x4, y4) plt.tight_layout() plt.show()

tight_layout的作用是自动调整子图间距,避免标题或轴标签互相重叠。如果还想让4个子图共用x轴或y轴,可以用sharex=True、sharey=True参数,这样坐标轴范围一致,对比起来更方便。

4.5 保存图片的坑:模糊、空白、不显示

保存图片也是高频问题。很多人上来就plt.savefig('fig.png'),结果图片模糊、白边巨大,甚至一片空白。

关键参数是这几个:

fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y) plt.savefig('output.png', dpi=300, bbox_inches='tight') plt.show()

dpi=300控制清晰度,默认值100在放大图片时会有明显锯齿感。bbox_inches='tight'会自动裁掉图片四周多余空白,让内容紧凑。还有一个顺序问题:保存图片应该放在plt.show()之前。如果在show之后调用savefig,保存下来的往往是一张空白图,因为画布已经被释放了。

如果你跟朋友协作,需要把很多图表统一保存,可以考虑把保存封装成一个函数,统一处理dpi、路径、格式,替换起来也方便。

5. 从“能出图”到“出好图”:我的几点真实体会

5.1 先想清楚图给谁看,再动手画

我刚开始学这些的时候,总是想把所有数据都画成折线图,因为折线图看起来“很技术”。后来有一次做模拟项目X的数据分析,导师看了我的图,说这像一幅外星信号图,看不出来重点。换成了柱状图加散点图的组合之后,数据之间的关系一目了然。

图表选择不是越炫越好。想对比大小,用柱状图;想看趋势,用折线图;想看分布,用直方图;想看相关性,用散点图。先想清楚“这张图要给谁看、看什么”,再去选类型,比不断调颜色和字体重要得多。

5.2 把rcParams配置固定下来

不同项目里翻来覆去设置字体和画布,是很烦的一件事。我的做法是把常用配置写成一段固定的初始化代码,放在一个公共模块里,每次绘图自动加载。

plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 100 plt.rcParams['savefig.dpi'] = 300 plt.rcParams['axes.grid'] = True

这样每张图的风格基本统一,省掉重复设置。尤其在做系列图表时,统一风格会让整个报告看起来更专业。颜色方面我也习惯用固定的几种色系,一张图最多三种颜色,重要数据用深色,次要数据用浅色加透明度,层次感自然就出来了。

5.3 调试小技巧:看不清数据就先画点

如果对数据本身没把握,不要一上来就用plot画折线。折线会把点连起来,个别数据跳变的地方会被“平滑”盖过去。改用ax.plot(x, y, 'o')把点画出来,异常点立刻现原形。

另一个小技巧是调整坐标轴范围来突出重点,用ax.set_xlim和ax.set_ylim直接框住你关心的区间。比如只看最近20天的走势,就把x轴范围设为[40, 60],图表立刻聚焦到有用的部分。画布大小也很值得调,图形太挤的时候,用fig.set_size_inches把整张图拉大,比改字号的体验好得多。

最后再分享一个习惯:每张图写完,我都会先打印一下数据的基本统计量,比如np.mean、np.std、np.min、np.max。如果图上曲线的均值和统计数据对不上,那肯定是数据处理环节出了问题,早发现早修,别等图都放进报告了才回头找bug。这套NumPy加Matplotlib的工作流,我用到现在,依然觉得它是最靠谱的组合——一个负责把数据讲清楚,一个负责把数据讲明白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询