学习笔记-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、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
    • Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
    • Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
    • Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.

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

Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
    • For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
    • The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.

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