学习笔记-Numpy基础与实战入门

一、NumPy概述

在进行Python的数据处理或科学计算时,NumPy 是一个绕不开的基础库。它提供了高效的多维数组对象(即 ndarray)和丰富的数学运算函数,能够让我们用 Python 以接近C语言的速度进行数值计算。相比 Python 原生的列表,NumPy 数组中的元素类型统一、在内存中连续存储,便于CPU向量化处理,因此计算效率极高。此外,许多科学计算和机器学习库(如 Pandas、TensorFlow 等)都是建立在 NumPy 之上的。总之,学好 NumPy 能为后续的数据处理和分析打下坚实基础。

二、创建 NumPy 数组

1. 使用 Python 序列创建 ndarray

最常用的方式是直接从 Python 的列表或元组创建 NumPy 数组。只需调用 np.array() 方法即可将序列转换为 ndarray 对象:

import numpy as np# 从列表创建一维数组
a = np.array([1, 2, 3, 4, 5])       # [1 2 3 4 5] -> 1行5列的一维数组
b = np.array((6, 7, 8, 9, 10))     # [ 6  7  8  9 10] -> 1行5列,由元组创建
print(a.dtype, b.shape)            # int64 (5,) 数据类型和形状

上述代码中,我们分别从列表和元组创建了一维数组 a 和 b。NumPy 自动将元素转换为同一种类型(这里都是整数型 int64),并将它们存储为连续内存。a.dtype 表示数组的元素类型,b.shape 则是数组的形状。输出结果表明两个数组都是长度为5的一维数组,元素类型为64位整数。

2. 使用 NumPy 内建函数创建数组

NumPy 提供了多种内建函数来方便地创建特定内容的数组:

# 使用 np.arange 创建连续整数序列
c = np.arange(1, 6)                 # [1 2 3 4 5] -> 1行5列,类似 range(),生成1到5的整数
d = np.arange(1.0, 6.0)             # [1. 2. 3. 4. 5.] -> 1行5列,起止为浮点数,则元素为 float64
print(c, c.dtype)                   # [1 2 3 4 5] int64
print(d, d.dtype)                   # [1. 2. 3. 4. 5.] float64# 创建全零或全一数组
e = np.zeros(5)                     # [0. 0. 0. 0. 0.] -> 长度5的全0数组,默认dtype=float64
f = np.ones(5, dtype=np.int32)      # [1 1 1 1 1] -> 长度5的全1数组,元素类型int32
g = np.ones_like(e)                 # [1. 1. 1. 1. 1.] -> 创建一个形状和e相同的全1数组,dtype跟e相同(float64)
print(e, f, g)                      # [0. 0. 0. 0. 0.] [1 1 1 1 1] [1. 1. 1. 1. 1.]# 创建等差数列数组的两种方式
h = np.linspace(1, 10, 4)           # [ 1.  4.  7. 10.] -> 等差序列,从1到10均匀取4个数
interval = 6.28 / 199
x = np.arange(-3.14, 3.14 + interval, interval)
y = np.linspace(-3.14, 3.14, 200)
print(len(x), x[0], x[-1])          # 200 -3.14 3.14 -> np.arange 实现等步长数列
print(len(y), y[0], y[-1])          # 200 -3.14 3.14 -> np.linspace 实现等步长数列# 创建随机数组
r = np.random.randn(2, 5)           # 创建一个2x5的标准正态分布随机数组
print(r.shape, r.mean())           # (2, 5) ~0 (二维数组形状,均值接近0)

以上演示了几种常见的数组创建方式:

  • 连续数组:np.arange(start, end, step) 类似于 Python 内置的 range(),可以生成等差序列。注意如果不指定 dtype,整数步长下默认生成 int64,而有浮点数参与时则生成 float64 类型。
  • 特殊数组:np.zeros(shape) 和 np.ones(shape) 分别生成给定形状的全0或全1数组。通过参数 dtype 可以指定数据类型。np.ones_like(e) 和 np.zeros_like(e) 则是基于现有数组形状创建全1或全0的新数组。
  • 等差数列:np.linspace(start, end, num) 用于在指定范围生成指定个数的等间隔点,常用于生成连续函数的自变量数组。上例中 h 从1到10生成4个值,而 x 和 y 示范了利用 np.arange 和 np.linspace 各生成200个从 -3.14 到 3.14 的等差点。
  • 随机数组:np.random.randn(m, n) 直接生成形状为 m×n 的标准正态分布随机数组(均值0、标准差1)。类似地,NumPy 还有 np.random.rand、np.random.randint 等函数用于生成均匀分布随机数或整数随机数数组。

三、NumPy 数组的运算

NumPy 数组支持向量化的元素运算,这意味着我们可以对整个数组进行算术或比较操作,而无需编写循环,NumPy会将操作应用到每个元素上,充分利用底层的优化。

例如,我们创建一个二维数组,然后对它进行加法和比较运算:

ary = np.array([range(4), range(4, 8)])   # 创建一个2x4的二维数组
print(ary)
# [[0 1 2 3]
#  [4 5 6 7]]
print(ary + 1)                            # 每个元素加1
# [[1 2 3 4]
#  [5 6 7 8]]
print(ary > 3)                            # 与标量比较,返回布尔矩阵
# [[False False False False]
#  [ True  True  True  True]]

可以看到,ary + 1 将数组中每个数都加上了1,得到的新数组仍是相同形状。同时,ary > 3 会对每个元素进行比较判断,返回一个同样形状的布尔数组,表示对应位置上元素是否大于3。这种逐元素的运算方式让代码既简洁又高效。在 NumPy 中,标量和数组之间的运算会自动广播(broadcasting):标量视作与数组形状兼容,算术运算时相当于扩展标量为同形状的数组再逐元素计算。除了与标量外,如果两个数组的形状满足一定规则(如其中一方维度为1或彼此维数相同),NumPy 也能进行广播运算,这是高级话题,这里不展开。

值得一提的是,NumPy 的算术和比较运算结果都是新的数组,不会修改原数组的值。如果要对原数组进行原地修改,可以使用赋值运算符配合切片或索引(稍后介绍)。

四、ndarray 的基本属性

NumPy ndarray 对象包含一些描述数组性质的重要属性,例如形状、维度、数据类型等等。了解和使用这些属性可以帮助我们更好地掌握数组的结构和特性。

以下代码创建了一个 2x4 的数组,演示了 ndarray 的常见属性:

arr = np.arange(8, dtype=np.float32).reshape(2, 4)
print(arr.shape)    # (2, 4) -> 数组形状为2行4列
print(arr.ndim)     # 2 -> 数组维度数(秩),二维数组
print(arr.size)     # 8 -> 数组元素总个数
print(len(arr))     # 2 -> len 返回数组第一维长度,这里是2
print(arr.dtype)    # float32 -> 数组元素的数据类型
print(arr.itemsize) # 4 -> 每个元素占用字节数(float32每个4字节)
print(arr.nbytes)   # 32 -> 数组总字节大小(size * itemsize = 8*4)

我们创建了 arr 包含0到7的 8个元素,并reshape成2x4形状。输出中:

  • shape:数组的形状,用一个元组表示各维度大小,这里是 (2, 4),表示2行4列。
  • ndim:数组的维度数,这里是2,表示 arr 是二维数组。
  • size:数组元素的总数量,这里是8。
  • len(arr):等价于 arr.shape[0],即数组第一维的长度,此例中为2。
  • dtype:数组元素的数据类型,本例中 np.float32(32位浮点)。
  • itemsize:每个元素占用的字节数,float32 每个元素4字节。
  • nbytes:数组占用的总字节数,等于 size * itemsize,这里是32字节。 通过这些属性,我们可以方便地获知数组的结构和存储信息。例如,当检查数据读入是否正确,或调优存储和性能时,这些属性都非常有用。

五、数组的索引和切片

NumPy 数组的索引(indexing)和切片(slicing)操作与 Python 列表类似,但也有一些值得注意的区别和特性。

1. 基本索引

一维数组可以用与列表相同的语法通过索引访问元素,例如 arr[i] 获取第 i 个元素(从0开始计数)。多维数组则可以用逗号分隔的索引来访问特定位置,例如 arr2d[i, j] 表示获取二维数组第 i 行、第 j 列的元素。也可以逐层索引:arr2d[i][j] 等价于 arr2d[i, j]。

2. 切片获取子数组

对 ndarray 使用切片语法(如 arr[start:stop:step])可以获取数组的某一部分子数组。与列表不同的是,对多维数组可以对每一维分别进行切片,例如 arr2d[0:2, 1:3] 可以同时在行和列两个维度上切片。

需要注意,数组切片返回的是原数组的视图(view),并不拷贝数据。也就是说,修改切片结果会影响到原数组对应的部分。这一点与 Python 列表的切片不同(列表切片会生成新的列表)。如果需要获得一份独立的拷贝,可以使用 np.copy() 或调用数组的 .copy() 方法。

下面通过例子说明索引和切片的差异:

arr = np.arange(1, 9).reshape(2, 4)
print(arr[1], arr[1].shape)    # 访问第2行(索引1),返回1维数组,shape=(4,)
print(arr[1:2], arr[1:2].shape)  # 切片第2行,返回保留行维度的2维数组,shape=(1, 4)# 切片是视图,修改它会影响原数组
sub_arr = arr[0, 1:3]          # 取出第1行(索引0)的第2-3列子数组
sub_arr[0] = 99                # 修改子数组的第1个元素
print(arr[0])                  # 原数组第1行相应元素也变为99
# [ 1 99 99 4]

上例中,arr[1] 直接索引第二行,得到的是一个一维数组(长度4);而 arr[1:2] 使用切片截取第二行,得到的仍是二维数组,只是行数为1。单索引会降低维度,而切片会保留原有维度。另外,我们将 arr[0, 1:3](第1行中第2和第3列)赋给 sub_arr 后,修改了 sub_arr 中的元素,结果原数组 arr 中对应位置的值也改变了。这证明切片得到的 sub_arr 并没有独立拷贝数据,它与原数组共享内存。如果我们希望对提取出的子数组进行操作但不影响原数据,可以在切片时加上 .copy() 来生成副本。

六、数据类型与转换

NumPy 支持多种数据类型(dtype),不仅有常见的数值类型(int、float),还有布尔型、字符串,甚至日期时间和复合类型。灵活地运用数据类型有时能提高效率或方便地处理特殊数据。下面我们分几类介绍。

1. 基本数值类型及 astype 转换

在 NumPy 中,整数默认是int64,浮点数默认是float64(除非显式指定 dtype)。我们可以使用 .astype() 方法在不同类型之间转换:

x = np.array([1, 2, 3], dtype=np.int64)
print(x.dtype)           # int64
y = x.astype(np.float32) 
print(y, y.dtype)        # [1. 2. 3.] float32 -> 将整数转为32位浮点

上例中,我们将整型数组 x 转换为了浮点型数组 y。astype 会返回一个新数组,原数组 x 保持不变。NumPy 会尽可能地执行安全转换,例如整数转浮点是允许的。但如果把浮点转换为整数,NumPy 默认会截断小数部分(不是四舍五入),这一点需要注意。

2. 布尔类型和字符串类型

NumPy 用 bool 类型表示布尔值,True 在内部会存储为1,False 为0。布尔数组经常用于作为掩码筛选数据。布尔型的存储比较节省,每个元素通常占用1个字节。

NumPy 的字符串类型(通常显示为 <U)表示定长的 Unicode 字符串,其中 <U 表示Unicode字符串,n表示字符长度上限。NumPy 会根据数组中最长的字符串长度来确定 n 的大小,所有元素都会被截断或填充到这个长度。需要注意,NumPy 字符串dtype主要适用于简单的固定长度文本处理,对于更灵活的字符串操作,通常还是使用Python的字符串或pandas的字符串功能。

来看一个例子,我们创建布尔数组和字符串数组,观察它们的 dtype 和内存占用:

bool_arr = np.zeros(5, dtype=bool)
print(bool_arr, bool_arr.dtype, bool_arr.itemsize)  
# [False False False False False] bool 1  -> 5个False,类型bool,每个占1字节str_arr = np.array(['NumPy', '数据分析', 'Python3.9'])
print(str_arr.dtype, str_arr.itemsize, str_arr.nbytes)  
# <U6 24 72 -> dtype为<U6,每个元素固定6个字符(24字节),总数组占72字节(3*24)

上面的 bool_arr 是长度5的布尔数组,打印可见元素值为 False,dtype 为 bool,每个元素 itemsize 为1字节。str_arr 是包含三个字符串的数组:‘NumPy’ 长度5,‘数据分析’ 长度4(两个汉字长度为2,每个汉字算一个字符),‘Python3.9’ 长度8。但是 NumPy 为整个数组统一分配了长度6(能够容纳最长的8字符吗?这里汉字两个可能占2字符位置,各实现不同,但dtype给出<U6可能基于最长unicode code points数)。结果 dtype 为 <U6,表示每个元素固定为6个字符。itemsize 为24,意味着每个元素用24字节(6个Unicode字符,每字符4字节)存储,总 nbytes 为72字节。可以看到,对于短字符串来说,这种定长存储会有些浪费空间。

3. 时间日期类型

NumPy 引入了专门的日期时间类型 datetime64 和时间间隔类型 timedelta64 来方便地进行时间相关的数据运算。例如,我们可以把字符串表示的日期转换为 datetime64:

dates = np.array(['2023', '2024-01-01', '2025-03-07 15:30:21'])
print(dates.dtype)            # <U19 (字符串类型)
dates = dates.astype('datetime64[s]')
print(dates, dates.dtype)
# ['2023-01-01T00:00:00' '2024-01-01T00:00:00' '2025-03-07T15:30:21'] datetime64[s]
print(dates.astype('int64'))
# [1672531200 1704067200 1741361421]  -> 转换为Unix时间戳(秒)

上例中,我们定义了三个日期字符串:‘2023’(只有年份)、‘2024-01-01’(年月日)、‘2025-03-07 15:30:21’(完整日期时间)。转换为 datetime64[s] 后,NumPy 将它们统一解释为秒级别的时间点:缺省的日期部分自动补为 01-01,时间补为午夜。打印结果可以看到,‘2023’ 变为了 ‘2023-01-01T00:00:00’。接着,我们又将 datetime64 数组转换为 int64,结果变成了对应的 Unix 时间戳(从1970-01-01起的秒数)。这在需要计算两个日期差或做时间排序时非常有用。

4. 结构化数据类型(复合类型)

有时候,我们希望在一个数组中存放不同类型的字段,比如一张表格数据包含姓名(字符串)、年龄(整数)、成绩列表(整数列表)等。NumPy 的结构化数组允许我们自定义这样复合的数据类型。可以将结构化dtype理解为“元素为元组的数组”,每个元素包含多个不同类型的值,类似于数据库或表格的一行。

我们可以通过 dtype 参数来定义结构化类型。比如,定义一个 dtype 包含姓名(name)、三个科目成绩(scores)和年龄(age)三个字段:

data = [
    ('张三', [85, 92, 78], 21),
    ('李四', [75, 85, 89], 19),
    ('王五', [90, 88, 95], 20)
]
dtype = {
    'names':   ('name',   'scores',    'age'),
    'formats': ('U2',     '3int32',    'int32')
}
arr = np.array(data, dtype=dtype)
print(arr[0])          # ('张三', [85, 92, 78], 21)
print(arr['name'])     # ['张三' '李四' '王五']
print(arr['age'].mean())   # 20.0 -> 年龄字段的平均值

在这个例子中,我们构建了一个列表,每个元素是 (姓名, [三门课成绩], 年龄) 的元组。然后通过 dtype 字典指定每个字段的名称和类型:姓名为2位Unicode字符串(‘U2’),成绩为3个32位整数组成的数组(‘3int32’),年龄为32位整数。np.array 根据这个dtype创建了结构化数组 arr。

打印 arr[0] 可以看到,每个元素显示为一个复合的元组。我们可以像字典一样通过字段名访问数据,比如 arr[‘name’] 会提取所有元素的姓名字段组成新的数组,arr[‘age’] 则是所有年龄的数组。因此我们可以对年龄字段直接调用 .mean() 来计算平均年龄。结构化数组让我们可以在NumPy中方便地处理表格化的数据,但需要注意它的元素是复合类型,某些NumPy通用函数并不直接支持逐字段操作,在使用时要仔细选择和处理。

七、读取外部数据为数组

在实际应用中,我们经常需要将外部数据(例如文本文件、CSV文件)读入 NumPy 数组中。NumPy 提供了诸如 np.loadtxt、np.genfromtxt 等方法直接读取文件,但这里我们展示一个手动读取并转换的过程,以理解数据清洗到数组的步骤。

假设我们有一个股票数据文件 aapl.csv,其中每行包含日期和当日股票的开盘价、最高价、最低价、收盘价和成交量,比如:

2021-01-04,133.52,133.6116,126.76,129.41,143301900
2021-01-05,128.89,131.74,128.43,131.01,97664900
...

我们可以逐行读取文件,将每行数据解析后存入 NumPy 数组:

f = open('aapl.csv')
data_list = []
for line in f:
    # 去除换行符并按逗号分割
    date, open_, high, low, close, volume = line.strip().split(',')
    # 将每行数据转换成对应类型的tuple后添加到列表
    data_list.append((date, float(open_), float(high), float(low), float(close), int(volume)))
f.close()
# 定义结构化dtype,与文件列一一对应
dtype = [('date', 'U10'), ('open', 'f4'), ('high', 'f4'), ('low', 'f4'), ('close', 'f4'), ('volume', 'u8')]
data = np.array(data_list, dtype=dtype)
print(data['high'].max())   # 145.09  -> 最高价(示例输出)

这段代码中,我们打开文件后逐行读取。对于每一行,用 strip().split(’,’) 拆分出各字段,然后构造出对应类型的元组。例如日期保持字符串,价格用 float 转换,成交量用 int 转换。将所有元组收集到列表后,通过 np.array(…, dtype=自定义dtype) 一次性转换为结构化的 NumPy 数组。这里我们定义了 dtype 包含6个字段,对应日期(10位以内字符串),开盘、最高、最低、收盘价(32位浮点),以及成交量(无符号64位整数)。

最后,我们打印了 data[‘high’].max(),即利用 NumPy 数组的便捷性,直接对 最高价 一列求最大值。这将输出整个数据中股票的历史最高价(这里假设为 145.09,仅为示例)。通过这种方式,我们可以轻松地对结构化数组的任意一列进行统计计算。

提示: 实际使用中,可以直接使用 np.loadtxt 或 np.genfromtxt 来读取CSV等文本数据为数组,并指定分隔符和dtype,从而减少手动解析的代码。不过,上述例子有助于理解数据读入和dtype指定的过程。

八、数组形状变换

多维数组的形状(shape)可以通过多种方式改变,例如改变维度的排列组合,或增加/减少维度。NumPy 提供的操作主要有 reshape、flatten/ravel 以及 resize 等。需要注意的是,不同操作有不同的内存机制:有的会返回原数组的视图,有的会产生新的数组。

1. reshape:视图变维

ndarray.reshape(new_shape) 可以返回一个改变了形状的新数组(视图)。在不违反内存连续性规则的前提下,reshape 不会复制原数据,因此修改 reshape 后的数组也会影响原数组。

a = np.arange(12)               # 创建包含0~11的一维数组,共12个元素
b = a.reshape(3, 4)             # 将a视图reshape为3行4列的二维数组
b[0, 0] = 99                    # 修改b中的元素
print(a)                        # [99  1  2  3  4  5  6  7  8  9 10 11]
print(b)                        # [[99  1  2  3]
#                                [ 4  5  6  7]
#                                [ 8  9 10 11]]

可以看到,我们将一维数组 a 通过 reshape(3,4) 得到二维数组 b。当我们修改 b[0,0] 为 99 后,再查看原数组 a,第一个元素也变为了 99。说明 a 和 b 实际上共享底层的数据内存,只是视图不同。

需要注意,如果 a 在内存中不是连续存储(比如对非连续切片调用reshape),那么可能会触发复制行为或报错。但对于像上例这样由连续数组得到的新形状,reshape 是高效且安全的。

2. flatten:复制变维

如果希望获得数组的一维拷贝,可以使用 flatten() 方法。它会返回一个新的数组(深拷贝),与原数组不再共享数据。

c = a.flatten()                # 将数组a展开为一维并复制
c[0] = 0
print(a)                       # [99  1  2  3  4  5  6  7  8  9 10 11] 原数组a不受影响
print(c)                       # [0 1 2 3 4 5 6 7 8 9 10 11] 新数组c的修改不影响a

我们对 a.flatten() 得到的新数组 c 修改了第一个元素为0,可以看到原数组 a 仍然保持原样(以99开头),并没有变。这证明 flatten 返回的是数据的副本。类似地,np.ravel() 则尽可能返回视图(与 reshape(-1,) 类似),只有在无法视图化展开时才会复制。有时候我们也用 arr.reshape(-1) 达到展平效果,这种方式等价于 ravel。

3. resize:就地变维

ndarray.resize(new_shape) 方法会直接在原数组上改变其形状。与 reshape 不同,resize 是就地操作,会修改原数组本身。如果新尺寸比原来的总元素数少,后面的元素会被截断;如果新尺寸更大,则会用0填充新增的位置。

a.resize(3, 3)                 # 将数组a直接改为3x3形状
print(a)
# [[99  1  2]
#  [ 3  4  5]
#  [ 6  7  8]]

我们对 a 调用了 resize(3,3)。原先 a 有12个元素,调整为3x3后只能容纳9个元素,因此最后的 [9,10,11] 三个值被舍弃了。resize 修改了原数组本身的 shape,现在 a 变成了3行3列的新数组。如果我们打印 a 可以看到,它包含了原来前9个元素(包括之前修改的99),其余被丢弃。

需谨慎使用 resize,因为它会破坏原数组的数据。如果只是想得到特定形状的数据副本,优先考虑 reshape 返回新视图,或者 np.copy 后再 reshape,以免无意中修改原始数据。

九、布尔索引与位置索引

在前面第三节我们已经看过布尔数组的用法,现在来详细介绍布尔索引和位置索引。这两者都是 NumPy 中强大的索引机制,可用于从数组中筛选出我们需要的元素或重排数组顺序。

1. 布尔索引(Boolean Indexing)

布尔索引是指使用一个布尔数组来索引另一个数组。布尔数组中为 True 的位置,会在结果中保留下来。

例如,我们有一个 1 到 9 的数组,现在想筛选出其中是 3 的倍数的元素:

ary = np.arange(1, 10)
print(ary % 3 == 0)           # [False False  True False False  True False False  True]
print(ary[ary % 3 == 0])      # [3 6 9] -> 筛选出3的倍数

ary % 3 == 0 会得到一个和 ary 等长的布尔数组,只有当元素是3的倍数时对应位置为 True。将这个布尔数组作为索引传给 ary,就获得了所有 True 对应位置上的元素 [3, 6, 9]。

我们也可以构造自己的布尔列表来手动选择元素:

mask = [True, False, True, False, False, True, False, True, False]
print(ary[mask])              # [1 3 6 8] -> True 所在位置的元素被选出

上面 mask 列表长度必须和 ary 相同,其中 True 的位置有0,2,5,7(对应元素1,3,6,8)被筛选出来。

布尔索引常用的一个场景是直接对数组施加条件筛选,如上例所示。而对于更复杂的条件,可以结合位运算符逐步构造。例如筛选同时是3和7的倍数的元素,可以这样:

cond = (ary % 3 == 0) & (ary % 7 == 0)
print(cond)                   # [False False False False False False False False False]
print(ary[cond])              # [] -> 1~9中没有同时满足3和7倍数条件的数字

这里 & 表示逐位与运算,相当于逻辑上的“且”。由于1到9中不存在既是3又是7的倍数的数,结果为空数组。如果换一个范围,比如1到100,就可以找到21,42,63,84这样的值了。

2. 位置索引

位置索引是使用整数数组(或列表)来指定想要的索引位置。通过位置索引,我们可以重新排列原数组或者抽取任意指定位置的元素组成新数组。

例如:

idx = [3, 2, 0, 1]            # 定义一个索引顺序
arr = np.arange(1, 6)         # [1 2 3 4 5]
print(arr[idx])               # [4 3 1 2] -> 按idx顺序重新排列得到的新数组

这里我们使用索引列表 [3,2,0,1] 来索引数组 [1,2,3,4,5],结果就是按照给定顺序取出对应元素:第3号元素(值4)、第2号元素(值3)、第0号元素(值1)、第1号元素(值2),组合成 [4, 3, 1, 2]。通过这种方法可以实现对数组的任意重排。

位置索引也支持重复取相同位置。例如:

idx2 = [0, 0, 1, 1]
print(arr[idx2])              # [1 1 2 2] -> 0和1索引各出现两次

结果 [1, 1, 2, 2] 中,原数组第0元素1和第1元素2各被取了两次。可见,索引得到的新数组和原数组的元素个数无关,而取决于索引列表的长度。

3. 利用布尔索引修改值

布尔索引不但可以用于筛选数据,还可以用于直接修改满足条件的元素。例如,我们有一个矩阵,每个元素代表一位学生的成绩,现在希望把及格分(>=60)的标记为1,不及格的标记为0:

scores = np.array([[66.6, 54.2, 88.8],
                   [98.7, 45.6, 12.3],
                   [56.7, 89.0, 33.3]])
mask = scores >= 60
scores[mask] = 1
scores[~mask] = 0
print(scores)
# [[1. 0. 1.]
#  [1. 0. 0.]
#  [0. 1. 0.]]

这里,我们首先用 scores >= 60 得到了一个同形状的布尔矩阵 mask,表示每个分数是否及格。然后利用布尔索引,直接对原数组赋值:scores[mask] = 1 将所有及格的位置设为1,scores[~mask] = 0 将不及格的位置(~mask 为mask取反)设为0。最终 scores 数组中的值全都变成了0或1,实现了我们想要的标记功能。

需要注意,像 scores[mask] = 1 这样的操作会作用于原数组,因此务必要确定这样做是安全且符合预期的。如果只想得到一个标记矩阵而不修改原数据,可以先用 scores.copy() 复制一份再操作,或者直接使用 astype 生成,如 (scores >= 60).astype(int),这会返回一个新的0/1数组,不影响原来的 scores。

十、数组的组合与拆分

当我们有多个数组时,常常需要将它们合并在一起,或者将一个大数组拆分成多个小数组。NumPy 为此提供了多种方便的函数。这里我们介绍按行(垂直)、按列(水平)的组合与拆分,以及简单提及更高维度的情况。

1. 数组的组合 (拼接)

  • 垂直组合:使用 np.vstack((a, b, …)) 可以沿着行方向将数组堆叠起来。要求待组合数组列数相同。等价的方法是 np.concatenate((a,b), axis=0) 其中 axis=0 表示按行拼接。
  • 水平组合:使用 np.hstack((a, b, …)) 沿着列方向拼接数组,要求行数相同。等价于 np.concatenate((a,b), axis=1)。
  • 深度组合:使用 np.dstack((a, b, …)) 可以在第三维度上堆叠数组,要求待组合数组形状相同。结果数组维度会加一(例如两个2x2矩阵深度组合得到 shape=(2,2,2) 的三维数组)。
  • 不规则组合:如果组合的数组在目标轴长度不同,可以用 np.concatenate 或 np.stack 等一般拼接函数自行指定 axis(或使用 np.pad 补齐后组合)。np.stack 和 np.concatenate 的区别在于,stack 会增加一个新维度(比如在新轴叠加形成更高维数组),而 concatenate 是在已有轴上拼接。 下面以垂直和水平组合为例:
a = np.array([[1, 2],
              [3, 4]])
b = np.array([[5, 6],
              [7, 8]])
v = np.vstack((a, b))
h = np.hstack((a, b))
d = np.dstack((a, b))
print(v)
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]
print(h)
# [[1 2 5 6]
#  [3 4 7 8]]
print(d.shape)                # (2, 2, 2) -> 深度组合后数组的形状

这里 a 和 b 都是 2x2 矩阵。vstack 在行上堆叠,结果 v 是 4x2 矩阵;hstack 在列上拼接,结果 h 是 2x4 矩阵。dstack 则把两个矩阵沿第三维组合成一个三维数组(2行2列2层),我们打印了其形状 (2, 2, 2)。如果打印 d 本身,可以看到它其实相当于把 a 和 b 当作两层“叠”在一起。

2. 数组的拆分 (分割)

与组合相反,NumPy 提供了对应的拆分函数:

  • 垂直拆分:np.vsplit(array, sections) 将数组在行方向拆分为若干部分。通常 sections 是一个可以整除行数的整数,表示等分数量;也可以是一个索引列表,表示沿轴切割的位置。类似地可以使用 np.split(array, sections, axis=0) 达到同样效果。
  • 水平拆分:np.hsplit(array, sections) 在列方向拆分数组,要求列数可等分,或提供列索引列表。等价于 np.split(array, sections, axis=1)。
  • 深度拆分:np.dsplit(array, sections) 在第三维上拆分三维数组。同理使用 axis=2 的 split 也行。 例子:将一个 2x6 的数组水平拆分为三个 2x2 的小数组,以及将其垂直拆分为两块:
c = np.arange(1, 13).reshape(2, 6)
print(c)
# [[ 1  2  3  4  5  6]
#  [ 7  8  9 10 11 12]]
print(np.hsplit(c, 3))
# [array([[1, 2],
#        [7, 8]]), array([[3, 4],
#        [9, 10]]), array([[ 5,  6],
#        [11, 12]])]
print(np.vsplit(c, 2))
# [array([[1, 2, 3, 4, 5, 6]]), array([[ 7,  8,  9, 10, 11, 12]])]

我们创建了 c 为 2x6 矩阵,内容是1到12。np.hsplit(c, 3) 将其按列切成3份,每份2列,结果是包含三个 2x2 数组的列表(可以看到输出被分成了三部分)。np.vsplit(c, 2) 则将矩阵按行一分为二,得到两个 1x6 的行向量。使用 np.split 函数也可以实现同样效果,只需指定轴参数。

如果想按不均等的部分拆分,比如将上面的 c 按列拆成前两列和其余列两部分,可以使用索引列表参数:np.hsplit(c, [2])(在列索引2处切开)。组合与拆分操作可以灵活地对数组进行结构调整,对于处理分块数据或将结果分组非常有用。

十一、其他常用属性和方法

最后介绍几个 ndarray 对象的其它有用属性和方法:

  • 实部和虚部 (.real, .imag):对于复数类型的数组,可以通过 .real 和 .imag 属性访问它的实部和虚部部分。
  • 转置 (.T):二维数组的 .T 属性可以获取它的转置(行列互换)。更高维数组的 .transpose() 方法也可以按指定轴顺序转置。
  • 平展迭代 (.flat):.flat 属性提供了一个迭代器,可以用来逐元素遍历数组(依次按行展开)。
  • 转列表 (.tolist()):将数组转换为嵌套的 Python 列表,常用于将结果数据传递给不支持 ndarray 的接口。 下面通过一个包含复数的数组来演示这些属性:
A = np.array([[1+1j, 2+4j, 3+7j],
              [4+2j, 5+5j, 6+8j],
              [7+3j, 8+6j, 9+9j]])
print(A.real)
# [[1. 2. 3.]
#  [4. 5. 6.]
#  [7. 8. 9.]]
print(A.imag)
# [[1. 4. 7.]
#  [2. 5. 8.]
#  [3. 6. 9.]]
print(A.T)
# [[1.+1.j 4.+2.j 7.+3.j]
#  [2.+4.j 5.+5.j 8.+6.j]
#  [3.+7.j 6.+8.j 9.+9.j]]
print([x for x in A.flat])   # [(1+1j), (2+4j), (3+7j), (4+2j), (5+5j), (6+8j), (7+3j), (8+6j), (9+9j)]
print(A.tolist())           # [[(1+1j), (2+4j), (3+7j)], [(4+2j), (5+5j), (6+8j)], [(7+3j), (8+6j), (9+9j)]]

在这个 3x3 复数矩阵 A 中:

  • A.real 提取了实部组成一个新的浮点数组,A.imag 则提取虚部。
  • A.T 给出了矩阵的转置,行列对调了。
  • [x for x in A.flat] 使用 flat 迭代器将所有元素按行展平成一维并收集到列表中,结果就是包含9个复数的 Python 列表。
  • A.tolist() 则直接得到一个三层嵌套列表,与原矩阵结构对应,每个元素还是 Python 内置的复数类型。 这些属性和方法使我们对 ndarray 的操作更加便捷。例如,如果需要把 NumPy 数组传给纯 Python 代码处理,可以用 .tolist() 转换。如果需要逐元素地处理数组(尽管一般应避免 Python 层面的循环),.flat 提供了方便的遍历手段。而对复数数组进行运算时,.real 和 .imag 则可以轻松地分离出实部和虚部进行分析。
Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实, 仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。 示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.