14 章: 进阶 NumPy

import numpy as np  # 导入 NumPy 库,并将其别名设置为 np
rng = np.random.default_rng(seed=12345)  # 创建一个随机数生成器,并设置种子为 12345,以确保结果可复现

简介

我们已经学习了 NumPy 的基础知识。现在,让我们更深入地探索!🤿 我们将学习:

  • ndarray 的内部结构。
  • 高级数组操作技巧。
  • 以及一些很酷的技巧!😎

主题大纲

  • ndarray 对象内部结构 🤔
  • 数据类型层级 🌳
  • 数组操作 🔄➕➗🔁
  • 广播机制 📡
  • ufunc 高级用法 🚀
  • 结构化数组 🏢
  • 排序 ️️️️️⬆️🔎
  • 使用 Numba 编写快速 NumPy 函数 🏎️

ndarray 对象内部结构 🤔

  • NumPy 的 ndarray 将一块同质类型数据(所有元素具有相同数据类型)的内存块解释为多维数组。
  • 它可以是连续的跨步的
  • 关键组成部分
    • 数据类型 (dtype):数据如何被解释(浮点数、整数、布尔值等)。
    • 跨步视图:允许像 arr[::2, ::-1] 这样的操作,而无需复制数据!⚡️

ndarray 内部结构

一个 ndarray 包含:

  1. 数据指针:RAM 或内存映射文件中的一块数据。
  2. 数据类型 (dtype):描述固定大小的值单元格(例如,float64、int32)。
  3. 形状元组:数组维度(例如,(10, 5) 表示 10x5 的数组)。
  4. 跨度元组:沿维度前进一个元素所需的“步长”字节数。

ndarray 可视化 🖼️

  • 数据:实际的数组数据。
  • dtype:数据类型信息(例如,float64)。
  • 形状:数组维度(例如,(3, 4, 5))。
  • 跨度:跳转到每个维度中下一个元素所需的字节数。

形状和跨度示例

  • 一个 10 x 5 的数组具有形状 (10, 5)
arr_2d = np.ones((10, 5))  # 创建一个 10x5 的全 1 数组
arr_2d.shape  # 查看数组的形状
(10, 5)
  • 一个 3 x 4 x 5 的 float64(8 字节)数组通常具有跨度 (160, 40, 8)(C 顺序):
arr_3d = np.ones((3, 4, 5), dtype=np.float64)  # 创建一个 3x4x5 的 float64 类型全 1 数组
arr_3d.strides  # 查看数组的跨度
(160, 40, 8)

理解跨度 🚶

  • 跨度:在内存中移动以到达沿每个维度的下一个元素所需的字节数。
  • 示例arr_3d.strides = (160, 40, 8)
    • 第一维度(行):移动 160 字节。
    • 第二维度(列):移动 40 字节。
    • 第三维度:移动 8 字节(float64 大小)。
  • 轴上较大的跨度通常意味着沿该轴的计算成本更高
  • 跨度可以是负数!

NumPy 数据类型层级 🌳

  • NumPy 具有丰富的数据类型层级。
  • 使用超类(例如,np.integernp.floating)和 np.issubdtype 来检查数组类型:
ints = np.ones(10, dtype=np.uint16)  # 创建一个 uint16 类型的全 1 数组
floats = np.ones(10, dtype=np.float32)  # 创建一个 float32 类型的全 1 数组
print(np.issubdtype(ints.dtype, np.integer))  # 检查 ints.dtype 是否是 np.integer 的子类型
print(np.issubdtype(floats.dtype, np.floating))  # 检查 floats.dtype 是否是 np.floating 的子类型
True
True

数据类型层级(续)

  • 使用 .mro()(方法解析顺序)查看父类:
np.float64.mro()  # 查看 np.float64 的方法解析顺序(继承关系)
[numpy.float64,
 numpy.floating,
 numpy.inexact,
 numpy.number,
 numpy.generic,
 float,
 object]
  • 这表明 np.float64 继承自 np.floatingnp.inexact、…、object

NumPy 数据类型层级可视化 📊

  • generic 是根
  • number, bool_ 等, 是 generic 的子类

高级数组操作:重塑 🔄

  • 重塑:改变数组的形状,不复制数据
  • 使用 reshape() 方法和一个形状元组:
arr = np.arange(8)  # 创建一个包含 0 到 7 的一维数组
arr.reshape((4, 2))  # 将数组重塑为 4x2 的二维数组
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7]])

重塑多维数组

arr.reshape((4, 2)).reshape((2, 4))  # 将数组先重塑为 4x2,再重塑为 2x4
array([[0, 1, 2, 3],
       [4, 5, 6, 7]])
  • 对一个维度使用 -1 来推断大小:
arr = np.arange(15)  # 创建一个包含 0 到 14 的一维数组
arr.reshape((5, -1))  # 将数组重塑为 5 行,列数自动推断(这里是 3)
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11],
       [12, 13, 14]])

使用形状属性重塑

other_arr = np.ones((3, 5))  # 创建一个 3x5 的全 1 数组
arr.reshape(other_arr.shape)  # 使用 other_arr 的形状重塑 arr
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])

扁平化或展开 ➡️

  • 扁平化/展开:将多维数组转换为一维数组。
  • ravel():如果值是连续的,则复制。
arr = np.arange(15).reshape((5, 3))  # 创建一个 5x3 的数组
arr.ravel()  # 将数组展开为一维数组
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
  • flatten():始终返回副本
arr.flatten()  # 将数组展开为一维数组(返回副本)
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

C vs. FORTRAN 顺序 🔀

  • 行主序(C):连续的行元素存储在一起。
  • 列主序(FORTRAN):连续的列元素存储在一起。
  • reshaperavel 接受一个 order 参数('C''F')。
arr = np.arange(12).reshape((3, 4))  # 创建一个 3x4 的数组
print(arr.ravel())      # 默认:'C' 顺序
print(arr.ravel('F'))  # FORTRAN 顺序
[ 0  1  2  3  4  5  6  7  8  9 10 11]
[ 0  4  8  1  5  9  2  6 10  3  7 11]

C vs. FORTRAN 顺序(可视化)

  • C/行主序首先遍历更高的维度。
  • FORTRAN/列主序最后遍历更高的维度。

连接数组 ➕

  • numpy.concatenate:沿现有轴连接数组。
arr1 = np.array([[1, 2, 3], [4, 5, 6]])  # 创建一个 2x3 的数组
arr2 = np.array([[7, 8, 9], [10, 11, 12]])  # 创建一个 2x3 的数组
print(np.concatenate([arr1, arr2], axis=0))  # 沿行(axis=0)堆叠(垂直堆叠)
print(np.concatenate([arr1, arr2], axis=1))  # 沿列(axis=1)堆叠(水平堆叠)
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]
[[ 1  2  3  7  8  9]
 [ 4  5  6 10 11 12]]

连接辅助函数

  • vstack, row_stack:按行堆叠(axis 0)。
  • hstack:按列堆叠(axis 1)。
  • column_stack:类似于 hstack,但首先将一维数组转换为二维列。
  • dstack:按“深度”堆叠(axis 2)。
print(np.vstack((arr1, arr2)))  # 垂直堆叠
print(np.hstack((arr1, arr2)))  # 水平堆叠
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]
[[ 1  2  3  7  8  9]
 [ 4  5  6 10 11 12]]

拆分数组 ➗

  • split:将数组沿轴拆分为多个数组。
arr = rng.standard_normal((5, 2))  # 创建一个 5x2 的标准正态分布数组
first, second, third = np.split(arr, [1, 3])  # 将数组在索引 1 和 3 处拆分
print(f"{first=}")  # 打印第一个拆分结果
print(f"{second=}")  # 打印第二个拆分结果
print(f"{third=}")  # 打印第三个拆分结果
first=array([[-1.42382504,  1.26372846]])
second=array([[-0.87066174, -0.25917323],
       [-0.07534331, -0.74088465]])
third=array([[-1.3677927 ,  0.6488928 ],
       [ 0.36105811, -1.95286306]])
  • hsplit/vsplit:分别在轴 0 和 1 上拆分。

数组连接函数表

函数 描述
concatenate 通用函数,沿轴连接数组。
vstack, row_stack 按行堆叠数组(axis 0)。
hstack 按列堆叠数组(axis 1)。
column_stack 类似于 hstack,但将一维数组转换为二维列。
dstack 按“深度”堆叠数组(axis 2)。
split 在沿轴的位置拆分数组。
hsplit/vsplit 分别在轴 0 和 1 上拆分。

堆叠辅助对象:r_c_

  • r_c_ 使堆叠更简洁:
arr = np.arange(6)  # 创建一个包含 0 到 5 的一维数组
arr1 = arr.reshape((3, 2))  # 将数组重塑为 3x2
arr2 = rng.standard_normal((3, 2))  # 创建一个 3x2 的标准正态分布数组
print(np.r_[arr1, arr2])  # 类似于 row_stack
print(np.c_[np.r_[arr1, arr2], arr])  # 将 arr 作为新列连接
[[ 0.          1.        ]
 [ 2.          3.        ]
 [ 4.          5.        ]
 [ 2.34740965  0.96849691]
 [-0.75938718  0.90219827]
 [-0.46695317 -0.06068952]]
[[ 0.          1.          0.        ]
 [ 2.          3.          1.        ]
 [ 4.          5.          2.        ]
 [ 2.34740965  0.96849691  3.        ]
 [-0.75938718  0.90219827  4.        ]
 [-0.46695317 -0.06068952  5.        ]]

重复元素:tilerepeat 🔁

  • repeat:复制每个元素:
arr = np.arange(3)  # 创建一个包含 0 到 2 的一维数组
print(arr.repeat(3))  # 每个元素重复 3 次
print(arr.repeat([2, 3, 4]))  # 每个元素分别重复 2、3、4 次
[0 0 0 1 1 1 2 2 2]
[0 0 1 1 1 2 2 2 2]
  • 二维数组示例:
arr = rng.standard_normal((2, 2))  # 创建一个 2x2 的标准正态分布数组
arr.repeat(2, axis=0) # 沿着行重复
array([[ 0.78884434, -1.25666813],
       [ 0.78884434, -1.25666813],
       [ 0.57585751,  1.39897899],
       [ 0.57585751,  1.39897899]])

tile:堆叠副本

  • tile:沿轴堆叠数组的副本:
arr = rng.standard_normal((2, 2))  # 创建一个 2x2 的标准正态分布数组
print(np.tile(arr, 2))  # 沿行重复数组两次
print(np.tile(arr, (2, 1)))  # 沿行重复 2 次,沿列重复 1 次
print(np.tile(arr, (3, 2)))  # 沿行重复 3 次,沿列重复 2 次
[[ 1.32229806 -0.29969852  1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273  0.90291934 -1.62158273]]
[[ 1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273]
 [ 1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273]]
[[ 1.32229806 -0.29969852  1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273  0.90291934 -1.62158273]
 [ 1.32229806 -0.29969852  1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273  0.90291934 -1.62158273]
 [ 1.32229806 -0.29969852  1.32229806 -0.29969852]
 [ 0.90291934 -1.62158273  0.90291934 -1.62158273]]

使用 takeput 进行花式索引

  • take:通过整数索引选择元素:
arr = np.arange(10) * 100  # 创建一个 [0, 100, 200, ..., 900] 的数组
inds = [7, 1, 2, 6]  # 定义索引列表
arr.take(inds)  # 类似于 arr[inds]
array([700, 100, 200, 600])
  • put:将值分配给索引(原地)。接受轴参数:
arr.put(inds, 42)  # 将索引位置的值设置为 42
print(arr)  # 打印修改后的数组
arr.put(inds, [40, 41, 42, 43])  # 将索引位置的值分别设置为 40、41、42、43
print(arr)  # 打印修改后的数组
[  0  42  42 300 400 500  42  42 800 900]
[  0  41  42 300 400 500  43  40 800 900]

广播机制 📡

  • 广播机制不同形状的数组之间如何进行运算。
  • 最简单的情况:标量和数组:
arr = np.arange(5)  # 创建一个包含 0 到 4 的一维数组
arr * 4  # 4 被“广播”到所有元素
array([ 0,  4,  8, 12, 16])
  • 均值消减:
arr = rng.standard_normal((4, 3))   # 创建一个4x3的标准正态分布的数组
demeaned = arr - arr.mean(0)  #  arr.mean(0)是计算每列的均值
demeaned
array([[ 0.00978669,  0.05184267, -1.55788029],
       [ 0.08628836,  1.32709867,  2.40388021],
       [ 0.94533729,  0.43099193, -1.17326753],
       [-1.04141234, -1.80993328,  0.32726761]])

广播规则 📏

  • 如果对于每个尾随维度
    1. 轴长度匹配,或者
    2. 其中一个长度为 1。
  • 则数组是兼容的。
  • 广播发生在缺失或长度为 1 的维度上。

广播规则(可视化)

  • 这是一个将形状为 (3,) 的一维数组沿着 0 轴广播到形状为 (4, 3) 的二维数组的例子

广播示例:减去行均值

arr = rng.standard_normal((4, 3))  # 创建一个 4x3 的标准正态分布数组
row_means = arr.mean(1)  # 计算每行的均值
print(row_means.shape)  # 打印行均值的形状

# 将 row_means 重塑为 (4, 1) 以进行广播
demeaned = arr - row_means.reshape((4, 1))  # 减去行均值
print(demeaned.mean(1))  # 验证每行的均值现在为 0(或接近 0)
(4,)
[ 3.70074342e-17 -1.85037171e-17 -1.85037171e-17  0.00000000e+00]

在其他轴上广播

  • 重塑以在轴 0 以外的轴上进行广播。
  • 使用 np.newaxis 和切片添加一个长度为 1 的新轴:
arr = np.zeros((4, 4))  # 创建一个 4x4 的全 0 数组
arr_3d = arr[:, np.newaxis, :]  # 在中间添加一个新轴
print(arr_3d.shape)  # 打印新数组的形状

arr_1d = rng.standard_normal(3)  # 创建一个长度为 3 的标准正态分布数组
print(arr_1d[:, np.newaxis])  # 转换为列向量
print(arr_1d[np.newaxis, :])  # 转换为行向量
(4, 1, 4)
[[0.06114402]
 [0.0709146 ]
 [0.43365454]]
[[0.06114402 0.0709146  0.43365454]]

通过广播设置值

  • 广播适用于设置值:
arr = np.zeros((4, 3))  # 创建一个 4x3 的全 0 数组
arr[:] = 5  # 将所有元素设置为 5
print(arr)  # 打印修改后的数组

col = np.array([1.28, -0.42, 0.44, 1.6])  # 创建一个包含 4 个元素的一维数组
arr[:] = col[:, np.newaxis]  # 将 col 广播到 arr 的每一行
print(arr)  # 打印修改后的数组
[[5. 5. 5.]
 [5. 5. 5.]
 [5. 5. 5.]
 [5. 5. 5.]]
[[ 1.28  1.28  1.28]
 [-0.42 -0.42 -0.42]
 [ 0.44  0.44  0.44]
 [ 1.6   1.6   1.6 ]]

ufunc 高级用法 🚀

  • 通用函数 (ufunc) 具有用于向量化操作的方法。
  • reduce:通过二元运算进行聚合:
arr = np.arange(10)  # 创建一个包含 0 到 9 的一维数组
np.add.reduce(arr)  # 类似于 arr.sum(),对数组元素求和
np.int64(45)

ufunc 方法:accumulateouter

  • accumulate:中间“累积”值:
arr = np.arange(15).reshape((3, 5))  # 创建一个 3x5 的数组
np.add.accumulate(arr, axis=1)  # 沿行计算累积和
array([[ 0,  1,  3,  6, 10],
       [ 5, 11, 18, 26, 35],
       [10, 21, 33, 46, 60]])
  • outer:成对的叉积:
arr = np.arange(3).repeat([1, 2, 2])  # 创建一个 [0, 1, 1, 2, 2] 的数组
np.multiply.outer(arr, np.arange(5))  # 计算 arr 和 [0, 1, 2, 3, 4] 的外积
array([[0, 0, 0, 0, 0],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 2, 4, 6, 8],
       [0, 2, 4, 6, 8]])

ufunc 方法:reduceat

  • reduceat:“局部”归约(数组分组):
arr = np.arange(10)  # 创建一个包含 0 到 9 的一维数组
np.add.reduceat(arr, [0, 5, 8])  # 归约 [0:5]、[5:8]、[8:]
array([10, 18, 17])
arr = np.multiply.outer(np.arange(4), np.arange(5))  # 计算外积
np.add.reduceat(arr, [0, 2, 4], axis=1) # 沿列归约
array([[ 0,  0,  0],
       [ 1,  5,  4],
       [ 2, 10,  8],
       [ 3, 15, 12]])

ufunc 方法表

方法 描述
accumulate(x) 聚合,保留部分聚合。
at(x, i, b=None) 在索引 i 处对 x 进行原地操作。
reduce(x) 通过连续操作进行聚合。
reduceat(x, bins) “局部”归约/分组;归约切片以生成聚合数组。
outer(x, y) 将操作应用于所有对;结果具有形状 x.shape + y.shape

Numba:快速 NumPy 函数 🏎️

  • Numba:为类 NumPy 数据(CPU、GPU 等)创建快速函数。
  • 使用 LLVM 将 Python 转换为机器码。
  • 示例:纯 Python 函数:
import numpy as np  # 导入 NumPy 库

def mean_distance(x, y):  # 定义一个计算平均距离的函数
    nx = len(x)  # 获取 x 的长度
    result = 0.0  # 初始化结果
    count = 0  # 初始化计数器
    for i in range(nx):  # 遍历 x
        result += x[i] - y[i]  # 累加差值
        count += 1  # 计数器加 1
    return result / count  # 返回平均值

Numba 编译

  • 这很。使用 numba.jit 编译:
import numba as nb  # 导入 Numba 库

numba_mean_distance = nb.jit(mean_distance)  # 使用 jit 装饰器编译 mean_distance 函数

# 或者,使用装饰器:
@nb.jit  # 使用 jit 装饰器
def numba_mean_distance(x, y):  # 定义一个计算平均距离的函数
    nx = len(x)  # 获取 x 的长度
    result = 0.0  # 初始化结果
    count = 0  # 初始化计数器
    for i in range(nx):  # 遍历 x
        result += x[i] - y[i]  # 累加差值
        count += 1  # 计数器加 1
    return result / count  # 返回平均值
  • numba_mean_distance 速度快得多(甚至可能比 NumPy 的版本还要快!)。

使用 Numba 创建自定义 ufunc

  • numba.vectorize 创建编译的 NumPy ufunc
from numba import vectorize  # 从 Numba 导入 vectorize 装饰器

@vectorize  # 使用 vectorize 装饰器
def nb_add(x, y):  # 定义一个加法函数
    return x + y  # 返回 x + y
  • 现在,nb_add 充当 ufunc

结构化数组 🏢

  • ndarray 通常是同质的。
  • 结构化数组:每个元素代表一个“结构”(类似于 C 中的结构)或 SQL 表行。
dtype = [('x', np.float64), ('y', np.int32)]  # 字段名称和类型
sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype)  # 创建结构化数组
print(sarr)  # 打印数组
print(sarr[0])  # 打印第一个元素
print(sarr[0]['y'])  # 访问第一个元素的 'y' 字段
[(1.5       ,  6) (3.14159265, -2)]
(1.5, 6)
6

嵌套数据类型和多维字段

dtype = [('x', np.int64, 3), ('y', np.int32)]  # 'x' 是一个包含 3 个 int64 的数组
arr = np.zeros(4, dtype=dtype)  # 创建一个结构化数组
print(arr)  # 打印数组
print(arr[0]['x'])  # 访问第一个元素的 'x' 字段
[([0, 0, 0], 0) ([0, 0, 0], 0) ([0, 0, 0], 0) ([0, 0, 0], 0)]
[0 0 0]

为什么使用结构化数组?

  • 将内存解释为表格结构。
  • 对于磁盘 I/O(包括内存映射)很有效。
  • 表示来自 C/C++ 代码的数据。
  • 比 pandas DataFrame 更底层。

排序 ️️️️️⬆️

  • ndarray.sort():原地排序:
arr = rng.standard_normal(6)  # 创建一个包含 6 个标准正态分布元素的数组
arr.sort()  # 升序排序
print(arr)  # 打印排序后的数组
[-0.79501746  0.27748366  0.30003095  0.53025239  0.53672097  0.61835001]
  • numpy.sort():创建一个新的、排序的副本。
arr = rng.standard_normal(5)  # 创建一个包含 5 个标准正态分布元素的数组
print(np.sort(arr))  # 排序副本
[-1.60270159 -1.26162378 -0.07127081  0.26679883  0.47404973]
  • 两种方法都接受 axis 参数
arr = rng.standard_normal((3, 5))   # 创建一个3x5的标准正态分布的数组
arr.sort(axis=1)  # 对每一行进行排序
print(arr)  # 打印排序后的数组
[[-1.64041784 -0.85725882 -0.41485376  0.0977165   0.68828179]
 [-1.38835995 -1.15452958 -1.09542531 -0.90738246  0.65045239]
 [-1.06580785 -0.18147274  0.00714569  0.5343599   1.6219518 ]]

间接排序:argsortlexsort

  • 间接排序:返回整数索引以重新排序数据。
  • argsort():返回将对数组进行排序的索引:
values = np.array([5, 0, 1, 3, 2])  # 创建一个一维数组
indexer = values.argsort()  # 获取排序后的索引
print(indexer)  # 打印索引
print(values[indexer])  # 使用索引对数组进行排序
[1 2 4 3 0]
[0 1 2 3 5]

lexsort:多个键

  • lexsort():对多个键进行字典序排序(最后一个数组是主键):
first_name = np.array(['Bob', 'Jane', 'Steve', 'Bill', 'Barbara'])  # 创建一个包含名字的数组
last_name = np.array(['Jones', 'Arnold', 'Arnold', 'Jones', 'Walters'])  # 创建一个包含姓氏的数组
sorter = np.lexsort((first_name, last_name))  # 先按姓氏排序,再按名字排序
print(list(zip(last_name[sorter], first_name[sorter])))  # 打印排序后的姓名
[(np.str_('Arnold'), np.str_('Jane')), (np.str_('Arnold'), np.str_('Steve')), (np.str_('Jones'), np.str_('Bill')), (np.str_('Jones'), np.str_('Bob')), (np.str_('Walters'), np.str_('Barbara'))]

替代排序算法

  • 算法:quicksort(默认)、mergesortheapsorttimsort
  • 稳定排序:保留相等元素的相对位置(mergesort 是稳定的)。
values = np.array(['2:first', '2:second', '1:first', '1:second', '1:third'])  # 创建一个字符串数组
key = np.array([2, 2, 1, 1, 1])  # 创建一个键数组
indexer = key.argsort(kind='mergesort')  # 使用稳定的 mergesort 进行排序
print(values.take(indexer))  # 使用索引对值数组进行排序
['1:first' '1:second' '1:third' '2:first' '2:second']

数组排序方法表

种类 速度 稳定性 工作空间 最坏情况
‘quicksort’ 1 0 O(n^2)
‘mergesort’ 2 ~n/2 O(n log n)
‘heapsort’ 3 0 O(n log n)
‘timsort’ 4 ~n/2 O(n log n)
  • 注意:timsort 也是稳定的,并且通常非常高效。

部分排序数组

  • numpy.partition, np.argpartition:围绕第 k 个最小元素进行分区。
rng = np.random.default_rng(12345)  # 创建一个随机数生成器
arr = rng.standard_normal(20)  # 创建一个包含 20 个标准正态分布元素的数组
np.partition(arr, 3)  # 前 3 个是最小的(未排序)
array([-1.95286306, -1.42382504, -1.3677927 , -1.25666813, -0.87066174,
       -0.75938718, -0.74088465, -0.46695317, -0.25917323, -0.07534331,
       -0.06068952,  0.36105811,  0.57585751,  0.6488928 ,  0.78884434,
        0.90219827,  0.96849691,  1.26372846,  1.39897899,  2.34740965])
  • np.argpartition 返回索引:
indices = np.argpartition(arr, 3)  # 获取部分排序的索引
arr.take(indices)  # 部分排序
array([-1.95286306, -1.42382504, -1.3677927 , -1.25666813, -0.87066174,
       -0.75938718, -0.74088465, -0.46695317, -0.25917323, -0.07534331,
       -0.06068952,  0.36105811,  0.57585751,  0.6488928 ,  0.78884434,
        0.90219827,  0.96849691,  1.26372846,  1.39897899,  2.34740965])

numpy.searchsorted:查找元素 🔎

  • searchsorted:在排序数组上进行二分查找;返回插入索引。
arr = np.array([0, 1, 7, 12, 15])  # 创建一个排序数组
print(arr.searchsorted(9))   # 在哪里插入 9?
print(arr.searchsorted([0, 8, 11, 16]))  # 查找多个值的插入位置
3
[0 3 3 5]
  • side='right' 更改相等值的行为。

searchsorted 示例:分箱

data = np.floor(rng.uniform(0, 10000, size=50))  # 生成 50 个 0 到 10000 之间的随机整数
bins = np.array([0, 100, 1000, 5000, 10000])  # 定义分箱边界
labels = bins.searchsorted(data)  # 对每个数据点进行分箱
print(labels)  # 打印每个数据点所属的箱子标签
[2 3 3 3 3 4 3 3 2 4 4 4 4 4 4 4 4 4 3 3 3 4 3 4 3 3 3 3 1 4 3 2 4 3 3 3 3
 3 3 3 3 3 3 3 3 3 3 3 4 3]
  • 与 pandas 的 groupby 结合使用以获取箱子统计信息。

内存映射文件 💾

  • 内存映射文件:与磁盘上的二进制数据交互,就像它在内存中一样。
  • memmap:NumPy 的类似 ndarray 的对象。读取/写入段,而无需加载整个文件。
  • 使用 np.memmap 创建:指定路径、dtype、形状、模式:
mmap = np.memmap('mymmap', dtype='float64', mode='w+',
                 shape=(10000, 10000))  # 创建一个内存映射文件
print(mmap)  # 打印内存映射对象
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]

内存映射文件(续)

  • 切片返回磁盘上的视图
section = mmap[:5]  # 前 5 行
  • 分配缓冲区在内存中;使用 flush() 写入:
section[:] = rng.standard_normal((5, 10000))  # 将数据写入切片
mmap.flush()  # 将更改刷新到磁盘
  • 打开现有映射仍然需要 dtype 和形状:
mmap = np.memmap('mymmap', dtype='float64', shape=(10000, 10000))  # 打开现有的内存映射文件
print(mmap)  # 打印内存映射对象
[[-0.90738246 -1.09542531  0.00714569 ...  0.27528689 -1.164065
   0.85209933]
 [-0.01030507 -0.06457559 -1.06146483 ... -1.10033268  0.25046196
   0.58323566]
 [ 0.45830978  1.2992377   1.71366921 ...  0.86913463 -0.78886549
  -0.24314164]
 ...
 [ 0.          0.          0.         ...  0.          0.
   0.        ]
 [ 0.          0.          0.         ...  0.          0.
   0.        ]
 [ 0.          0.          0.         ...  0.          0.
   0.        ]]
  • memmap 适用于结构化数据类型。

性能提示 🚀

  • 关键:用 NumPy 数组/布尔运算替换循环/条件语句。
  • 使用广播。
  • 使用数组视图(切片)– 避免复制。
  • 使用 ufunc 和 ufunc 方法。
  • 如果需要,考虑 C、FORTRAN 或 Cython。

连续内存 🧠

  • 内存布局影响性能。
  • 连续:元素按顺序存储(C 或 FORTRAN)。
  • 访问连续块最快(CPU 缓存)。
  • NumPy 数组默认为 C 连续;转置是 Fortran 连续的。
  • 使用 flags 检查:
arr_c = np.ones((100, 10000), order='C')  # 创建一个 C 连续数组
arr_f = np.ones((100, 10000), order='F')  # 创建一个 Fortran 连续数组
print(arr_c.flags)  # C_CONTIGUOUS: True, F_CONTIGUOUS: False
print(arr_f.flags)  # C_CONTIGUOUS: False, F_CONTIGUOUS: True
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False

  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False

连续内存(续)

  • 对 C 连续数组的行求和通常更快。
  • 如果需要,使用 copy()'C''F'
arr_f.copy('C').flags  # 将 Fortran 连续数组复制为 C 连续数组
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  • 重要:视图不保证连续;检查 flags.contiguous

总结 📚

  • ndarray 内部结构:dtype、形状、跨度。
  • 数组操作:重塑、连接、拆分、重复。
  • 广播机制。
  • 高级 ufunc 方法。
  • 结构化数组。
  • 排序(间接、部分)。
  • 内存映射文件。
  • 性能:连续内存。
  • Numba!

思考与讨论 💭

  • 如何应用这些技术?
  • 内存映射文件在什么时候有用?
  • 什么时候使用 Numba?
  • 结构化数组 vs. pandas DataFrame?
  • 排序算法选择?
  • 向量化操作 vs. 循环?
  • 进一步探索 Numba!