NumPy+Matplotlib:从数组计算到数据可视化的完整实战

📅 发布时间:2026/10/14 14:42:01
NumPy+Matplotlib:从数组计算到数据可视化的完整实战
做数据分析的人十有八九都绕不开这两个库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, labelsin(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, s20, alpha0.7) ax.set_title(带噪声的线性关系) ax.set_xlabel(x) ax.set_ylabel(y2xnoise) 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, sizex.shape) mean_val np.mean(signal) fig, ax plt.subplots(figsize(10, 5)) ax.plot(x, signal, label带噪声信号, alpha0.7) ax.axhline(ymean_val, colorred, linestyle--, labelf均值{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(loc25, scale8, size365) 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每日最高气温, alpha0.7) ax.scatter(hot_indices 1, hot_values, colorred, s12, label高温天(32°C)) ax.axhline(y32, colororange, linestyle--, label32°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, size500) fig, ax plt.subplots(figsize(8, 5)) ax.hist(ages, bins15, edgecolorwhite) ax.set_title(用户年龄分布直方图) ax.set_xlabel(年龄) ax.set_ylabel(人数) plt.show()柱状图适合看类别型数据的频次categories np.random.choice([A, B, C, D], size200, p[0.1, 0.3, 0.4, 0.2]) unique_cats, counts np.unique(categories, return_countsTrue) 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, bins15)然后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, s25, alpha0.6) ax.set_title(fx与y的散点图 (相关系数r{corr:.2f})) ax.set_xlabel(x) ax.set_ylabel(y2xnoise) 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, modevalid) 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模拟收盘价, alpha0.7) ax.plot(x[4:], ma5, labelMA5) ax.plot(x[19:], ma20, labelMA20) ax.legend() plt.show()画图时最麻烦的是x轴对齐price有60个点MA5只有56个点MA20只有41个点。直接用ax.plot(ma5)的话这条线会从x0开始跟价格曲线对不上。np.convolve(data, np.ones(window)/window, modevalid)的含义是用一个长度为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 YaHeimacOS下用PingFang SCLinux下用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轴可以用sharexTrue、shareyTrue参数这样坐标轴范围一致对比起来更方便。4.5 保存图片的坑模糊、空白、不显示保存图片也是高频问题。很多人上来就plt.savefig(fig.png)结果图片模糊、白边巨大甚至一片空白。关键参数是这几个fig, ax plt.subplots(figsize(8, 5)) ax.plot(x, y) plt.savefig(output.png, dpi300, bbox_inchestight) plt.show()dpi300控制清晰度默认值100在放大图片时会有明显锯齿感。bbox_inchestight会自动裁掉图片四周多余空白让内容紧凑。还有一个顺序问题保存图片应该放在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的工作流我用到现在依然觉得它是最靠谱的组合——一个负责把数据讲清楚一个负责把数据讲明白。