欢迎光临
我们一直在努力

Python NumPy 与 Pandas 完整学习文档

适用版本:NumPy 2.x | Pandas 2.x | Python 3.10+


目录

  • NumPy 基础与原理
  • NumPy 数组操作
  • NumPy 数学运算与广播
  • NumPy 高级功能
  • Pandas 基础与原理
  • Pandas 数据读写
  • Pandas 数据清洗
  • Pandas 数据分析与聚合
  • Pandas 高级操作
  • NumPy 与 Pandas 综合实战
  • 性能优化最佳实践

  • 1. NumPy 基础与原理

    1.1 NumPy 是什么,为什么快

    NumPy(Numerical Python)是 Python 科学计算的基石库。它的核心是 ndarray(N-dimensional array,N 维数组)——一种同质、固定大小、连续内存存储的多维数组。

    理解 NumPy 为什么比 Python 原生列表快,需要从底层看差异:

    Python 列表(list)的内存布局:
    ┌─────┬─────┬─────┬─────┐
    │ ptr │ ptr │ ptr │ ptr │ 每个元素是指向 Python 对象的指针
    └──┬──┴──┬──┴──┬──┴──┬──┘
    ↓ ↓ ↓ ↓
    [int] [int] [int] [int] Python 对象分散在堆内存各处
    28B 28B 28B 28B 每个 int 对象占 28 字节!

    NumPy ndarray 的内存布局:
    ┌────┬────┬────┬────┐
    │ 4B │ 4B │ 4B │ 4B │ 原始数据紧密排列(int32:每个4字节)
    └────┴────┴────┴────┘
    连续内存块,CPU 缓存命中率极高

    NumPy 快的三个核心原因:

    连续内存:数组元素在内存中紧密排列,CPU 读取时 Cache Line 利用率极高,避免大量缓存未命中。

    向量化运算(Vectorization):底层 C/Fortran 代码对数组批量运算,避免 Python 解释器逐元素循环的开销,并且现代 CPU 的 SIMD 指令(如 AVX2)可一次处理多个元素。

    无 GIL 限制:NumPy 的 C 扩展代码在执行纯数值计算时不持有 Python GIL,天然支持多核并行。

    1.2 安装与导入

    # 安装
    # pip install numpy pandas

    import numpy as np
    import pandas as pd

    # 查看版本
    print(np.__version__) # 2.x
    print(pd.__version__) # 2.x

    # 查看配置信息(包括 BLAS/LAPACK 后端)
    np.show_config()

    1.3 创建 ndarray

    import numpy as np

    # ─── 从 Python 对象创建 ───────────────────────────────────

    # 从列表创建(自动推断数据类型)
    a = np.array([1, 2, 3, 4, 5])
    print(a.dtype) # int64(64位系统)

    # 从嵌套列表创建二维数组
    b = np.array([[1, 2, 3],
    [4, 5, 6]])
    print(b.shape) # (2, 3)
    print(b.ndim) # 2(维度数)

    # 显式指定数据类型
    c = np.array([1.0, 2.0, 3.0], dtype=np.float32)
    d = np.array([True, False, True], dtype=np.bool_)

    # ─── 工厂函数创建 ─────────────────────────────────────────

    # 全零数组
    z = np.zeros((3, 4)) # 3行4列,默认 float64
    z_int = np.zeros((3, 4), dtype=np.int32)

    # 全一数组
    o = np.ones((2, 3))

    # 常数填充
    f = np.full((3, 3), fill_value=7) # [[7,7,7],[7,7,7],[7,7,7]]

    # 单位矩阵
    eye = np.eye(4) # 4×4 单位矩阵
    eye3 = np.eye(3, k=1) # k=1 表示偏移1的对角线

    # 未初始化数组(值不确定,比 zeros 略快)
    e = np.empty((1000, 1000))

    # 与另一数组形状/类型相同的新数组
    like = np.zeros_like(b) # 与 b 形状相同的全零数组
    like_ones = np.ones_like(b, dtype=float)

    # ─── 数值序列 ─────────────────────────────────────────────

    # 等差序列(类似 range)
    arr = np.arange(10) # [0, 1, …, 9]
    arr = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
    arr = np.arange(1.0, 2.0, 0.1) # 浮点步长(注意精度问题)

    # 等间距序列(推荐替代浮点 arange)
    lin = np.linspace(0, 1, 11) # 0 到 1 之间均匀取 11 个点
    lin = np.linspace(0, 1, 5, endpoint=False) # 不含终点

    # 对数等间距
    log = np.logspace(0, 3, 4) # [1, 10, 100, 1000](10^0 到 10^3)
    log = np.logspace(0, 1, 3, base=2) # 以2为底

    # ─── 随机数组 ────────────────────────────────────────────

    rng = np.random.default_rng(seed=42) # 推荐:使用新式 Generator

    rng.random((3, 3)) # [0, 1) 均匀分布
    rng.integers(0, 100, (3, 3)) # [0, 100) 随机整数
    rng.normal(loc=0, scale=1, size=(3, 3)) # 正态分布
    rng.uniform(low=–1, high=1, size=1000) # 均匀分布
    rng.choice([1, 2, 3, 4, 5], size=10, replace=True) # 有放回采样
    rng.permutation(10) # 0~9 的随机排列

    1.4 ndarray 核心属性

    import numpy as np

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

    print(a.shape) # (2, 3) ← 各维度大小的元组
    print(a.ndim) # 2 ← 维度数
    print(a.size) # 6 ← 元素总数
    print(a.dtype) # float64 ← 数据类型
    print(a.itemsize) # 8 ← 单个元素字节数(float64=8B)
    print(a.nbytes) # 48 ← 总内存(字节)= size × itemsize
    print(a.strides) # (24, 8) ← 各维度步幅(字节)
    print(a.flags) # 内存布局标志(C_CONTIGUOUS, WRITEABLE 等)
    print(a.data) # 底层内存缓冲区对象

    strides 原理(理解这个对高级操作至关重要):

    strides = (24, 8) 表示:
    – 沿第 0 轴(行)移动一步,需要跳过 24 字节(= 3列 × 8字节/元素)
    – 沿第 1 轴(列)移动一步,需要跳过 8 字节(= 1个 float64)

    访问 a[i, j] 的内存地址 = base_ptr + i*24 + j*8

    1.5 数据类型系统

    import numpy as np

    # NumPy 支持的主要数据类型
    dtypes = {
    # 整数
    'int8': np.int8, # -128 ~ 127
    'int16': np.int16, # -32768 ~ 32767
    'int32': np.int32, # -2^31 ~ 2^31-1
    'int64': np.int64, # -2^63 ~ 2^63-1
    'uint8': np.uint8, # 0 ~ 255(图像像素常用)
    'uint32': np.uint32, # 0 ~ 2^32-1
    # 浮点
    'float16': np.float16, # 半精度(深度学习常用)
    'float32': np.float32, # 单精度
    'float64': np.float64, # 双精度(默认)
    # 复数
    'complex64': np.complex64,
    'complex128': np.complex128,
    # 布尔
    'bool_': np.bool_,
    # 字符串(固定长度)
    'str': np.str_,
    # 对象(可存储任意 Python 对象,失去性能优势)
    'object': np.object_,
    }

    # 数据类型转换
    a = np.array([1.7, 2.3, 3.9])
    b = a.astype(np.int32) # [1, 2, 3](截断,不是四舍五入)
    c = a.astype(np.int32, casting='unsafe') # 明确指定允许不安全转换

    # 数值范围查询
    print(np.iinfo(np.int8)) # min=-128, max=127
    print(np.finfo(np.float32)) # eps, max, min 等

    # 选择合适的数据类型(节省内存)
    prices = np.array([1.5, 2.3, 100.0], dtype=np.float32) # 够用就不用 float64
    counts = np.array([1, 2, 300], dtype=np.uint16) # 正整数用 uint


    2. NumPy 数组操作

    2.1 索引与切片

    import numpy as np

    # ─── 一维数组索引 ─────────────────────────────────────────
    a = np.arange(10) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    a[0] # 0(正向索引)
    a[–1] # 9(反向索引)
    a[2:5] # [2, 3, 4](切片,左闭右开)
    a[::2] # [0, 2, 4, 6, 8](步长2)
    a[::–1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0](反转)
    a[2:8:3] # [2, 5](从2到8步长3)

    # ─── 多维数组索引 ─────────────────────────────────────────
    b = np.array([[1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]])

    b[0] # [1, 2, 3, 4](第0行)
    b[0, 2] # 3(第0行第2列)
    b[–1, –1] # 12(最后行最后列)
    b[0:2, 1:3] # [[2,3],[6,7]](子矩阵)
    b[:, 1] # [2, 6, 10](第1列)
    b[1, :] # [5, 6, 7, 8](第1行)

    # ─── 切片是视图,不是拷贝!───────────────────────────────
    c = b[0:2, 0:2] # c 是 b 的视图
    c[0, 0] = 999 # 修改 c 会影响 b!
    print(b[0, 0]) # 999

    # 明确拷贝:
    c = b[0:2, 0:2].copy()
    c[0, 0] = –1
    print(b[0, 0]) # 999(不受影响)

    # ─── 花式索引(Fancy Indexing)──────────────────────────
    # 使用整数数组索引(返回拷贝,不是视图)
    a = np.array([10, 20, 30, 40, 50])
    idx = [0, 2, 4]
    print(a[idx]) # [10, 30, 50]

    # 二维花式索引
    b = np.arange(1, 10).reshape(3, 3)
    rows = [0, 2]
    cols = [1, 2]
    print(b[rows, cols]) # [b[0,1], b[2,2]] = [2, 9](配对索引)

    # 用 np.ix_ 构建网格索引(交叉组合)
    row_idx = np.ix_([0, 2], [0, 2])
    print(b[row_idx]) # [[1, 3], [7, 9]](子矩阵)

    # ─── 布尔索引(Boolean Masking)──────────────────────────
    a = np.array([1, –2, 3, –4, 5, –6])
    mask = a > 0
    print(mask) # [True False True False True False]
    print(a[mask]) # [1, 3, 5](返回拷贝)

    # 直接用条件
    print(a[a > 0]) # [1, 3, 5]
    print(a[a % 2 == 0]) # [-2, -4, -6]

    # 多条件(用 & | ~ 而不是 and or not)
    print(a[(a > 0) & (a < 5)]) # [1, 3]
    print(a[(a < 0) | (a > 3)]) # [-2, -4, 5, -6]
    print(a[~(a > 0)]) # [-2, -4, -6]

    # 布尔索引修改值
    a[a < 0] = 0
    print(a) # [1, 0, 3, 0, 5, 0]

    2.2 形状变换

    import numpy as np

    a = np.arange(24)

    # reshape:改变形状(不改变数据,返回视图)
    b = a.reshape(4, 6) # 4行6列
    c = a.reshape(2, 3, 4) # 三维:2×3×4
    d = a.reshape(4, –1) # -1 表示自动推断(=6)
    e = a.reshape(–1, 4) # 自动推断行数(=6)

    # 注意:reshape 尽量返回视图,但不保证
    # 用 np.shares_memory() 检查
    print(np.shares_memory(a, b)) # True(通常是视图)

    # flatten vs ravel
    flat1 = b.flatten() # 总是返回拷贝,行优先(C顺序)
    flat2 = b.ravel() # 尽量返回视图,行优先
    flat3 = b.ravel('F') # 列优先(Fortran顺序)

    # squeeze / expand_dims:增删维度
    a = np.array([[[1, 2, 3]]]) # shape (1, 1, 3)
    s = a.squeeze() # shape (3,),移除所有大小为1的维度
    s = a.squeeze(axis=0) # shape (1, 3),只移除指定轴

    b = np.array([1, 2, 3]) # shape (3,)
    e = np.expand_dims(b, axis=0) # shape (1, 3)
    e = np.expand_dims(b, axis=–1) # shape (3, 1)
    e = b[np.newaxis, :] # 等价,shape (1, 3)
    e = b[:, np.newaxis] # shape (3, 1)

    # 转置
    a = np.arange(6).reshape(2, 3)
    print(a.T) # shape (3, 2)
    print(a.transpose()) # 同上
    print(a.transpose(1, 0)) # 同上,指定轴顺序

    # 三维转置
    b = np.arange(24).reshape(2, 3, 4)
    print(b.transpose(0, 2, 1).shape) # (2, 4, 3),交换后两轴

    2.3 数组拼接与分割

    import numpy as np

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

    # ─── 拼接 ────────────────────────────────────────────────
    # concatenate:沿指定轴拼接
    np.concatenate([a, b], axis=0) # 竖向拼接,shape (4, 2)
    np.concatenate([a, b], axis=1) # 横向拼接,shape (2, 4)

    # vstack / hstack / dstack(语义更清晰)
    np.vstack([a, b]) # 竖向堆叠(沿 axis=0),shape (4, 2)
    np.hstack([a, b]) # 横向堆叠(沿 axis=1),shape (2, 4)
    np.dstack([a, b]) # 沿第3轴堆叠,shape (2, 2, 2)

    # stack:创建新维度后堆叠
    np.stack([a, b], axis=0) # shape (2, 2, 2)
    np.stack([a, b], axis=1) # shape (2, 2, 2)

    # block:用嵌套列表构建分块矩阵(直观!)
    result = np.block([
    [a, np.zeros((2, 2))],
    [np.zeros((2, 2)), b]
    ])
    # [[1, 2, 0, 0],
    # [3, 4, 0, 0],
    # [0, 0, 5, 6],
    # [0, 0, 7, 8]]

    # ─── 分割 ─────────────────────────────────────────────────
    c = np.arange(12).reshape(3, 4)

    # split:等分
    parts = np.split(c, 3, axis=0) # 沿行分为3份,每份shape(1,4)
    parts = np.split(c, [1, 3], axis=1) # 按位置分割:[:,0], [:,1:3], [:,3:]

    # vsplit / hsplit(更语义化)
    rows = np.vsplit(c, 3) # 竖向均分为3份
    cols = np.hsplit(c, 4) # 横向均分为4份
    cols = np.hsplit(c, [1, 3]) # 按列位置切割

    2.4 排序与搜索

    import numpy as np

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

    # 排序
    sorted_a = np.sort(a) # 返回排序后的新数组
    a.sort() # 就地排序

    # argsort:返回排序后的索引
    idx = np.argsort(a) # 升序索引
    idx_desc = np.argsort(a)[::–1] # 降序索引
    print(a[idx]) # 等价于 np.sort(a)

    # 二维排序
    b = np.array([[3, 1, 4], [2, 8, 5]])
    np.sort(b, axis=0) # 按列排序(纵向)
    np.sort(b, axis=1) # 按行排序(横向)

    # lexsort:多键排序(最后一个键为主键)
    names = np.array(['Charlie', 'Alice', 'Bob', 'Alice'])
    scores = np.array([85, 92, 78, 88])
    idx = np.lexsort((scores, names)) # 先按 names 排,同名再按 scores 排
    print(names[idx]) # ['Alice', 'Alice', 'Bob', 'Charlie']
    print(scores[idx]) # [88, 92, 78, 85] ← Alice 的两个成绩升序

    # 搜索
    a = np.array([10, 20, 30, 40, 50])

    print(np.argmin(a)) # 0(最小值的索引)
    print(np.argmax(a)) # 4(最大值的索引)
    print(np.where(a > 25)) # (array([2, 3, 4]),)(满足条件的索引)

    # where:三元表达式
    b = np.where(a > 25, a, –1) # 大于25保留,否则替换为-1
    # [-1, -1, 30, 40, 50]

    # searchsorted:二分查找(要求已排序)
    sorted_arr = np.array([1, 3, 5, 7, 9])
    print(np.searchsorted(sorted_arr, 4)) # 2(应插入位置)
    print(np.searchsorted(sorted_arr, [2, 6, 10])) # [1, 3, 5]

    # nonzero / flatnonzero
    a = np.array([0, 1, 0, 3, 0, 5])
    print(np.nonzero(a)) # (array([1, 3, 5]),)
    print(np.flatnonzero(a)) # [1, 3, 5](一维形式)

    # unique
    a = np.array([3, 1, 4, 1, 5, 9, 2, 6, 5])
    unique_vals = np.unique(a) # [1, 2, 3, 4, 5, 6, 9]
    vals, counts = np.unique(a, return_counts=True) # 同时返回计数
    vals, idx = np.unique(a, return_index=True) # 返回首次出现的索引


    3. NumPy 数学运算与广播

    3.1 通用函数(ufunc)

    通用函数(Universal Function)是 NumPy 的核心运算引擎,对数组逐元素操作,底层用 C 实现。

    import numpy as np

    a = np.array([1.0, 4.0, 9.0, 16.0])
    b = np.array([2.0, 3.0, 4.0, 5.0])

    # ─── 算术运算(ufunc) ─────────────────────────────────────
    np.add(a, b) # a + b → [3., 7., 13., 21.]
    np.subtract(a, b) # a – b
    np.multiply(a, b) # a * b
    np.divide(a, b) # a / b(真除,结果为浮点)
    np.floor_divide(a, b) # a // b(整除)
    np.mod(a, b) # a % b(取余)
    np.power(a, b) # a ** b
    np.negative(a) # -a

    # 等价的运算符(Python 重载,实际调用 ufunc)
    a + b ; a – b ; a * b ; a / b ; a ** b ; a % b

    # ─── 数学函数 ─────────────────────────────────────────────
    np.sqrt(a) # 平方根
    np.square(a) # 平方(比 ** 2 更快)
    np.abs(a) # 绝对值(np.absolute)
    np.log(a) # 自然对数
    np.log2(a) # 以2为底
    np.log10(a) # 以10为底
    np.exp(a) # e^x
    np.exp2(a) # 2^x

    # 三角函数
    angles = np.linspace(0, 2*np.pi, 5)
    np.sin(angles) ; np.cos(angles) ; np.tan(angles)
    np.arcsin(a) ; np.arccos(a) ; np.arctan(a)
    np.arctan2(b, a) # 两参数反正切(处理象限)

    # ─── 聚合函数 ─────────────────────────────────────────────
    a = np.arange(12).reshape(3, 4).astype(float)

    np.sum(a) # 全部元素求和
    np.sum(a, axis=0) # 按列求和,shape (4,)
    np.sum(a, axis=1) # 按行求和,shape (3,)
    np.sum(a, axis=1, keepdims=True) # 保持维度,shape (3, 1)

    np.mean(a) # 均值
    np.median(a) # 中位数
    np.std(a) # 标准差
    np.var(a) # 方差
    np.min(a) ; np.max(a) # 最小/最大值
    np.cumsum(a, axis=1) # 累积求和
    np.cumprod(a, axis=0) # 累积乘积
    np.prod(a) # 全部元素之积
    np.any(a > 5) # 是否有元素满足条件
    np.all(a > 0) # 是否所有元素满足条件

    # ─── out 参数(就地运算,避免临时数组) ──────────────────
    result = np.empty_like(a)
    np.sqrt(a, out=result) # 直接写入 result,不创建中间数组

    # ─── reduce / accumulate / outer ──────────────────────────
    np.add.reduce(a, axis=0) # 沿轴0逐步加法(等价于 sum)
    np.add.accumulate(a, axis=1) # 累积加法(等价于 cumsum)
    np.multiply.outer([1, 2, 3], [10, 20]) # 外积:shape (3, 2)
    # [[10, 20], [20, 40], [30, 60]]

    3.2 广播机制(Broadcasting)

    广播是 NumPy 中最重要的概念之一,允许不同形状的数组进行运算,无需实际复制数据。

    广播规则(从尾部对齐维度):

    规则1:若两个数组维度数不同,低维数组的 shape 在左侧用 1 填充
    规则2:两个数组在某维度上大小为 1,则该维度被"拉伸"到另一数组的大小
    规则3:两个数组在所有维度上大小相同,或至少有一个为 1,才能广播

    示例:
    A.shape = (3, 4) B.shape = (4,)
    → B 被填充为 (1, 4)
    → B 被广播为 (3, 4)
    → 运算可以进行

    A.shape = (3, 1) B.shape = (1, 4)
    → A 广播为 (3, 4),B 广播为 (3, 4)
    → 运算可以进行

    A.shape = (3, 4) B.shape = (3,)
    → B 被填充为 (1, 3)
    → (3, 4) vs (1, 3) → 倒数第1维 4≠3 且都不为1 → 报错!

    import numpy as np

    # 示例1:标量与数组
    a = np.array([1, 2, 3])
    print(a + 10) # [11, 12, 13],10 被广播到 shape (3,)

    # 示例2:一维与二维
    a = np.ones((3, 4)) # shape (3, 4)
    b = np.array([1, 2, 3, 4]) # shape (4,) → 广播为 (3, 4)
    print((a + b).shape) # (3, 4)

    # 示例3:列向量与行向量
    col = np.array([[1], [2], [3]]) # shape (3, 1)
    row = np.array([[10, 20, 30]]) # shape (1, 3)
    result = col + row # shape (3, 3)
    # [[11, 21, 31],
    # [12, 22, 32],
    # [13, 23, 33]]

    # 实战:中心化(每列减去均值)
    data = np.random.randn(100, 5) # 100个样本,5个特征
    col_means = data.mean(axis=0) # shape (5,)
    centered = data – col_means # 广播:(100,5) – (5,) → (100,5)

    # 实战:归一化
    col_std = data.std(axis=0) # shape (5,)
    normalized = (data – col_means) / col_std # Z-score 标准化

    # 实战:计算欧氏距离矩阵
    points = np.random.rand(5, 2) # 5个二维点
    # 利用广播计算所有点对之间的距离
    diff = points[:, np.newaxis, :] – points[np.newaxis, :, :]
    # (5, 1, 2) – (1, 5, 2) → (5, 5, 2)
    dist_matrix = np.sqrt((diff**2).sum(axis=2)) # (5, 5)

    # 手动检查广播是否可行
    def check_broadcast(*shapes):
    try:
    np.broadcast_shapes(*shapes)
    print(f"{shapes} 可以广播为 {np.broadcast_shapes(*shapes)}")
    except ValueError as e:
    print(f"广播失败:{e}")

    check_broadcast((3, 4), (4,)) # (3, 4)
    check_broadcast((3, 1), (1, 4)) # (3, 4)
    check_broadcast((3, 4), (3,)) # 失败!

    3.3 线性代数

    import numpy as np

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

    # ─── 矩阵运算 ─────────────────────────────────────────────
    np.dot(A, B) # 矩阵乘法(也支持向量点积)
    A @ B # Python 3.5+ 矩阵乘法运算符(推荐)
    A @ v # 矩阵与向量的乘法

    np.dot(v, v) # 向量点积 = 1×1 + 2×2 = 5
    np.inner(v, v) # 内积(一维等同于 dot)
    np.outer(v, v) # 外积(向量 × 向量^T)
    np.cross(v, [1, 0]) # 叉积(3D向量用)

    A * B # 逐元素乘法(Hadamard 积)

    # ─── 矩阵分解 ─────────────────────────────────────────────
    # 特征值分解
    eigenvalues, eigenvectors = np.linalg.eig(A)
    # A @ eigenvectors[:,i] = eigenvalues[i] * eigenvectors[:,i]

    # 奇异值分解(SVD)
    U, S, Vh = np.linalg.svd(A)
    # A = U @ np.diag(S) @ Vh

    # QR 分解
    Q, R = np.linalg.qr(A)
    # A = Q @ R,Q 为正交矩阵,R 为上三角矩阵

    # LU 分解(通过 scipy)
    # from scipy.linalg import lu
    # P, L, U = lu(A)

    # Cholesky 分解(对正定矩阵)
    C = np.array([[4, 2], [2, 3]], dtype=float)
    L = np.linalg.cholesky(C) # C = L @ L.T

    # ─── 矩阵属性 ─────────────────────────────────────────────
    np.linalg.det(A) # 行列式
    np.linalg.matrix_rank(A) # 秩
    np.linalg.norm(A) # 矩阵范数(默认 Frobenius)
    np.linalg.norm(v) # 向量 L2 范数
    np.linalg.norm(v, ord=1) # L1 范数
    np.trace(A) # 迹(对角线之和)

    # ─── 线性方程组求解 ───────────────────────────────────────
    # Ax = b
    A = np.array([[2, 1], [1, 3]], dtype=float)
    b = np.array([5, 10], dtype=float)
    x = np.linalg.solve(A, b) # 精确求解(A必须可逆)
    print(np.allclose(A @ x, b)) # True

    # 最小二乘解(A不必是方阵)
    A_over = np.random.rand(10, 3)
    b_over = np.random.rand(10)
    x, residuals, rank, sv = np.linalg.lstsq(A_over, b_over, rcond=None)

    # 矩阵求逆(慎用,数值不稳定,优先用 solve)
    A_inv = np.linalg.inv(A)
    print(np.allclose(A @ A_inv, np.eye(2))) # True

    # 伪逆(Moore-Penrose)
    A_pinv = np.linalg.pinv(A)


    4. NumPy 高级功能

    4.1 结构化数组

    import numpy as np

    # 定义结构化数组的 dtype
    dtype = np.dtype([
    ('name', 'U20'), # Unicode字符串,最长20字符
    ('age', np.int32),
    ('salary', np.float64),
    ('active', np.bool_)
    ])

    # 创建结构化数组
    employees = np.array([
    ('Alice', 30, 85000.0, True),
    ('Bob', 25, 72000.0, True),
    ('Charlie', 35, 95000.0, False),
    ], dtype=dtype)

    # 按字段名访问(返回视图)
    print(employees['name']) # ['Alice' 'Bob' 'Charlie']
    print(employees['salary']) # [85000. 72000. 95000.]

    # 单行访问
    print(employees[0]) # ('Alice', 30, 85000., True)
    print(employees[0]['age']) # 30

    # 布尔过滤
    active = employees[employees['active']]
    high_salary = employees[employees['salary'] > 80000]

    4.2 内存视图与 stride tricks

    import numpy as np
    from numpy.lib.stride_tricks import as_strided, sliding_window_view

    a = np.arange(10, dtype=np.float64)

    # ─── as_strided(低级,需谨慎)──────────────────────────
    # 手动构造滑动窗口(窗口大小3,步长1)
    window_size = 3
    windows = as_strided(
    a,
    shape=(len(a) – window_size + 1, window_size),
    strides=(a.strides[0], a.strides[0])
    )
    # [[0,1,2],[1,2,3],[2,3,4],…,[7,8,9]]

    # ─── sliding_window_view(推荐,安全) ──────────────────
    windows = sliding_window_view(a, window_shape=3)
    # 等价效果,但更安全

    # 二维滑动窗口(图像处理常用)
    image = np.random.rand(8, 8)
    patches = sliding_window_view(image, window_shape=(3, 3))
    print(patches.shape) # (6, 6, 3, 3)

    # ─── 内存共享检查 ─────────────────────────────────────────
    a = np.arange(12)
    b = a.reshape(3, 4)
    print(np.shares_memory(a, b)) # True(视图)
    print(np.may_share_memory(a, b)) # True

    4.3 向量化与性能

    import numpy as np

    # ─── np.vectorize(语法糖,性能不如真正的向量化)──────────
    def my_func(x, y):
    return x ** 2 + y if x > 0 else y

    vfunc = np.vectorize(my_func)
    a = np.array([–1, 2, 3])
    b = np.array([10, 20, 30])
    print(vfunc(a, b)) # [10, 24, 39]
    # 注意:np.vectorize 本质仍是 Python 循环,仅简化语法

    # ─── 真正的向量化(推荐)────────────────────────────────
    # 用 np.where 替代条件判断
    result = np.where(a > 0, a**2 + b, b)
    print(result) # [10, 24, 39] # 更快!

    # 避免循环的向量化模式
    # 低效:
    # total = 0
    # for x in arr:
    # total += x ** 2

    # 高效:
    arr = np.random.rand(1_000_000)
    total = np.sum(arr ** 2) # 一行,快几十倍

    # ─── np.einsum(爱因斯坦求和)────────────────────────────
    A = np.random.rand(3, 4)
    B = np.random.rand(4, 5)

    # 矩阵乘法
    np.einsum('ij,jk->ik', A, B) # 等价于 A @ B

    # 批量矩阵乘法
    batch_A = np.random.rand(10, 3, 4)
    batch_B = np.random.rand(10, 4, 5)
    np.einsum('bij,bjk->bik', batch_A, batch_B) # (10, 3, 5)

    # 迹(对角线求和)
    np.einsum('ii->', A[:3, :3]) # trace

    # 向量外积
    v1 = np.array([1, 2, 3])
    v2 = np.array([4, 5, 6])
    np.einsum('i,j->ij', v1, v2) # 外积,等价于 np.outer(v1, v2)

    # ─── 性能对比 ─────────────────────────────────────────────
    import time

    n = 1_000_000
    arr = np.random.rand(n)

    # Python 循环
    start = time.perf_counter()
    total = sum(x**2 for x in arr)
    print(f"Python 循环: {time.perf_counter()–start:.3f}s")

    # NumPy 向量化
    start = time.perf_counter()
    total = np.sum(arr**2)
    print(f"NumPy 向量化: {time.perf_counter()–start:.3f}s")
    # 通常快 50~200 倍

    4.4 文件 IO

    import numpy as np

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

    # ─── 二进制格式(npy / npz)─────────────────────────────
    np.save('array.npy', a) # 单数组
    b = np.load('array.npy')

    np.savez('arrays.npz', arr1=a, arr2=a*2) # 多数组(未压缩)
    np.savez_compressed('arrays_c.npz', arr1=a) # 压缩存储

    data = np.load('arrays.npz')
    print(data['arr1']) # 按名称访问

    # ─── 文本格式(txt / csv)───────────────────────────────
    np.savetxt('data.csv', a, delimiter=',', fmt='%d',
    header='col1,col2,col3', comments='')
    b = np.loadtxt('data.csv', delimiter=',', skiprows=1, dtype=int)

    # 处理缺失值
    b = np.genfromtxt('data.csv', delimiter=',', skip_header=1,
    filling_values=0, dtype=float)


    5. Pandas 基础与原理

    5.1 Pandas 核心数据结构原理

    Pandas 建立在 NumPy 之上,提供了两种核心数据结构:

    DataFrame 内部结构:

    列A 列B 列C
    ┌────┐ ┌────┐ ┌────┐
    index │ np │ │ np │ │ np │ ← 每列是一个 Series(底层是 ndarray)
    │arr │ │arr │ │arr │
    └────┘ └────┘ └────┘
    ↑
    BlockManager(内部块管理器)
    相同类型的列合并为一个 NumPy 块,减少内存碎片

    Pandas 2.x 的重要改变:

    • 默认使用 Copy-on-Write(写时拷贝),链式赋值更安全
    • 支持 Apache Arrow 作为后端(pd.ArrowDtype)
    • pd.NA 统一缺失值表示,替代 np.nan + None 的混乱

    5.2 Series

    import pandas as pd
    import numpy as np

    # ─── 创建 Series ──────────────────────────────────────────
    # 从列表创建(默认整数索引)
    s1 = pd.Series([10, 20, 30, 40, 50])
    print(s1.index) # RangeIndex(start=0, stop=5, step=1)

    # 自定义索引
    s2 = pd.Series([10, 20, 30], index=['a', 'b', 'c'])

    # 从字典创建(键作为索引)
    s3 = pd.Series({'Beijing': 2154, 'Shanghai': 2428, 'Guangzhou': 1867})

    # 常数填充
    s4 = pd.Series(5, index=range(5)) # [5, 5, 5, 5, 5]

    # ─── Series 属性 ──────────────────────────────────────────
    print(s2.values) # array([10, 20, 30])(NumPy 数组)
    print(s2.index) # Index(['a', 'b', 'c'])
    print(s2.dtype) # int64
    print(s2.shape) # (3,)
    print(s2.name) # None(可设置)
    s2.name = 'score'

    # ─── 索引与访问 ───────────────────────────────────────────
    # 标签索引(.loc)
    print(s2.loc['a']) # 10
    print(s2.loc['a':'b']) # a:10, b:20(注意:标签切片含右端点!)

    # 位置索引(.iloc)
    print(s2.iloc[0]) # 10
    print(s2.iloc[0:2]) # a:10, b:20

    # 直接索引(模糊,不推荐)
    print(s2['a']) # 10

    # 布尔索引
    print(s2[s2 > 15]) # b:20, c:30

    # ─── 运算 ─────────────────────────────────────────────────
    s2 * 2 # 逐元素乘以2
    s2 + 100 # 广播加法
    np.sqrt(s2) # NumPy 函数直接作用于 Series

    # Series 间运算:自动按索引对齐
    s_a = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
    s_b = pd.Series([10, 20, 30], index=['b', 'c', 'd'])
    result = s_a + s_b
    # a NaN(a 在 s_b 中不存在)
    # b 22.0
    # c 33.0
    # d NaN(d 在 s_a 中不存在)

    # 使用 fill_value 填充缺失
    result = s_a.add(s_b, fill_value=0)
    # a 1.0 (1+0)
    # b 22.0
    # c 33.0
    # d 30.0 (0+30)

    # ─── 常用方法 ─────────────────────────────────────────────
    s = pd.Series([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])

    s.sum() ; s.mean() ; s.std() ; s.median()
    s.min() ; s.max() ; s.count() ; s.nunique()
    s.value_counts() # 每个值的出现次数
    s.describe() # 统计摘要
    s.sort_values() # 按值排序
    s.sort_index() # 按索引排序
    s.rank() # 排名
    s.cumsum() # 累积求和
    s.diff() # 差分(相邻元素之差)
    s.shift(1) # 向下移动1位(时间序列常用)
    s.rolling(3).mean() # 滑动平均(窗口3)

    5.3 DataFrame 创建

    import pandas as pd
    import numpy as np

    # ─── 从字典创建(最常用)────────────────────────────────
    df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie', 'David'],
    'age': [28, 35, 42, 25],
    'salary': [75000.0, 92000.0, 110000.0, 68000.0],
    'dept': ['Engineering', 'Marketing', 'Engineering', 'HR']
    })

    # ─── 从列表的字典 ─────────────────────────────────────────
    df2 = pd.DataFrame([
    {'name': 'Alice', 'age': 28},
    {'name': 'Bob', 'age': 35},
    {'name': 'Charlie'} # 缺失的键自动填充 NaN
    ])

    # ─── 从 NumPy 数组 ────────────────────────────────────────
    arr = np.random.rand(5, 3)
    df3 = pd.DataFrame(arr, columns=['A', 'B', 'C'])

    # ─── 自定义索引 ───────────────────────────────────────────
    df4 = pd.DataFrame({'val': [1, 2, 3]}, index=['x', 'y', 'z'])

    # ─── DataFrame 核心属性 ───────────────────────────────────
    print(df.shape) # (4, 4)
    print(df.dtypes) # 每列的数据类型
    print(df.columns) # Index(['name', 'age', 'salary', 'dept'])
    print(df.index) # RangeIndex(0, 4, 1)
    print(df.values) # NumPy 数组(所有列同类型时高效)
    print(df.to_numpy()) # 推荐用法(pandas 2.x)
    print(df.info()) # 列类型、非空计数、内存
    print(df.describe()) # 数值列统计摘要
    print(df.head(3)) # 前3行
    print(df.tail(2)) # 后2行
    print(df.sample(2)) # 随机2行

    5.4 DataFrame 索引详解

    import pandas as pd

    df = pd.DataFrame({
    'A': [1, 2, 3, 4],
    'B': [10, 20, 30, 40],
    'C': [100, 200, 300, 400]
    }, index=['w', 'x', 'y', 'z'])

    # ─── 列访问 ───────────────────────────────────────────────
    df['A'] # 单列,返回 Series
    df[['A', 'C']] # 多列,返回 DataFrame

    # ─── .loc:标签索引(含右端点)────────────────────────────
    df.loc['x'] # 第 x 行,返回 Series
    df.loc['x', 'A'] # 第 x 行 A 列的值 = 2
    df.loc['x':'y'] # x 到 y 行(含 y!)
    df.loc['x':'y', 'A':'B'] # 子矩阵
    df.loc[:, 'A'] # A 列全部
    df.loc[['w', 'z'], 'B'] # 多个标签行,B列

    # 布尔索引
    df.loc[df['A'] > 2]
    df.loc[df['B'] > 15, 'A':'B'] # 满足条件的行,A到B列

    # ─── .iloc:位置索引(不含右端点)────────────────────────
    df.iloc[1] # 第2行(位置1)
    df.iloc[1, 0] # 第2行第1列 = 2
    df.iloc[0:2] # 前2行(位置0,1)
    df.iloc[0:2, 0:2] # 子矩阵(前2行,前2列)
    df.iloc[[0, 3], :] # 第1行和第4行

    # ─── .at / .iat:快速标量访问(最快)──────────────────────
    df.at['x', 'A'] # 标签访问单个值(比 loc 快)
    df.iat[1, 0] # 位置访问单个值(比 iloc 快)

    # ─── 赋值 ─────────────────────────────────────────────────
    df.loc['x', 'A'] = 999
    df.loc[df['A'] > 2, 'C'] = 0 # 条件赋值
    df['D'] = df['A'] + df['B'] # 新增列


    6. Pandas 数据读写

    6.1 CSV 读写

    import pandas as pd

    # ─── 读取 CSV ─────────────────────────────────────────────
    # 基础读取
    df = pd.read_csv('data.csv')

    # 完整参数示例
    df = pd.read_csv(
    'sales_data.csv',
    sep=',', # 分隔符(默认逗号)
    header=0, # 表头行(0=第一行,None=无表头)
    names=['id', 'name', 'amount', 'date'], # 自定义列名
    index_col='id', # 将某列作为索引
    usecols=['name', 'amount', 'date'], # 只读取指定列(节省内存)
    dtype={
    'id': 'int32',
    'amount': 'float32',
    'name': 'string' # Pandas StringDtype(比 object 好)
    },
    parse_dates=['date'], # 自动解析日期列
    date_format='%Y-%m-%d', # 日期格式
    na_values=['', 'N/A', '-', 'NULL'], # 额外视为缺失值的字符串
    keep_default_na=True,
    nrows=1000, # 只读前1000行(用于测试)
    skiprows=1, # 跳过前1行
    skipfooter=2, # 跳过末尾2行
    encoding='utf-8',
    engine='python', # c(快)/ python(功能多)/ pyarrow(最快)
    chunksize=50000, # 分块读取(返回迭代器,用于大文件)
    low_memory=False, # 不分块推断类型(更准确)
    comment='#', # 以#开头的行视为注释
    )

    # 分块处理大文件
    total_amount = 0
    for chunk in pd.read_csv('large.csv', chunksize=50000):
    total_amount += chunk['amount'].sum()

    # ─── 写入 CSV ─────────────────────────────────────────────
    df.to_csv(
    'output.csv',
    sep=',',
    index=False, # 不写入行索引
    header=True,
    encoding='utf-8-sig', # BOM,Excel 打开中文不乱码
    na_rep='', # NaN 的表示方式
    float_format='%.2f', # 浮点格式
    date_format='%Y-%m-%d',
    columns=['name', 'amount'], # 只写指定列
    )

    6.2 Excel、JSON、Parquet

    import pandas as pd

    # ─── Excel ────────────────────────────────────────────────
    # 读取(需要 pip install openpyxl)
    df = pd.read_excel(
    'report.xlsx',
    sheet_name='Sheet1', # 工作表名或索引(None=读取所有)
    header=0,
    usecols='A:D', # 列范围
    skiprows=2, # 跳过前2行
    engine='openpyxl'
    )

    # 读取所有工作表
    all_sheets = pd.read_excel('report.xlsx', sheet_name=None)
    # 返回 dict:{'Sheet1': df1, 'Sheet2': df2}

    # 写入 Excel(多个 Sheet)
    with pd.ExcelWriter('output.xlsx', engine='openpyxl') as writer:
    df.to_excel(writer, sheet_name='数据', index=False)
    df.describe().to_excel(writer, sheet_name='统计')

    # ─── JSON ─────────────────────────────────────────────────
    # 多种 orient 格式
    df.to_json('data.json', orient='records', force_ascii=False, indent=2)
    # orient='records' → [{"a":1,"b":2}, {"a":3,"b":4}]
    # orient='split' → {"columns":["a","b"],"data":[[1,2],[3,4]]}
    # orient='table' → 带 schema 的完整格式
    # orient='index' → {0:{"a":1}, 1:{"a":3}}

    df_json = pd.read_json('data.json', orient='records')

    # ─── Parquet(列式存储,推荐大数据场景)─────────────────
    # 需要 pip install pyarrow
    df.to_parquet('data.parquet', engine='pyarrow', compression='snappy', index=False)
    df_parquet = pd.read_parquet('data.parquet', columns=['name', 'amount'])

    # Parquet 优势:
    # – 列式存储,只读所需列(节省 IO)
    # – 保留数据类型(不像 CSV 全是字符串)
    # – 压缩效率高(snappy/gzip/zstd)
    # – 读写速度远快于 CSV

    # ─── SQL 数据库 ───────────────────────────────────────────
    # 需要 pip install sqlalchemy
    from sqlalchemy import create_engine

    engine = create_engine('postgresql://user:pass@host:5432/dbname')
    # 或 SQLite:create_engine('sqlite:///local.db')

    # 从 SQL 读取
    df = pd.read_sql('SELECT * FROM orders WHERE amount > 100', engine)
    df = pd.read_sql_table('orders', engine, schema='public')
    df = pd.read_sql_query(
    'SELECT id, name, SUM(amount) FROM orders GROUP BY id, name',
    engine
    )

    # 写入 SQL
    df.to_sql(
    'orders_summary',
    engine,
    if_exists='replace', # 'fail' / 'replace' / 'append'
    index=False,
    chunksize=1000, # 分批写入
    method='multi' # 批量 INSERT(比逐行快)
    )


    7. Pandas 数据清洗

    7.1 缺失值处理

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'name': ['Alice', 'Bob', None, 'David', 'Eve'],
    'age': [28, np.nan, 35, np.nan, 22],
    'salary': [75000, 92000, np.nan, np.nan, 58000],
    'dept': ['Eng', 'Mkt', 'Eng', None, 'Eng']
    })

    # ─── 检测缺失值 ───────────────────────────────────────────
    df.isna() # 每个元素是否为 NaN(DataFrame of bool)
    df.isnull() # 同 isna()
    df.notna() # 非 NaN
    df.isna().sum() # 每列缺失值数量
    df.isna().sum() / len(df) * 100 # 缺失率百分比
    df.isna().any(axis=1) # 每行是否含有任何缺失值

    # ─── 删除缺失值 ───────────────────────────────────────────
    df.dropna() # 删除含任何 NaN 的行
    df.dropna(axis=1) # 删除含任何 NaN 的列
    df.dropna(how='all') # 只删除全部为 NaN 的行
    df.dropna(how='any') # 删除含任何 NaN 的行(默认)
    df.dropna(subset=['name', 'age']) # 只考虑指定列
    df.dropna(thresh=3) # 保留至少有3个非NaN值的行

    # ─── 填充缺失值 ───────────────────────────────────────────
    # 固定值填充
    df.fillna(0)
    df.fillna({'age': 0, 'salary': df['salary'].median()})

    # 前向填充 / 后向填充(时间序列常用)
    df.ffill() # 用前一个有效值填充(forward fill)
    df.bfill() # 用后一个有效值填充(backward fill)
    df['age'].ffill(limit=1) # 最多连续填充1个

    # 统计量填充
    df['age'].fillna(df['age'].mean())
    df['age'].fillna(df['age'].median())

    # 插值填充(数值列)
    df['salary'] = df['salary'].interpolate(method='linear')

    # ─── Pandas 2.x 推荐:pd.NA ──────────────────────────────
    # pd.NA 是统一的缺失值标记,替代不同类型下 np.nan/None 的混乱
    s = pd.array([1, pd.NA, 3], dtype='Int64') # 可空整数
    s = pd.array(['a', pd.NA, 'c'], dtype='string') # 可空字符串

    7.2 数据类型转换

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'id': ['1', '2', '3', '4'],
    'price': ['12.5', '8.3', 'N/A', '20.1'],
    'date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
    'category': ['A', 'B', 'A', 'C'],
    'active': ['True', 'False', 'True', 'True']
    })

    # ─── astype:强制类型转换 ─────────────────────────────────
    df['id'] = df['id'].astype('int32')
    df['category'] = df['category'].astype('category') # 节省内存

    # ─── to_numeric:安全数值转换 ────────────────────────────
    df['price'] = pd.to_numeric(df['price'], errors='coerce')
    # errors='coerce' 将无法转换的值变为 NaN
    # errors='ignore' 保持原样
    # errors='raise' 抛出异常(默认)

    # 整列转换并降低数值类型
    df['id'] = pd.to_numeric(df['id'], downcast='integer') # 自动选最小整数类型

    # ─── to_datetime:日期转换 ───────────────────────────────
    df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
    df['date'] = pd.to_datetime(df['date'], errors='coerce') # 容错
    df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True) # 自动推断

    # 时间戳运算
    df['date'] + pd.Timedelta(days=7) # 加7天
    df['date'].dt.year # 提取年份
    df['date'].dt.month # 提取月份
    df['date'].dt.day # 提取日
    df['date'].dt.dayofweek # 星期几(0=周一)
    df['date'].dt.quarter # 季度
    df['date'].dt.strftime('%Y/%m/%d') # 格式化为字符串

    # ─── 布尔转换 ─────────────────────────────────────────────
    df['active'] = df['active'].map({'True': True, 'False': False})

    # ─── 内存优化:自动推断更合适的类型(Pandas 2.x)──────────
    df = df.convert_dtypes() # 自动将 object 列转换为更优类型
    # 如:object → string,float64 → Int64(可空整数)

    7.3 字符串操作(.str 访问器)

    import pandas as pd

    df = pd.DataFrame({
    'name': [' Alice Smith ', 'BOB JONES', 'charlie brown'],
    'email': ['alice@example.com', 'bob@test.org', 'charlie@mail.net'],
    'phone': ['138-1234-5678', '021-5555-1234', '0755-8888-9999'],
    'tags': ['python,data,ml', 'java,backend', 'python,web,frontend']
    })

    # ─── 清洗 ─────────────────────────────────────────────────
    df['name'].str.strip() # 去首尾空格
    df['name'].str.lower() # 转小写
    df['name'].str.upper() # 转大写
    df['name'].str.title() # 首字母大写

    # ─── 检测 ─────────────────────────────────────────────────
    df['email'].str.contains('@') # 是否含有 @
    df['email'].str.contains('@', na=False) # NaN 处理为 False
    df['name'].str.startswith('A')
    df['name'].str.endswith('h')
    df['name'].str.match(r'^[A-Z]') # 正则匹配开头
    df['name'].str.len() # 字符串长度

    # ─── 提取 ─────────────────────────────────────────────────
    # split:按分隔符分割
    df['tags'].str.split(',') # 每行返回列表
    df['tags'].str.split(',', expand=True) # 展开为多列

    # extract:正则提取(返回分组)
    df['email'].str.extract(r'([^@]+)@([^.]+)\\.(\\w+)')
    # 返回 DataFrame,每个括号组为一列

    df['phone'].str.extract(r'(?P<area>\\d{3,4})-(?P<prefix>\\d{4})')
    # 使用命名分组,列名自动设置

    # extractall:提取所有匹配(返回 MultiIndex)
    df['tags'].str.extractall(r'(\\w+)')

    # ─── 替换 ─────────────────────────────────────────────────
    df['phone'].str.replace('-', '', regex=False) # 移除横线
    df['name'].str.replace(r'\\s+', ' ', regex=True) # 多空格换一个

    # ─── 格式化 ───────────────────────────────────────────────
    df['name'].str.pad(20, side='right', fillchar=' ') # 填充到20位
    df['name'].str.zfill(20) # 左侧补0
    df['name'].str.center(20, '*') # 居中填充 *
    df['name'].str.ljust(20, '-') # 左对齐填充

    7.4 重复值与异常值处理

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'id': [1, 2, 2, 3, 4, 4, 5],
    'name': ['A', 'B', 'B', 'C', 'D', 'D', 'E'],
    'salary': [5000, 8000, 8000, 7000, 999999, 7500, 6000]
    })

    # ─── 重复值 ───────────────────────────────────────────────
    df.duplicated() # 每行是否为重复行(默认保留第一次)
    df.duplicated(keep='last') # 保留最后一次
    df.duplicated(keep=False) # 所有重复行都标记为 True
    df.duplicated(subset=['name']) # 只考虑指定列

    df.drop_duplicates() # 删除重复行(保留第一次)
    df.drop_duplicates(subset=['id'], keep='last')

    # ─── 异常值检测 ───────────────────────────────────────────
    salaries = df['salary']

    # 方法1:IQR 法(四分位距)
    Q1 = salaries.quantile(0.25)
    Q3 = salaries.quantile(0.75)
    IQR = Q3 – Q1
    lower = Q1 – 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    outliers = salaries[(salaries < lower) | (salaries > upper)]
    print(outliers)

    # 方法2:Z-Score 法
    from scipy import stats
    z_scores = np.abs(stats.zscore(salaries))
    outliers_z = salaries[z_scores > 3]

    # 方法3:自定义阈值
    salaries_clean = salaries.clip(lower=3000, upper=50000) # 截断到合理范围
    salaries_clean = salaries.where(salaries < 100000, other=np.nan) # 超出则置NaN

    # ─── 数据规范化 ───────────────────────────────────────────
    # Min-Max 归一化
    def minmax(s):
    return (s – s.min()) / (s.max() – s.min())

    # Z-score 标准化
    def zscore(s):
    return (s – s.mean()) / s.std()

    df['salary_norm'] = minmax(df['salary'])
    df['salary_std'] = zscore(df['salary'])


    8. Pandas 数据分析与聚合

    8.1 groupby 详解

    groupby 是 Pandas 最强大的功能之一,其工作原理是分组→应用→合并(Split-Apply-Combine):

    Split: 按 'dept' 分组
    Engineering: [行0, 行2]
    Marketing: [行1]
    HR: [行3]
    ↓
    Apply: 对每组执行操作(如 sum / mean / agg)
    ↓
    Combine: 将结果合并为一个 DataFrame / Series

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'dept': ['Eng', 'Mkt', 'Eng', 'HR', 'Mkt', 'Eng'],
    'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
    'salary': [85000, 72000, 95000, 68000, 76000, 88000],
    'years': [3, 5, 8, 2, 4, 6],
    'gender': ['F', 'M', 'M', 'M', 'F', 'M']
    })

    # ─── 基础 groupby ─────────────────────────────────────────
    g = df.groupby('dept') # 创建 GroupBy 对象(懒计算)
    g = df.groupby(['dept', 'gender']) # 多列分组

    # 查看分组
    g.groups # dict:{组名: 行索引列表}
    g.get_group('Eng') # 获取某一组的 DataFrame
    list(g) # 迭代:[(组名, DataFrame), …]

    # ─── 聚合函数 ─────────────────────────────────────────────
    df.groupby('dept')['salary'].sum() # 每个部门薪资总和
    df.groupby('dept')['salary'].mean() # 均值
    df.groupby('dept')['salary'].agg(['mean', 'min', 'max', 'std'])
    df.groupby('dept').agg({
    'salary': ['mean', 'sum', 'count'],
    'years': ['mean', 'max']
    })

    # 自定义聚合函数
    df.groupby('dept')['salary'].agg(lambda x: x.max() – x.min()) # 极差

    # ─── agg 的新语法(推荐,Pandas 0.25+)──────────────────
    df.groupby('dept').agg(
    avg_salary = ('salary', 'mean'),
    max_salary = ('salary', 'max'),
    total_salary = ('salary', 'sum'),
    headcount = ('name', 'count'),
    avg_years = ('years', 'mean')
    )

    # ─── transform:保持原始 DataFrame 形状 ──────────────────
    # transform 的结果与原 DataFrame 行数相同,用于新增列
    df['dept_avg_salary'] = df.groupby('dept')['salary'].transform('mean')
    df['salary_vs_dept'] = df['salary'] – df['dept_avg_salary']
    df['dept_rank'] = df.groupby('dept')['salary'].transform('rank', ascending=False)

    # ─── filter:按组过滤 ─────────────────────────────────────
    # 只保留部门平均薪资 > 80000 的员工
    df.groupby('dept').filter(lambda x: x['salary'].mean() > 80000)

    # ─── apply:最灵活的分组操作 ──────────────────────────────
    # apply 对每个组执行任意函数,返回结果可以是标量/Series/DataFrame
    def dept_summary(group):
    return pd.Series({
    'count': len(group),
    'avg_salary': group['salary'].mean(),
    'top_earner': group.loc[group['salary'].idxmax(), 'name']
    })

    df.groupby('dept').apply(dept_summary, include_groups=False)

    # 组内排序
    df.groupby('dept').apply(
    lambda g: g.sort_values('salary', ascending=False).head(2),
    include_groups=False
    )

    8.2 pivot_table 与 crosstab

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'region': ['North', 'North', 'South', 'South', 'East', 'East'] * 3,
    'product': ['A', 'B'] * 9,
    'quarter': ['Q1', 'Q2', 'Q3'] * 6,
    'sales': np.random.randint(1000, 5000, 18),
    'profit': np.random.randint(100, 1000, 18),
    })

    # ─── pivot_table:数据透视表 ─────────────────────────────
    pivot = pd.pivot_table(
    df,
    values='sales', # 聚合的值列
    index='region', # 行标签
    columns='product', # 列标签
    aggfunc='sum', # 聚合函数
    fill_value=0, # NaN 填充值
    margins=True, # 添加汇总行/列(All)
    margins_name='总计'
    )

    # 多值聚合
    pivot2 = pd.pivot_table(
    df,
    values=['sales', 'profit'],
    index=['region'],
    columns=['quarter'],
    aggfunc={'sales': 'sum', 'profit': 'mean'},
    )

    # ─── crosstab:交叉频率表 ─────────────────────────────────
    pd.crosstab(df['region'], df['product']) # 频数
    pd.crosstab(df['region'], df['product'],
    values=df['sales'], aggfunc='sum') # 带值的交叉表
    pd.crosstab(df['region'], df['product'],
    normalize='index') # 行归一化(行内百分比)

    # ─── melt:宽表转长表 ─────────────────────────────────────
    wide_df = pd.DataFrame({
    'name': ['Alice', 'Bob'],
    'math': [90, 85],
    'english': [88, 92],
    'science': [76, 95]
    })

    long_df = wide_df.melt(
    id_vars='name', # 保持不变的列
    value_vars=['math', 'english', 'science'], # 要转换的列
    var_name='subject', # 新的变量列名
    value_name='score' # 新的值列名
    )
    # name subject score
    # Alice math 90
    # Alice english 88
    # …

    # ─── pivot:长表转宽表 ────────────────────────────────────
    wide_back = long_df.pivot(index='name', columns='subject', values='score')
    wide_back.columns.name = None # 去掉列的分组名
    wide_back = wide_back.reset_index()

    8.3 合并与连接

    import pandas as pd

    left = pd.DataFrame({
    'key': ['A', 'B', 'C', 'D'],
    'val1': [1, 2, 3, 4]
    })
    right = pd.DataFrame({
    'key': ['B', 'C', 'D', 'E'],
    'val2': [20, 30, 40, 50]
    })

    # ─── merge:类似 SQL JOIN ─────────────────────────────────
    # 内连接(默认):只保留两表都有的键
    pd.merge(left, right, on='key') # INNER JOIN
    pd.merge(left, right, on='key', how='inner') # 同上

    # 外连接
    pd.merge(left, right, on='key', how='left') # LEFT JOIN(保留左表所有行)
    pd.merge(left, right, on='key', how='right') # RIGHT JOIN
    pd.merge(left, right, on='key', how='outer') # FULL OUTER JOIN

    # 不同列名的连接
    pd.merge(left, right, left_on='key', right_on='id')

    # 多列连接
    pd.merge(orders, customers, on=['customer_id', 'date'])

    # 指示器列(标记行来源)
    pd.merge(left, right, on='key', how='outer', indicator=True)
    # _merge 列:'left_only' / 'right_only' / 'both'

    # 重复列名处理
    pd.merge(left, right, on='key', suffixes=('_left', '_right'))

    # ─── join:基于索引的连接 ────────────────────────────────
    left_i = left.set_index('key')
    right_i = right.set_index('key')
    left_i.join(right_i, how='inner') # 比 merge 更简洁(基于索引)

    # ─── concat:沿轴拼接 ────────────────────────────────────
    # 竖向拼接(行增加)
    pd.concat([df1, df2], axis=0, ignore_index=True)

    # 横向拼接(列增加,按索引对齐)
    pd.concat([df1, df2], axis=1)

    # 带 keys 的分层索引
    pd.concat([df1, df2], keys=['2023', '2024'])

    # ─── 集合运算 ─────────────────────────────────────────────
    # 找出 left 中不在 right 的行(差集)
    merged = pd.merge(left, right, on='key', how='left', indicator=True)
    only_left = merged[merged['_merge'] == 'left_only'].drop('_merge', axis=1)

    8.4 时间序列分析

    import pandas as pd
    import numpy as np

    # ─── 创建时间序列 ─────────────────────────────────────────
    dates = pd.date_range('2024-01-01', periods=365, freq='D')
    ts = pd.Series(np.random.randn(365), index=dates)

    # 常用频率别名
    # 'D' 日 'B' 工作日 'W' 周
    # 'ME' 月末 'QE' 季末 'YE' 年末
    # 'h' 小时 'min' 分钟 's' 秒

    # ─── 时间索引操作 ─────────────────────────────────────────
    ts['2024-03'] # 选取3月份数据
    ts['2024-01':'2024-03'] # 选取1月到3月
    ts[ts.index.month == 6] # 选取6月份

    # dt 访问器属性
    df = pd.DataFrame({'date': dates[:10], 'val': range(10)})
    df['date'].dt.year
    df['date'].dt.month
    df['date'].dt.day
    df['date'].dt.dayofweek # 0=周一
    df['date'].dt.is_weekend # 是否周末(不是内置属性,需计算)
    df['date'].dt.quarter
    df['date'].dt.week # 周数(ISO)

    # ─── 重采样(resample)────────────────────────────────────
    # 降采样:日 → 月
    monthly = ts.resample('ME').mean() # 月均值
    monthly = ts.resample('ME').agg(['mean', 'min', 'max', 'std'])

    # 升采样:月 → 日(需要填充)
    monthly_ts = pd.Series([1, 2, 3],
    index=pd.date_range('2024-01', periods=3, freq='ME'))
    daily = monthly_ts.resample('D').ffill() # 前向填充
    daily = monthly_ts.resample('D').interpolate('linear') # 插值

    # ─── 滑动窗口(rolling)──────────────────────────────────
    ts.rolling(window=7).mean() # 7日滑动均值
    ts.rolling(window=30, min_periods=1).std() # 30日滑动标准差
    ts.rolling(window=7).agg(['mean', 'std']) # 多统计量

    # 指数加权移动平均(EWMA)
    ts.ewm(span=7, adjust=False).mean() # 7日半衰期

    # ─── 时间偏移 ─────────────────────────────────────────────
    ts.shift(1) # 向后移动1天(过去值)
    ts.shift(–1) # 向前移动1天(未来值)
    ts.diff(1) # 日收益(与前一天的差)
    ts.pct_change(1) # 日涨跌幅(百分比)
    ts.pct_change(30) # 30日涨幅

    # ─── 时区处理 ─────────────────────────────────────────────
    ts_utc = ts.tz_localize('UTC') # 设置时区
    ts_cn = ts_utc.tz_convert('Asia/Shanghai') # 转换时区


    9. Pandas 高级操作

    9.1 多级索引(MultiIndex)

    import pandas as pd
    import numpy as np

    # ─── 创建 MultiIndex ──────────────────────────────────────
    arrays = [
    ['Eng', 'Eng', 'Mkt', 'Mkt'],
    ['Alice', 'Bob', 'Charlie', 'David']
    ]
    index = pd.MultiIndex.from_arrays(arrays, names=['dept', 'name'])

    df = pd.DataFrame({
    'salary': [85000, 92000, 76000, 68000],
    'years': [3, 5, 4, 2]
    }, index=index)

    # 从 groupby 得到 MultiIndex
    df_multi = (pd.DataFrame({
    'dept': ['Eng', 'Eng', 'Mkt', 'Mkt'],
    'name': ['Alice', 'Bob', 'Charlie', 'David'],
    'q1': [100, 120, 90, 110],
    'q2': [110, 130, 95, 105]
    })
    .set_index(['dept', 'name']))

    # ─── MultiIndex 索引 ──────────────────────────────────────
    df.loc['Eng'] # 第一层
    df.loc[('Eng', 'Alice')] # 两层精确定位
    df.loc['Eng', 'salary'] # 第一层 + 列
    df.loc[('Eng', 'Alice'), :] # 完整行

    # xs:跨级切片(更方便)
    df.xs('Alice', level='name') # 所有 Alice 行
    df.xs('Eng', level='dept') # Eng 部门所有行

    # ─── MultiIndex 操作 ──────────────────────────────────────
    df.index.get_level_values(0) # 获取第0层所有值
    df.index.get_level_values('dept') # 按名称获取

    df.swaplevel('dept', 'name') # 交换两层
    df.sort_index(level='dept') # 按某层排序

    df.reset_index() # MultiIndex 转为普通列
    df.reset_index(level='dept') # 只降级 dept 层

    # ─── 列的 MultiIndex ──────────────────────────────────────
    df_wide = pd.DataFrame(
    np.random.rand(3, 6),
    columns=pd.MultiIndex.from_product(
    [['2023', '2024'], ['Q1', 'Q2', 'Q3']],
    names=['year', 'quarter']
    )
    )

    df_wide['2024'] # 2024年全部列
    df_wide[('2024', 'Q1')] # 精确列
    df_wide.xs('Q1', axis=1, level='quarter') # 跨列切片

    9.2 apply、map 与 applymap

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie'],
    'salary': [85000, 72000, 95000],
    'years': [3, 5, 8]
    })

    # ─── map(Series):逐元素映射 ───────────────────────────
    # 用字典映射
    df['salary'].map({85000: '中', 72000: '低', 95000: '高'})

    # 用函数映射(逐元素)
    df['salary'].map(lambda x: f'¥{x:,}')

    # ─── apply(Series):逐元素或整体聚合 ───────────────────
    df['salary'].apply(lambda x: x * 1.1) # 逐元素(等同 map)
    df['salary'].apply(lambda x: x * 1.1 if x > 80000 else x)

    # ─── apply(DataFrame):按轴应用 ────────────────────────
    # axis=0(默认):对每列调用函数
    df[['salary', 'years']].apply(lambda col: col.max() – col.min())

    # axis=1:对每行调用函数(返回 Series 则结果是 DataFrame)
    df.apply(lambda row: row['salary'] / row['years'], axis=1)

    df.apply(lambda row: pd.Series({
    'annual': row['salary'],
    'monthly': row['salary'] / 12,
    'daily': row['salary'] / 365
    }), axis=1)

    # ─── map(DataFrame,Pandas 2.1+ 替代 applymap)──────────
    df[['salary', 'years']].map(lambda x: round(x, 2))

    # ─── 性能说明 ─────────────────────────────────────────────
    # 性能排序(快 → 慢):
    # 向量化操作(df['a'] + df['b'])≫ np.vectorize ≫ apply ≫ 循环

    # ❌ 慢:apply 逐行
    df.apply(lambda row: row['salary'] + row['years'] * 1000, axis=1)

    # ✅ 快:向量化
    df['salary'] + df['years'] * 1000

    9.3 Category 类型与内存优化

    import pandas as pd
    import numpy as np

    # 生成示例数据
    n = 1_000_000
    df = pd.DataFrame({
    'dept': np.random.choice(['Eng', 'Mkt', 'HR', 'Finance'], n),
    'level': np.random.choice(['Junior', 'Mid', 'Senior', 'Lead'], n),
    'salary': np.random.randint(50000, 200000, n),
    'score': np.random.rand(n),
    })

    # 查看内存前
    print(df.memory_usage(deep=True).sum() / 1024**2, 'MB') # 约 46MB

    # ─── 转换为 Category 类型(基数低的列) ──────────────────
    df['dept'] = df['dept'].astype('category')
    df['level'] = df['level'].astype('category')

    # 降低数值类型精度
    df['salary'] = df['salary'].astype('int32') # int64 → int32 省一半
    df['score'] = df['score'].astype('float32') # float64 → float32 省一半

    # 查看内存后
    print(df.memory_usage(deep=True).sum() / 1024**2, 'MB') # 约 8MB,节省80%

    # ─── Category 的优势 ──────────────────────────────────────
    cat = df['dept']
    cat.cat.categories # Index(['Eng', 'Finance', 'HR', 'Mkt'])
    cat.cat.codes # 底层整数编码(0,1,2,3)
    cat.cat.ordered # 是否有序(默认False)

    # 有序 Category
    level_order = pd.CategoricalDtype(
    categories=['Junior', 'Mid', 'Senior', 'Lead'],
    ordered=True
    )
    df['level'] = df['level'].astype(level_order)
    df[df['level'] > 'Mid'] # 可以直接比较顺序

    # groupby Category 会保留所有类别(即使某类无数据)
    df.groupby('dept', observed=False)['salary'].mean()

    # ─── 自动优化(Pandas 2.x)─────────────────────────────
    # convert_dtypes:自动推断最优类型
    df_optimized = df.convert_dtypes()


    10. NumPy 与 Pandas 综合实战

    10.1 实战一:电商销售数据分析

    import pandas as pd
    import numpy as np
    from datetime import datetime

    # ─── 1. 模拟数据生成 ─────────────────────────────────────
    rng = np.random.default_rng(42)
    n = 10000

    categories = ['Electronics', 'Clothing', 'Food', 'Books', 'Sports']
    regions = ['North', 'South', 'East', 'West', 'Central']

    df = pd.DataFrame({
    'order_id': np.arange(1000, 1000 + n),
    'date': pd.date_range('2024-01-01', periods=n, freq='2h'),
    'category': rng.choice(categories, n),
    'region': rng.choice(regions, n),
    'quantity': rng.integers(1, 20, n),
    'unit_price': rng.uniform(10, 500, n).round(2),
    'discount': rng.choice([0, 0.05, 0.10, 0.15, 0.20], n, p=[0.5,0.2,0.15,0.1,0.05]),
    'customer_id': rng.integers(1, 500, n),
    })

    # ─── 2. 数据清洗 ─────────────────────────────────────────
    # 注入一些缺失值和异常值
    df.loc[rng.choice(n, 200, replace=False), 'unit_price'] = np.nan
    df.loc[rng.choice(n, 50, replace=False), 'quantity'] = –1

    # 清洗:处理缺失值
    df['unit_price'] = df['unit_price'].fillna(df.groupby('category')['unit_price'].transform('median'))

    # 清洗:处理负值
    df['quantity'] = df['quantity'].clip(lower=1)

    # 类型优化
    df['category'] = df['category'].astype('category')
    df['region'] = df['region'].astype('category')
    df['quantity'] = df['quantity'].astype('int16')
    df['unit_price'] = df['unit_price'].astype('float32')

    # ─── 3. 特征工程 ─────────────────────────────────────────
    # 计算实际金额
    df['gross_amount'] = df['quantity'] * df['unit_price']
    df['net_amount'] = df['gross_amount'] * (1 – df['discount'])

    # 时间特征
    df['year'] = df['date'].dt.year
    df['month'] = df['date'].dt.month
    df['week'] = df['date'].dt.isocalendar().week.astype(int)
    df['weekday'] = df['date'].dt.dayofweek
    df['hour'] = df['date'].dt.hour
    df['is_weekend'] = df['weekday'] >= 5

    # ─── 4. 分析:月度销售趋势 ───────────────────────────────
    monthly = df.resample('ME', on='date').agg(
    orders = ('order_id', 'count'),
    revenue = ('net_amount', 'sum'),
    avg_order = ('net_amount', 'mean'),
    customers = ('customer_id', 'nunique')
    ).round(2)
    monthly['mom_growth'] = monthly['revenue'].pct_change() * 100 # 月环比

    # ─── 5. 分析:类别 × 地区矩阵 ────────────────────────────
    cat_region = df.pivot_table(
    values='net_amount',
    index='category',
    columns='region',
    aggfunc='sum',
    margins=True
    ).round(0).astype(int)

    # ─── 6. 分析:RFM 客户价值模型 ───────────────────────────
    snapshot_date = df['date'].max()

    rfm = df.groupby('customer_id').agg(
    recency = ('date', lambda x: (snapshot_date – x.max()).days),
    frequency = ('order_id', 'count'),
    monetary = ('net_amount', 'sum')
    ).round(2)

    # 分位数打分(1~4分,分越高越好)
    rfm['R_score'] = pd.qcut(rfm['recency'], 4, labels=[4,3,2,1]).astype(int)
    rfm['F_score'] = pd.qcut(rfm['frequency'].rank(method='first'), 4, labels=[1,2,3,4]).astype(int)
    rfm['M_score'] = pd.qcut(rfm['monetary'], 4, labels=[1,2,3,4]).astype(int)
    rfm['RFM_score'] = rfm['R_score'] + rfm['F_score'] + rfm['M_score']

    # 客户分层
    def classify_customer(score):
    if score >= 10: return '高价值客户'
    elif score >= 7: return '潜力客户'
    elif score >= 5: return '普通客户'
    else: return '流失风险客户'

    rfm['segment'] = rfm['RFM_score'].apply(classify_customer)
    print(rfm['segment'].value_counts())

    # ─── 7. 分析:商品关联分析(简化版)────────────────────────
    # 同一客户同一天是否同时购买了多个品类
    same_day = df.groupby(['customer_id', df['date'].dt.date])['category'].apply(list)
    multi_cat = same_day[same_day.apply(len) > 1]

    # 统计 Electronics 同时购买的其他品类
    electronics_days = same_day[same_day.apply(lambda x: 'Electronics' in x)]
    co_purchased = (electronics_days
    .explode()
    .value_counts()
    .drop('Electronics', errors='ignore'))
    print("与 Electronics 共同购买频次:")
    print(co_purchased)

    # ─── 8. 输出报告 ─────────────────────────────────────────
    with pd.ExcelWriter('sales_report.xlsx', engine='openpyxl') as writer:
    monthly.to_excel(writer, sheet_name='月度趋势')
    cat_region.to_excel(writer, sheet_name='类别地区矩阵')
    rfm.head(100).to_excel(writer, sheet_name='RFM分析', index=True)

    print("✅ 报告已生成:sales_report.xlsx")

    10.2 实战二:NumPy 实现机器学习基础算法

    import numpy as np

    # ─── 线性回归(最小二乘法)───────────────────────────────
    class LinearRegression:
    def __init__(self, learning_rate=0.01, n_iter=1000):
    self.lr = learning_rate
    self.n_iter = n_iter
    self.weights = None
    self.bias = None
    self.loss_history = []

    def fit(self, X, y):
    n_samples, n_features = X.shape
    self.weights = np.zeros(n_features)
    self.bias = 0.0

    for _ in range(self.n_iter):
    # 前向传播
    y_pred = X @ self.weights + self.bias

    # 计算 MSE 损失
    loss = np.mean((y_pred – y) ** 2)
    self.loss_history.append(loss)

    # 梯度计算
    dw = (2 / n_samples) * X.T @ (y_pred – y)
    db = (2 / n_samples) * np.sum(y_pred – y)

    # 参数更新
    self.weights -= self.lr * dw
    self.bias -= self.lr * db

    return self

    def predict(self, X):
    return X @ self.weights + self.bias

    def r2_score(self, X, y):
    y_pred = self.predict(X)
    ss_res = np.sum((y – y_pred) ** 2)
    ss_tot = np.sum((y – np.mean(y)) ** 2)
    return 1 – ss_res / ss_tot

    # ─── K-Means 聚类 ─────────────────────────────────────────
    class KMeans:
    def __init__(self, k=3, max_iter=100, tol=1e-4):
    self.k = k
    self.max_iter = max_iter
    self.tol = tol
    self.centers = None
    self.labels_ = None

    def fit(self, X):
    rng = np.random.default_rng(42)
    # 随机初始化聚类中心
    idx = rng.choice(len(X), self.k, replace=False)
    self.centers = X[idx].copy()

    for iteration in range(self.max_iter):
    # 分配步骤:计算每个点到各中心的距离
    distances = self._calc_distances(X)
    labels = np.argmin(distances, axis=1)

    # 更新步骤:重新计算聚类中心
    new_centers = np.array([
    X[labels == i].mean(axis=0) if np.sum(labels == i) > 0
    else self.centers[i]
    for i in range(self.k)
    ])

    # 收敛检查
    shift = np.linalg.norm(new_centers – self.centers)
    self.centers = new_centers
    self.labels_ = labels

    if shift < self.tol:
    print(f"第 {iteration+1} 次迭代收敛")
    break

    return self

    def _calc_distances(self, X):
    # 利用广播计算所有点到所有中心的欧氏距离
    # X: (n, d), centers: (k, d)
    diff = X[:, np.newaxis, :] – self.centers[np.newaxis, :, :]
    # (n, 1, d) – (1, k, d) → (n, k, d)
    return np.sqrt((diff ** 2).sum(axis=2)) # (n, k)

    def inertia(self, X):
    """簇内平方和(越小越好)"""
    distances = self._calc_distances(X)
    return np.sum(np.min(distances, axis=1) ** 2)

    def predict(self, X):
    return np.argmin(self._calc_distances(X), axis=1)

    # ─── 使用示例 ─────────────────────────────────────────────
    rng = np.random.default_rng(42)

    # 线性回归示例
    X = rng.standard_normal((200, 3))
    true_weights = np.array([2.0, –1.5, 0.8])
    y = X @ true_weights + 3.0 + rng.normal(0, 0.5, 200)

    model = LinearRegression(learning_rate=0.05, n_iter=500)
    model.fit(X, y)
    print(f"R² Score: {model.r2_score(X, y):.4f}")
    print(f"学到的权重:{model.weights.round(3)}")
    print(f"真实权重: {true_weights}")

    # K-Means 示例
    centers_true = np.array([[0, 0], [5, 5], [–5, 5]])
    X_cluster = np.vstack([
    rng.normal(c, 0.8, (100, 2)) for c in centers_true
    ])

    kmeans = KMeans(k=3)
    kmeans.fit(X_cluster)
    print(f"\\nK-Means 聚类中心:\\n{kmeans.centers.round(2)}")
    print(f"簇内平方和:{kmeans.inertia(X_cluster):.2f}")

    10.3 实战三:时间序列预测数据准备

    import pandas as pd
    import numpy as np

    def prepare_ts_features(series: pd.Series, lags: int = 7,
    rolling_windows: list = None) –> pd.DataFrame:
    """
    将时间序列转换为监督学习特征矩阵

    Args:
    series: 时间序列(DatetimeIndex)
    lags: 滞后特征数量
    rolling_windows: 滑动窗口列表

    Returns:
    包含原始值、滞后特征、滑动统计量、时间特征的 DataFrame
    """
    if rolling_windows is None:
    rolling_windows = [7, 14, 30]

    df = pd.DataFrame({'y': series})

    # ─── 时间特征 ─────────────────────────────────────────
    df['day_of_week'] = series.index.dayofweek
    df['day_of_month'] = series.index.day
    df['month'] = series.index.month
    df['quarter'] = series.index.quarter
    df['is_weekend'] = (series.index.dayofweek >= 5).astype(int)

    # 傅里叶特征(捕捉周期性)
    for period in [7, 365]:
    for order in [1, 2]:
    df[f'sin_{period}_{order}'] = np.sin(
    2 * np.pi * order * series.index.dayofyear / period)
    df[f'cos_{period}_{order}'] = np.cos(
    2 * np.pi * order * series.index.dayofyear / period)

    # ─── 滞后特征 ─────────────────────────────────────────
    for lag in range(1, lags + 1):
    df[f'lag_{lag}'] = series.shift(lag)

    # ─── 滑动统计量 ───────────────────────────────────────
    for window in rolling_windows:
    roll = series.shift(1).rolling(window) # shift(1) 防止数据泄露
    df[f'roll_mean_{window}'] = roll.mean()
    df[f'roll_std_{window}'] = roll.std()
    df[f'roll_min_{window}'] = roll.min()
    df[f'roll_max_{window}'] = roll.max()
    df[f'roll_skew_{window}'] = roll.skew()

    # ─── 差分特征(捕捉趋势)─────────────────────────────
    df['diff_1'] = series.diff(1) # 日差分
    df['diff_7'] = series.diff(7) # 周差分
    df['diff_30'] = series.diff(30) # 月差分

    # ─── 删除因滞后产生的 NaN 行 ─────────────────────────
    df = df.dropna()

    return df

    # 使用示例:模拟销售时间序列
    rng = np.random.default_rng(42)
    dates = pd.date_range('2022-01-01', '2024-12-31', freq='D')
    n = len(dates)

    # 模拟带有趋势、周期性和噪声的时间序列
    trend = np.linspace(1000, 1500, n)
    weekly_pattern = 200 * np.sin(2 * np.pi * np.arange(n) / 7)
    annual_pattern = 300 * np.sin(2 * np.pi * np.arange(n) / 365)
    noise = rng.normal(0, 50, n)
    sales = trend + weekly_pattern + annual_pattern + noise

    ts = pd.Series(sales, index=dates, name='sales')

    features = prepare_ts_features(ts, lags=14, rolling_windows=[7, 14, 30])
    print(f"特征矩阵形状:{features.shape}")
    print(f"特征列:{list(features.columns)}")

    # 分割训练集和测试集(时间序列不能随机分割!)
    cutoff = '2024-10-01'
    X_train = features.loc[:cutoff].drop('y', axis=1)
    y_train = features.loc[:cutoff, 'y']
    X_test = features.loc[cutoff:].drop('y', axis=1)
    y_test = features.loc[cutoff:, 'y']

    print(f"\\n训练集:{X_train.shape},测试集:{X_test.shape}")


    11. 性能优化最佳实践

    11.1 NumPy 性能陷阱与优化

    import numpy as np
    import time

    n = 1_000_000
    arr = np.random.rand(n)

    # ─── 陷阱1:Python 循环 ───────────────────────────────────
    def slow_sum_of_squares(arr):
    total = 0
    for x in arr:
    total += x ** 2
    return total

    # ─── 优化1:完全向量化 ────────────────────────────────────
    def fast_sum_of_squares(arr):
    return np.dot(arr, arr) # 比 np.sum(arr**2) 更快(一次遍历)

    # ─── 陷阱2:不必要的中间数组 ────────────────────────────
    # 每步运算都创建新数组,内存分配代价高
    result = arr * 2 + 1 # 创建2个中间数组

    # ─── 优化2:使用 out 参数就地运算 ────────────────────────
    result = np.empty_like(arr)
    np.multiply(arr, 2, out=result)
    np.add(result, 1, out=result) # 零额外内存分配

    # ─── 陷阱3:低效的条件分支 ───────────────────────────────
    def slow_relu(arr):
    return np.array([max(0, x) for x in arr])

    # ─── 优化3:np.maximum ────────────────────────────────────
    def fast_relu(arr):
    return np.maximum(arr, 0)

    # ─── 陷阱4:重复 .reshape() 与 .copy() ──────────────────
    # 确认是视图还是拷贝
    a = np.arange(12)
    b = a.reshape(3, 4)
    print(np.shares_memory(a, b)) # True → 视图,不浪费内存

    # ─── 优化4:利用内存布局 ─────────────────────────────────
    # 行优先(C order)遍历时,按行操作更快
    a = np.random.rand(1000, 1000)
    # 按列聚合(沿 axis=0)利用 C-order 缓存局部性
    np.sum(a, axis=0) # 快
    np.sum(a, axis=1) # 稍慢(对 C-order 来说)

    # 如果需要频繁按列操作,考虑转置后操作
    a_F = np.asfortranarray(a) # Fortran order(列优先)
    np.sum(a_F, axis=0) # 现在按列操作更快

    # ─── 使用 numexpr 加速复杂表达式 ─────────────────────────
    # pip install numexpr
    import numexpr as ne
    a = np.random.rand(1_000_000)
    b = np.random.rand(1_000_000)

    # NumPy(创建多个中间数组)
    result_np = 2 * a**2 + 3 * b – a * b

    # numexpr(单次遍历,多线程,无中间数组)
    result_ne = ne.evaluate('2 * a**2 + 3 * b – a * b')

    11.2 Pandas 性能优化

    import pandas as pd
    import numpy as np

    n = 1_000_000
    df = pd.DataFrame({
    'a': np.random.rand(n),
    'b': np.random.rand(n),
    'cat': np.random.choice(['X', 'Y', 'Z'], n)
    })

    # ─── 规则1:用向量化替代 apply ────────────────────────────
    # ❌ 慢:apply 逐行
    df.apply(lambda row: row['a'] ** 2 + row['b'], axis=1)

    # ✅ 快:向量化(快100x)
    df['a'] ** 2 + df['b']

    # ─── 规则2:eval() 和 query() ──────────────────────────
    # pd.eval 用 numexpr 引擎,无中间数组,多线程
    result = pd.eval('df.a ** 2 + df.b * 2 – df.a * df.b')

    df.eval('c = a ** 2 + b', inplace=True) # 直接新增列

    # query() 比布尔索引更快(大数据集)
    df.query('a > 0.5 and b < 0.3')
    df.query('cat == "X" and a > @threshold', local_dict={'threshold': 0.5})

    # ─── 规则3:读取时优化类型 ────────────────────────────────
    df = pd.read_csv('data.csv', dtype={
    'category': 'category', # 直接指定 category
    'amount': 'float32', # 不要默认的 float64
    'count': 'int32',
    })

    # ─── 规则4:避免 chained indexing ─────────────────────────
    # ❌ 可能触发 SettingWithCopyWarning
    df[df['a'] > 0.5]['b'] = 0

    # ✅ 用 .loc 直接操作
    df.loc[df['a'] > 0.5, 'b'] = 0

    # ─── 规则5:字符串操作使用 .str ───────────────────────────
    # ❌ 慢:apply + Python 字符串
    df['cat'].apply(lambda x: x.lower())

    # ✅ 快:.str 访问器(向量化)
    df['cat'].str.lower()

    # ─── 规则6:合理使用 inplace ──────────────────────────────
    # Pandas 2.x 中 inplace 几乎不省内存(内部仍创建副本)
    # 推荐赋值写法
    df = df.dropna() # 比 df.dropna(inplace=True) 更清晰
    df = df.reset_index(drop=True)

    # ─── 规则7:大数据使用 Parquet 而非 CSV ──────────────────
    # CSV 读写慢,无类型信息,文件大
    # Parquet 列式存储,压缩好,保留类型

    df.to_parquet('data.parquet', compression='snappy')
    df_back = pd.read_parquet('data.parquet', columns=['a', 'b'])

    # ─── 规则8:超大数据使用 Dask ─────────────────────────────
    # pip install dask
    import dask.dataframe as dd

    ddf = dd.from_pandas(df, npartitions=4) # 分4个分区并行
    result = ddf.groupby('cat')['a'].mean().compute() # .compute() 触发执行

    11.3 内存分析工具

    import pandas as pd
    import numpy as np

    # ─── 分析 DataFrame 内存 ──────────────────────────────────
    df = pd.DataFrame({
    'int_col': np.random.randint(0, 100, 100000),
    'float_col': np.random.rand(100000),
    'str_col': np.random.choice(['A', 'B', 'C'], 100000),
    'object_col': [str(i) for i in range(100000)]
    })

    # 查看各列内存(deep=True 准确计算 object 类型)
    memory = df.memory_usage(deep=True)
    print("各列内存使用:")
    print(memory.apply(lambda x: f'{x / 1024**2:.2f} MB'))
    print(f"\\n总内存:{memory.sum() / 1024**2:.2f} MB")

    # ─── 系统级分析(推荐:memory_profiler)─────────────────
    # pip install memory_profiler psutil

    def optimize_dtypes(df: pd.DataFrame) –> pd.DataFrame:
    """自动优化 DataFrame 的数据类型以减少内存"""
    df = df.copy()

    for col in df.columns:
    col_type = df[col].dtype

    if col_type == 'object':
    # 低基数字符串转 category
    if df[col].nunique() / len(df) < 0.05:
    df[col] = df[col].astype('category')
    else:
    df[col] = df[col].astype('string')

    elif col_type in ['int64', 'int32']:
    col_min = df[col].min()
    col_max = df[col].max()
    if col_min >= 0:
    if col_max < 255:
    df[col] = df[col].astype(np.uint8)
    elif col_max < 65535:
    df[col] = df[col].astype(np.uint16)
    elif col_max < 4294967295:
    df[col] = df[col].astype(np.uint32)
    else:
    if col_min > –128 and col_max < 127:
    df[col] = df[col].astype(np.int8)
    elif col_min > –32768 and col_max < 32767:
    df[col] = df[col].astype(np.int16)
    elif col_min > –2147483648 and col_max < 2147483647:
    df[col] = df[col].astype(np.int32)

    elif col_type == 'float64':
    df[col] = df[col].astype(np.float32)

    return df

    before_mem = df.memory_usage(deep=True).sum() / 1024**2
    df_opt = optimize_dtypes(df)
    after_mem = df_opt.memory_usage(deep=True).sum() / 1024**2

    print(f"优化前:{before_mem:.2f} MB")
    print(f"优化后:{after_mem:.2f} MB")
    print(f"节省:{(1 – after_mem/before_mem)*100:.1f}%")

    赞(0)
    未经允许不得转载:171主机测评 » Python NumPy 与 Pandas 完整学习文档
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址