文科生の量化闯关记 : 代码篇- Python进阶1

我是宽客我是宽客··994 浏览

一、引言

::: hljs-center

Python 是一种“胶水语言”,能够整合多种库与工具!

:::

::: hljs-center
image.png
:::

Python 是“胶水语言”,能够将各种不同的库和工具粘合在一起,创造出强大的解决方案。

在上一次的介绍中,我们已经讲解了 Python 基础的 4 个模块(数据结构、流程控制、函数用法、面向对象);本次将聚焦 Python 的进阶使用方法,以及数据分析领域的核心工具库。对于刚开始接触量化分析或数据分析初学者而言,最常用的两个基础工具库便是 NumPy 和 Pandas。

对于量化分析人员而言,NumPy 和 Pandas 是最核心、几乎不可或缺的基础工具库——它们分别从底层计算和数据处理两个维度,支撑了量化策略开发、数据清洗、回测分析等关键环节的高效实现。在我之前从事数据分析相关工作期间,Pandas 也是我最常用的工具库之一。

没有 Python 基础的朋友,建议先翻一翻上次的基础篇:
https://www.pandaai.online/community/article/203
咱们就正式开启 NumPy 和 Pandas 的学习吧!

二、Numpy

首先介绍NumPy。 Numpy 是 Numerical Python 的缩写,从其全称可看出它是 Python 专门处理数组 (array) 的计算的工具包,其官网地址是 https://numpy.org/。

# 在使用NumPy包之前,需要引进它并检查它的版本
import numpy
numpy.__version__

根据上面的代码可以用 numpy 里面所有的内置方法 (build-in methods) 了,比如求 numpy.sum() 与均值 numpy.mean()。通常给 numpy 起个别名 np,语法如下:

import numpy as np

这样所有出现 numpy 的地方都可以用 np 替代,上面的求和、均值可写 np.sum() 和 np.mean()

下面将使用NumPy从创建数组开始,介绍数组的保存与加载、数组的索引和切片、数组的变形以及数据的计算等操作。

(一)Numpy简介

NumPy数组是由相同类型元素组成的集合构成的数据结构。在NumPy数组中,数值型元素的使用频率最高;通常情况下,一维数组、二维数组、三维数组分别对应“线”“面”“体”的空间概念。

  • 一维数组:类似“一条线”(例如一列连续的数字),仅对应单一维度的轴(axis)方向。
  • 二维数组:类似“一张平面表格”(包含多行多列数据),对应行、列两个维度的轴方向。
  • 三维数组:类似“一叠平面表格”(由多个二维数组堆叠而成),对应行、列、层三个维度的轴方向。

从计算机底层逻辑的严格定义来看:数组本质上是计算机内存中一块连续的一维内存块(1D memory),其数据结构由一套索引方案(indexing scheme)和数据类型(data type)共同定义。

因此,NumPy数组的核心构成可概括为:

NumPy 数组 = 内存块 + 索引方案 + 数据类型

其中:

  • 内存块:存储数组的原始数据,是数组的“数据载体”;
  • 索引方案:用于定位数组中任意元素的位置,是数组的“定位规则”;
  • 数据类型:描述数组中元素的具体类型(如整数、浮点数等),是数组的“数据属性说明”。

为什么要专门学习数组?

看下面数组和列表之间的计算效率对比:两者的大小均为 100 万,将每个元素乘以 10,重复运行 20 次,并使用魔法命令 %time记录时间。

::: hljs-center
image.png
:::

从上述结果可以发现,数组的计算效率约为列表的 14 倍。需要注意的是,每次运行的具体时间可能不完全相同,但两者的效率差距始终保持在 10~15 倍 左右。

由此可见,若元素全部为数值型变量,数组是一种非常高效的数据结构。因此,我们非常有必要学习数组的使用。

(二)创建数值型和结构数组

1、创建 NumPy 数组

创建 NumPy 数组有三种方式:

  1. 基础构造法:将函数 np.array()用在列表和元组上。
  2. 规则生成法:用函数 np.arange() 和 np.linspace() 创建。
  3. 快速初始化法:用函数 np.ones(), np.zeros(), np.eye() 和np.random.random()创建。
# 用「列表」和「元组」当“原材料”,用基础构造法的 np.array() 将它们转换成 numpy 数组。
list1 = [3, 5, 2, 8, 4]
tuple1 = (4, 8, 2, 5, 3)

arrL = np.array(list1)
arrT = np.array(tuple1)

print("arrL =",arrL)
print("arrT =",arrT)
# 规则生成法
# arange() 函数有三个参数,分别为起点、终点,、间隔
print(np.arange(10))
print(np.arange(1,10))
print(np.arange(1,10,2))

# linspace() 函数有三个参数,分别为起点、终点、点数
print(np.linspace(1,6,6))
print(np.linspace(3,8,11))
# 快速初始化法
#用 zeros() 创建元素全是 0 的数组
print(np.zeros(10))

#用 ones() 创建元素全是 1 的数组
print(np.ones((2,3)))

#用 random() 创建随机元素的数组
print(np.random.random((2,2)))

#用 eye() 创建对角线都是 1 其他元素都是 0 的二维数组
print(np.eye(4,3))

2、数组属性

我们可以使用dir(arr)查看所有属性。关键属性包括:

  • type(arr)<class 'numpy.ndarray'>
  • arr.ndim:维度数(轴数)。
  • len(arr):第一维长度(仅一维有意义)。
  • arr.size:元素总数。
  • arr.shape:形状元组(各维大小)。
  • arr.dtype:元素类型(如float64int32)。
  • arr.itemsize:每个元素字节数(如float64为8)。
  • arr.strides:跨度元组(各维推进字节数,反映内存布局)。
# Example 1: One-dimensional array
L = [4.7, 6.2, 1.9, 7.3, 3.8]
arr = np.array(L)

print('The type is', type(arr))           # The type is <class 'numpy.ndarray'>
print('The dimension is', arr.ndim)        # The dimension is 1
print('The length of array is', len(arr))  # The length of array is 5
print('The number of elements is', arr.size)  # The number of elements is 5
print('The shape of array is', arr.shape)  # The shape of array is (5,)
print('The type of elements is', arr.dtype)  # The type of elements is float64
print('The item size of', arr.dtype, 'is', arr.itemsize)  # The item size of float64 is 8
print('The stride of array is', arr.strides)  # The stride of array is (8,)

# Example 2: Two-dimensional array
L2 = [[2, 4, 6], [8, 10, 12]]
arr2d = np.array(L2)
print(arr2d)                             # [[ 2  4  6]
                                         #  [ 8 10 12]]

print('The type is', type(arr2d))        # The type is <class 'numpy.ndarray'>
print('The dimension is', arr2d.ndim)     # The dimension is 2
print('The length of array is', len(arr2d))  # The length of array is 2
print('The number of elements is', arr2d.size)  # The number of elements is 6
print('The shape of array is', arr2d.shape)  # The shape of array is (2, 3)
print('The type of elements is', arr2d.dtype)  # The type of elements is int32
print('The item size of', arr2d.dtype, 'is', arr2d.itemsize)  # The item size of int32 is 4
print('The stride of array is', arr2d.strides)  # The stride of array is (12, 4)

# Example 3: Four-dimensional array
arr4d = np.random.random((2, 2, 2, 3))
print(arr4d)                             # [[[[random values between 0 and 1]]], ...] (specific values vary)
print('The type is', type(arr4d))        # The type is <class 'numpy.ndarray'>
print('The dimension is', arr4d.ndim)     # The dimension is 4
print('The length of array is', len(arr4d))  # The length of array is 2
print('The number of elements is', arr4d.size)  # The number of elements is 24
print('The stride of array is', arr4d.strides)  # The stride of array is (96, 48, 24, 8)
print('The shape of array is', arr4d.shape)  # The shape of array is (2, 2, 2, 3)
print('The type of elements is', arr4d.dtype)  # The type of elements is float64

3、结构数组(Structured Array)

Numpy中还有一种特殊的数组叫做结构数组 (structured array),它最大的特点就是数组中的元素类型不同,因此创建结构数组的关键在于如何创建这个元素类型,即 dtype

# 定义一个结构化数组的 dtype,包含不同列类型
dt = np.dtype([('企业', 'S12'), ('代码', 'S6'), ('价格/股数', 'f4', 2)])

# 使用定义的 dtype 创建一个结构化数组
s = np.array([('Tesla', 'TSLA', (250.3, 34567)),
              ('Facebook', 'META', (320.1, 45678))], dtype=dt)

# 将字节字符串解码为普通字符串
bstr = b'Tesla'
sstr = bstr.decode("utf-8")
print(type(bstr), bstr)             # <class 'bytes'> b'Tesla'
print(type(sstr), sstr)           # <class 'str'> Tesla

# 访问 '企业' 字段
s['企业']                      # array([b'Tesla', b'Facebook'], dtype='|S12')

# 计算 '价格/股数' 中第一列值的均值
s['价格/股数'][:, 0].mean()         # 285.2 (250.3 和 320.1 的平均值)

# 访问第二行的 '代码' 字段
s[1]['代码']                        # b'META'
  • dtype 定义dt 使用元组列表定义结构化数组的列类型。'S12' 表示长度为 12 的字符串,'S6' 表示长度为 6 的字符串,'f4' 表示 4 字节 (32 位) 浮点数,'2' 表示每行包含 2 个浮点值。
  • 结构化数组创建s 包含两行数据,每行有企业名称、代码和一个 (价格, 股数) 元组。数据类型由 dt 控制。
  • 字节解码bstr 是一个字节字符串,sstr 通过 decode("utf-8") 转换为普通字符串,展示类型转换过程。
  • 字段访问s['企业'] 返回企业名称数组,s['价格/股数'][:, 0].mean() 计算价格列均值,s[1]['代码'] 获取第二行代码。
  • 结果注释:均值 285.2 是 (250.3 + 320.1) / 2,具体值随输入变化,但逻辑一致。

(三)数组的保存和存载

下面介绍NumPy数组的保存与加载方法,并以 .npy.txt.csv 三种格式为例详细说明:

  1. 自身 .npy 格式
  • 保存:使用 np.save(npy_file, arr) 函数,将NumPy数组保存为 .npy 格式文件。
  • 加载:使用 np.load(npy_file) 函数,加载已保存的 .npy 格式数组。
  • 优势:适合NumPy内部使用,加载快速,无需额外参数。
  1. 文本 .txt 格式
  • 保存:通过 np.savetxt(txt_file, arr) 函数,把数组保存为 .txt 文本格式。
  • 加载:利用 np.loadtxt(txt_file) 函数,加载 .txt 格式的数组。
  • 注意:默认使用空格分隔,适合简单数值数据。
  1. 文本 .csv 格式
  • 加载:假设CSV文件中元素以分号 ; 分隔,可使用 np.genfromtxt(csv_file, delimiter=';') 加载(需指定分隔符 delimiter,否则可能因识别不到数字而显示 nan)。
  • 注意:适合分隔符自定义的逗号分隔值文件,常见于数据交换。
# Create and save a one-dimensional array
arr = np.arange(10)
np.save('data_np', arr)
print(arr)                          # [0 1 2 3 4 5 6 7 8 9]

# Load the saved array
arr_loaded = np.load('data_np.npy') #可以指定本地路径
print(arr_loaded)                   # [0 1 2 3 4 5 6 7 8 9]

# Create and save a two-dimensional array as text
lst = [[7, 8, 9], [10, 11, 12]]
arr = np.array(lst)
np.savetxt('data_text', arr)
arr_loaded = np.loadtxt('data_text.txt')
print(arr_loaded)                   # [ 7.  8.  9. 10. 11. 12.]

#假设已经在 csv 文件里写进去了 [[1,2,3], [4,5,6]],每行的元素是由「分号 ;」来分隔的。
# Load data from a CSV file with default and custom delimiter
arr_loaded = np.genfromtxt('data_csv.csv')
print(arr_loaded)    # [[nan nan] [nan nan]] (if no valid data or delimiter mismatch)

arr_loaded = np.genfromtxt('data_csv.csv', delimiter=',')
print(arr_loaded)   # [[ 1.  2.] [ 3.  4.]] (assuming data like "1,2\n3,4" in file)

(四)数组的获取

NumPy 数组提供三种核心访问方式:普通索引(normal indexing)、布尔索引(boolean indexing) 和 花式索引(fancy indexing)。下面我们将从一维数组出发,逐步扩展到多维数组场景,重点解析 索引与切片的区别视图机制原理,并结合实际应用场景说明其用法。

1. 普通获取(Normal Indexing)

NumPy 数组的基础访问方式通过 索引(获取单个元素) 和 切片(获取子数组) 实现,支持一维到多维扩展,核心分析点围绕 维度处理、视图机制、多维扩展规则 展开。

  • 一维数组
    • 创建示例:arr = np.array([0, 1, 2, 3, 4])
    • 索引:从0开始计数。单个元素如arr[3]返回3;多个元素如arr[[1,2,3]]返回[1, 2, 3]
    • 切片:含头不含尾,如arr[1:4]返回[1, 2, 3]arr[:]返回整个数组。
    • 视图 vs. 复制:NumPy切片返回视图(view),修改切片会影响原数组(如arr_slice = arr[:3]; arr_slice[0] = 100 会改变arr)。而Python列表切片是复制(copy),不会影响原列表。下面图展示了这一区别,强调NumPy设计是为了大型数组的效率优化。如果需要复制,可用copy()arr_copy = arr[1:4].copy()

::: hljs-center
image.png
:::

  • 二维数组
    • 创建示例:arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    • 索引
      • 行状获取:arr2d[2]返回[7, 8, 9]
      • 元素获取:arr2d[0][2]arr2d[0, 2]返回3。前者多括号,后者单括号(更高效,尤其高维)。
    • 切片
      • 行状:arr2d[:2]返回前两行[[1,2,3], [4,5,6]]
      • 列状:arr2d[:, [0,2]]返回第一和第三列[[1,3], [4,6], [7,9]]
      • 块状:arr2d[:2, 2]返回[3,6](降维);arr2d[1:2, :2]返回[[4,5]](保持维度)。
    • 维度保持规律:索引降维,切片保持维度。
  • 三维数组(N维扩展)
    • 创建示例:arr3d = np.arange(24).reshape((3,2,4))(形状(3,2,4))。
    • 索引:指定每个轴的位置,如arr3d[1,1,2]返回14。
    • 切片一维:沿单轴切片,如arr3d[2,0,1]返回17;需用:表示其他轴全取。
    • 切片二维:沿两轴切片,如arr3d[1](等价arr3d[1,:,:])返回二维子数组。
    • 分析:高维时,默认省略后轴的:,但需显式添加以避免混淆。强调N维通用性:代码范式arr[a,b,c,...],a/b/c可为索引/切片/列表。

普通获取的分析点:高效但需注意视图修改原数组;多维时,逗号分隔轴,便于扩展。

# 创建一个一维数组
arr = np.array([10, 20, 30, 40, 50])

# 索引单个元素
arr[2]                          # 30 (第三个元素,从0开始计数)

# 索引多个元素
arr[[1, 3]]                     # [20 40]

# 切片获取子数组
arr[0:3]                        # [10 20 30] (前三个元素,含头不含尾)

# 演示列表切片 vs 数组切片
l = [10, 20, 30, 40, 50]
l_slice = l[1:4]
l_slice[0] = 999
print(l_slice)                  # [999, 30, 40]
print(l)                        # [10, 20, 30, 40, 50] (修改切片不影响原列表)

arr_slice = arr[1:4]
arr_slice[0] = 999
print(arr_slice)                # [999 30 40]
print(arr)                      # [10 999 30 40 50] (修改切片影响原数组)

# 使用 copy() 创建独立副本
arr_copy = arr[0:3].copy()
arr_copy[1] = 888
print(arr)                      # [10 999 30 40 50] (原数组未变)
print(arr_copy)                 # [10 888 30]

# 创建一个二维数组
arr2d = np.array([[3, 6, 9], [12, 15, 18], [21, 24, 27]])
print(arr2d)                    # [[ 3  6  9]
                                #  [12 15 18]
                                #  [21 24 27]]

# 行状索引
arr2d[0]                        # [3 6 9] (第一行)

# 元素索引
arr2d[1][2]                     # 18 (第二行第三列)
arr2d[1, 2]                     # 18 (等价写法)

# 行状切片
arr2d[1:]                       # [[12 15 18]
                                #  [21 24 27]] (第二行到最后)

# 列状切片
arr2d[:, [1]]                   # [[ 6]
                                #  [15]
                                #  [24]] (第二列)

# 块状切片
arr2d[0:2, 1:]                  # [[ 6  9]
                                #  [15 18]] (前两行后两列)

# 维度保持对比
print(arr2d[2], arr2d[2].shape)         # [21 24 27] (3,) (降维)
print(arr2d[2, :], arr2d[2, :].shape)   # [21 24 27] (3,) (降维)
print(arr2d[2:3, :], arr2d[2:3, :].shape)  # [[21 24 27]] (1, 3) (保持维度)

# 创建一个三维数组
arr3d = np.arange(18).reshape((3, 3, 2))
print(arr3d)                    # [[[ 0  1]
                                #   [ 2  3]
                                #   [ 4  5]]
                                #  [[ 6  7]
                                #   [ 8  9]
                                #   [10 11]]
                                #  [[12 13]
                                #   [14 15]
                                #   [16 17]]]

# 元素获取
arr3d[0, 2, 1]                  # 5 (第一层第三行第二列)
arr3d[2, 1, 0]                  # 14 (第三层第二行第一列)

# 沿着轴 2 切片
arr3d[2, 0]                     # [12 13] (等价于 arr3d[2, 0, :])

# 沿着轴 1 切片
arr3d[1, :, 1]                  # [ 7  9 11] (第二层所有行第二列)

# 沿着轴 0 切片
arr3d[:, 2, 0]                  # [ 4 10 16] (所有层第三行第一列)

# 沿着轴 1 和 2 切片
arr3d[0]                        # [[0 1]
                                #  [2 3]
                                #  [4 5]] (等价于 arr3d[0, :, :])

# 沿着轴 0 和 2 切片
arr3d[:, 1]                     # [[ 2  3]
                                #  [ 8  9]
                                #  [14 15]] (等价于 arr3d[:, 1, :])

# 沿着轴 0 和 1 切片
arr3d[:, :, 0]                  # [[ 0  2  4]
                                #  [ 6  8 10]
                                #  [12 14 16]] (所有层所有行第一列)

2. 布尔获取(Boolean Indexing)

用布尔数组(True/False组成)作为索引,筛选符合条件的元素。适合数据过滤,如股票分析。布尔获取就是用一个由布尔类型值组成的数组来获取元素的方法。

import numpy as np

# 创建股票代码数组
codes = np.array(['AAPL', 'GOOG', 'AMZN', 'AAPL', 'AMZN', 'GOOG'])

# 创建股票价格数组(每行:开盘价, 最高价, 收盘价)
prices = np.array([[150, 152, 151], [2800, 2820, 2810], [3000, 3020, 3010],
                   [155, 157, 156], [3050, 3070, 3060], [2850, 2870, 2860]])

print(prices)
# [[ 150  152  151]
#  [2800 2820 2810]
#  [3000 3020 3010]
#  [ 155  157  156]
#  [3050 3070 3060]
#  [2850 2870 2860]]

# 生成布尔数组,筛选 'AAPL' 的行
code_aapl = codes == 'AAPL'
print(code_aapl)                    # [ True False False  True False False]

# 使用布尔索引获取 'AAPL' 的最高价和收盘价(第2列和第3列)
prices[code_aapl, 1:]               # [[152 151]
                                    #  [157 156]]

# 组合条件:筛选 'GOOG' 或 'AMZN' 的行
cond = (codes == 'GOOG') | (codes == 'AMZN')
print(cond)                         # [False  True  True False  True  True]

# 使用布尔索引获取符合条件的行
prices[cond]                        # [[2800 2820 2810]
                                    #  [3000 3020 3010]
                                    #  [3050 3070 3060]
                                    #  [2850 2870 2860]]

# 使用布尔索引进行赋值:将收盘价低于 2000 的值设为 0
prices[prices[:, 2] < 2000] = 0
print(prices)                       # [[   0    0    0]
                                    #  [2800 2820 2810]
                                    #  [3000 3020 3010]
                                    #  [   0    0    0]
                                    #  [3050 3070 3060]
                                    #  [2850 2870 2860]]

3. 花式获取(Fancy Indexing)

用整数列表/数组指定特定顺序的位置,灵活获取非连续元素。花式索引是用获取数组中特定元素的有效方法。

# 创建一个 6x5 的二维数组
arr = np.arange(30).reshape((6, 5))
print(arr)                      # [[ 0  1  2  3  4]
                                #  [ 5  6  7  8  9]
                                #  [10 11 12 13 14]
                                #  [15 16 17 18 19]
                                #  [20 21 22 23 24]
                                #  [25 26 27 28 29]]

# 花式索引指定行顺序
arr[[3, 1, 5, 0]]               # [[15 16 17 18 19]
                                #  [ 5  6  7  8  9]
                                #  [25 26 27 28 29]
                                #  [ 0  1  2  3  4]]

# 使用负索引从末尾选择行
arr[[-2, -4, -1]]               # [[20 21 22 23 24]
                                #  [10 11 12 13 14]
                                #  [25 26 27 28 29]]

# 多维花式索引,选择特定位置的元素
arr[[0, 2, 4, 5], [1, 3, 2, 4]] # [ 1 13 22 29]

# 结合切片和花式索引,重排列
arr[[0, 2, 4, 5]][:, [0, 4, 2, 1, 3]]  # [[ 0  4  2  1  3]
                                       #  [10 14 12 11 13]
                                       #  [20 24 22 21 23]
                                       #  [25 29 27 26 28]]

# 使用 take 函数沿轴 0 获取指定行
np.take(arr, [1, 3], axis=0)    # [[ 5  6  7  8  9]
                                #  [15 16 17 18 19]]

# 使用 take 函数沿轴 1 获取指定列
np.take(arr, [0, 2, 4], axis=1) # [[ 0  2  4]
                                #  [ 5  7  9]
                                #  [10 12 14]
                                #  [15 17 19]
                                #  [20 22 24]
                                #  [25 27 29]]

# 使用 put 函数替换指定索引的值
np.put(arr, [0, 10], -5)        # 原数组第一个元素和第十一个元素被替换为 -5
print(arr)                      # [[-5  1  2  3  4]
                                #  [ 5  6  7  8  9]
                                #  [-5 11 12 13 14]
                                #  [15 16 17 18 19]
                                #  [20 21 22 23 24]
                                #  [25 26 27 28 29]]

# 使用 put 函数并 clip 模式处理越界索引
np.put(arr, 35, -10, mode='clip')  # 索引 35 越界,clip 到最后一个元素
print(arr)                      # [[-5  1  2  3  4]
                                #  [ 5  6  7  8  9]
                                #  [-5 11 12 13 14]
                                #  [15 16 17 18 19]
                                #  [20 21 22 23 24]
                                #  [25 26 27 28 -10]]

(五)数组的变形

下面将介绍 NumPy 数组的五种核心变形操作:重塑与打平、合并与分裂、重复与拼接、排序与增减、视图与复制。内容从基础概念出发,逐步延伸至多维数组场景,重点解析各操作的内存机制、视图与复制的区别,并结合代码示例与解析说明其实际应用。分析聚焦于操作的底层机制、参数影响及典型场景(如数据重构与效率优化),同时提示潜在使用陷阱,突出实用价值。

1. 重塑和打平

这部分解释了维度变换操作,仅改变视图而不修改底层数据。重塑(reshape)从低维到高维,打平(flatten/ravel)从高维到低维。关键依赖内存布局(行主序/列主序)。

  • 重塑(reshape) - 从低维到高维
    • 使用np.reshape(shape)arr.reshape(shape),shape为元组。示例:一维数组arr = np.arange(1,13)重塑为(4,3)得到[[1 2 3] [4 5 6] [7 8 9] [10 11 12]](行主序填充)。
    • 支持1自动计算维度,如arr.reshape((2,-1))输出(2,6)数组。
    • 问题解答:默认行主序(order='C'),元素按行填充;若order='F'(列主序),则按列填充,得到[[1 5 9] [2 6 10] [3 7 11] [4 8 12]]
    • 分析:重塑是视图操作,不复制数据,便于多维数据重组。但需确保总元素数不变,否则报错。高维扩展:适用于N维数组,order参数控制填充方向。
  • 打平(ravel/flatten) - 从高维到低维
    • arr.ravel()arr.flatten(),默认行主序打平。示例:二维数组打平为[1 2 3 4 5 6 7 8 9 10 11 12]
    • order='F'打平为[1 4 7 10 2 5 8 11 3 6 9 12]
    • 区别:ravel()默认不复制(视图),仅order='F'时复制;flatten()总是复制。通过修改打平数组验证:ravel(C)修改原数组,ravel(F)/flatten不改。
    • 分析:视图机制提升效率,但风险是意外修改原数据。高维时,order影响填充顺序,适用于数据扁平化如机器学习输入。
# 创建一个一维数组
arr = np.arange(9)  # [0 1 2 3 4 5 6 7 8]

# 重塑为一维到二维 (3x3),默认行主序 (order='C')
arr.reshape((3, 3))  # [[0 1 2]
                     #  [3 4 5]
                     #  [6 7 8]]

# 重塑为二维,使用列主序 (order='F')
arr.reshape((3, 3), order='F')  # [[0 3 6]
                                #  [1 4 7]
                                #  [2 5 8]]

# 使用 -1 自动计算维度
arr.reshape((3, -1))  # [[0 1 2]
                      #  [3 4 5]
                      #  [6 7 8]]

# 创建一个二维数组
arr2d = np.arange(1, 10).reshape((3, 3))  # [[1 2 3]
                                           #  [4 5 6]
                                           #  [7 8 9]]

# 打平为二维到一维,默认行主序 (order='C')
arr2d.ravel()  # [1 2 3 4 5 6 7 8 9]

# 打平,使用列主序 (order='F')
arr2d.ravel(order='F')  # [1 4 7 2 5 8 3 6 9]

# 使用 flatten() 打平,默认行主序 (总是复制)
arr2d.flatten()  # [1 2 3 4 5 6 7 8 9]

# 演示 ravel() 和 flatten() 的视图 vs 复制区别
arr_ravel = arr2d.ravel()  # 视图
arr_ravel[0] = 100
print(arr2d)  # [[100   2   3]
              #  [  4   5   6]
              #  [  7   8   9]] (修改 ravel 影响原数组)

arr_flatten = arr2d.flatten()  # 复制
arr_flatten[1] = 200
print(arr2d)  # [[100   2   3]
              #  [  4   5   6]
              #  [  7   8   9]] (修改 flatten 不影响原数组)

# ravel() 使用 order='F' (复制)
arr_ravel_f = arr2d.ravel(order='F')
arr_ravel_f[2] = 300
print(arr2d)  # [[100   2   3]
              #  [  4   5   6]
              #  [  7   8   9]] (修改 ravel F 不影响原数组)

2. 合并和分裂

这部分聚焦数组的分合操作,改变维度或结构,但保持元素。

  • 合并(stack) - 多合一
    • 三种方式:通用np.concatenate(arrs, axis);专用vstack/hstack/dstack;极简r_/c_
    • 示例:两个(2,3)数组,按axis=0(vstack)合并为(4,3);axis=1(hstack)为(2,6);dstack为(2,3,2)(新增深度轴)。
    • 三种合并:vstack竖堆、hstack水平并、dstack深度叠。分析dstack结果(2,3,2)而非直观(2,2,3),因轴优先级(轴2新增)。
    • r_/c_:r_按行、c_按列;支持切片/列表合并,如np.r_[-2:2:1, [0]*3, 5, 6]。高级形式'a,b,c'控制轴/最小维/升维,如'0,2,0'升维后轴0合并。强调灵活性但语法复杂。
    • 分析:合并需形状兼容(除合并轴外)。适用于数据连接,如合并数据集。dstack增加维度,适合图像通道。
  • 分裂(split) - 一分多
    • 通用np.split(arr, indices_or_sections, axis);专用vsplit/hsplit
    • 示例:(5,5)数组,按[1,3]分裂为三部分(:1,1:3,3:)。默认axis=0(vsplit竖分);axis=1(hsplit水平分)。
    • 对比:vsplit分行、hsplit分列。indices为切片点,sections为等分数。
    • 分析:分裂返回列表视图,便于数据分区如训练/测试集。需确保可分,否则报错。专用函数简化语法。
# 创建两个 3x2 的数组
a = np.array([[1, 2], [3, 4], [5, 6]])
b = np.array([[7, 8], [9, 10], [11, 12]])

# 通用合并:沿轴 0 合并 (垂直堆叠)
np.concatenate([a, b], axis=0)  # [[ 1  2]
                                #  [ 3  4]
                                #  [ 5  6]
                                #  [ 7  8]
                                #  [ 9 10]
                                #  [11 12]]

# 专用垂直堆叠 vstack
np.vstack([a, b])               # [[ 1  2]
                                #  [ 3  4]
                                #  [ 5  6]
                                #  [ 7  8]
                                #  [ 9 10]
                                #  [11 12]]

# 通用合并:沿轴 1 合并 (水平并列)
np.concatenate([a, b], axis=1)  # [[ 1  2  7  8]
                                #  [ 3  4  9 10]
                                #  [ 5  6 11 12]]

# 专用水平并列 hstack
np.hstack([a, b])               # [[ 1  2  7  8]
                                #  [ 3  4  9 10]
                                #  [ 5  6 11 12]]

# 专用深度叠加 dstack (沿轴 2 合并)
np.dstack([a, b])               # [[[ 1  7]
                                #   [ 2  8]]
                                #  [[ 3  9]
                                #   [ 4 10]]
                                #  [[ 5 11]
                                #   [ 6 12]]]

# 极简 r_ 沿轴 0 合并
np.r_[a, b]                     # [[ 1  2]
                                #  [ 3  4]
                                #  [ 5  6]
                                #  [ 7  8]
                                #  [ 9 10]
                                #  [11 12]]

# 极简 c_ 沿轴 1 合并
np.c_[a, b]                     # [[ 1  2  7  8]
                                #  [ 3  4  9 10]
                                #  [ 5  6 11 12]]

# 创建一个 4x4 的数组用于分裂
arr = np.arange(16).reshape(4, 4)
print(arr)                      # [[ 0  1  2  3]
                                #  [ 4  5  6  7]
                                #  [ 8  9 10 11]
                                #  [12 13 14 15]]

# 通用分裂:沿轴 0 分成两部分
np.split(arr, [2], axis=0)      # [array([[0, 1, 2, 3],
                                #         [4, 5, 6, 7]]),
                                #  array([[ 8,  9, 10, 11],
                                #         [12, 13, 14, 15]])]

# 专用 vsplit 垂直分裂
np.vsplit(arr, [2])             # [array([[0, 1, 2, 3],
                                #         [4, 5, 6, 7]]),
                                #  array([[ 8,  9, 10, 11],
                                #         [12, 13, 14, 15]])]

# 通用分裂:沿轴 1 分成两部分
np.split(arr, [2], axis=1)      # [array([[ 0,  1],
                                #         [ 4,  5],
                                #         [ 8,  9],
                                #         [12, 13]]),
                                #  array([[ 2,  3],
                                #         [ 6,  7],
                                #         [10, 11],
                                #         [14, 15]])]

# 专用 hsplit 水平分裂
np.hsplit(arr, [2])             # [array([[ 0,  1],
                                #         [ 4,  5],
                                #         [ 8,  9],
                                #         [12, 13]]),
                                #  array([[ 2,  3],
                                #         [ 6,  7],
                                #         [10, 11],
                                #         [14, 15]])]

3. 重复和拼接

本质是复制操作,元素级(repeat) vs. 数组级(tile)。

  • 重复(repeat) - 元素层面复制
    • arr.repeat(repeats, axis)。一维:整数/列表控制重复次数,如[0 1 2].repeat(3)[0 0 0 1 1 1 2 2 2]
    • 多维:加axis,如repeat(2, axis=0)行重复。列表指定每个元素重复。
    • 分析:扩展维度,适用于数据增强。无axis时扁平化重复。
  • 拼接(tile) - 数组层面复制
    • np.tile(arr, reps)。整数:列重复,如tile(arr2d, 2)为水平复制。
    • 元组:形状复制,如(2,3)为2行3列 tiling,形成更大网格。
    • 分析:视arr为“瓷砖”,reps为铺设模式。高维如(2,3,4)创建3D网格。
# 创建一个 2x3 的数组
arr = np.array([[1, 2, 3], [4, 5, 6]])

# 使用 repeat 沿指定轴重复数组元素
np.repeat(arr, 2, axis=0)  # [[1 2 3]
                            #  [1 2 3]
                            #  [4 5 6]
                            #  [4 5 6]]

# 使用 repeat 沿轴 1 重复
np.repeat(arr, 3, axis=1)  # [[1 1 1 2 2 2 3 3 3]
                           #  [4 4 4 5 5 5 6 6 6]]

# 使用 tile 重复整个数组
np.tile(arr, (2, 1))       # [[1 2 3]
                            #  [4 5 6]
                            #  [1 2 3]
                            #  [4 5 6]]

# 使用 tile 重复并扩展列
np.tile(arr, (1, 2))       # [[1 2 3 1 2 3]
                            #  [4 5 6 4 5 6]]

# 创建两个数组用于拼接
a = np.array([10, 20, 30])
b = np.array([40, 50, 60])

# 使用 append 拼接一维数组
np.append(a, b)            # [10 20 30 40 50 60]

# 使用 column_stack 垂直拼接为一列
np.column_stack((a, b))    # [[10 40]
                           #  [20 50]
                           #  [30 60]]

# 使用 row_stack 水平拼接
np.row_stack((a, b))       # [[10 20 30]
                           #  [40 50 60]]

# 创建一个 2x3 数组用于重复操作
arr2 = np.array([[7, 8, 9], [10, 11, 12]])

# 使用 repeat 重复特定元素
np.repeat(arr2, [1, 2], axis=0)  # [[ 7  8  9]
                                 #  [10 11 12]
                                 #  [10 11 12]]

# 使用 tile 重复特定次数
np.tile(arr2, (2, 3))      # [[ 7  8  9  7  8  9  7  8  9]
                           #  [10 11 12 10 11 12 10 11 12]]

4. 排序和增减

排序分直接/间接,增减类似列表操作。

  • 排序
    • 直接排序(sort)arr.sort(axis)原地升序。np.sort(arr)复制版。axis=0跨行、1跨列。示例:随机数组排序前后比较。
    • 无降序参数,用[::-1]反转。分析:原地高效,但永久修改;复制版安全。
    • 间接排序(argsort/lexsort):argsort返回索引,如[100 60 99 80 91].argsort()为[1 3 4 2 0](位置排序)。
    • lexsort多序列:后序列优先,如last_name+first_name排序。示例分析平手时前序列决胜。
    • 分析:argsort用于排序键,lexsort如Excel多列排序。适用于数据排名/分组。
  • 增减
    • np.insert(arr, obj, values)前插;np.delete(arr, obj)删除。obj为索引/列表。
    • 分析:返回新数组,不原地。简单但多维需指定轴。
import numpy as np

# 创建一个一维数组
arr = np.array([45, 12, 78, 23, 91, 34])

# 沿轴 0 排序一维数组
np.sort(arr)                    # [12 23 34 45 78 91]

# 获取排序后的索引
np.argsort(arr)                 # [1 3 5 0 2 4]

# 创建一个二维数组
arr2d = np.array([[5, 9, 2], [8, 1, 6], [4, 3, 7]])

# 沿轴 0 排序 (每列排序)
np.sort(arr2d, axis=0)          # [[4 1 2]
                                #  [5 3 6]
                                #  [8 9 7]]

# 沿轴 1 排序 (每行排序)
np.sort(arr2d, axis=1)          # [[2 5 9]
                                #  [1 6 8]
                                #  [3 4 7]]

# 使用 sort 方法修改原数组
arr2d.sort(axis=1)
print(arr2d)                    # [[2 5 9]
                                #  [1 6 8]
                                #  [3 4 7]]

# 增加新元素到一维数组
np.append(arr, [15, 60])        # [45 12 78 23 91 34 15 60]

# 在指定位置插入元素
np.insert(arr, 2, [25, 30])     # [45 12 25 30 78 23 91 34]

# 删除指定位置的元素
np.delete(arr, [0, 4])          # [12 78 23 34]

# 创建一个 3x3 数组用于增减操作
arr3d = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]])

# 沿轴 0 插入新行
np.insert(arr3d, 1, [15, 25, 35], axis=0)  # [[10 20 30]
                                            #  [15 25 35]
                                            #  [40 50 60]
                                            #  [70 80 90]]

# 沿轴 1 插入新列
np.insert(arr3d, 1, [5, 15, 25], axis=1)   # [[10  5 20 30]
                                            #  [40 15 50 60]
                                            #  [70 25 80 90]]

# 删除沿轴 0 的指定行
np.delete(arr3d, 1, axis=0)     # [[10 20 30]
                                #  [70 80 90]]

5. 视图和复制

  • arr.view():共享内存,修改影响原数组。
  • arr.copy():深复制,独立。
  • 示例对比:view修改原,copy不改。
  • 分析:视图节省内存,但风险高。适用于临时变换。

整体分析:本节强调NumPy变形的高效(视图优先),但需注意order/axis影响结果。图表可视化内存/操作,便于理解。潜在问题:形状不兼容、意外修改。适用于数据预处理。

# 创建一个一维数组
arr = np.array([15, 25, 35, 45, 55])

# 获取视图 (view)
view = arr[1:4]
view[0] = 100
print(view)                     # [100  35  45]
print(arr)                      # [ 15 100  35  45  55] (修改视图影响原数组)

# 获取复制 (copy)
copy = arr[0:3].copy()
copy[1] = 200
print(copy)                     # [ 15 200  35]
print(arr)                      # [ 15 100  35  45  55] (修改复制不影响原数组)

# 创建一个二维数组
arr2d = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]])

# 获取二维视图 (沿轴 0 切片)
view2d = arr2d[0:2, :]
view2d[0, 0] = 300
print(view2d)                   # [[300  20  30]
                                #  [ 40  50  60]]
print(arr2d)                    # [[300  20  30]
                                #  [ 40  50  60]
                                #  [ 70  80  90]] (视图修改影响原数组)

# 获取二维复制
copy2d = arr2d[1:3, :].copy()
copy2d[0, 1] = 400
print(copy2d)                   # [[ 40 400  60]
                                #  [ 70  80  90]]
print(arr2d)                    # [[300  20  30]
                                #  [ 40  50  60]
                                #  [ 70  80  90]] (复制修改不影响原数组)

# 使用 view 方法获取视图
view_method = arr2d.view()
view_method[1, 2] = 500
print(view_method)              # [[300  20  30]
                                #  [ 40  50 500]
                                #  [ 70  80  90]]
print(arr2d)                    # [[300  20  30]
                                #  [ 40  50 500]
                                #  [ 70  80  90]] (view 方法也生成视图)

# 验证数据共享 (检查 base 属性)
print(view2d.base is arr2d)     # True (视图共享原数组内存)
print(copy2d.base is None)      # True (复制不共享内存)

(六)数组的计算

本节详细介绍了NumPy数组的四大类计算操作:元素层面计算(element-wise)、线性代数计算(linear algebra)、元素整合计算(aggregation)和广播机制计算(broadcasting)。

这些计算方式是强调NumPy计算的向量化(vectorization)优势,即无需循环即可并行处理元素,提高效率。

每个类别配有代码示例,突出数组 vs. 矩阵的区别、多维扩展以及广播规则的实用性。分析重点在于操作机制、参数影响(如axis、order)和潜在陷阱(如形状不兼容)。以下逐部分分析:

1. 元素层面计算(Element-Wise)

元素层面计算(Element-Wise)是 NumPy 的核心特性之一,通过向量化技术实现数组元素的并行操作,无需显式循环即可完成批量计算。

其操作类型主要包括 二元运算(如加减乘除)、函数运算(如倒数、平方、开方、三角函数、指数、对数等数学函数)和 比较运算(如大小比较及逻辑判断)。二元运算支持标量与数组的混合计算(如标量通过广播机制自动扩展为同形数组),返回新数组并适用于批量数据处理,但需注意形状不匹配导致的错误;函数运算依托 NumPy 提供的高效向量化数学函数(默认 float64 数据类型),计算效率显著高于 Python 循环,同时需关注整数除法等潜在精度问题;比较运算生成布尔数组,既可用于逻辑判断,也能作为掩码(masking)实现数据过滤,其中 np.allclose方法通过设定误差阈值解决了浮点数比较的精度难题。此类操作是数据清洗与转换的基础工具,其向量化特性大幅提升了计算速度(如显著快于传统 for 循环)。

# 创建一个一维数组
arr = np.array([2, 4, 6, 8, 10])

# 逐元素加法
arr + 5                        # [ 7  9 11 13 15]

# 逐元素乘法
arr * 2                        # [ 4  8 12 16 20]

# 逐元素比较
arr > 6                        # [False False False  True  True]

# 创建两个一维数组进行元素级运算
arr2 = np.array([1, 3, 5, 7, 9])

# 逐元素加法
arr + arr2                     # [ 3  7 11 15 19]

# 逐元素除法
arr / arr2                     # [2.  1.33333333 1.2 1.14285714 1.11111111]

# 创建一个二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])

# 逐元素平方
arr2d ** 2                     # [[ 1  4  9]
                               #  [16 25 36]]

# 逐元素逻辑与
(arr2d > 2) & (arr2d < 5)      # [[False False False]
                               #  [ True  False False]]

# 逐元素应用自定义函数
def double(x):
    return x * 2

np.vectorize(double)(arr2d)     # [[ 2  4  6]
                               #  [ 8 10 12]]

# 逐元素取模
arr2d % 2                      # [[1 0 1]
                              #  [0 1 0]]

2. 线性代数计算(Linear Algebra)

NumPy 的线性代数计算模块区分了 数组(多维通用)矩阵(二维专用) 的操作逻辑,尽管矩阵是数组的子类,但其在转置、求逆、相乘等线性代数场景中进行了针对性优化。

核心操作包括 创建(数组通过 np.array生成,矩阵通过 np.asmatrix或 np.mat创建)、转置(数组用 .T或 .transpose(),矩阵同理,高维数组需通过 axes参数控制轴顺序)、求逆(仅适用于方阵,通过 np.linalg.inv计算,非奇异矩阵会报错)、相乘(数组默认元素级乘法,矩阵乘法需通过 *运算符(矩阵对象)或 np.dot/@/matmul函数实现,高维场景下 np.dot支持通用点乘规则)。

矩阵与数组的操作差异体现了 NumPy 在通用性与专用性之间的平衡,此类功能是连接 NumPy 与 SciPy 线性代数模块的桥梁,广泛应用于机器学习中的矩阵分解等任务,但需注意形状匹配(如矩阵乘法的维度兼容性)及奇异矩阵导致的计算错误。

# 创建两个二维数组(矩阵)
A = np.array([[2, 3], [4, 5]])
B = np.array([[1, 2], [3, 4]])

# 矩阵乘法
np.dot(A, B)                    # [[11 16]
                                #  [19 28]]
# 矩阵乘法使用 @ 运算符
A @ B                           # [[11 16]
                                #  [19 28]]

# 求矩阵的行列式
np.linalg.det(A)                # -2.0

# 求矩阵的逆
np.linalg.inv(A)                # [[-2.5   1.5]
                                #  [ 2.   -1. ]]

# 求特征值和特征向量
eigenvalues, eigenvectors = np.linalg.eig(A)
print(eigenvalues)              # [-0.46410162  7.46410162]
print(eigenvectors)             # [[-0.57604932 -0.75863022]
                                #  [ 0.81741556 -0.65157366]]

# 求矩阵的转置
A.T                             # [[2 4]
                                #  [3 5]]

# 求迹(对角线元素之和)
np.trace(A)                     # 7

# 解线性方程组 Ax = b,b = [1, 2]
b = np.array([1, 2])
x = np.linalg.solve(A, b)
print(x)                        # [-1.  1.]

3. 元素整合计算(Aggregation)

元素整合计算(Aggregation)通过聚合函数(如求和、均值、标准差、极值等)沿指定轴(axis)或整体对数组元素进行统计分析,其中 轴(axis) 是理解高维数据聚合的关键概念(外层括号对应轴 0,逐内递增)。

操作涵盖基础统计函数(如 sum/mean/std/var/min/max)、位置函数(如 argmin/argmax定位极值索引)、累积函数(如 cumsum/cumprod实现累加/累乘),并支持 nan版本(如 nansum自动忽略缺失值)。高维数据聚合时,沿轴操作会降低维度(除非设置 keepdims=True保持结构),而整体聚合(如无轴指定)则返回单一统计值。此类操作是统计分析的基础工具,其核心在于通过轴参数控制聚合方向,错误使用可能导致维度混淆,而 keepdims则为链式操作提供了便利。

import numpy as np

# 创建一个一维数组
arr = np.array([3, 7, 1, 9, 4])

# 求数组的总和
np.sum(arr)                     # 24

# 求数组的最大值
np.max(arr)                     # 9

# 求数组的最小值
np.min(arr)                     # 1

# 求数组的平均值
np.mean(arr)                    # 4.8

# 求数组的方差
np.var(arr)                     # 8.16

# 创建一个二维数组
arr2d = np.array([[2, 5, 8], [1, 9, 3], [6, 4, 7]])

# 沿轴 0 求和 (列和)
np.sum(arr2d, axis=0)           # [ 9 18 18]

# 沿轴 1 求均值 (行均值)
np.mean(arr2d, axis=1)          # [5.  4.33333333 5.66666667]

# 沿轴 0 求最大值
np.max(arr2d, axis=0)           # [6 9 8]

# 累计和 (沿整个数组)
np.cumsum(arr2d)                # [ 2  7 15 16 25 28 34 38 45]

# 累计乘积 (沿整个数组)
np.cumprod(arr2d)               # [  2   10   80   80  720 2160 12960 51840 362880]

# 统计非零元素的个数
np.count_nonzero(arr2d)         # 9

# 统计沿轴 0 的非零元素
np.count_nonzero(arr2d, axis=0) # [3 3 3]

4. 广播机制计算(Broadcasting)

广播机制(Broadcasting)是 NumPy 处理形状不同数组间元素操作的核心技术,通过自动扩展数组维度(从后向前检查,补 1 后维度相等或某一维为 1 即视为兼容)实现高效计算,最终形状取各维度最大值。

规则应用包括 标量与数组操作(标量自动扩展)、行/列广播(如 (4,3) 数组与 (3,) 或 (4,1) 数组运算时分别沿行或列扩展)、高维广播(如 (1,2,3) 与 (2,1,3) 运算生成 (2,2,3) 结果)及 赋值广播(如通过 np.newaxis升维辅助扩展)。广播避免了显式循环,显著提升计算效率与内存利用率,但隐式扩展可能导致意外结果(如维度不匹配时报 ValueError),建议通过显式升维(如 np.newaxis)明确操作意图。该机制是 NumPy 批量操作的核心优势之一,适用于大规模数据的快速处理。

# 创建一个一维数组和一个标量
arr1 = np.array([1, 2, 3, 4])
scalar = 5

# 标量与一维数组的加法(广播)
arr1 + scalar                   # [6 7 8 9]

# 标量与一维数组的乘法(广播)
arr1 * scalar                   # [ 5 10 15 20]

# 创建一个二维数组和一维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
arr1d = np.array([10, 20, 30])

# 一维数组广播到二维(沿列方向)
arr2d + arr1d                   # [[11 22 33]
                                #  [14 25 36]]

# 创建一个二维数组和一个不符合直接广播的数组
arr2d_new = np.array([[1, 2], [3, 4], [5, 6]])
arr1d_new = np.array([7, 8])

# 调整形状以启用广播
arr1d_new = arr1d_new[:, np.newaxis]  # 转换为列向量 [[7], [8]]
arr2d_new + arr1d_new                 # [[ 8  9]
                                      #  [10 11]
                                      #  [12 13]]

# 验证广播维度兼容性
try:
    arr2d + arr1d_new  # 维度不匹配,抛出 ValueError
except ValueError as e:
    print("Error:", e)  # Error: operands could not be broadcast together with shapes (2,3) (2,1)

# 使用广播进行条件操作
mask = arr2d > 2
arr2d[mask] = 100
print(arr2d)                    # [[  1   2 100]
                                #  [100 100 100]]

本次系统介绍了 NumPy 工具库的核心功能,涵盖数组基础操作与高级应用:首先讲解数值型与结构数组的创建方法,以及 npy、txt 和 csv 三种格式的数组存载技术;接着深入探讨数组元素的多种索引与切片方式(包括普通、布尔和花式索引),以及数组形态变换(低维到高维的重塑与高维到低维的打平);随后阐述数组的组合与分割操作(多合一的合并与一分多的分裂)、数组复制与拼接技术(元素层面的复制与数组层面的拼接),以及数组元素的排序与增减处理;进一步介绍数组的视图与复制操作、元素层面的二元计算与函数操作(含比较计算)、线性代数相关操作(矩阵创建、转置、求逆与相乘)、基于不同轴或整体的元素整合方法,以及形状不同数组间的广播机制应用,全面覆盖从数组构建到复杂计算的全流程操作。

三、Pandas

下面我们开始讲Pandas, 也是数据分析时常用的Python库。Pandas 是 Panel data analysis 的缩写,从其全称可看出它是 Python 里处理数据分析 (data analysis) 的工具包,其官网链接是 https://pandas.pydata.org/。

import pandas 
pandas.__version__ 

# ,通常给 pandas起个别名 pd
import pandas as pd 
pd.Series()  
pd.DataFrame()  

下面我们将使用Pandas从创建表格开始,介绍数据的保存与加载、数据获取、数据合并、数据重塑、数据分析、可视化以及数据预处理等相关操作。

(一)介绍Pandas

Pandas 里的数据结构(比如表格)本质上是 “带标签的数组”,可以理解为 NumPy 数组的升级版。

::: hljs-center
image.png
:::

  • NumPy 数组:只存纯数字,没有“这一列是啥意思”“这一行是谁”的说明(比如一个二维数组,你只知道是一堆数,但不知道哪列是股价、哪行是日期)。
  • Pandas 数据结构:在 NumPy 数组的基础上,加了“描述信息”(比如行名、列名),让数据更易懂(比如表格里直接标着“平安银行”“茅台”,列名写着“2024-1-3 到 2025-1-3 的股价”)。

具体对应关系:

  • 一维数据(序列/Series) = 一维数组(比如一列数) + 行索引(比如这列数是哪个股票的价格)
  • 二维数据(数据帧/DataFrame) = 二维数组(比如多行多列的表格) + 行索引(比如每行是哪一天) + 列索引(比如每列是哪个股票)
  • 三维数据(面板/Panel) = 三维数组(比如多个二维表格叠一起) + 行/列/项索引(但 Pandas 1.0 后不用了,用多层索引的 DataFrame 代替)

重点区别:Pandas 是为**表格类杂数据(比如股价、姓名、日期混在一起)设计的,NumPy 是为纯数字的同类型数据(比如全是整数或小数)**设计的(除非用 NumPy 的特殊结构数组)。

::: hljs-center
image.png
:::

(二)创建Series & DataFrame

Pandas中有两大核心数据结构——Series(序列,一维)和DataFrame(数据帧,二维)的创建方法。内容从基础语法入手,逐步扩展到参数设置、属性访问和简单操作,强调Pandas数据结构的灵活性和信息完整性。每个创建方式配有代码示例、输出结果和解释,突出与NumPy数组的相似性(如values返回数组),但Pandas更注重标签(index/columns)和名称(name)的语义化。分析重点在于输入类型(列表/数组/字典)的处理、缺失值(NaN)的引入、维度扩展(MultiIndex),以及查看函数(head/tail/describe/info)的实用性。以下逐部分分析:

  • 数据结构分类:根据维度划分,Series为一维(类似Python列表或NumPy一维数组),DataFrame为二维(类似Excel sheet或R data.frame)。DataFrame被描述为Series的容器(container),即每一列是一个Series。
    • 分析:这一介绍桥接了3.1节的“数据表=数组+描述”公式,强调Pandas的异质性(可混类型数据)。未提及三维Panel(已废弃),焦点在实用二维数据上。教学点:初学者易将Series视为增强列表,DataFrame视为表格,代码简洁,便于上手。

1. Series创建(一维序列)

  • 基本语法pd.Series(x, index=idx),x为数据值(位置参数),index为行标签(默认range(0, len(x))),可为整数/字符串/时间对象。

    • 分析:语法简明,index的默认值确保兼容NumPy,但显式设置提升可读性(如日期标签)。添加name参数(如's2.name = '海底捞股价'')使Series更完整,类似于数据列的标题。
  • 用列表创建

    • 示例:s = pd.Series([27.2, 27.65, 27.70, 28]),输出带默认index(0-3)的float64 Series。访问s.values返回数组,s.index返回RangeIndex。
    • 改进:用pd.date_range('20190401', periods=4)生成日期index,创建s2,频率Freq=D(每日)。
    • 操作示例:
      • 索引:s2[0]s2['2019-04-01'](位置/标签)。
      • 切片:s2[1:3](前闭后开)或s2['2019-04-01':'2019-04-02'](前闭后闭),返回子Series(含标签)。
      • 布尔筛选:s2[s2 > 27.5]返回符合条件的子Series。
      • 广播/元素运算:s2 * 2 + 3np.log(s2),保留index。
    • 分析:示例数据为“海底捞股价”(2019-04-01到04-04),真实场景化,便于理解时间序列。操作演示了Pandas的向量化(vectorized),效率高于循环。潜在陷阱:切片返回子Series而非列表,修改需小心视图机制(view)。
  • 用数组创建

    • 示例:s = pd.Series(np.array([27.2, 27.65, 27.70, 28, 28, np.nan]))
    • 属性分析:
      • len(s):6(总元素,包括NaN)。
      • s.shape:(6,)(形状元组)。
      • s.count():5(非NaN元素)。
      • s.unique():返回唯一值数组(含NaN)。
      • s.value_counts():频次统计(忽略NaN)。
    • 分析:引入NaN演示缺失值处理,强调Pandas的鲁棒性。unique/value_counts实用(如频次分析),但NaN不计入count/value_counts,需注意数据清洗场景。
  • 用字典创建

    • 示例:data = {'BABA': 187.07, 'PDD': 21.83, 'JD': 30.79, 'BIDU': 184.77}s3 = pd.Series(data, name='中概股'),键→index,值→data。添加s3.index.name = '股票代号'
    • 自定义index:s4 = pd.Series(data, index=['FB', 'BABA', 'PDD', 'JD']),无键处为NaN。
    • 加法:s3 + s4,自动对齐index,缺失→NaN。
    • 分析:字典创建自然映射键值对,适合标签语义强的场景(如股票代码)。对齐机制(alignment)是Pandas亮点,避免手动匹配。示例为“中概股”(BABA等),贴合金融读者。陷阱:自定义index引入NaN,需后续处理(如fillna)。
    import pandas as pd
    
    # 创建一个 Series,包含股票代码和价格数据,指定索引
    data = [150.5, 2800.0, 3050.0]
    stocks = ['TSLA', 'GOOGL', 'AMZN']
    series = pd.Series(data, index=stocks)
    
    # 显示 Series
    print(series)                   # TSLA     150.5
                                    # GOOGL   2800.0
                                    # AMZN    3050.0
                                    # dtype: float64
    
    # 通过索引访问单个值
    series['GOOGL']                 # 2800.0
    
    # 通过索引访问多个值
    series[['TSLA', 'AMZN']]        # TSLA     150.5
                                    # AMZN    3050.0
                                    # dtype: float64
    
    # 使用布尔索引筛选
    series[series > 1000]           # GOOGL   2800.0
                                    # AMZN    3050.0
                                    # dtype: float64
    
    # 创建 Series 并设置缺失值
    data_with_na = [120.0, None, 1800.0]
    stocks_with_na = ['MSFT', 'FB', 'NVDA']
    series_with_na = pd.Series(data_with_na, index=stocks_with_na)
    
    # 检查缺失值
    print(series_with_na.isna())    # MSFT    False
                                    # FB       True
                                    # NVDA    False
                                    # dtype: bool
    
    # 填充缺失值
    series_with_na.fillna(0)        # MSFT     120.0
                                    # FB         0.0
                                    # NVDA    1800.0
                                    # dtype: float64
    

2. DataFrame创建(二维数据帧)

  • 基本语法pd.DataFrame(x, index=idx, columns=col),x为二维列表/数组/字典(值=1D列表/数组/Series)或其他DataFrame。index/columns默认range。
  • 用列表/数组创建
    • 示例:lst = [[1, 2, 3], [4, 5, 6]]df1 = pd.DataFrame(lst),默认index/columns=0-1/0-2。
    • 分析:简单但标签无语义,适合快速原型。强调形状兼容(x.shape[0]=行,[1]=列)。
  • 用字典创建
    • 示例:data = {'行业': ['电商',...], '价格': [176.92,...], ...}df2 = pd.DataFrame(data, index=['BABA', 'JD',...]),添加name='美股',index.name='代号',columns.name='特征'。
    • 属性:df2.values返回object数组(混类型),df2.columns/df2.index返回带name的Index。
    • 分析:字典键→columns,值→列数据,行标签需额外指定。示例为“美股”数据(BABA等,含行业/价格/交易量/雇员),异质(object/float64/int64)。values为object dtype,反映Pandas的灵活性。
  • 查看函数
    • df2.head()/tail(3):首/尾行(默认5)。
    • df2.describe():数值列统计(count/mean/std/min/25%/50%/75%/max),检查缺失/异常。
    • df2.info():详尽信息(类/行/列/非空/dtype/内存)。
    • 分析:这些函数是EDA起点,describe忽略object列,info显示内存使用(240.0+ bytes)。教学点:先用info检查数据完整性。
  • 升维(MultiIndex)
    • 示例:df2.index = pd.MultiIndex.from_tuples([('中国公司','BABA'), ...]),添加多层行标签(中国/美国公司)。
    • 分析:MultiIndex.from_tuples用元组列表创建,扩展维度(模拟Panel)。示例分组公司国籍,便于分层分析。陷阱:多层标签复杂化索引,需后续学习loc/iloc。
import pandas as pd

# 创建一个 DataFrame,包含股票数据
data = {
    'Symbol': ['TSLA', 'GOOGL', 'MSFT', 'AMZN'],
    'Price': [250.75, 2750.00, 300.50, 3300.25],
    'Volume': [1500000, 800000, 1200000, 900000]
}
df = pd.DataFrame(data)

# 显示 DataFrame
print(df)                       #    Symbol   Price  Volume
                                # 0    TSLA  250.75  1500000
                                # 1  GOOGL  2750.00   800000
                                # 2   MSFT  300.50  1200000
                                # 3   AMZN  3300.25   900000

# 访问特定列
df['Price']                     # 0     250.75
                                # 1    2750.00
                                # 2     300.50
                                # 3    3300.25
                                # Name: Price, dtype: float64

# 访问多列
df[['Symbol', 'Volume']]        #    Symbol  Volume
                                # 0    TSLA  1500000
                                # 1  GOOGL   800000
                                # 2   MSFT  1200000
                                # 3   AMZN   900000

# 使用 loc 按标签访问行
df.loc[1:2, ['Symbol', 'Price']]  #    Symbol   Price
                                  # 1  GOOGL  2750.00
                                  # 2   MSFT   300.50

# 使用 iloc 按位置访问行
df.iloc[0:2, 0:2]               #    Symbol   Price
                                # 0    TSLA  250.75
                                # 1  GOOGL  2750.00

# 添加新列 (价格变化百分比)
df['Price_Change'] = df['Price'] * 0.05
print(df)                       #    Symbol   Price  Volume  Price_Change
                                # 0    TSLA  250.75  1500000       12.5375
                                # 1  GOOGL  2750.00   800000      137.5000
                                # 2   MSFT  300.50  1200000       15.0250
                                # 3   AMZN  3300.25   900000      165.0125

(三)数据保存&加载

在数据分析流程中,为避免重复生成序列或数据帧(如DataFrame),通常会将生成的中间结果保存,后续直接加载复用。这一过程包含两个核心操作:保存(save)(为下次加载而存储数据)和加载(load)(从之前存储的位置读取数据)。

以常用的数据帧(DataFrame,如变量df)为例,支持多种通用存储格式(如Excel、CSV、SQL、HDF5),其保存操作通过通用语法 df.to_format() 实现,具体包括 df.to_excel()df.to_csv()df.to_sql()df.to_hdf()

对应的加载操作则通过 pd.read_format() 实现(如 pd.read_excel()pd.read_csv() 等),核心目的是通过存载机制减少重复计算,提升分析效率。

存载操作表

操作类型 核心功能 常用方法/语法(DataFrame为例) 典型格式
保存(Save) 将数据帧存储为文件,供后续复用 df.to_format()(如 to_excel()to_csv()to_sql()to_hdf() Excel / CSV / SQL / HDF5
加载(Load) 从存储文件中读取数据,恢复为数据帧 pd.read_format()(如 read_excel()read_csv()read_sql()read_hdf() Excel / CSV / SQL / HDF5
import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    'Code': ['600519.SH', '000001.SZ', '601318.SH', '600036.SH'],
    'Open_Price': [15.80, 12.45, 25.30, 30.10],
    'Volume': [12000000, 8500000, 9500000, 7000000]
}
df = pd.DataFrame(data)

# 保存 DataFrame 到 CSV 文件
df.to_csv('stock_data.csv', index=False)
# 文件 'stock_data.csv' 已生成,内容类似:
# Code,Open_Price,Volume
# 600519.SH,15.8,12000000
# 000001.SZ,12.45,8500000
# 601318.SH,25.3,9500000
# 600036.SH,30.1,7000000

# 从 CSV 文件加载数据
loaded_df = pd.read_csv('stock_data.csv')
print(loaded_df)                #       Code  Open_Price  Volume
                                # 0  600519.SH        15.8  12000000
                                # 1  000001.SZ        12.45   8500000
                                # 2  601318.SH        25.3   9500000
                                # 3  600036.SH        30.1   7000000

# 保存 DataFrame 到 Excel 文件
df.to_excel('stock_data.xlsx', index=False)
# 文件 'china_stock_data.xlsx' 已生成,内容与 CSV 类似

# 从 Excel 文件加载数据
loaded_excel = pd.read_excel('stock_data.xlsx')
print(loaded_excel)             #       Code  Open_Price  Volume
                                # 0  600519.SH        15.8  12000000
                                # 1  000001.SZ        12.45   8500000
                                # 2  601318.SH        25.3   9500000
                                # 3  600036.SH        30.1   7000000

# 保存到 JSON 格式
df.to_json('stock_data.json', orient='records')
# 文件 'china_stock_data.json' 已生成,内容类似:
# [{"Code":"600519.SH","Open_Price":15.8,"Volume":12000000}, ...]

# 从 JSON 文件加载数据
loaded_json = pd.read_json('stock_data.json')
print(loaded_json)              #       Code  Open_Price  Volume
                                # 0  600519.SH        15.8  12000000
                                # 1  000001.SZ        12.45   8500000
                                # 2  601318.SH        25.3   9500000
                                # 3  600036.SH        30.1   7000000

(四)数据获取

Pandas 中的 DataFrame(数据帧)的获取操作,涵盖元素、列、行、块及高级获取方式。本次内容从基础语法入手,强调其与 NumPy 数组的区别(Pandas 支持标签索引(label-based)和位置索引(position-based)),通过代码示例展开,每种获取方式均配有“场景”分类、代码输出及操作建议,突出 loc/iloc的清晰性与一致性。分析重点包括语法对比、潜在歧义(如 []的多义性)、视图机制(获取操作返回子视图)及实际应用(如股票数据的筛选过滤)。

1. 元素获取(单元素)

单元素获取聚焦于精确提取 DataFrame 中的某个标量值,核心通过 标签索引(at)位置索引(iat) 实现。

两种方式均直接返回标量结果,避免了通用索引(如 [])优先按列检索的歧义问题。其中,at基于行/列标签定位(如 df.at['index_i', 'attribute_j']),iat基于行/列数字位置定位(如 df.iat[i, j])。此方法的优势在于清晰性与一致性:无多义性风险,且执行效率高,特别适用于需要直接更新单个值(如修改特定股票价格)的场景。

潜在陷阱需注意:若标签不存在会触发 KeyError,位置索引越界则报 IndexError,需提前校验索引有效性。

import pandas as pd

# 创建一个简单的 DataFrame
data = {
    '行业': ['科技', '金融', '零售', '制造'],
    '价格': [150.5, 2800.0, 300.5, 1800.0],
    '交易量': [1200000, 800000, 1000000, 900000]
}
df = pd.DataFrame(data, index=['公司A', '公司B', '公司C', '公司D'])

# 使用 at 基于标签获取元素
df.at['公司B', '价格']           # 2800.0

# 使用 iat 基于位置获取元素
df.iat[2, 1]                    # 300.5

# 尝试使用 [] 获取元素 (可能歧义)
df['价格'][1]                   # 2800.0 (但 [] 优先列访问,需注意)

# 修改元素值使用 at
df.at['公司C', '交易量'] = 950000
print(df.loc['公司C', '交易量']) # 950000

2. 列状获取

列状获取针对 DataFrame 的垂直方向数据(列),分为单列与多列两类操作。

单列提取支持四种方式(如属性式 df.列名、标签式 df['列名']、全行标签定位 df.loc[:, '列名']、全行位置定位 df.iloc[:, i]),核心差异在于标签是否允许空格(如 df.列名要求列名无空格)、语法灵活性(如 df['列名']更通用但易与列操作混淆)及一致性(loc/iloc定位明确且跨维度统一)。

多列提取则通过列表传参(如 df[['列1', '列2']])、标签切片(如 df.loc[:, '列1':'列2'],闭区间含端点)或位置切片(如 df.iloc[:, i:j],前闭后开)实现,返回子 DataFrame 视图。此类操作的关键在于视图机制——修改提取的列会影响原 DataFrame,且需注意标签切片的闭区间特性与位置切片的前闭后开差异。推荐使用 loc/iloc避免 []的多义性,尤其适用于特征选择(如机器学习变量提取)。

import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    '代码': ['600519', '000001', '601318', '300059'],
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '类型': ['白酒', '银行', '保险', '科技']
}
df = pd.DataFrame(data, index=['贵州茅台', '平安银行', '中国平安', '东方财富'])

# 属性方式访问单列 (标签无空格)
df.价格                          # 贵州茅台    15.80
                                # 平安银行    12.45
                                # 中国平安    25.30
                                # 东方财富    18.75
                                # Name: 价格, dtype: float64

# 中括号方式访问单列
df['交易量']                     # 贵州茅台    12000000
                                # 平安银行     8500000
                                # 中国平安     9500000
                                # 东方财富    11000000
                                # Name: 交易量, dtype: int64

# loc 方式访问单列
df.loc[:, '类型']               # 贵州茅台     白酒
                                # 平安银行     银行
                                # 中国平安     保险
                                # 东方财富     科技
                                # Name: 类型, dtype: object

# iloc 方式访问单列
df.iloc[:, 2]                   # 贵州茅台    12000000
                                # 平安银行     8500000
                                # 中国平安     9500000
                                # 东方财富    11000000
                                # Name: 交易量, dtype: int64

# 列表方式访问多列
df[['代码', '价格']]             #          代码    价格
                                # 贵州茅台  600519  15.80
                                # 平安银行  000001  12.45
                                # 中国平安  601318  25.30
                                # 东方财富  300059  18.75

# loc 切片访问多列
df.loc[:, '价格':'类型']         #          价格      交易量  类型
                                # 贵州茅台  15.80  12000000  白酒
                                # 平安银行  12.45   8500000  银行
                                # 中国平安  25.30   9500000  保险
                                # 东方财富  18.75  11000000  科技

# iloc 切片访问多列
df.iloc[:, 1:3]                 #          价格      交易量
                                # 贵州茅台  15.80  12000000
                                # 平安银行  12.45   8500000
                                # 中国平安  25.30   9500000
                                # 东方财富  18.75  11000000

3. 行状获取

行状获取聚焦于 DataFrame 的水平方向数据(行),分为单行与多行两类操作。

单行提取可通过标签定位(如 df.loc['行标签', :]返回 Series 降维)、位置定位(如 df.iloc[i, :]返回 Series)、标签切片(如 df['行标签':'行标签']返回单行 DataFrame 保持维度)或位置切片(如 df[i:i+1]返回单行 DataFrame 保持维度)实现,其中 :表示选取所有列。

多行提取则通过标签区间(如 df.loc['起始行':'结束行', :])、位置区间(如 df.iloc[i:j, :])、标签切片(如 df['起始行':'结束行'])或位置切片(如 df[i:j])完成,同样返回子 DataFrame 视图。行状操作的核心差异在于:loc/iloc返回 Series(单行降维)或子 DataFrame(多行),而 []切片可能因优先级问题误判为列操作(如 df['行标签']优先查列)。

此外,标签切片为闭区间(含两端)、位置切片为前闭后开(不含右端点),且需注意 i:i+1的维度保持技巧。推荐优先使用 loc/iloc以保证语法一致性与操作清晰性,尤其适用于时间序列等连续行数据的提取。

import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    '代码': ['600519', '000001', '601318', '300059'],
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '类型': ['白酒', '银行', '保险', '科技']
}
df = pd.DataFrame(data, index=['贵州茅台', '平安银行', '中国平安', '东方财富'])

# 使用 loc 访问单行 (基于标签,返回 Series)
df.loc['平安银行', :]            # 代码      000001
                                # 价格        12.45
                                # 交易量     8500000
                                # 类型         银行
                                # Name: 平安银行, dtype: object

# 使用 iloc 访问单行 (基于位置,返回 Series)
df.iloc[2, :]                   # 代码      601318
                                # 价格        25.3
                                # 交易量     9500000
                                # 类型         保险
                                # Name: 中国平安, dtype: object

# 使用 [] 访问单行 (标签切片,返回 DataFrame)
df['中国平安':'中国平安']         #          代码   价格     交易量  类型
                                # 中国平安  601318  25.3 9500000  保险

# 使用 [] 访问单行 (位置切片,返回 DataFrame)
df[1:2]                         #          代码   价格    交易量  类型
                                # 平安银行  000001 12.45 8500000  银行

# 使用 loc 访问多行 (基于标签,返回 DataFrame)
df.loc['平安银行':'东方财富', :]   #          代码    价格      交易量  类型
                                # 平安银行  000001  12.45   8500000  银行
                                # 中国平安  601318  25.30   9500000  保险
                                # 东方财富  300059  18.75  11000000  科技

# 使用 iloc 访问多行 (基于位置,返回 DataFrame)
df.iloc[0:3, :]                 #          代码    价格      交易量  类型
                                # 贵州茅台  600519  15.80  12000000  白酒
                                # 平安银行  000001  12.45   8500000  银行
                                # 中国平安  601318  25.30   9500000  保险

# 使用 [] 访问多行 (标签切片,返回 DataFrame)
df['贵州茅台':'中国平安']         #          代码    价格     交易量  类型
                                # 贵州茅台  600519  15.80 12000000  白酒
                                # 平安银行  000001  12.45  8500000  银行
                                # 中国平安  601318  25.30  9500000  保险

# 使用 [] 访问多行 (位置切片,返回 DataFrame)
df[1:4]                         #          代码    价格      交易量  类型
                                # 平安银行  000001  12.45   8500000  银行
                                # 中国平安  601318  25.30   9500000  保险
                                # 东方财富  300059  18.75  11000000  科技

4. 块状获取(返回子DataFrame)

块状获取针对 DataFrame 的二维连续区域(行与列的组合),仅支持通过标签切片(如 df.loc['起始行':'结束行', '起始列':'结束列'],标签闭区间)或位置切片(如 df.iloc[i:j, k:l],位置前闭后开)两种方式,返回子 DataFrame 视图。

此类操作的本质是行列切片的组合应用,无需依赖通用索引(如 []),从而规避了多义性风险。其核心特点是直接提取二维连续块(如交叉特征矩阵),适用于需要同时分析多行多列数据的场景(如多变量关联分析)。

需强调标签切片的闭区间特性(含两端)与位置切片的前闭后开差异(不含右端点),并指出块状获取是唯一无需 []即可实现二维连续提取的方式,进一步强化了 loc/iloc在高维数据操作中的通用性与可靠性。

import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    '代码': ['600519', '000001', '601318', '300059'],
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '类型': ['白酒', '银行', '保险', '科技']
}
df = pd.DataFrame(data, index=['贵州茅台', '平安银行', '中国平安', '东方财富'])

# 使用 loc 获取块状数据 (基于标签)
df.loc['平安银行':'中国平安', '价格':'交易量']  #          价格      交易量  类型
                                              # 平安银行  12.45   8500000  银行
                                              # 中国平安  25.30   9500000  保险

# 使用 iloc 获取块状数据 (基于位置)
df.iloc[1:3, 1:3]                  #          价格      交易量
                                  # 平安银行  12.45   8500000
                                  # 中国平安  25.30   9500000

# 使用 loc 获取特定行和列 (混合标签)
df.loc[['贵州茅台', '东方财富'], ['代码', '类型']]  #          代码  类型
                                                  # 贵州茅台  600519  白酒
                                                  # 东方财富  300059  科技

# 使用 iloc 获取特定行和列 (混合位置)
df.iloc[[0, 3], [0, 3]]           #          代码  类型
                                  # 贵州茅台  600519  白酒
                                  # 东方财富  300059  科技

# 使用 loc 结合布尔条件获取块状数据
df.loc[df['价格'] > 15, '代码':'交易量']  #          代码    价格      交易量  类型
                                        # 贵州茅台  600519  15.80  12000000  白酒
                                        # 中国平安  601318  25.30   9500000  保险
                                        # 东方财富  300059  18.75  11000000  科技

# 使用 iloc 结合切片获取块状数据
df.iloc[0:2, 0:2]                  #          代码    价格
                                  # 贵州茅台  600519  15.80
                                  # 平安银行  000001  12.45

5. 高级获取

高级获取通过布尔索引(如 df[条件表达式])或可调用对象(如 df.loc[:, lambda df: 列筛选函数])实现动态数据筛选,通常与 loc/iloc/[]结合使用。

布尔索引常见于行级筛选(如 df.loc[df['价格'] > 100, :]提取高价股),通过生成布尔 Series 过滤满足条件的行;可调用对象则更灵活(如 df.loc[:, lambda df: df.columns.str.contains('价')]匹配列名含“价”的列),适用于列级条件筛选(但较少见,因列名通常固定且数量有限)。

此类操作的核心价值在于动态生成目标索引,返回视图并支持链式操作(如连续筛选与计算)。潜在陷阱包括布尔列筛选的罕见性(因列名固定时直接指定更高效)、以及条件表达式的逻辑复杂性(需确保条件生成正确)。
该操作适用于需按条件动态提取数据的场景,例如异常值检测、特征工程预处理等。

import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    '代码': ['600519', '000001', '601318', '300059'],
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '类型': ['白酒', '银行', '保险', '科技']
}
df = pd.DataFrame(data, index=['贵州茅台', '平安银行', '中国平安', '东方财富'])

# 使用布尔索引筛选价格大于 20 的行 (常见用法)
df[df['价格'] > 20]              #          代码    价格     交易量  类型
                                # 中国平安  601318  25.30 9500000  保险

# 使用布尔索引筛选交易量小于 10000000 的行 (常见用法)
df[df['交易量'] < 10000000]      #          代码    价格    交易量  类型
                                # 平安银行  000001  12.45 8500000  银行
                                # 中国平安  601318  25.30 9500000  保险

# 使用 callable (lambda) 筛选列名包含 '价' 的列 (罕见用法)
df.loc[:, lambda df: df.columns.str.contains('价')]  #          价格
                                                     # 贵州茅台  15.80
                                                     # 平安银行  12.45
                                                     # 中国平安  25.30
                                                     # 东方财富  18.75

# 使用 callable 筛选行标签包含 '平' 的行 (罕见用法)
df.loc[lambda df: df.index.str.contains('平'), :]  #          代码    价格    交易量  类型
                                                   # 平安银行  000001  12.45 8500000  银行
                                                   # 中国平安  601318  25.30 9500000  保险

# 组合布尔索引与 callable
df[df['交易量'] > 9000000].loc[:, lambda df: ['代码', '类型']]  #          代码  类型
                                                               # 贵州茅台  600519  白酒
                                                               # 中国平安  601318  保险
                                                               # 东方财富  300059  科技

6. 高级获取

层级获取主要针对含多层索引的序列与数据帧,核心是基于多层索引的层级关系提取数据,具体内容可分为“层级获取序列”和“层级获取数据帧”两部分:

(1)层级获取序列

对于含多层行索引的序列(如“日期+股票代码”双层索引),其索引以“元组”形式存储(每个元组包含各层级标签),获取逻辑围绕“按层级标签筛选”设计。基础操作包括:通过 []或 loc指定第一层索引标签(如日期)获取该层级下的全部数据;通过 loc[层级0标签, 层级1标签]精准定位两层标签交叉的数据。

跨层级筛选则通过固定某一层标签(如第二层“股票代码”)并遍历其他层(如所有日期),例如 loc[:, 'GS']可获取所有日期中“GS”对应的序列数据。

此类操作的核心是依托元组结构的层级标签,通过单层或多层组合筛选实现数据定位。

(2)层级获取数据帧

对于行索引和列索引均为多层的数据帧(如行索引“地区+代号”、列索引“数据类型+细分特征”),获取逻辑需同时考虑行层级与列层级的组合关系。

行层级获取分为单层(如通过 loc[第一层标签, :]获取某一地区所有列数据)和多层组合(如 loc[(第一层标签, 第二层标签), :]定位特定地区与代号交叉的行数据);列层级获取同理,支持单层(如 loc[:, 第一层列标签]获取某一数据类型所有行数据)和多层组合(如 loc[:, (第一层列标签, 第二层列标签)]定位细分特征交叉的列数据)。

更复杂的行列组合筛选(如 loc[(行层级组合), (列层级组合)])可精准定位单个或多个数据点。此外,索引辅助操作(如 swaplevel交换层级顺序、set_index/reset_index调整索引结构、sort_index按层级排序)进一步增强了数据获取的灵活性与效率,适配不同分析场景的需求。

import pandas as pd

# 创建一个多级索引 DataFrame,使用中国股市数据
index = pd.MultiIndex.from_tuples([('上海', '600519'), 
('深圳', '000001'), ('上海', '601318'), ('深圳', '300059')], names=['地区', '代码'])
data = {
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '类型': ['白酒', '银行', '保险', '科技']
}
df = pd.DataFrame(data, index=index)

# 显示 DataFrame
print(df)                       #            价格      交易量  类型
                                # 地区 代码                         
                                # 上海 600519  15.80  12000000  白酒
                                # 深圳 000001  12.45   8500000  银行
                                # 上海 601318  25.30   9500000  保险
                                # 深圳 300059  18.75  11000000  科技

# 层级获取:使用元组访问特定行
df.loc[('上海', '600519')]      # 价格       15.8
                                # 交易量  12000000
                                # 类型         白酒
                                # Name: (上海, 600519), dtype: object

# 层级获取:访问特定地区的子 DataFrame
df.loc['深圳']                   #          价格     交易量  类型
                                # 代码                        
                                # 000001 12.45 8500000  银行
                                # 300059 18.75 11000000 科技

# 层级获取:使用切片访问多行
df.loc[('上海', '600519'):('深圳', '000001')]  #            价格     交易量  类型
                                              # 地区 代码                        
                                              # 上海 600519 15.80 12000000  白酒
                                              # 深圳 000001 12.45  8500000  银行

# 层级获取:结合列访问块状数据
df.loc['上海', '价格':'类型']     #          价格     交易量  类型
                                # 代码                        
                                # 600519 15.80 12000000  白酒
                                # 601318 25.30  9500000  保险

(五)数据结合(data combination)

Pandas 中的数据结合(data combination)方法丰富多样,涵盖 合并(merge)连接(join)拼接(concat)追加(append)。内容从基础操作入手,逐步扩展至多维度场景(如多列合并、多索引层级),重点强调各方法的 参数灵活性(如 howonjoin等关键参数的用法)。

每个方法均配有代码示例,通过具体场景(如股票数据整合)突出其操作逻辑与实际应用价值。分析聚焦于 合并规则差异(如内连接/外连接的取舍)、多维度扩展能力(应对复杂数据结构)、性能对比(如 join与 concat的效率权衡),以及 潜在陷阱(如重复列名导致的冲突或数据冗余)。

1. 合并(Merge)

合并(pd.merge)是 Pandas 中最核心的数据结合方法,通过列值匹配实现类似 SQL JOIN 的功能,支持多维度场景扩展。

其基本语法为 pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None),核心参数 how(控制连接类型:内连接/外连接/左连接/右连接)与 on/left_on/right_on(指定匹配列)共同决定了关联逻辑。内连接(inner)仅保留左右表中匹配列的共同值(交集),高效但数据量较少;外连接(outer)保留所有值(并集),缺失部分填充 NaN,全面但可能冗余;左连接(left)与右连接(right)分别优先保留左表或右表的全部索引,另一表的缺失值补 NaN。

多列合并时需所有指定列的值严格对齐(如同时匹配“代号”和“日期”),适合处理时间序列等需多条件关联的场景。关键注意事项包括:匹配列名不一致时需用 left_on/right_on,但列值类型必须相同;多列合并可能导致重复列名(如“价格x”“价格y”),需后续重命名;连接类型的选择直接影响结果的数据量与完整性,需根据分析目标权衡。

2. 连接(Join)

连接(df1.join(df2, how='left'))是专为索引设计的合并方法,本质是 merge的索引简化版——默认基于行索引(而非列值)进行关联,参数 how同样控制连接类型(内/外/左/右),但无需指定匹配列(直接以索引为关联键)。

单索引连接要求索引唯一(否则需先重置索引),多索引连接则支持多层索引(如按国家/股票分组的数据),扩展了维度灵活性。

相比 mergejoin因少参数(无需手动指定列)、索引预排序特性,在索引对齐场景(如时间序列数据)中性能更优;但 merge支持多列复杂条件匹配,灵活性更高。教学核心在于根据数据结构选择方法:若关联键为索引,优先用 join;若需按列值匹配多条件,则用 merge。潜在陷阱包括索引层级不匹配(多索引时需严格对齐)或索引非唯一(需预处理),均可能导致报错。

3. 拼接(Concat)

拼接(pd.concat)通过沿指定轴(行或列)堆叠多个 DataFrame 实现数据结合,语法为 pd.concat([df1, df2], axis=0/1, join='outer/inner', ignore_index=False),核心参数 axis(0 为行、1 为列)决定堆叠方向,join控制交集(inner)或并集(outer)保留规则,ignore_index可重置索引避免重复。

行拼接(axis=0)适合追加数据(如合并多日股票记录),通过堆叠行并填充 NaN 处理非对齐索引;

列拼接(axis=1)适合特征扩展(如新增雇员信息列),但需注意 join='inner'会丢弃非交集索引行(仅保留共有的索引)。

相比 mergeconcat逻辑更简单(无需匹配列),但灵活性较低(仅支持轴堆叠);ignore_index参数是处理索引重复的关键工具。教学重点在于根据需求选择堆叠方向(行/列)与连接规则(交集/并集),并利用 ignore_index维护索引唯一性。

4. 追加(Append)

追加(df1.append(df2))是 concat(axis=0)的简化版,专门用于沿行轴堆叠 DataFrame,语法更直观但性能稍逊(底层仍复制数据)。该方法在早期 Pandas 版本中常用,但在 2.0 版本后已被官方标记为废弃,推荐使用 concat替代。其核心逻辑与行拼接一致(堆叠行、填充 NaN),适合历史代码兼容场景,但新项目应优先选择功能更全面的 concat

import pandas as pd

# 创建第一个 DataFrame,包含部分股票数据
df1 = pd.DataFrame({
    '代码': ['600519', '000001', '601318'],
    '价格': [15.80, 12.45, 25.30],
    '类型': ['白酒', '银行', '保险']
}, index=['贵州茅台', '平安银行', '中国平安'])

# 创建第二个 DataFrame,包含部分重叠股票数据
df2 = pd.DataFrame({
    '代码': ['601318', '300059', '000001'],
    '交易量': [9500000, 11000000, 8500000],
    '市值': [180000, 120000, 150000]
}, index=['中国平安', '东方财富', '平安银行'])

# 合并 (merge) 使用内连接
pd.merge(df1, df2, on='代码', how='inner')  #     代码    价格  类型      交易量     市值
                                            # 0  000001  12.45  银行   8500000  150000
                                            # 1  601318  25.30  保险   9500000  180000

# 合并 (merge) 使用外连接
pd.merge(df1, df2, on='代码', how='outer')  #     代码    价格   类型      交易量     市值
                                            # 0  000001  12.45   银行   8500000  150000
                                            # 1  300059    NaN  NaN  11000000  120000
                                            # 2  600519  15.80   白酒       NaN     NaN
                                            # 3  601318  25.30   保险   9500000  180000

# 连接 (join) 使用左连接
df1.join(df2, how='left')       #          代码    价格  类型      交易量     市值
                                # 贵州茅台  600519  15.80  白酒       NaN     NaN
                                # 平安银行  000001  12.45  银行   8500000  150000
                                # 中国平安  601318  25.30  保险   9500000  180000

# 拼接 (concat) 沿行轴 (axis=0)
pd.concat([df1, df2], axis=0, ignore_index=True)  #     代码    价格   类型      交易量     市值
                                                  # 0  600519  15.80   白酒       NaN     NaN
                                                  # 1  000001  12.45   银行       NaN     NaN
                                                  # 2  601318  25.30   保险       NaN     NaN
                                                  # 3  601318    NaN  NaN   9500000  180000
                                                  # 4  300059    NaN  NaN  11000000  120000
                                                  # 5  000001    NaN  NaN   8500000  150000

# 拼接 (concat) 沿列轴 (axis=1)
pd.concat([df1, df2], axis=1)   #          代码    价格  类型     代码    价格   类型      交易量     市值
                                # 中国平安  NaN    NaN NaN  601318   NaN  NaN   9500000  180000
                                # 东方财富  NaN    NaN NaN  300059   NaN  NaN  11000000  120000
                                # 贵州茅台  600519 15.80 白酒     NaN   NaN  NaN       NaN     NaN
                                # 平安银行  000001 12.45 银行  000001   NaN  NaN   8500000  150000

# 追加 (append) 两个 DataFrame
df1.append(df2)                 #          代码    价格  类型      交易量     市值
                                # 贵州茅台  600519 15.80 白酒       NaN     NaN
                                # 平安银行  000001 12.45 银行       NaN     NaN
                                # 中国平安  601318 25.30 保险       NaN     NaN
                                # 中国平安  601318   NaN NaN   9500000  180000
                                # 东方财富  300059   NaN NaN  11000000  120000
                                # 平安银行  000001   NaN NaN   8500000  150000

(六)数据重塑(reshape)

Pandas 中的数据重塑(data reshaping)方法丰富多样,涵盖 透视表(pivot)熔化(melt)堆叠(stack)去堆叠(unstack)设置索引(set_index)重置索引(reset_index) 以及 多级索引操作

我将从基础操作入手,逐步扩展至多维度场景(如多列透视、多级索引嵌套),重点强调重塑操作的 灵活性语义化表达(即通过变换让数据结构更贴合分析需求)。

每个方法均配有代码示例,通过具体场景(如股票数据分析)突出其操作逻辑与实际应用价值。分析聚焦于 维度变换规则(如行列转换逻辑)、索引/列的动态调整机制(如层级索引的生成与拆分)、性能对比(如 stack与 pivot的效率差异),以及 潜在陷阱(如多级索引冲突导致的键值重复或数据丢失)。

1、行列互转

  • 使用 melt 函数将宽格式数据转换为长格式(unpivot),通过指定 ID 变量和值变量重塑数据。
  • 使用 pivot 函数将长格式数据转换为宽格式(pivot),通过索引、列和值列重新组织数据。
  • 示例代码展示如何将列转为行或行转为列,适用于数据透视表生成。

2、堆叠与取消堆叠

  • stack 方法将 DataFrame 的列压缩为多级索引的行,创建长格式数据。
  • unstack 方法将多级索引的行展开为列,恢复宽格式数据。
  • 强调堆叠和取消堆叠的逆操作特性,适合处理多层次数据。

3、透视表

  • pivot_table 函数创建多维汇总表,支持按行、列和值进行聚合(默认使用均值)。
  • 可指定聚合函数(如 sum、count)以及多级索引,适用于复杂数据分析场景。
  • 示例展示如何根据类别汇总数据,如按地区和产品统计销售额。

4、交叉表

  • crosstab 函数生成频率或统计表,用于分析两个或多个分类变量之间的关系。
  • 支持添加权重列或自定义聚合函数,适用于统计分布和关联性分析。
  • 示例可能包括按性别和职业分组的计数表。
import pandas as pd

# 创建一个初始 DataFrame,包含中国股市数据
data = {
    '公司': ['贵州茅台', '平安银行', '中国平安', '东方财富'],
    '2023年': [15.80, 12.45, 25.30, 18.75],
    '2024年': [16.20, 12.80, 26.50, 19.30]
}
df = pd.DataFrame(data)

# 使用 melt 将宽格式转为长格式
pd.melt(df, id_vars=['公司'], value_vars=['2023年', '2024年'], 
        var_name='年份', value_name='价格')  #      公司  年份    价格
                                           # 0  贵州茅台 2023年  15.80
                                           # 1  平安银行 2023年  12.45
                                           # 2  中国平安 2023年  25.30
                                           # 3  东方财富 2023年  18.75
                                           # 4  贵州茅台 2024年  16.20
                                           # 5  平安银行 2024年  12.80
                                           # 6  中国平安 2024年  26.50
                                           # 7  东方财富 2024年  19.30

# 使用 pivot 将长格式转为宽格式
long_df = pd.melt(df, id_vars=['公司'], value_vars=['2023年', '2024年'], 
                  var_name='年份', value_name='价格')
long_df.pivot(index='公司', columns='年份', values='价格')  # 年份     2023年    2024年
                                                           # 公司                   
                                                           # 东方财富  18.75    19.30
                                                           # 平安银行  12.45    12.80
                                                           # 贵州茅台  15.80    16.20
                                                           # 中国平安  25.30    26.50

# 使用 stack 将列转为多级索引
df.set_index('公司').stack()    # 公司         2023年    15.80
                               # 东方财富     2024年    19.30
                               # 平安银行     2023年    12.45
                               #             2024年    12.80
                               # 贵州茅台     2023年    15.80
                               #             2024年    16.20
                               # 中国平安     2023年    25.30
                               #             2024年    26.50
                               # dtype: float64

# 使用 unstack 恢复多级索引为列
stacked = df.set_index('公司').stack()
stacked.unstack()              # 年份     2023年    2024年
                               # 公司                   
                               # 东方财富  18.75    19.30
                               # 平安银行  12.45    12.80
                               # 贵州茅台  15.80    16.20
                               # 中国平安  25.30    26.50

# 使用 pivot_table 创建透视表,按公司和年份汇总平均价格 (修正为使用现有列)
pd.pivot_table(df, values='2023年', index='公司', columns='2024年', aggfunc='mean')  # 2024年    12.80    16.20    19.30    26.50
                                                                                   # 公司                         
                                                                                   # 东方财富    NaN      NaN     19.30     NaN
                                                                                   # 平安银行  12.80     NaN      NaN      NaN
                                                                                   # 贵州茅台    NaN     16.20     NaN      NaN
                                                                                   # 中国平安    NaN      NaN      NaN     26.50

# 使用 crosstab 创建交叉表,按公司和类型统计频率
df['类型'] = ['白酒', '银行', '保险', '科技']
pd.crosstab(df['公司'], df['类型'])  # 类型      白酒  银行  保险  科技
                                     # 公司                    
                                     # 东方财富    0     0    0    1
                                     # 平安银行    0     1    0    0
                                     # 贵州茅台   1     0    0    0
                                     # 中国平安    0     0    1    0

(七) 数据分析

下面将介绍 Pandas 中数据分析的核心方法,涵盖统计描述(describe)、分组聚合(groupby)、时间序列分析(resample)和滑动窗口分析(rolling)。内容从基础统计入手,逐步扩展到复杂场景(如多级分组、时间频率转换),强调 Pandas 的分析灵活性和与 NumPy 的互操作性。

每种方法都会有举例说明操作的逻辑和应用场景(如股票数据统计)。分析重点在于统计指标的计算规则、分组轴的动态调整、时间序列的频率处理以及滑动窗口的性能优化。

1. 统计描述(Describe)

统计描述(df.describe)提供数值列的统计摘要,是数据探索的常用工具。其基本语法为 df.describe(percentiles=None, include=None, exclude=None),通过参数 percentiles自定义分位数(如 [0.1, 0.9]),include/exclude控制纳入统计的列类型(如仅数值列或包含非数值列)。默认输出包含计数(count,含 NaN 的总行数)、均值(mean)、标准差(std,反映数据波动)、最小值(min)、四分位数(25%/50%/75%)和最大值(max),仅针对数值列(如价格、交易量)进行分析,自动忽略非数值列(如行业)。多类型描述(include='all')可扩展至非数值列,额外输出众数(top)及其频次(freq),但需注意 NaN 会影响计数结果。教学核心在于通过快速统计摘要把握数据分布特征(如集中趋势、离散程度),为后续分析提供基础认知。

2. 分组聚合(Group By)

分组聚合(df.groupby)通过指定分组键(如行业、代号)对数据进行分类统计,逻辑类似 SQL 的分组操作。基本语法为 df.groupby(by, axis=0, level=None).agg(func),其中 by为分组依据(单键或多键),axis指定分组方向(行或列),level用于多级索引的层级选择,agg支持多种聚合函数(如均值、求和)。

单键分组(如按“行业”聚合)沿行方向计算各组的统计量(如行业均价),自动排除非数值列;多键分组(如按“行业+代号”聚合)通过字典指定列与函数的映射关系(如价格取均值、交易量求和),生成多级索引的统计结果,适合复杂场景(如行业-股票联合分析)。

多级索引分组(通过 level参数)可灵活处理层级数据(如按国家/代号聚合),但需确保层级结构匹配,否则报错。分析重点包括分组逻辑的灵活性(单键/多键/层级)、非数值列的自动排除规则,以及聚合函数对数据特征的提取能力。

3. 时间序列分析(Resample)

时间序列重采样(df.resample)针对时间索引数据,通过指定频率规则(如日/月/年)对数据进行聚合,逻辑类似时间轴的分段统计。基本语法为 df.resample(rule, closed='left', label='left').agg(func),其中 rule定义重采样频率(如 'D'日、'M'月、'Y'年),closed和 label控制时间区间的开闭与标签归属。

基本重采样(如按月均值)将高频数据(如日数据)转换为低频统计(如月均股价),适合分析时间趋势;高级重采样支持多指标聚合(如价格均值与交易量求和),并通过参数调整时间区间的包含规则(如左闭区间)。

核心注意事项包括:时间索引必须为 Datetime 类型(非时间索引会报错),重采样频率需适配数据周期(如日数据可转为周/月/年),以及多指标聚合时需明确各列的统计函数。分析价值在于通过时间维度的灵活聚合,揭示数据的周期性、趋势性特征(如股价的季节性波动)。

4. 滑动窗口分析(Rolling)

滑动窗口分析(df.rolling)通过定义窗口大小(如 3 天、5 周),计算窗口内的滑动统计量(如移动平均、累计求和),是时间序列局部特征提取的核心工具。基本语法为 df.rolling(window, min_periods=None).agg(func),其中 window指定窗口包含的数据点数量,min_periods控制窗口内最少有效数据点(允许部分缺失)。基本滑动(如 3 天移动平均)通过对连续窗口内的数据计算统计量(如均值),实现趋势平滑(如过滤股价短期波动);高级滑动支持多指标同时计算(如价格均值与交易量求和),并通过 min_periods调整窗口的有效性阈值(如至少 3 个有效点才计算)。注意事项包括:窗口过大会导致数据丢失(边缘数据点不足时被剔除),大窗口计算可能影响性能(需测试优化),以及 min_periods的设置需平衡数据完整性与统计有效性。分析重点在于通过局部统计捕捉时间序列的短期模式(如均线交叉、波动聚集),为短期决策提供依据。

import pandas as pd

# 创建一个包含中国股市数据的 DataFrame
data = {
    '代码': ['600519', '000001', '601318', '300059'],
    '价格': [15.80, 12.45, 25.30, 18.75],
    '交易量': [12000000, 8500000, 9500000, 11000000],
    '日期': ['2025-09-28', '2025-09-29', '2025-09-30', '2025-09-30']
}
df = pd.DataFrame(data)
df['日期'] = pd.to_datetime(df['日期'])

# 统计描述 (describe)
df.describe()                   #             价格         交易量
                                # count  4.000000  4.000000e+00
                                # mean   18.075000  9.750000e+06
                                # std     5.262477  1.707825e+06
                                # min    12.450000  8.500000e+06
                                # 25%    14.637500  8.875000e+06
                                # 50%    17.275000  9.750000e+06
                                # 75%    20.662500  1.075000e+07
                                # max    25.300000  1.200000e+07

# 分组聚合 (groupby) 按日期计算平均值
df.groupby('日期').mean()        #             价格         交易量
                                # 日期                         
                                # 2025-09-28  15.80  1.200000e+07
                                # 2025-09-29  12.45  8.500000e+06
                                # 2025-09-30  22.025 1.025000e+07

# 时间序列分析 (resample) 按日统计交易量总和
df.set_index('日期').resample('D')['交易量'].sum()  # 日期
                                                  # 2025-09-28    12000000
                                                  # 2025-09-29     8500000
                                                  # 2025-09-30    20500000
                                                  # Freq: D, Name: 交易量, dtype: int64

# 滑动窗口分析 (rolling) 计算 2 天滑动平均价格
df.set_index('日期')['价格'].rolling(window=2).mean()  # 日期
                                                      # 2025-09-28       NaN
                                                      # 2025-09-29    14.125
                                                      # 2025-09-30    18.875
                                                      # Name: 价格, dtype: float64

(八)数据可视化

接下来我要介绍的是 Pandas 自带的数据可视化方法——也就是用 Series.plot()或 DataFrame.plot()直接画图。这里要先提一下,大家可能听说过两个超常用的画图工具包:Matplotlib 和 **Seaborn,**它们功能超强,能画出各种复杂又漂亮的图。

Pandas 自带的 plot()方法(比如 df.plot()或 series.plot())最大的特点是 “快” ——你只需要一行代码,就能把数据变成图表,快速看到数据之间的关系(比如哪列数据在涨、哪列数据有波动)。它画出来的图 “颜值”不会太高(可能不够美观),而且 信息量可能没那么全(比如细节不够丰富)。但它特别适合 “先看看数据大概长啥样” 的场景——比如你刚拿到一组数据,想快速检查下趋势、对比下数值,用它就特别方便。

图表类型均结合具体数据场景(如股票数据、业务指标数据)说明用法,重点覆盖 6 类基础图表:

  1. 线状图:主要用于展示数据随时间的变化趋势,如股票每日收盘价的连续波动、月度销售额的变化曲线,通过 df.plot(kind='line') 实现,支持标注坐标轴、添加图例以清晰呈现趋势。
  2. 散点图:用于分析两个变量间的关联关系,如股票成交量与价格的相关性、广告投入与销量的对应关系,通过 df.plot(kind='scatter') 生成,可通过颜色、大小区分不同类别数据。
  3. 条形图:适用于对比不同类别数据的数值差异,如不同行业股票的平均收益率、不同产品的月度销量,通过 df.plot(kind='bar') 实现,支持横向 / 纵向条形图切换,适配不同类别名称长度的展示需求。
  4. 箱形图:用于展示数据的分布特征(如四分位数、异常值),如某股票价格的波动范围、不同区域用户消费金额的分布差异,通过 df.plot(kind='box') 生成,可快速识别数据中的异常值(如远超正常范围的极端价格)。
  5. 直方图:用于呈现数据的频率分布,如股票日收益率的分布情况、用户年龄的分布区间,通过 df.plot(kind='hist') 实现,支持调整分组数量(bins)以优化分布细节的展示。
  6. 饼状图:用于展示各部分占总体的比例关系,如不同板块股票在投资组合中的权重、不同渠道收入占总营收的比例,通过 df.plot(kind='pie') 实现,支持标注百分比、调整起始角度以提升可读性。
import pandas as pd
import matplotlib.pyplot as plt

# 创建一个包含中国股市数据的 DataFrame
data = {
    '公司': ['贵州茅台', '平安银行', '中国平安', '东方财富'],
    '收盘价': [15.80, 12.45, 25.30, 18.75],
    '成交量': [12000000, 8500000, 9500000, 11000000],
    '日期': ['2025-09-28', '2025-09-29', '2025-09-30', '2025-09-30'],
    '板块': ['白酒', '银行', '保险', '科技'],
    '收益率': [0.02, -0.01, 0.03, 0.015]
}
df = pd.DataFrame(data)
df['日期'] = pd.to_datetime(df['日期'])

# 线状图:展示收盘价随日期的变化趋势
df.plot(x='日期', y='收盘价', kind='line', marker='o', legend=True)
plt.title('每日收盘价趋势')
plt.xlabel('日期')
plt.ylabel('收盘价 (元)')
plt.grid(True)
plt.show()                     # [生成线状图,显示各公司收盘价随日期波动,点标记,包含图例和网格]

# 散点图:分析成交量与收益率的相关性 (修正:手动映射颜色)
color_map = {'白酒': 'red', '银行': 'blue', '保险': 'green', '科技': 'purple'}
colors = [color_map[plate] for plate in df['板块']]
df.plot(x='成交量', y='收益率', kind='scatter', c=colors, s=100)
plt.title('成交量与收益率关系')
plt.xlabel('成交量')
plt.ylabel('收益率')
plt.show()                     # [生成散点图,成交量 vs 收益率,颜色手动映射板块,点大小 100]

# 条形图:对比不同公司平均收盘价
df.groupby('公司')['收盘价'].mean().plot(kind='bar', color='skyblue')
plt.title('公司平均收盘价对比')
plt.xlabel('公司')
plt.ylabel('平均收盘价 (元)')
plt.xticks(rotation=45)
plt.show()                     # [生成纵向条形图,显示各公司平均收盘价,标签倾斜 45 度]

# 箱形图:展示收盘价分布特征
df.boxplot(column='收盘价', by='板块', grid=False)
plt.title('各板块收盘价分布')
plt.suptitle('')  # 移除默认标题
plt.xlabel('板块')
plt.ylabel('收盘价 (元)')
plt.show()                     # [生成箱形图,显示各板块收盘价分布,包括四分位和异常值]

# 直方图:呈现收益率的频率分布
df['收益率'].plot(kind='hist', bins=5, color='green', edgecolor='black')
plt.title('收益率频率分布')
plt.xlabel('收益率')
plt.ylabel('频率')
plt.show()                     # [生成直方图,显示收益率分布,5 个分组,绿色填充]

# 饼状图:展示各板块在总成交量中的比例
sector_volumes = df.groupby('板块')['成交量'].sum()
sector_volumes.plot(kind='pie', autopct='%1.1f%%', startangle=90)
plt.title('板块成交量占比')
plt.ylabel('')  # 移除 y 轴标签
plt.show()                     # [生成饼状图,显示各板块成交量比例,标注百分比,从 90 度开始]

::: hljs-center
image.png
:::

(九)数据处理

最后我们总结 Pandas 的核心内容,重点讲解数据处理的核心技术。需要注意的是,前面课程中使用的所有数据均已经过处理,呈现为“干净”状态;但在实际工作中,原始数据通常是“杂乱”的,因此数据预处理是分析的第一步。数据处理主要可分为两类:

  • 数据清洗(data cleansing):处理缺失值、离群值(异常值);
  • 数据转换(data transforming):包括编码、分组等操作。

核心模块一:数据清洗

数据清洗是处理数据的基础环节,核心是“剔除无效数据、修复不完整数据”,该部分重点介绍 4 类常见问题的解决方案,均结合 Pandas 实操方法,适配小白理解与应用:

  1. 缺失值处理:针对数据中“空值(NaN)”问题,提供两类核心策略——
    • 「删除法」:通过 dropna() 函数删除含缺失值的行或列(如删除股票数据中“收盘价”为空的记录),支持通过参数指定“删除含任意缺失值的行”或“删除全为缺失值的行”,避免无效数据干扰分析;
    • 「填补法」:通过 fillna() 函数用合理值填补缺失值(如用股票价格的“均值”“中位数”填补空值,或用前一天价格“向前填充”),适配不同场景下的缺失值修复需求,减少数据丢失。
  2. 重复值处理:针对数据中“完全重复或部分重复”的记录,通过 duplicated() 函数识别重复数据(返回布尔值标记是否重复),再用 drop_duplicates() 函数删除重复项,例如清理股票数据中“同一时间、同一股票代码”的重复记录,保证数据唯一性。
  3. 异常值处理:针对数据中“超出合理范围”的极端值(如股票价格突然出现的异常高值/低值),提供两类常用方法——
    • 「统计法」:结合描述性统计(如 describe() 查看四分位数),用“3σ 原则”或“箱形图法”识别异常值(如超出上下四分位数 1.5 倍四分位距的值);
    • 「过滤法」:通过布尔索引筛选出合理范围的数据(如 df[df['价格'] < 1000] 剔除股票价格超 1000 的异常值),或用 clip() 函数将极端值限制在合理区间内。
  4. 格式与内容纠错:针对数据“格式不统一”(如日期格式混乱、文本大小写不一致)或“内容错误”(如股票代码多写字符)的问题,通过 Pandas 字符串方法(如 str.strip() 去除多余空格、str.upper() 统一文本大写)、日期转换函数(如 pd.to_datetime() 规范日期格式)修正,确保数据格式符合分析要求。

核心模块二:数据转换

数据转换是为了“让数据结构/格式适配分析场景”,该部分重点介绍 3 类高频转换操作,均结合实际场景(如股票数据处理)说明用法:

  1. 数据类型转换:针对数据类型不匹配的问题(如“价格”字段误存为文本类型),通过 astype() 函数转换数据类型(如 df['价格'].astype('float') 将文本转为数值型),或用 pd.to_numeric() 处理含非数字字符的数值字段(如 pd.to_numeric(df['成交量'], errors='coerce') 将无法转换的值设为 NaN),确保后续统计计算正常进行。
  2. 数据格式重塑:针对数据结构不便于分析的问题(如“宽表”转“长表”、行列互换),衔接前文“数据重塑”内容,重点应用 melt()(将多列数据“熔化”为长表,如将多只股票的价格列转为“股票代码-价格”的两列结构)、pivot()(将长表“透视”为宽表,如将“日期-股票代码-价格”的长表转为“日期为行、股票代码为列”的宽表),适配不同分析场景下的数据结构需求。
  3. 自定义转换:针对个性化分析需求(如股票收益率计算、数据分组映射),通过 apply()map() 函数实现自定义转换——
    • apply():对行或列应用自定义函数(如 df['收益率'].apply(lambda x: x*100) 将收益率转为百分比);
    • map():对列数据进行映射转换(如将“行业代码”映射为“行业名称”,如 df['行业代码'].map({1:'电商', 2:'科技'})),让数据更贴合分析语义。
import pandas as pd
import numpy as np

# 创建一个包含中国股市数据的 DataFrame 模拟原始“杂乱”数据
data = {
    '代码': ['600519', '000001', '601318', '300059', '600519'],
    '价格': [15.80, '12.45', 25.30, np.nan, 15.80],
    '交易量': ['12,000,000', '8,500,000', '9,500,000', '11,000,000', '12,000,000'],
    '日期': ['2025-09-28', '29-09-2025', '2025/09/30', '30-09-2025', '2025-09-28'],
    '行业': ['白酒', '银行 ', ' 保险', '科技', '白酒']
}
df = pd.DataFrame(data)

# 缺失值处理 - 删除法
df_cleaned_drop = df.dropna(subset=['价格'])  # 删除价格为 NaN 的行
print(df_cleaned_drop)              #    代码    价格   交易量       日期  行业
                                    # 0  600519  15.80  12,000,000  2025-09-28  白酒
                                    # 1  000001  12.45   8,500,000  29-09-2025  银行
                                    # 2  601318  25.30   9,500,000  2025/09/30  保险
                                    # 4  600519  15.80  12,000,000  2025-09-28  白酒

# 缺失值处理 - 填补法
df_filled = df.fillna({'价格': df['价格'].mean()})  # 用均值填补价格缺失值
print(df_filled['价格'])             # 0    15.80
                                    # 1    12.45
                                    # 2    25.30
                                    # 3    17.4625
                                    # 4    15.80
                                    # Name: 价格, dtype: object

# 重复值处理
df_no_duplicates = df.drop_duplicates(subset=['代码', '日期'])
print(df_no_duplicates)             #    代码    价格   交易量       日期  行业
                                    # 0  600519  15.80  12,000,000  2025-09-28  白酒
                                    # 1  000001  12.45   8,500,000  29-09-2025  银行
                                    # 2  601318  25.30   9,500,000  2025/09/30  保险
                                    # 3  300059    NaN  11,000,000  30-09-2025  科技

# 异常值处理 - 过滤法
df_filtered = df[df['价格'].astype(float) < 20]  # 筛选价格小于 20 的数据
print(df_filtered)                  #    代码    价格   交易量       日期  行业
                                    # 0  600519  15.80  12,000,000  2025-09-28  白酒
                                    # 1  000001  12.45   8,500,000  29-09-2025  银行
                                    # 4  600519  15.80  12,000,000  2025-09-28  白酒

# 格式与内容纠错 - 日期格式与文本清洗
df['日期'] = pd.to_datetime(df['日期'], errors='coerce')
df['行业'] = df['行业'].str.strip().str.lower()
print(df['日期'])                    # 0   2025-09-28
                                    # 1   2025-09-29
                                    # 2   2025-09-30
                                    # 3   2025-09-30
                                    # 4   2025-09-28
                                    # Name: 日期, dtype: datetime64[ns]
print(df['行业'])                    # 0    白酒
                                    # 1    银行
                                    # 2    保险
                                    # 3    科技
                                    # 4    白酒
                                    # Name: 行业, dtype: object

# 数据类型转换
df['价格'] = pd.to_numeric(df['价格'], errors='coerce')
df['交易量'] = df['交易量'].str.replace(',', '').astype(int)
print(df.dtypes)                    # 代码         object
                                    # 价格        float64
                                    # 交易量        int64
                                    # 日期     datetime64[ns]
                                    # 行业         object
                                    # dtype: object

# 数据格式重塑 - melt
df_melted = pd.melt(df, id_vars=['代码', '日期'], value_vars=['价格', '交易量'], 
                    var_name='指标', value_name='值')
print(df_melted.head())             #    代码       日期   指标       值
                                    # 0  600519 2025-09-28  价格    15.80
                                    # 1  000001 2025-09-29  价格    12.45
                                    # 2  601318 2025-09-30  价格    25.30
                                    # 3  300059 2025-09-30  价格     NaN
                                    # 4  600519 2025-09-28  价格    15.80

# 自定义转换 - apply
df['收益率'] = df['价格'].apply(lambda x: x * 0.01 if pd.notna(x) else x)
print(df['收益率'])                 # 0    0.1580
                                    # 1    0.1245
                                    # 2    0.2530
                                    # 3       NaN
                                    # 4    0.1580
                                    # Name: 收益率, dtype: float64

# 自定义转换 - map
df['行业类别'] = df['行业'].map({'白酒': '消费', '银行': '金融', '保险': '金融', '科技': '科技'})
print(df['行业类别'])               # 0    消费
                                    # 1    金融
                                    # 2    金融
                                    # 3    科技
                                    # 4    消费
                                    # Name: 行业类别, dtype: object

我们简单总结上述 Pandas 相关内容。Pandas 工具包主要用于数组操作,前文系统介绍了其八大核心应用方向:首先涵盖基础数据结构创建(构建一维序列和二维数据帧),随后涉及数据存载(支持 xlsx 和 csv 格式的序列/数据帧存储与加载);接着深入探讨数据获取(通过元素、列状、行状、块状、高级及层级六种方式索引或切片数据帧元素),以及数据整合(基于键的合并和按轴的连接操作);然后聚焦数据形态变换(通过行列互转重塑索引以及长宽互转调整表达形式),并进行数据分析(包括整体、分组、透视和交叉四类分析方法);还包括数据可视化(以线状图、散点图、条形图、箱形图、直方图和饼状图等多种形式呈现);最后讲解数据预处理(清洗缺失值和离群值,以及对字符型变量编码和连续型变量分组转换),全面覆盖从数据构建到分析的全流程操作。

总结

我们已经介绍了 NumPy 和 Pandas 工具库,它们是数据分析时最基础的两个工具库。没有 NumPy,量化分析的计算效率会大幅下降;没有 Pandas,数据处理与业务逻辑实现会变得极其繁琐。二者共同构成了量化研究的“基础设施”——就像“计算器 + Excel”的升级版,但功能更强大、效率更高。对于任何严肃的量化从业者(无论是主观策略开发者还是算法交易工程师),熟练掌握这两个库是入门门槛,更是长期竞争力所在。

下次课程会继续介绍 Python 的进阶内容,包括 Python 的一些高级用法和其他工具库。

评论

还没有评论,来说点什么吧
0 / 2000