Python数据分析入门:NumPy数组操作全攻略(附实战代码)

Python数据分析入门:NumPy数组操作全攻略(附实战代码)

如果你刚开始接触Python数据分析,面对海量数据时,可能会觉得用Python原生的列表(list)来处理既慢又麻烦。比如,你想对一组数据里的每个元素都加上10,用列表推导式当然可以,但当你需要处理成千上万行数据,或者进行矩阵乘法、统计计算时,原生列表就显得力不从心了。这时,NumPy就该登场了。它不是一个简单的库,而是整个Python科学计算生态的基石。无论是你之后想用Pandas做数据分析,用Scikit-learn搞机器学习,还是用Matplotlib画图,都绕不开NumPy。它最核心的魅力,在于其ndarray对象——一个高效、灵活的多维数组容器。今天这篇文章,我就从一个实践者的角度,带你从零开始,彻底搞懂NumPy数组的创建、操作和运算,并附上大量可直接运行的代码,让你看完就能上手干活。

1. 从零构建你的第一个NumPy数组

安装NumPy非常简单,打开你的终端或命令提示符,一行命令搞定:

pip install numpy

安装完成后,在Python脚本或Jupyter Notebook中,我们习惯用np作为别名导入:

import numpy as np
print(f"当前NumPy版本:{np.__version__}")

1.1 多种创建数组的“起手式”

创建数组是第一步,NumPy提供了极其丰富的方式,远不止从列表转换那么简单。

从Python序列创建:这是最直观的方式,np.array()函数能将列表、元组等转换为ndarray。

# 从列表创建一维数组
list_data = [1, 2, 3, 4, 5]
arr_from_list = np.array(list_data)
print(arr_from_list)  # 输出:[1 2 3 4 5]

# 从嵌套列表创建二维数组(矩阵)
matrix_data = [[1, 2, 3], [4, 5, 6]]
arr_2d = np.array(matrix_data)
print(arr_2d)
# 输出:
# [[1 2 3]
#  [4 5 6]]

注意:np.array()会尝试推断数组元素的数据类型(dtype)。如果源数据中混有整数和浮点数,结果会自动提升为浮点型(float)。

生成特定数值范围的数组:模拟数据或创建序列时非常常用。

  • np.arange(start, stop, step):类似Python的range(),但生成的是数组。
  • np.linspace(start, stop, num):在指定区间内生成等间隔的num个点。
# 生成0到9的整数数组
arr_range = np.arange(10)
print(arr_range)  # [0 1 2 3 4 5 6 7 8 9]

# 生成1到5之间,包含6个点的等差数列
arr_lin = np.linspace(1, 5, 6)
print(arr_lin)  # [1.  1.8 2.6 3.4 4.2 5. ]

创建特殊结构的数组:在做科学计算时,我们经常需要全零、全一或未初始化的数组作为“画布”。

# 创建一个3行4列的全零矩阵
zeros_arr = np.zeros((3, 4))
print(zeros_arr)

# 创建一个2x3x4的三维全一数组
ones_arr = np.ones((2, 3, 4))
print(ones_arr.shape)  # 输出形状:(2, 3, 4)

# 创建一个2x2的单位矩阵(主对角线为1,其余为0)
eye_arr = np.eye(2)
print(eye_arr)
# 输出:
# [[1. 0.]
#  [0. 1.]]

# 创建一个3x3的未初始化数组(内容为内存残留值,速度最快)
empty_arr = np.empty((3, 3))
print(empty_arr)

利用随机数生成数组:数据分析和机器学习中,生成模拟数据或初始化参数离不开随机数组。

# 生成一个2x3的数组,元素为[0.0, 1.0)区间的均匀分布随机数
random_arr = np.random.rand(2, 3)
print(random_arr)

# 生成一个符合标准正态分布(均值为0,标准差为1)的3x3数组
normal_arr = np.random.randn(3, 3)
print(normal_arr)

# 生成一个包含5个随机整数的数组,范围在[10, 20)
randint_arr = np.random.randint(10, 20, size=5)
print(randint_arr)

1.2 理解数组的“身份证”:核心属性

创建一个数组后,我们首先要了解它的基本信息。NumPy数组有几个关键属性,就像它的身份证。

# 创建一个示例数组
example_arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])

# 1. ndim: 数组的维度(轴)数量
print("维度数量 (ndim):", example_arr.ndim)  # 输出:2

# 2. shape: 数组的形状,一个元组,表示每个维度上的大小
print("形状 (shape):", example_arr.shape)   # 输出:(3, 4) 表示3行4列

# 3. size: 数组中元素的总数
print("元素总数 (size):", example_arr.size)  # 输出:12

# 4. dtype: 数组中元素的数据类型
print("数据类型 (dtype):", example_arr.dtype) # 输出:int32 或 int64(取决于系统)

# 5. itemsize: 每个元素占用的字节数
print("每个元素字节数 (itemsize):", example_arr.itemsize, "bytes")

# 6. nbytes: 整个数组数据占用的总字节数 (等于 size * itemsize)
print("数组总字节数 (nbytes):", example_arr.nbytes, "bytes")

理解这些属性是进行后续所有操作的基础。特别是shapedtype,在数据对齐、广播和性能优化中至关重要。

2. 庖丁解牛:数组的索引、切片与迭代

掌握了创建数组,下一步就是如何精准地获取和修改数组中的元素。NumPy的索引和切片语法非常强大且直观。

2.1 一维数组:和列表几乎一样

对于一维数组,其操作方式与Python列表高度相似。

arr_1d = np.arange(10)  # [0 1 2 3 4 5 6 7 8 9]

# 基础索引:获取单个元素
print(arr_1d[5])  # 输出:5

# 负数索引:从末尾开始计数
print(arr_1d[-2]) # 输出:8

# 切片:[start:stop:step]
print(arr_1d[2:7])      # 输出:[2 3 4 5 6] (索引2到6,不包含7)
print(arr_1d[::2])      # 输出:[0 2 4 6 8] (步长为2)
print(arr_1d[5:1:-1])   # 输出:[5 4 3 2] (反向切片)

2.2 多维数组:用逗号分隔维度

这是NumPy的精华所在。对于二维数组(矩阵),索引格式为[行, 列];对于更高维度,则依此类推。

arr_2d = np.array([[1, 2, 3, 4],
                   [5, 6, 7, 8],
                   [9, 10, 11, 12]])

# 获取单个元素:第2行(索引1),第3列(索引2)
print(arr_2d[1, 2])  # 输出:7

# 获取整行:第0行
print(arr_2d[0, :])  # 输出:[1 2 3 4]
# 简写为:
print(arr_2d[0])     # 输出相同

# 获取整列:第2列
print(arr_2d[:, 2])  # 输出:[ 3  7 11]

# 获取子区域:前两行的后三列
sub_arr = arr_2d[:2, 1:]
print(sub_arr)
# 输出:
# [[2 3 4]
#  [6 7 8]]

使用省略号(...):当数组维度很高时,可以用省略号代表剩下的所有维度。

arr_3d = np.random.rand(3, 4, 5)  # 一个3x4x5的三维数组

# 获取第一个“块”(第一维)的所有数据
print(arr_3d[0, ...].shape)  # 输出:(4, 5)
# 等价于 arr_3d[0, :, :]

# 获取所有“块”的第一行、所有列
print(arr_3d[..., 0, :].shape) # 输出:(3, 5)
# 等价于 arr_3d[:, 0, :]

2.3 花式索引:用数组来索引

这是NumPy非常强大的特性,允许你使用整数数组或布尔数组作为索引,进行非连续或条件性的元素选取。

整数数组索引

arr = np.arange(10, 20)
print(arr)  # [10 11 12 13 14 15 16 17 18 19]

# 用一个整数列表作为索引,取出对应位置的元素
indices = [1, 3, 5]
print(arr[indices])  # 输出:[11 13 15]

# 对于多维数组,可以分别指定每个维度的索引数组
arr_2d = np.array([[1,2], [3,4], [5,6]])
rows = [0, 1, 2]
cols = [0, 1, 0]
print(arr_2d[rows, cols])  # 输出:[1 4 5] (取(0,0), (1,1), (2,0)位置的元素)

布尔数组索引(掩码):这是数据清洗和筛选的利器。

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])

# 创建一个布尔条件数组
mask = arr > 5
print(mask)  # 输出:[False False False False False  True  True  True  True]

# 使用布尔数组索引,只返回条件为True的元素
print(arr[mask])  # 输出:[6 7 8 9]

# 更简洁的写法
print(arr[arr % 2 == 0])  # 输出所有偶数:[2 4 6 8]

2.4 遍历数组元素

虽然向量化操作是NumPy的首选,但有时遍历仍有必要。

arr_2d = np.array([[1, 2], [3, 4]])

# 直接迭代:遍历的是第一维(行)
for row in arr_2d:
    print(row)
# 输出:
# [1 2]
# [3 4]

# 使用flat属性迭代所有元素(展平后迭代)
for element in arr_2d.flat:
    print(element, end=' ')
# 输出:1 2 3 4

# 使用np.nditer进行更可控的迭代(例如以特定顺序)
for x in np.nditer(arr_2d, order='F'):  # Fortran顺序,列优先
    print(x, end=' ')
# 输出:1 3 2 4

3. 重塑、连接与拆分:改变数组的形态

数据处理中,经常需要改变数组的排列方式,或者将多个数组合并、分割。

3.1 改变数组形状

reshape()是最常用的方法,它返回一个新视图(如果内存连续),而不改变原数据。

arr = np.arange(12)
print(arr)  # [ 0  1  2  3  4  5  6  7  8  9 10 11]

# 重塑为3行4列的矩阵
arr_reshaped = arr.reshape(3, 4)
print(arr_reshaped)
# 输出:
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

# 使用-1自动计算该维度大小:将12个元素排成2行,列数自动计算
arr_auto = arr.reshape(2, -1)
print(arr_auto.shape)  # 输出:(2, 6)

# 注意:reshape要求新形状的元素总数(size)必须与原数组一致,否则会报错。

resize()方法则会直接修改原数组的形状,如果新形状更大,会用0填充;如果更小,则截断数据。

arr = np.array([1, 2, 3, 4])
arr.resize(3, 3)  # 原数组被改变
print(arr)
# 输出:新形状(3,3)需要9个元素,不足部分用0填充
# [[1 2 3]
#  [4 0 0]
#  [0 0 0]]

展平数组:将多维数组变成一维。

arr_2d = np.array([[1,2], [3,4]])
print(arr_2d.ravel())  # 返回视图:[1 2 3 4]
print(arr_2d.flatten()) # 返回副本:[1 2 3 4]

ravel()通常返回视图(更快),flatten()总是返回副本(更安全)。

3.2 数组的连接与堆叠

将多个数组合并成一个。

函数功能描述常用场景
np.concatenate()沿指定轴连接数组序列通用的连接操作
np.vstack()垂直堆叠(按行连接)合并具有相同列数的数据集
np.hstack()水平堆叠(按列连接)合并具有相同行数的特征
np.column_stack()将一维数组作为列堆叠成二维数组构建特征矩阵
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# 垂直堆叠(增加行数)
v_stack = np.vstack((a, b))
print("vstack:\n", v_stack)
# 输出:
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

# 水平堆叠(增加列数)
h_stack = np.hstack((a, b))
print("hstack:\n", h_stack)
# 输出:
# [[1 2 5 6]
#  [3 4 7 8]]

# 沿指定轴连接(axis=0 同 vstack, axis=1 同 hstack)
concat_axis0 = np.concatenate((a, b), axis=0)
print("concatenate axis=0:\n", concat_axis0)

3.3 数组的拆分

与连接相反,将一个数组拆分成多个较小的数组。

arr = np.arange(16).reshape(4, 4)
print("原数组:\n", arr)

# 水平拆分成2个数组(按列拆分)
h_split = np.hsplit(arr, 2)
print("\n水平拆分成2部分:")
for part in h_split:
    print(part)
# 输出两个 4x2 的数组

# 垂直拆分成2个数组(按行拆分)
v_split = np.vsplit(arr, 2)
print("\n垂直拆分成2部分:")
for part in v_split:
    print(part)
# 输出两个 2x4 的数组

# 使用array_split进行不等量拆分
arr_1d = np.arange(10)
split_uneven = np.array_split(arr_1d, 4) # 将10个元素分成4份
print("\n不等量拆分结果:", [a.tolist() for a in split_uneven])
# 输出:[[0, 1, 2], [3, 4, 5], [6, 7], [8, 9]]

4. 向量化运算与通用函数:效率的源泉

NumPy之所以快,核心在于向量化。它允许你对整个数组进行操作,而无需编写显式循环,这些操作在底层由预编译的C代码执行。

4.1 基本的算术与比较运算

所有算术运算符(+, -, *, /, **, %)和比较运算符(>, <, ==, !=等)都是按元素进行的。

a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])

print("加法:", a + b)      # [ 6  8 10 12]
print("乘法:", a * b)      # [ 5 12 21 32]
print("幂运算:", a ** 2)   # [ 1  4  9 16]
print("比较:", a > 2)      # [False False  True  True]

广播机制:这是NumPy最神奇的特性之一。它允许不同形状的数组进行算术运算,前提是它们的形状是“兼容”的。规则简要来说:

  1. 如果两个数组的维度数不同,将维度较小的数组的形状前面补1。
  2. 对于每个维度,大小必须相等,或者其中一个为1,或者其中一个不存在(补1后)。
  3. 在大小为1的维度上,数组会沿着该维度“广播”以匹配另一个数组的大小。
# 一个2x3的数组
matrix = np.array([[1, 2, 3],
                   [4, 5, 6]])
# 一个长度为3的一维数组
vector = np.array([10, 20, 30])

# 广播发生:vector被“拉伸”成2x3,与matrix相加
result = matrix + vector
print(result)
# 输出:
# [[11 22 33]
#  [14 25 36]]
# 相当于 vector 被复制成了 [[10,20,30], [10,20,30]]

4.2 强大的通用函数

通用函数(ufunc)是对ndarray中每个元素进行操作的函数。NumPy提供了大量的ufunc,涵盖数学、逻辑、统计等。

数学函数

arr = np.array([0, np.pi/2, np.pi])
print("正弦值:", np.sin(arr))   # [0.0000000e+00 1.0000000e+00 1.2246468e-16]
print("指数:", np.exp(arr))     # [ 1.          4.81047738 23.14069263]
print("平方根:", np.sqrt([1, 4, 9])) # [1. 2. 3.]

统计聚合函数:这些函数通常可以沿指定轴(axis)操作。

arr = np.random.randn(4, 5)  # 4x5的正态分布随机数组
print("数组:\n", arr)

print("\n全局求和:", np.sum(arr))
print("沿axis=0求和(按列,结果形状(5,)):", np.sum(arr, axis=0))
print("沿axis=1求和(按行,结果形状(4,)):", np.sum(arr, axis=1))

print("\n全局均值:", np.mean(arr))
print("每列的标准差:", np.std(arr, axis=0))
print("每行的最小值:", np.min(arr, axis=1))
print("每列的最大值索引:", np.argmax(arr, axis=0))

逻辑函数

a = np.array([1, 2, 3, 4])
b = np.array([4, 3, 2, 1])

print("逐元素比较取大:", np.maximum(a, b))  # [4 3 3 4]
print("逐元素比较取小:", np.minimum(a, b))  # [1 2 2 1]

# np.where: 三元向量操作,相当于 if-else 的向量化版本
condition = a > 2
print("where函数:", np.where(condition, a, b))
# 输出:a中大于2的元素保留,否则用b中对应元素替换
# [4 3 3 4]

4.3 矩阵的线性代数运算

虽然NumPy本身提供基础的线性代数功能,但对于复杂的矩阵运算,通常推荐使用numpy.linalg子模块或专门的库如SciPy。

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

# 矩阵乘法(注意不是逐元素乘法)
dot_product = np.dot(A, B)
# 等价于 A @ B (Python 3.5+ 运算符)
print("矩阵乘法 A @ B:\n", A @ B)
# 输出:
# [[19 22]
#  [43 50]]

# 转置
print("A的转置:\n", A.T)

# 求逆矩阵
A_inv = np.linalg.inv(A)
print("A的逆矩阵:\n", A_inv)

# 验证:A * A_inv 应近似于单位矩阵
print("验证 A * A_inv:\n", np.round(A @ A_inv, 10))

5. 视图与副本:避免意想不到的修改

这是NumPy中一个关键且容易出错的概念,关系到数据的安全性和内存效率。

5.1 完全不复制:简单的赋值

在Python中,变量是对象的引用。简单的赋值b = a不会创建新的数组对象,只是创建了一个指向同一数据的新引用。

a = np.array([1, 2, 3, 4])
b = a  # b是a的别名,指向同一块内存

print(b is a)  # True,是同一个对象
b[0] = 99
print(a)       # [99  2  3  4] !!!修改b影响了a

提示:如果你不想关联两个变量,就不要使用简单的赋值。

5.2 视图:共享数据,不同元数据

视图(view)是一个新的数组对象,但它与原始数组共享同一份数据。修改视图的数据会影响原始数组。创建视图的常见操作有:

  • 使用.view()方法。
  • 数组切片(绝大多数情况下)。
  • reshape()(当数组内存连续时)。
a = np.arange(10)  # [0 1 2 3 4 5 6 7 8 9]
v = a.view()       # v是a的一个视图

print(v is a)          # False,是不同的对象
print(v.base is a)     # True,v的数据基于a

v[0] = 100
print(a[0])            # 100!通过视图修改了原始数据

# 切片是更常见的视图
slice_view = a[3:7]    # 切片创建视图
slice_view[:] = 0
print(a)               # [100   1   2   0   0   0   0   7   8   9]

视图的优势是零拷贝,速度快,节省内存。但你必须清楚知道你在操作视图,否则可能无意中污染了原始数据。

5.3 副本:数据的完全独立拷贝

副本(copy)是数据的完整拷贝,会占用新的内存。对副本的修改不会影响原始数组。创建副本使用.copy()方法。

a = np.array([1, 2, 3])
c = a.copy()  # 创建a的完整副本

print(c is a)  # False
c[0] = 999
print(a)       # [1 2 3] 原始数组未受影响
print(c)       # [999   2   3]

何时使用副本?

  1. 当你需要对数据进行修改,但又想保留原始数据时。
  2. 当切片操作返回视图,但你想切断与原始数据的联系时。
  3. 当函数需要修改输入数组,但你不希望影响函数外部的原始数据时。

一个常见的实践是:在对大数据集进行切片并计划修改切片结果时,如果不希望影响原数据,就立即调用.copy()

big_data = np.random.rand(10000)
# 我们只想修改数据的一部分,且不影响原数据
subset = big_data[1000:2000].copy()
subset *= 2  # 安全地修改副本

理解视图和副本的区别,是写出高效且正确NumPy代码的关键。我个人的经验是,在不确定时,尤其是在函数中接收数组参数并可能修改它时,先考虑是否需要内部拷贝,这能避免很多隐蔽的bug。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值