Python 数据分析入门系列(一):从NumPy开始

Python 数据分析入门系列(一):从NumPy开始

🎯 适合人群:Python 初学者、想进入数据分析领域的开发者

⏱️ 阅读时间:约 20 分钟

💬 阅读建议:这是我学习 NumPy 的笔记整理,有踩坑有感悟,希望能帮你少走弯路


为什么从 NumPy 开始?

说实话,刚开始学数据分析时,我也想过直接上 Pandas------毕竟处理表格数据更直观嘛。

但后来发现,绕不开

Pandas 的 DataFrame 底层就是 NumPy 的 ndarray,Scikit-learn、TensorFlow 这些库也全都建立在 NumPy 之上。不理解 NumPy,用 Pandas 时遇到性能问题或奇怪的行为,根本不知道怎么回事。

所以我还是乖乖回来,先把 NumPy 搞明白。


一、NumPy 是什么?

NumPy(Numerical Python)是一个用于科学计算的 Python 库。听起来很高大上,但核心就三件事:

  1. 多维数组对象(ndarray) - 比原生列表更快、更省内存
  2. 丰富的数学函数 - 直接对数组进行向量化操作
  3. 线性代数工具 - 矩阵运算、傅里叶变换等

安装与导入

bash 复制代码
pip install numpy
python 复制代码
import numpy as np

💡 用 np 作为别名是全球 Python 开发者的约定俗成。我一开始觉得随便起个名字也行,后来发现看别人代码时全是 np,还是入乡随俗吧。


二、核心概念:ndarray

NumPy 的灵魂是 ndarray(N-dimensional array),即多维数组。

创建数组

python 复制代码
import numpy as np

# 从列表创建(最直观)
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.array([[1, 2, 3], [4, 5, 6]])  # 二维数组

# 特殊数组(我经常用 zeros 和 ones 来初始化)
zeros = np.zeros((3, 3))        # 3x3 全 0 数组
ones = np.ones((2, 4))          # 2x4 全 1 数组
empty = np.empty((2, 2))        # 未初始化数组(速度快,但值随机,慎用!)
full = np.full((2, 2), 7)       # 全填 7

# 序列数组
range_arr = np.arange(0, 10, 2)     # [0, 2, 4, 6, 8]
linspace_arr = np.linspace(0, 1, 5) # [0.0, 0.25, 0.5, 0.75, 1.0]

# 随机数组(做测试和模拟时超有用)
random_arr = np.random.rand(3, 3)       # 0-1 均匀分布
normal_arr = np.random.randn(3, 3)      # 标准正态分布
int_random = np.random.randint(0, 10, 5) # 5 个 0-9 的随机整数

数组属性

python 复制代码
arr = np.array([[1, 2, 3], [4, 5, 6]])

print(arr.ndim)    # 维度数:2(二维数组)
print(arr.shape)   # 形状:(2, 3)(2 行 3 列)
print(arr.size)    # 元素总数:6
print(arr.dtype)   # 数据类型:int64
print(arr.itemsize) # 每个元素的字节数:8

🤔 我的理解shape 最重要,它告诉你数组长什么样;ndimshape 的长度;sizeshape 所有维度的乘积。


二+、深入理解:ndarray 的内存布局(深入理解)

这部分学习Numpy过程中最核心的地方。可以在知道 NumPy 快的基础上,理解它为什么快。

1. ndarray 的三要素

每个 ndarray 本质上由三部分组成:

复制代码
┌─────────────────────────────────────────┐
│  1. 数据指针 (data pointer)              │  → 指向一块连续的内存
│  2. 形状信息 (shape)                     │  → (行数,列数,...)
│  3. 步长信息 (strides)                   │  → 每维移动多少字节
└─────────────────────────────────────────┘
python 复制代码
arr = np.array([[1, 2, 3], 
                [4, 5, 6]], dtype=np.int64)

print(arr.data)      # <memory at 0x...>  内存地址
print(arr.shape)     # (2, 3)             2 行 3 列
print(arr.strides)   # (24, 8)            行步长 24 字节,列步长 8 字节
print(arr.dtype)     # int64              每个元素 8 字节

strides 是什么意思?

  • strides[0] = 24:跳到下一行需要跳过 24 字节(3 个元素 × 8 字节)
  • strides[1] = 8:跳到下一列需要跳过 8 字节(1 个元素)

2. 内存中的真实样子

NumPy 默认使用 C-order(行优先),内存是连续的:

复制代码
内存地址:  [0x000]  [0x008]  [0x010]  [0x018]  [0x020]  [0x028]
            ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
数据:       │  1  │ │  2  │ │  3  │ │  4  │ │  5  │ │  6  │
            └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
逻辑视角:   ←─── 第 0 行 ───→  ←─── 第 1 行 ───→
python 复制代码
# 验证内存连续性
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(arr.flags['C_CONTIGUOUS'])  # True  C 序连续
print(arr.flags['F_CONTIGUOUS'])  # False Fortran 序不连续

3. C-order vs Fortran-order

python 复制代码
# C-order(行优先,默认):先存第一行,再存第二行...
arr_c = np.array([[1, 2], [3, 4]], order='C')
print(arr_c.strides)  # (16, 8)  对于 int64,2 列×8 字节=16

# Fortran-order(列优先):先存第一列,再存第二列...
arr_f = np.array([[1, 2], [3, 4]], order='F')
print(arr_f.strides)  # (8, 16)  对于 int64,2 行×8 字节=16

# 内存布局对比:
# C-order:    [1, 2, 3, 4]  → 按行展开
# F-order:    [1, 3, 2, 4]  → 按列展开

什么时候用 Fortran-order? (说实话,我到现在也没怎么用过)

  • 与 Fortran/MATLAB 代码交互时
  • 列操作远多于行操作时(罕见)
  • 某些线性代数库(如 LAPACK)要求 F-order

4. 视图 vs 副本:底层原理

这是我最想吐槽的坑!

python 复制代码
arr = np.array([1, 2, 3, 4, 5])

# 视图(View):共享同一块内存,只改变"查看方式"
view = arr[1:4]
print(view.base is arr)  # True  视图的 base 指向原数组
view[0] = 999
print(arr)  # [1, 999, 3, 4, 5]  原数组被修改了!!!我当时懵了

# 副本(Copy):复制数据到新内存
copy = arr[1:4].copy()
print(copy.base is None)  # True  副本没有 base
copy[0] = 888
print(arr)  # [1, 999, 3, 4, 5]  原数组不受影响

哪些操作返回视图?

  • 切片:arr[1:5]
  • 转置:arr.T
  • reshape(大多数情况):arr.reshape(3, 4)

哪些操作返回副本?

  • 索引数组:arr[[1, 3, 5]]
  • 布尔索引:arr[arr > 3]
  • 显式调用:arr.copy()

⚠️ 血泪教训 :如果不确定,就用 .copy() 明确复制。避免隐式修改内部数据造成不必要的麻烦.

5. 为什么向量化这么快?

理解了内存布局后,这个问题就清楚了:

复制代码
┌────────────────────────────────────────────────────┐
│  Python 列表加法:                                   │
│  [1, 2, 3] + [4, 5, 6]                             │
│  → 需要:类型检查 + 对象创建 + 循环迭代             │
│  → 每个元素都是独立的 Python 对象                    │
└────────────────────────────────────────────────────┘
                      vs
┌────────────────────────────────────────────────────┐
│  NumPy 数组加法:                                    │
│  np.array([1,2,3]) + np.array([4,5,6])             │
│  → 一块连续内存 + 一个 C 函数循环                     │
│  → CPU 缓存友好 + SIMD 指令加速                      │
└────────────────────────────────────────────────────┘

关键优势:

  1. 连续内存 → CPU 缓存命中率高
  2. 单一数据类型 → 无需类型检查
  3. C 语言实现 → 编译型速度
  4. SIMD 指令 → 一次处理多个数据

6. 实用技巧:检查内存布局

python 复制代码
arr = np.random.rand(1000, 1000)

# 检查是否连续(连续内存访问更快)
print(arr.flags['C_CONTIGUOUS'])

# 强制转为 C-order(某些操作需要)
arr_c = np.ascontiguousarray(arr)

# 转置后通常不连续
arr_t = arr.T
print(arr_t.flags['C_CONTIGUOUS'])  # False

# 转置后强制连续
arr_t_c = np.ascontiguousarray(arr_t)

💡 经验法则:如果要对数组进行大量迭代或传递给某些 C 扩展,确保它是 C-contiguous 的。这个我也是踩过坑才知道的。


三、数组操作

索引与切片

这部分和 Python 列表很像,上手比较快:

python 复制代码
arr = np.array([10, 20, 30, 40, 50])

# 基本索引
print(arr[0])    # 10
print(arr[-1])   # 50

# 切片(左闭右开,和 Python 一样)
print(arr[1:4])   # [20, 30, 40]
print(arr[:3])    # [10, 20, 30]
print(arr[::2])   # [10, 30, 50] 步长为 2

# 二维数组(这里开始有点不一样了)
arr2d = np.array([[1, 2, 3], 
                  [4, 5, 6], 
                  [7, 8, 9]])

print(arr2d[0, 1])    # 2(第 0 行第 1 列)
print(arr2d[:, 1])    # [2, 5, 8](所有行的第 1 列)
print(arr2d[1, :])    # [4, 5, 6](第 1 行所有列)

📝 注意 :二维索引用 arr2d[0, 1] 而不是 arr2d[0][1],虽然都能跑,但前者效率更高。

布尔索引(超实用!关联到Pandas数据选择)

python 复制代码
arr = np.array([1, 5, 3, 9, 2, 8])

# 条件筛选
mask = arr > 4
print(mask)        # [False, True, False, True, False, True]
print(arr[mask])   # [5, 9, 8]

# 一行搞定
print(arr[arr > 4])  # [5, 9, 8]

# 多条件(这里我踩过坑!)
print(arr[(arr > 2) & (arr < 8)])  # [5, 3, 2]  注意用 & 不是 and
print(arr[(arr < 3) | (arr > 8)])  # [1, 2, 9]  注意用 | 不是 or

⚠️ 注意 :多条件时用 &(与)、|(或)、~(非),别用 Python 的 and/or/not

数组变形

python 复制代码
arr = np.arange(12)  # [0, 1, 2, ..., 11]

# reshape(不改变原数组)
reshaped = arr.reshape(3, 4)
print(reshaped.shape)  # (3, 4)

# resize(改变原数组,我很少用)
arr.resize(2, 6)

# 展平
flattened = reshaped.flatten()  # 返回副本
raveled = reshaped.ravel()      # 返回视图(更快)

# 转置
transposed = reshaped.T

合并与分割

python 复制代码
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# 垂直堆叠
vstacked = np.vstack((a, b))
# [[1, 2],
#  [3, 4],
#  [5, 6],
#  [7, 8]]

# 水平堆叠
hstacked = np.hstack((a, b))
# [[1, 2, 5, 6],
#  [3, 4, 7, 8]]

# 分割
arr = np.arange(9).reshape(3, 3)
v_split = np.vsplit(arr, 3)  # 按行分成 3 份
h_split = np.hsplit(arr, 3)  # 按列分成 3 份

四、向量化运算(NumPy 的超能力)

NumPy 最牛的地方是向量化------不用写循环,直接对整个数组操作。

python 复制代码
arr = np.array([1, 2, 3, 4, 5])

# 算术运算
print(arr + 10)     # [11, 12, 13, 14, 15]
print(arr * 2)      # [2, 4, 6, 8, 10]
print(arr ** 2)     # [1, 4, 9, 16, 25]
print(arr / 2)      # [0.5, 1.0, 1.5, 2.0, 2.5]

# 两个数组运算
arr2 = np.array([5, 4, 3, 2, 1])
print(arr + arr2)   # [6, 6, 6, 6, 6]
print(arr * arr2)   # [5, 8, 9, 8, 5]

对比一下原生 Python 的写法:

python 复制代码
# Python 列表需要循环:
result = [x * 2 for x in my_list]

# NumPy 直接:
result = arr * 2  # 快 10-100 倍!

广播机制(Broadcasting)

简单来说,不同形状的数组也能运算,NumPy 会自动"广播"看下面的代码示例:

python 复制代码
arr = np.array([[1, 2, 3], 
                [4, 5, 6]])

# 标量广播
print(arr + 10)
# [[11, 12, 13],
#  [14, 15, 16]]

# 一维数组广播
row = np.array([10, 20, 30])
print(arr + row)
# [[11, 22, 33],
#  [14, 25, 36]]

🤔 理解一下就是:广播就是 NumPy 自动帮你把小的数组"拉伸"成和大数组一样的形状,但实际不复制数据,只是逻辑上的。


五、常用数学函数

这部分我觉得不用死记,用到再查就行:

python 复制代码
arr = np.array([1, 4, 9, 16, 25])

# 基础数学
print(np.sqrt(arr))    # [1., 2., 3., 4., 5.]
print(np.exp(arr))     # 指数
print(np.log(arr))     # 自然对数
print(np.sin(arr))     # 三角函数

# 统计函数
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2.mean())     # 3.5(整体平均值)
print(arr2.sum())      # 21(总和)
print(arr2.std())      # 标准差
print(arr2.min())      # 1
print(arr2.max())      # 6

# 指定轴(axis 这个概念很重要!)
print(arr2.mean(axis=0))  # [2.5, 3.5, 4.5]  每列的平均值
print(arr2.mean(axis=1))  # [2.0, 5.0]        每行的平均值

# 累积运算
print(arr2.cumsum())      # 累积和
print(arr2.cumprod())     # 累积积

📝 axis 的理解axis=0 是"沿着行"(跨行操作,结果是列),axis=1 是"沿着列"(跨列操作,结果是行)。记忆诀窍就是[从零开始,行列顺口],也就是程序员计数基本从0开始,大家说行和列的时候,也喜欢先说行再说列,也就是0->行,1->列


六、线性代数(略懂即可)

这部分我学得比较浅,毕竟不是专门搞数学的:

python 复制代码
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# 矩阵乘法
print(np.dot(a, b))
# 或
print(a @ b)  # Python 3.5+ 的语法糖

# 转置
print(a.T)

# 逆矩阵
print(np.linalg.inv(a))

# 行列式
print(np.linalg.det(a))

# 特征值(这个我只知道概念,实际没用过)
eigenvalues, eigenvectors = np.linalg.eig(a)

七、实战示例

示例 1:数据标准化

常用于机器学习预处理时:

python 复制代码
# 将数据标准化为均值 0、标准差 1
data = np.random.randn(100, 5)  # 100 行 5 列的随机数据

# 标准化公式:(x - mean) / std
normalized = (data - data.mean(axis=0)) / data.std(axis=0)

print(normalized.mean(axis=0))  # 接近 [0, 0, 0, 0, 0]
print(normalized.std(axis=0))   # 接近 [1, 1, 1, 1, 1]

示例 2:图像处理基础

NumPy 处理图像真的很方便:

python 复制代码
# 模拟一张 100x100 的灰度图
image = np.random.randint(0, 256, (100, 100), dtype=np.uint8)

# 反转颜色
inverted = 255 - image

# 调整亮度(乘以系数)
brighter = np.clip(image * 1.2, 0, 255).astype(np.uint8)

# 二值化(阈值处理)
binary = (image > 128).astype(np.uint8) * 255

示例 3:模拟蒙特卡洛方法

python 复制代码
# 用蒙特卡洛方法估算 π
n = 1000000
points = np.random.rand(n, 2)  # 生成 n 个随机点 (x, y)

# 计算到原点的距离
distances = np.sqrt(points[:, 0]**2 + points[:, 1]**2)

# 统计在单位圆内的点数
inside_circle = np.sum(distances <= 1)

# 估算 π
pi_estimate = 4 * inside_circle / n
print(f"估算的 π 值:{pi_estimate}")  # 接近 3.14159...

🎯 感悟:用 NumPy 写这种模拟,代码简洁到不可思议。换成原生 Python,估计得写几十行循环。


八、性能对比:NumPy vs 原生 Python

这个对比我第一次跑的时候被震撼到了:

python 复制代码
import numpy as np
import time

# 原生 Python 列表
size = 1000000
list1 = list(range(size))
list2 = list(range(size))

start = time.time()
result_list = [a + b for a, b in zip(list1, list2)]
print(f"Python 列表耗时:{time.time() - start:.4f}秒")

# NumPy 数组
arr1 = np.arange(size)
arr2 = np.arange(size)

start = time.time()
result_arr = arr1 + arr2
print(f"NumPy 数组耗时:{time.time() - start:.4f}秒")

# 通常 NumPy 快 10-50 倍!

九、常见坑与最佳实践

⚠️ 常见坑(都是我的血泪史)

python 复制代码
# 坑 1:视图 vs 副本(我踩过至少两次)
arr = np.array([1, 2, 3, 4, 5])
view = arr[1:4]      # 这是视图,修改会影响原数组
copy = arr[1:4].copy()  # 这才是副本

view[0] = 999
print(arr)  # [1, 999, 3, 4, 5]  原数组被改了!

# 坑 2:数据类型(隐式转换)
arr = np.array([1, 2, 3])
arr = arr / 2  # 自动变成浮点数 [0.5, 1.0, 1.5]

# 坑 3:多维索引
arr2d = np.array([[1, 2], [3, 4]])
# 正确:
print(arr2d[0, 1])  # 2
# 错误(虽然能跑但效率低):
print(arr2d[0][1])  # 2

✅ 最佳实践

  1. 优先用向量化操作,别写 for 循环
  2. 明确指定 dtype,避免隐式转换
  3. np.allclose() 比较浮点数 ,别用 ==
  4. 大数组用 np.memmap,避免内存爆炸
  5. 学会看文档np.info(np.array)https://numpy.org/doc/
  6. 注意内存连续性 :频繁迭代前用 np.ascontiguousarray() 确保连续
  7. 理解视图机制 :切片修改前想清楚要不要 .copy()

🔬 进阶:查看数组内存信息

python 复制代码
import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float64)

# 完整内存信息
print(arr.flags)
# C_CONTIGUOUS : True
# F_CONTIGUOUS : False
# OWNDATA : True
# WRITEABLE : True
# ALIGNED : True
# WRITEBACKIFCOPY : False

# 内存占用估算
memory_bytes = arr.size * arr.itemsize
print(f"内存占用:{memory_bytes} 字节")  # 48 字节

# 数据指针(底层内存地址)
print(f"数据地址:{arr.ctypes.data}")

十、性能优化小贴士

1. 选择合适的数据类型

这个是我在处理大数组时才意识到的:

python 复制代码
# 不要用 int64 存小整数
arr1 = np.array([1, 2, 3, 4, 5])        # 默认 int64,40 字节
arr2 = np.array([1, 2, 3, 4, 5], dtype=np.int8)  # 5 字节,省 87.5%

# 图像用 uint8(0-255)
image = np.zeros((1920, 1080, 3), dtype=np.uint8)  # ~6MB
# 如果用 float64 会是 ~50MB!

2. 预分配数组,避免动态增长

python 复制代码
# ❌ 糟糕:动态增长,反复分配内存(我刚开始就这么写的...)
result = np.array([])
for i in range(10000):
    result = np.append(result, i)

# ✅ 正确:预分配
result = np.empty(10000)
for i in range(10000):
    result[i] = i

3. 用 np.einsum 处理复杂张量运算

这个我还在学中,但感觉很强大:

python 复制代码
# 矩阵乘法
a = np.random.rand(100, 100)
b = np.random.rand(100, 100)

# 传统写法
result1 = np.dot(a, b)

# einsum 写法(更灵活)
result2 = np.einsum('ij,jk->ik', a, b)

# 批量矩阵乘法
a_batch = np.random.rand(10, 100, 100)
b_batch = np.random.rand(10, 100, 100)
result_batch = np.einsum('bij,bjk->bik', a_batch, b_batch)

4. 避免不必要的复制

python 复制代码
arr = np.random.rand(1000, 1000)

# ❌ 这会创建副本
for row in arr.copy():
    process(row)

# ✅ 直接迭代(不复制)
for row in arr:
    process(row)

# ❌ 不必要的 reshape 可能创建副本
reshaped = arr.reshape(1000000)

# ✅ 用 -1 让 NumPy 自动推断
reshaped = arr.reshape(-1)

十一、下一步学什么?

掌握 NumPy 后,我的学习路线是:

  1. Pandas - 基于 NumPy 的数据分析利器(处理表格数据)
  2. Matplotlib/Seaborn - 数据可视化
  3. Scikit-learn - 机器学习
  4. SciPy - 科学计算

总结

回顾我学习 NumPy 的过程,核心就三件事:

概念 作用 关键方法
ndarray 高效存储数据 np.array(), reshape, slice
向量化 快速计算 直接 arr * 2,别用循环
广播 灵活运算 不同形状数组自动对齐

记住一句话:能用 NumPy 向量化,就别用 Python 循环。

🤓 因为数据分析涉及大量的数学知识,很多总结也就是从官网demo看过来敲一下,记得住多少全凭天意,毕竟张教主学太极剑的时候也看完就忘了,关键是看过.


📚 参考资料:

下期预告:Pandas 数据处理完全指南


如果这篇博客对你有帮助,欢迎分享给更多小伙伴!也欢迎在评论区交流你学 NumPy 时踩过的坑~

相关推荐
Dontla18 小时前
用pip install -e .开发Python包时,Python项目目录结构(项目结构)(可编辑安装editable install)
python·pip
Thomas.Sir18 小时前
第三章:Python3 之 字符串
开发语言·python·字符串·string
MediaTea18 小时前
NumPy 函数手册:文件读写
numpy
威联通网络存储19 小时前
告别掉帧与素材损毁:威联通 QuTS hero 如何重塑影视后期协同工作流
前端·网络·人工智能·python
Dxy123931021619 小时前
Python 根据列表中某字段排序:从基础到进阶
开发语言·windows·python
splage20 小时前
Java进阶——IO 流
java·开发语言·python
cliffordl20 小时前
设计模式(python)
python·设计模式
always_TT20 小时前
从Python_Java转学C语言需要注意什么?
java·c语言·python
2301_7938046920 小时前
定时任务专家:Python Schedule库使用指南
jvm·数据库·python