3.1 结合 NumPy 进行数据处理 本节摘要:NumPy 是 Matplotlib 的数据地基。绘图函数底层接受的正是 NumPy 的 ndarray 数组,所以会造数、会清洗、会转换,就相当于在画图前先把料备齐。本节从数组为什么快讲起,再走一遍填缺失值、剔异常值、标准化、归一化、对数变换和分组聚合这几道工序,最后落到工程取舍:什么时候该用 NumPy,什么时候该把活交给 Pandas。读完你能让"数进来、图出去"这条链路跑得又稳又快。 本节目标 阅读完本节,你应当能够: 说清 NumPy 数组为什么是 Matplotlib 的数据基石,以及它比 Python 列表强在哪。 用 NumPy 生成绘图所需的序列、网格和带噪声的模拟数据。 用布尔掩码定位并填充缺失值、剔除异常值。
本节摘要:NumPy 是 Matplotlib 的数据地基。绘图函数底层接受的正是 NumPy 的 ndarray 数组,所以会造数、会清洗、会转换,就相当于在画图前先把料备齐。本节从数组为什么快讲起,再走一遍填缺失值、剔异常值、标准化、归一化、对数变换和分组聚合这几道工序,最后落到工程取舍:什么时候该用 NumPy,什么时候该把活交给 Pandas。读完你能让"数进来、图出去"这条链路跑得又稳又快。
阅读完本节,你应当能够:
很多人学 Matplotlib 时,把注意力全放在 plot 这个函数上,却忽略了喂给它的数据从哪来。实际上,一张图好不好,一半的功夫在画图之前:数是不是干净的、量纲是不是统一的、异常值有没有处理。这些活,NumPy 是 Python 生态里最顺手的工具。
先看一个最小例子,感受一下 NumPy 数组和 Matplotlib 的默契:
import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, 10, 200) # 0 到 10 之间等距的 200 个点 y = np.sin(x) + np.random.normal(0, 0.1, x.size) plt.plot(x, y) plt.show()
这里 np.linspace 造出 x,np.sin 和 np.random.normal 造出带噪声的 y,plt.plot 直接吃下这两个数组。整个过程没有一行是在"把列表转成数组",因为数据从一开始就是数组。
NumPy 数组值得单独拎出来讲,是因为它有三个 Python 列表比不了的本事。第一是性能:数组用连续内存存同一种类型的数据,底层是 C 实现,数值运算比列表快一个数量级起步,数据一多差距更明显。第二是广播:形状不同的数组能自动对齐做运算,比如拿一个标量去加一整个数组,或者拿一列去加一个矩阵的每一行,不用写循环。第三是函数齐全:统计、线性代数、随机数这些,NumPy 都内置了,直接作用在数组上。
广播值得单独举个例子。假设你有一列每天的销售额,想算每天的同比增长率,用列表得写循环,用 NumPy 直接这么写:
sales = np.array([100, 120, 150, 135, 160]) growth = (sales[1:] - sales[:-1]) / sales[:-1]
sales[1:] 是"从第二天起",sales[:-1] 是"到最后一天前",两个数组自动逐位对齐相减再除,一行就算出了每天的增长率。这种"切片、对齐、运算"一气呵成的感觉,是 NumPy 真正省心的地方。
这三条,恰好对应了数据分析可视化里最高频的三件事:跑得快、写起来省事、算起来方便。
探索阶段常常没有现成数据,或者你想先验证一个想法,这时候就得自己造。NumPy 的造数函数,可以按"均匀、随机、网格"三类来记。
均匀造数用 linspace 和 arange。linspace 给定起止和点数,适合做连续曲线的横轴;arange 给定起止和步长,适合做离散的整数序列。随机造数用 random 子模块:normal 造正态分布,uniform 造均匀分布,exponential 造指数分布,randint 造随机整数。想让结果可复现,先调 np.random.seed 固定种子,这一点在调试和给别人演示时很重要——同一份代码,别人跑出来要和你一样。
网格造数用 meshgrid,这是画等高线、三维曲面时的标配。它把两个一维坐标轴交叉成二维坐标点阵,后面 3.3 节的等高线图、曲面图都要靠它:
x = np.linspace(-3, 3, 50) y = np.linspace(-3, 3, 50) X, Y = np.meshgrid(x, y) Z = np.exp(-(X**2 + Y**2))
这里 X 和 Y 是同形状的二维数组,Z 在每个网格点上算出一个值。三块二维数组叠起来,就描述了一张曲面。
💡 造数据时先问自己一句:我要的是连续曲线、离散分类,还是二维平面?这三问对应 linspace、arange、meshgrid,套对了,后面的图就水到渠成。
真实数据几乎不会干干净净地摆在那里。缺失值和异常值是两道最常过的坎,NumPy 都能用布尔掩码这个统一的套路来处理。
先看缺失值。NumPy 用 np.nan 表示"没有值",用 np.isnan 检测它在哪:
data = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) mask = np.isnan(data) # 布尔掩码,True 的位置就是缺失值 data[mask] = np.nanmean(data) # 用忽略缺失的平均值填充
这段的关键是布尔掩码。mask 是一个和 data 等长的布尔数组,True 标出缺失位置,data[mask] 用布尔索引把这些位置挑出来,统一填成 np.nanmean 算出的均值。np.nanmean 专门在算平均时跳过 nan,比 np.mean 更稳——np.mean 碰到 nan 会直接返回 nan。
异常值比缺失值隐蔽,因为它看起来是个正常的数,只是大得离谱。常用的检测办法是标准差法:算均值和标准差,把偏离均值超过若干倍标准差的点判为异常。倍数通常取 3,也有的场景取 2.5 或 3.5,取决于数据有多"野"。
mean, std = data.mean(), data.std() outliers = np.abs(data - mean) > 3 * std clean = data[~outliers] # ~ 取反,留下正常值
这套"算阈值、建掩码、筛数据"的写法,和缺失值那套是一回事,只是掩码的判断条件换了。记住这个套路,后面遇到任何"挑出某些数据"的需求,都能套。
标准差法之外,还有一种更扛造的办法叫 IQR 法,也就是四分位距法。它先算第一四分位数 Q1 和第三四分位数 Q3,两者之差就是 IQR,正常范围定在 Q1 减 1.5 倍 IQR 到 Q3 加 1.5 倍 IQR 之间,落在外面就算异常。这个办法的好处是不受极端值本身影响——因为四分位数靠排序取中位,一个超大的离群点并不会把阈值也拽过去。数据里离群点比较多、分布又偏的时候,IQR 通常比标准差法更稳。NumPy 里用 np.percentile 就能拿到分位数,实现起来同样是一行建掩码的事。
⚠️ 用均值填充缺失值有个隐患:它会人为拉低方差,让分布看起来更集中。缺失比例高、或者数据明显偏态时,中位数通常比均值更扛造。还有,填充和剔除都会改变数据,别在原始数组上直接动手,先复制一份。
数据清洗之后,常常还要做一次转换,把不同量纲拉到同一把尺子上,或者把偏斜的分布掰正。三个最常见的转换是标准化、归一化、对数变换,它们看着像,目的却不一样。
标准化是减均值再除标准差,把数据变成均值 0、标准差 1 的样子。它不改变分布的形状,只改变尺度和位置,适合"要拿多个量纲不同的特征互相比较"的场景。比如身高单位是厘米、体重单位是千克,直接放一起没意义,标准化之后就能比了。
归一化是 Min-Max 缩放到 0 到 1 之间,公式是减最小值再除极差。它适合"需要把值压进固定区间"的场景,很多模型的输入、图像的像素值都这么干。
对数变换专门治右偏分布。收入、访问量、销量这类数据,常常是一小撮大值拖着一条长尾巴,直方图右边拖得老长。取对数之后,大值被压下来,分布更接近对称,图形也更好看、更好读。
三者的区别,一张表说清楚:
| 转换方式 | 核心思路 | 结果范围 | 主要用途 |
|---|---|---|---|
| 标准化 | 减均值、除标准差 | 无固定范围,均值 0、标准差 1 | 消除量纲,比较不同特征 |
| 归一化 | 减最小值、除极差 | 固定在 0 到 1 | 需要固定区间的场景 |
| 对数变换 | 对每个值取对数 | 压缩大值、拉开小值 | 处理右偏分布 |
三者的实现都很短,短到可以一眼记住:
z = (data - data.mean()) / data.std() # 标准化 norm = (data - data.min()) / (data.max() - data.min()) # 归一化 logdata = np.log(data) # 对数变换
选哪个,先看你要解决什么问题:要比较就用标准化,要压区间就用归一化,要掰正偏态就取对数。
很多时候,你要画的不是原始数据本身,而是它的统计量——平均值、中位数、总和、各分组的总和。NumPy 的统计函数配合布尔索引,就能完成"按组算数"这件事。
基本统计是一行一个:mean 算均值,median 算中位数,std 算标准差,sum 算总和,min 和 max 取极值。中位数和均值这对组合值得多说一句:均值对异常值敏感,一个离群点就能把它拽偏;中位数看的是"正中间那个数",更扛造。所以分布偏斜时,中位数往往比均值更能代表"典型值"。
分组聚合的思路是:先拿到唯一类别,再对每个类别建一个布尔掩码去筛数值,最后算统计量:
categories = np.array(['A', 'B', 'A', 'C', 'B', 'A']) values = np.array([10, 20, 15, 25, 18, 12]) uniq = np.unique(categories) means = [values[categories == c].mean() for c in uniq]
categories == c 这一步生成了"这个位置是不是属于类别 c"的布尔掩码,再用它去 values 里取子集,然后求均值。这个写法朴实但通用,几组、几十组都吃得下。算出各组均值后,交给 plt.bar 画柱状图,就是一张最典型的"分组对比"图。
⚠️ 分组聚合用 NumPy 手写循环,组数一多就显得啰嗦,而且容易漏掉空组、类别顺序这些细节。到了 DataFrame 那种带列名、带分组语义的数据,这活交给 Pandas 的 groupby 会干净得多——这正是下一节要讲的事。NumPy 适合"数组形态、逻辑简单"的场合。
到这里,我们已经把"造数、清洗、转换、聚合"这条链走通了。落回工程上,有几条取舍值得记下来。
第一,复制再改。清洗和转换会破坏原始数据,养成先 copy 一份再动手的习惯,出了问题还能回到起点。
第二,随机数要定种子。np.random.seed 让结果可复现,调试、写教程、给同事演示都离不开它。不定种子,你今天的图和明天都不一样,排查起来很痛苦。
第三,掩码是通用语言。填充缺失、剔异常、分组筛选,本质都是"造一个布尔掩码,再拿去索引"。把这个套路吃透,NumPy 的数据处理就学通了八成。
第四,别拿 NumPy 硬扛 Pandas 的活。数据一旦有了列名、类别、时间戳这些结构化语义,或者要做分组、透视、重采样,Pandas 的表达力更强。我们的分工是:NumPy 管数值运算和数组层面的清洗,Pandas 管结构化数据的整理,两者再一起交给 Matplotlib。
第五,画图前先摸清数据的形状和范围。用 data.shape 看几行几列,用 data.min 和 data.max 看范围,用 np.isnan(data).sum() 数一数缺失值有几个。这几眼扫过去,能提前拦住不少"图出来了才发现不对"的返工。
下一节我们把数据从"裸数组"升级到"带列名的表格",看看 Pandas 的 DataFrame 和 Series 怎么让可视化更省事。