Python 数据分析入门系列(一):从NumPy开始
🎯 适合人群:Python 初学者、想进入数据分析领域的开发者
⏱️ 阅读时间:约 20 分钟
💬 阅读建议:这是我学习 NumPy 的笔记整理,有踩坑有感悟,希望能帮你少走弯路
为什么从 NumPy 开始?
说实话,刚开始学数据分析时,我也想过直接上 Pandas------毕竟处理表格数据更直观嘛。
但后来发现,绕不开。
Pandas 的 DataFrame 底层就是 NumPy 的 ndarray,Scikit-learn、TensorFlow 这些库也全都建立在 NumPy 之上。不理解 NumPy,用 Pandas 时遇到性能问题或奇怪的行为,根本不知道怎么回事。
所以我还是乖乖回来,先把 NumPy 搞明白。
一、NumPy 是什么?
NumPy(Numerical Python)是一个用于科学计算的 Python 库。听起来很高大上,但核心就三件事:
- 多维数组对象(ndarray) - 比原生列表更快、更省内存
- 丰富的数学函数 - 直接对数组进行向量化操作
- 线性代数工具 - 矩阵运算、傅里叶变换等
安装与导入
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最重要,它告诉你数组长什么样;ndim是shape的长度;size是shape所有维度的乘积。
二+、深入理解: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 指令加速 │
└────────────────────────────────────────────────────┘
关键优势:
- 连续内存 → CPU 缓存命中率高
- 单一数据类型 → 无需类型检查
- C 语言实现 → 编译型速度
- 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
✅ 最佳实践
- 优先用向量化操作,别写 for 循环
- 明确指定 dtype,避免隐式转换
- 用
np.allclose()比较浮点数 ,别用== - 大数组用
np.memmap,避免内存爆炸 - 学会看文档 :
np.info(np.array)或 https://numpy.org/doc/ - 注意内存连续性 :频繁迭代前用
np.ascontiguousarray()确保连续 - 理解视图机制 :切片修改前想清楚要不要
.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 后,我的学习路线是:
- Pandas - 基于 NumPy 的数据分析利器(处理表格数据)
- Matplotlib/Seaborn - 数据可视化
- Scikit-learn - 机器学习
- SciPy - 科学计算
总结
回顾我学习 NumPy 的过程,核心就三件事:
| 概念 | 作用 | 关键方法 |
|---|---|---|
| ndarray | 高效存储数据 | np.array(), reshape, slice |
| 向量化 | 快速计算 | 直接 arr * 2,别用循环 |
| 广播 | 灵活运算 | 不同形状数组自动对齐 |
记住一句话:能用 NumPy 向量化,就别用 Python 循环。
🤓 因为数据分析涉及大量的数学知识,很多总结也就是从官网demo看过来敲一下,记得住多少全凭天意,毕竟张教主学太极剑的时候也看完就忘了,关键是看过.
📚 参考资料:
- NumPy 官方文档:https://numpy.org/doc/
- NumPy 快速入门:https://numpy.org/devdocs/user/quickstart.html
- 本书代码仓库:(待创建)
下期预告:Pandas 数据处理完全指南
如果这篇博客对你有帮助,欢迎分享给更多小伙伴!也欢迎在评论区交流你学 NumPy 时踩过的坑~