4.3 利用数组进行数据处理

NumPy数组使你可以将许多种数据处理任务表述为简洁的数组表达式(否则需要编写循环)。用数组表达式代替循环的做法,通常被称为矢量化。一般来说,矢量化数组运算要比等价的纯Python方式快上一两个数量级(甚至更多),尤其是各种数值计算。在后面内容中(见附录A)我将介绍广播,这是一种针对矢量化计算的强大手段。

作为简单的例子,假设我们想要在一组值(网格型)上计算函数sqrt(x^2+y^2)。np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的(x,y)对):

  1. In [155]: points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
  2. In [156]: xs, ys = np.meshgrid(points, points)
  3. In [157]: ys
  4. Out[157]:
  5. array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
  6. [-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
  7. [-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
  8. ...,
  9. [ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
  10. [ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
  11. [ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])

现在,对该函数的求值运算就好办了,把这两个数组当做两个浮点数那样编写表达式即可:

  1. In [158]: z = np.sqrt(xs ** 2 + ys ** 2)
  2. In [159]: z
  3. Out[159]:
  4. array([[ 7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ],
  5. [ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
  6. [ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
  7. ...,
  8. [ 7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
  9. [ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
  10. [ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])

作为第9章的先导,我用matplotlib创建了这个二维数组的可视化:

  1. In [160]: import matplotlib.pyplot as plt
  2. In [161]: plt.imshow(z, cmap=plt.cm.gray); plt.colorbar()
  3. Out[161]: <matplotlib.colorbar.Colorbar at 0x7f715e3fa630>
  4. In [162]: plt.title("Image plot of $\sqrt{x^2 + y^2}$ for a grid of values")
  5. Out[162]: <matplotlib.text.Text at 0x7f715d2de748>

见图4-3。这张图是用matplotlib的imshow函数创建的。

图4-3 根据网格对函数求值的结果

将条件逻辑表述为数组运算

numpy.where函数是三元表达式x if condition else y的矢量化版本。假设我们有一个布尔数组和两个值数组:

  1. In [165]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
  2. In [166]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
  3. In [167]: cond = np.array([True, False, True, True, False])

假设我们想要根据cond中的值选取xarr和yarr的值:当cond中的值为True时,选取xarr的值,否则从yarr中选取。列表推导式的写法应该如下所示:

  1. In [168]: result = [(x if c else y)
  2. .....: for x, y, c in zip(xarr, yarr, cond)]
  3. In [169]: result
  4. Out[169]: [1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]

这有几个问题。第一,它对大数组的处理速度不是很快(因为所有工作都是由纯Python完成的)。第二,无法用于多维数组。若使用np.where,则可以将该功能写得非常简洁:

  1. In [170]: result = np.where(cond, xarr, yarr)
  2. In [171]: result
  3. Out[171]: array([ 1.1, 2.2, 1.3, 1.4, 2.5])

np.where的第二个和第三个参数不必是数组,它们都可以是标量值。在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组。假设有一个由随机数据组成的矩阵,你希望将所有正值替换为2,将所有负值替换为-2。若利用np.where,则会非常简单:

  1. In [172]: arr = np.random.randn(4, 4)
  2. In [173]: arr
  3. Out[173]:
  4. array([[-0.5031, -0.6223, -0.9212, -0.7262],
  5. [ 0.2229, 0.0513, -1.1577, 0.8167],
  6. [ 0.4336, 1.0107, 1.8249, -0.9975],
  7. [ 0.8506, -0.1316, 0.9124, 0.1882]])
  8. In [174]: arr > 0
  9. Out[174]:
  10. array([[False, False, False, False],
  11. [ True, True, False, True],
  12. [ True, True, True, False],
  13. [ True, False, True, True]], dtype=bool)
  14. In [175]: np.where(arr > 0, 2, -2)
  15. Out[175]:
  16. array([[-2, -2, -2, -2],
  17. [ 2, 2, -2, 2],
  18. [ 2, 2, 2, -2],
  19. [ 2, -2, 2, 2]])

使用np.where,可以将标量和数组结合起来。例如,我可用常数2替换arr中所有正的值:

  1. In [176]: np.where(arr > 0, 2, arr) # set only positive values to 2
  2. Out[176]:
  3. array([[-0.5031, -0.6223, -0.9212, -0.7262],
  4. [ 2. , 2. , -1.1577, 2. ],
  5. [ 2. , 2. , 2. , -0.9975],
  6. [ 2. , -0.1316, 2. , 2. ]])

传递给where的数组大小可以不相等,甚至可以是标量值。

数学和统计方法

可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。sum、mean以及标准差std等聚合计算(aggregation,通常叫做约简(reduction))既可以当做数组的实例方法调用,也可以当做顶级NumPy函数使用。

这里,我生成了一些正态分布随机数据,然后做了聚类统计:

  1. In [177]: arr = np.random.randn(5, 4)
  2. In [178]: arr
  3. Out[178]:
  4. array([[ 2.1695, -0.1149, 2.0037, 0.0296],
  5. [ 0.7953, 0.1181, -0.7485, 0.585 ],
  6. [ 0.1527, -1.5657, -0.5625, -0.0327],
  7. [-0.929 , -0.4826, -0.0363, 1.0954],
  8. [ 0.9809, -0.5895, 1.5817, -0.5287]])
  9. In [179]: arr.mean()
  10. Out[179]: 0.19607051119998253
  11. In [180]: np.mean(arr)
  12. Out[180]: 0.19607051119998253
  13. In [181]: arr.sum()
  14. Out[181]: 3.9214102239996507

mean和sum这类的函数可以接受一个axis选项参数,用于计算该轴向上的统计值,最终结果是一个少一维的数组:

  1. In [182]: arr.mean(axis=1)
  2. Out[182]: array([ 1.022 , 0.1875, -0.502 , -0.0881, 0.3611])
  3. In [183]: arr.sum(axis=0)
  4. Out[183]: array([ 3.1693, -2.6345, 2.2381, 1.1486])

这里,arr.mean(1)是“计算行的平均值”,arr.sum(0)是“计算每列的和”。

其他如cumsum和cumprod之类的方法则不聚合,而是产生一个由中间结果组成的数组:

  1. In [184]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
  2. In [185]: arr.cumsum()
  3. Out[185]: array([ 0, 1, 3, 6, 10, 15, 21, 28])

在多维数组中,累加函数(如cumsum)返回的是同样大小的数组,但是会根据每个低维的切片沿着标记轴计算部分聚类:

  1. In [186]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
  2. In [187]: arr
  3. Out[187]:
  4. array([[0, 1, 2],
  5. [3, 4, 5],
  6. [6, 7, 8]])
  7. In [188]: arr.cumsum(axis=0)
  8. Out[188]:
  9. array([[ 0, 1, 2],
  10. [ 3, 5, 7],
  11. [ 9, 12, 15]])
  12. In [189]: arr.cumprod(axis=1)
  13. Out[189]:
  14. array([[ 0, 0, 0],
  15. [ 3, 12, 60],
  16. [ 6, 42, 336]])

表4-5列出了全部的基本数组统计方法。后续章节中有很多例子都会用到这些方法。

4.3 利用数组进行数据处理 - 图2

4.3 利用数组进行数据处理 - 图3

用于布尔型数组的方法

在上面这些方法中,布尔值会被强制转换为1(True)和0(False)。因此,sum经常被用来对布尔型数组中的True值计数:

  1. In [190]: arr = np.random.randn(100)
  2. In [191]: (arr > 0).sum() # Number of positive values
  3. Out[191]: 42

另外还有两个方法any和all,它们对布尔型数组非常有用。any用于测试数组中是否存在一个或多个True,而all则检查数组中所有值是否都是True:

  1. In [192]: bools = np.array([False, False, True, False])
  2. In [193]: bools.any()
  3. Out[193]: True
  4. In [194]: bools.all()
  5. Out[194]: False

这两个方法也能用于非布尔型数组,所有非0元素将会被当做True。

排序

跟Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序:

  1. In [195]: arr = np.random.randn(6)
  2. In [196]: arr
  3. Out[196]: array([ 0.6095, -0.4938, 1.24 , -0.1357, 1.43 , -0.8469])
  4. In [197]: arr.sort()
  5. In [198]: arr
  6. Out[198]: array([-0.8469, -0.4938, -0.1357, 0.6095, 1.24 , 1.43 ])

多维数组可以在任何一个轴向上进行排序,只需将轴编号传给sort即可:

  1. In [199]: arr = np.random.randn(5, 3)
  2. In [200]: arr
  3. Out[200]:
  4. array([[ 0.6033, 1.2636, -0.2555],
  5. [-0.4457, 0.4684, -0.9616],
  6. [-1.8245, 0.6254, 1.0229],
  7. [ 1.1074, 0.0909, -0.3501],
  8. [ 0.218 , -0.8948, -1.7415]])
  9. In [201]: arr.sort(1)
  10. In [202]: arr
  11. Out[202]:
  12. array([[-0.2555, 0.6033, 1.2636],
  13. [-0.9616, -0.4457, 0.4684],
  14. [-1.8245, 0.6254, 1.0229],
  15. [-0.3501, 0.0909, 1.1074],
  16. [-1.7415, -0.8948, 0.218 ]])

顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。计算数组分位数最简单的办法是对其进行排序,然后选取特定位置的值:

  1. In [203]: large_arr = np.random.randn(1000)
  2. In [204]: large_arr.sort()
  3. In [205]: large_arr[int(0.05 * len(large_arr))] # 5% quantile
  4. Out[205]: -1.5311513550102103

更多关于NumPy排序方法以及诸如间接排序之类的高级技术,请参阅附录A。在pandas中还可以找到一些其他跟排序有关的数据操作(比如根据一列或多列对表格型数据进行排序)。

唯一化以及其它的集合逻辑

NumPy提供了一些针对一维ndarray的基本集合运算。最常用的可能要数np.unique了,它用于找出数组中的唯一值并返回已排序的结果:

  1. In [206]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
  2. In [207]: np.unique(names)
  3. Out[207]:
  4. array(['Bob', 'Joe', 'Will'],
  5. dtype='<U4')
  6. In [208]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
  7. In [209]: np.unique(ints)
  8. Out[209]: array([1, 2, 3, 4])

拿跟np.unique等价的纯Python代码来对比一下:

  1. In [210]: sorted(set(names))
  2. Out[210]: ['Bob', 'Joe', 'Will']

另一个函数np.in1d用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:

  1. In [211]: values = np.array([6, 0, 0, 3, 2, 5, 6])
  2. In [212]: np.in1d(values, [2, 3, 6])
  3. Out[212]: array([ True, False, False, True, True, False, True], dtype=bool)

NumPy中的集合函数请参见表4-6。
4.3 利用数组进行数据处理 - 图4