38  数据特殊结构 数组

38.1 引言数组在数值计算中的核心地位

NumPy的历史与重要性:

NumPy(Numerical Python)是Python科学计算生态系统的基础库,最初由Travis Oliphant于2005年创建。它提供了高性能的多维数组对象和用于处理这些数组的工具。

理论背景:为什么需要数组?

Python原生列表的局限性: 1. 性能问题: 列表存储的是对象的指针,访问需要间接寻址 2. 内存效率: 每个元素都是完整的Python对象,内存开销大 3. 类型单一: 列表可以混合类型,但这不利于向量化计算

NumPy数组的设计哲学: - 连续内存: 数组元素在内存中连续存储 - 同构数据: 所有元素类型相同 - 向量化操作: 通过SIMD指令并行处理

性能对比示例:

# Python列表: 1000万元素求和 ≈ 150ms
# NumPy数组: 1000万元素求和 ≈ 5ms
# 性能提升: 30倍

金融应用场景: - 向量计算: 同时处理上千只股票收益率 - 矩阵运算: 投资组合优化、因子模型 - 时间序列: 高频数据处理、技术指标计算

38.2 NumPy数组基础

38.2.1 数组创建方法

补充说明:数组创建的多种策略

  1. 从Python序列转换:
    • np.array([1, 2, 3]): 从列表创建
    • np.arange(10): 类似range()的数组版本
    • 适合:小规模数据或已有数据
  2. 使用NumPy生成函数:
    • np.zeros(): 全零数组
    • np.ones(): 全1数组
    • np.random.*(): 随机数生成
    • 适合:初始化、模拟数据
  3. 从文件读取:
    • np.loadtxt(): 从文本文件
    • np.genfromtxt(): 更灵活的文本读取
    • 适合:导入外部数据
列表 38.1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目一
import numpy as np
stocks = 2000   # 2000支股票
days =  500  # 两年大约500个交易日

# 生成服从正态分布:均值期望=0,标准差=1的序列
stock_day = np.random.standard_normal((stocks, days))   
print(stock_day.shape)   #打印数据组结构
# 打印出前五只股票,头五个交易日的涨跌幅情况
print(stock_day[0:5, :5])

代码深度解析:

  1. shape属性:
    • 返回元组 (n_rows, n_cols)
    • 表示数组的维度
    • 例如 (2000, 500) 表示 2000×500 的矩阵
  2. ndim属性:
    • 返回数组的维度数
    • 1维数组: ndim=1
    • 2维数组: ndim=2
  3. size属性:
    • 返回数组元素总数
    • 等于 shape 各维度的乘积
  4. dtype属性:
    • 表示数组元素的数据类型
    • 常见类型: int32, float64, bool, object

38.2.2 数组的索引与切片

理论背景:多维数组的索引机制

NumPy数组的索引遵循从0开始的规则,切片使用[start:stop:step]语法。

关键概念: - 视图(View): 切片返回原数组的视图,共享内存 - 副本(Copy): 使用.copy()创建独立副本 - 广播(Broadcasting): 不同形状数组的运算规则

列表 38.2
# =============================================================================
# 题目:NumPy数组的索引与切片操作
# =============================================================================
# 本任务演示数组的索引、切片、花式索引和布尔索引
# 在金融数据分析中,索引用于提取特定时间段的数据、筛选满足条件的股票等

# ==================== 创建一维数组用于演示 ====================
# np.array():从列表创建一维数组
# 数组内容:10, 20, 30, ..., 100(10个等差数列)
# 金融应用:可以代表10个交易日的收盘价、10只股票的收益率等
arr_1d = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])

# ==================== 一维数组的基本索引 ====================
print('一维数组索引:')  # 打印标题
# arr_1d[0]:正索引,从0开始,访问第一个元素
# 返回值:10
# 金融应用:获取第一天的股价、第一个月的收益率等
print(f'  arr_1d[0]: {arr_1d[0]}')  # 打印第一个元素

# arr_1d[-1]:负索引,-1表示最后一个元素
# 返回值:100
# 金融应用:获取最新的股价、最近一天的数据
print(f'  arr_1d[-1]: {arr_1d[-1]}')  # 打印最后一个元素

# arr_1d[2:7]:切片操作,语法为[start:stop],start包含,stop不包含
# 2:7表示索引2到6(不包括7)
# 返回值:[30, 40, 50, 60, 70]
# 金融应用:提取特定时间段的数据,如第3天到第7天的股价
print(f'  arr_1d[2:7]: {arr_1d[2:7]}')  # 打印切片(索引2到6)

# arr_1d[::2]:切片操作,语法为[start:stop:step]
# ::2表示从开头到结尾,步长为2(每隔一个取一个)
# 返回值:[10, 30, 50, 70, 90](取奇数位置的元素)
# 金融应用:提取隔日数据、周频数据(从日频数据中提取)
print(f'  arr_1d[::2]: {arr_1d[::2]}')  # 打印步长为2的切片

# ==================== 创建二维数组用于演示 ====================
# 创建3×4的二维数组(3行4列)
# 金融应用:可以代表3只股票在4个交易日的收盘价矩阵
arr_2d = np.array([
    [1, 2, 3, 4],      # 第1行
    [5, 6, 7, 8],      # 第2行
    [9, 10, 11, 12]    # 第3行
])

# ==================== 二维数组的索引 ====================
print('\n二维数组索引:')  # 打印标题
# arr_2d[0, 0]:二维数组索引,语法为[row, col]
# 0, 0表示第0行第0列(第一行第一列)
# 返回值:1
# 金融应用:获取特定股票在特定日期的收盘价
print(f'  arr_2d[0, 0]: {arr_2d[0, 0]}')  # 打印第0行第0列

# arr_2d[1, :]:冒号:表示选择该维度的所有元素
# 1, :表示第1行的所有列(整行)
# 返回值:[5, 6, 7, 8]
# 金融应用:获取某只股票在所有日期的价格序列
print(f'  arr_2d[1, :]: {arr_2d[1, :]}')  # 打印第1行所有列

# arr_2d[:, 2]::表示所有行,2表示第2列
# :, 2表示所有行的第2列(整列)
# 返回值:[3, 7, 11]
# 金融应用:获取所有股票在某一天的价格
print(f'  arr_2d[:, 2]: {arr_2d[:, 2]}')  # 打印所有行第2列

# arr_2d[0:2, 1:3]:切片两个维度
# 0:2表示第0-1行,1:3表示第1-2列
# 返回值:2×2的子矩阵 [[2, 3], [6, 7]]
# 金融应用:提取特定时间范围和特定股票的数据子集
print(f'  arr_2d[0:2, 1:3]:\n{arr_2d[0:2, 1:3]}')  # 打印子矩阵

# ==================== 花式索引(Fancy Indexing) ====================
# 花式索引:使用整数数组作为索引,可以不连续地访问元素
# indices:索引数组,指定要访问的位置 [0, 2, 4]
indices = [0, 2, 4]  # 定义索引列表
# arr_1d[indices]:使用索引数组,提取第0、2、4个元素
# 返回值:[10, 30, 50]
# 金融应用:提取特定日期的数据,如季末交易日、财报发布日
print(f'\n花式索引 arr_1d[{indices}]: {arr_1d[indices]}')  # 打印花式索引结果

# ==================== 布尔索引 ====================
# 布尔索引:使用布尔数组作为索引,提取满足条件的元素
# arr_1d > 50:比较操作,返回布尔数组 [False, False, False, False, False, True, True, True, True, True]
# True表示对应位置的元素>50,False表示≤50
bool_idx = arr_1d > 50  # 生成布尔数组
# arr_1d[bool_idx]:使用布尔数组索引,只保留True位置的元素
# 返回值:[60, 70, 80, 90, 100]
# 金融应用:筛选上涨的股票、收益率为正的交易日、满足特定条件的股票
print(f'布尔索引 arr_1d[arr_1d > 50]: {arr_1d[bool_idx]}')  # 打印大于50的元素

# ==================== 视图(View) vs 副本(Copy) ====================
# 这是NumPy的重要概念:切片返回的是视图还是副本?
# 视图:共享原数组的内存,修改视图会影响原数组
# 副本:独立拷贝,修改副本不影响原数组

# arr_1d[2:5]:切片操作,返回视图(View)
# slice_view是arr_1d的视图,指向索引2-4的元素
slice_view = arr_1d[2:5]  # 创建切片视图

# arr_1d[2:5].copy():显式创建副本(Copy)
# .copy()方法创建独立副本,深拷贝数据
slice_copy = arr_1d[2:5].copy()  # 创建切片副本

# 修改视图的第0个元素(原数组索引2的元素)
slice_view[0] = 999  # 视图修改会影响原数组
# 修改副本的第1个元素(不影响原数组)
slice_copy[1] = 888  # 副本修改不影响原数组

print(f'\n修改视图后原数组: {arr_1d}')  # 视图修改会影响原数组(索引2变为999)
print(f'修改副本后原数组: {arr_1d}')  # 副本修改不影响原数组(索引3仍为40)

代码深度解析:

  1. 切片语法: [start:stop:step]
    • start: 起始索引(包含)
    • stop: 结束索引(不包含)
    • step: 步长
    • 省略时使用默认值
  2. 花式索引:
    • 使用整数数组作为索引
    • 返回新数组的副本
    • 不连续访问的高效方法
  3. 布尔索引:
    • 使用布尔数组作为索引
    • 常用于条件过滤
    • 类似于SQL的WHERE子句

38.3 数组的运算

38.3.1 向量化运算

理论背景:向量化的数学原理

向量化(Vectorization)是指用数组表达式代替显式循环,利用底层C/Fortran实现的SIMD(单指令多数据)指令并行处理。

性能优势: - 避免Python循环: 减少解释器开销 - 内存连续: 利用CPU缓存 - 并行计算: SIMD指令一次处理多个数据

Amdahl定律: \[ S(N) = \frac{1}{(1-p) + \frac{p}{N}} \]

其中: - \(S(N)\): 加速比 - \(p\): 可并行部分比例 - \(N\): 处理器数量

列表 38.3
# =============================================================================
# 题目:向量化运算的性能优势演示
# =============================================================================
# 本任务对比Python循环和NumPy向量化运算的性能差异
# 在金融分析中,向量化运算可以大幅提升计算速度,特别是处理大规模数据时

# ==================== 导入必要的库 ====================
import numpy as np  # NumPy数值计算库
import time  # time模块,用于计时

# ==================== 创建大规模测试数组 ====================
# 定义数组大小:1000万元素
# 金融应用:模拟1000万条交易记录、1000万个价格数据点
n = 10_000_000  # 使用下划线分隔数字,提高可读性(Python 3.6+特性)

# np.random.randn(n):生成n个标准正态分布随机数
# arr1和arr2各包含1000万个随机数
# 金融应用:模拟两只股票的1000万个收益率数据
arr1 = np.random.randn(n)  # 生成第一个随机数组
arr2 = np.random.randn(n)  # 生成第二个随机数组

# ==================== 方法1:使用Python循环逐元素相加 ====================
print('方法1:Python循环(慢)')  # 打印方法说明
# time.time():返回当前时间戳(秒数,浮点数)
# 记录开始时间
start = time.time()

# for循环:Python原生循环,逐元素访问
# range(n):生成0到n-1的整数序列
# result_loop.append():将结果追加到列表末尾
# 性能问题:Python循环开销大,每次迭代都有类型检查、函数调用等开销
result_loop = []  # 初始化结果列表
for i in range(n):  # 循环n次(1000万次!)
    result_loop.append(arr1[i] + arr2[i])  # 访问第i个元素并相加

# 计算耗时:结束时间 - 开始时间
time_loop = time.time() - start

# ==================== 方法2:使用NumPy向量化运算 ====================
print('方法2:NumPy向量化(快)')  # 打印方法说明
# 记录开始时间
start = time.time()

# arr1 + arr2:NumPy向量化运算,使用加法运算符
# 这一行代码等价于上面的整个for循环
# 底层实现:C/Fortran优化的SIMD指令,并行处理多个数据
# 性能优势:避免Python解释器开销,利用CPU缓存和并行指令
result_vectorized = arr1 + arr2  # 向量化加法,一次性完成所有元素的加法

# 计算耗时:结束时间 - 开始时间
time_vectorized = time.time() - start

# ==================== 性能对比报告 ====================
print(f'数组大小: {n:,} 元素')  # f-string格式化,:n添加千位分隔符
print(f'Python循环时间: {time_loop:.4f}秒')  # 保留4位小数
print(f'向量化时间: {time_vectorized:.4f}秒')  # 保留4位小数
print(f'性能提升: {time_loop / time_vectorized:.1f}倍')  # 计算加速比

# ==================== 验证结果一致性 ====================
# np.allclose(a, b):检查两个数组的元素是否在容差范围内相等
# 容差:默认相对容差1e-5,绝对容差1e-8
# 返回值:True表示结果一致,False表示不一致
print(f'\n结果一致: {np.allclose(result_loop, result_vectorized)}')
# 预期输出:True,说明两种方法得到相同结果(浮点数可能有微小差异)

金融应用: 计算投资组合日收益率

列表 38.4
# =============================================================================
# 题目:向量化计算投资组合的日收益率和风险指标
# =============================================================================
# 本任务演示如何使用向量化运算计算投资组合的收益率、波动率和夏普比率
# 金融应用:现代投资组合理论(MPT),评估投资组合的风险调整后收益

# ==================== 定义投资组合参数 ====================
# n_stocks:投资组合中的股票数量
# 金融应用:A股约有5000只股票,一个分散化的组合通常包含50-200只股票
n_stocks = 100  # 100只股票的组合

# n_days:交易日数量
# A股每年约252个交易日(扣除周末和节假日)
n_days = 252  # 1年的交易日

# ==================== 生成随机权重并归一化 ====================
# np.random.random(n):生成n个[0, 1)均匀分布的随机数
# 返回值:一维数组,长度为n_stocks
# 金融应用:随机生成初始权重,然后进行优化
weights = np.random.random(n_stocks)  # 生成100个随机权重(0-1之间)

# weights / weights.sum():归一化操作,确保权重和为1
# weights.sum():计算权重总和
# 除法:每个权重除以总和,确保权重和为1(100%投资)
# 金融应用:投资组合权重必须满足预算约束(全部资金投资)
weights = weights / weights.sum()  # 归一化权重,使总和为1

# ==================== 生成股票收益率矩阵 ====================
# np.random.seed(42):设置随机种子,确保结果可复现
# 金融应用:调试模型时需要固定随机数,以便对比不同算法
np.random.seed(42)  # 固定随机种子

# np.random.randn(n_stocks, n_days):生成标准正态分布的随机矩阵
# 返回值:n_stocks×n_days的矩阵
# * 0.02:乘以0.02(2%),将标准差调整为2%(典型的日收益率波动率)
# 金融应用:模拟100只股票在252个交易日的日收益率
returns = np.random.randn(n_stocks, n_days) * 0.02  # 日收益率矩阵

# ==================== 向量化计算投资组合收益率 ====================
# weights @ returns:矩阵乘法运算符
# weights形状:(100,),一维数组
# returns形状:(100, 252),二维矩阵
# 运算规则:weights自动广播为(1, 100),然后执行矩阵乘法
# 返回值:(252,),一维数组,表示252天的投资组合收益率
# 金融应用:计算投资组合的每日收益率 = Σ(权重i × 股票i的收益率)
portfolio_returns = weights @ returns  # 矩阵乘法,计算组合收益率

# ==================== 计算投资组合统计指标 ====================
print('投资组合统计:')  # 打印标题

# 计算年化收益率
# portfolio_returns.mean():计算日收益率均值(日平均收益率)
# * 252:年化,将日收益率转换为年收益率(假设252个交易日)
# 金融应用:评估投资组合的年度表现
print(f'年化收益率: {portfolio_returns.mean() * 252:.4f}')

# 计算年化波动率
# portfolio_returns.std():计算日收益率标准差(日波动率)
# np.sqrt(252):根号252,用于年化波动率
# 金融公式:年化波动率 = 日波动率 × √252(假设收益率不相关)
# 金融应用:衡量投资组合的风险水平
print(f'年化波动率: {portfolio_returns.std() * np.sqrt(252):.4f}')

# 计算夏普比率(Sharpe Ratio)
# 夏普比率 = (年化收益率) / (年化波动率)
# 简化版:这里假设无风险利率为0
# 标准公式:SR = (Rp - Rf) / σp,其中Rp是组合收益率,Rf是无风险利率
# 金融应用:衡量单位风险的超额收益,评估风险调整后收益
print(f'夏普比率: {(portfolio_returns.mean() * 252) / (portfolio_returns.std() * np.sqrt(252)):.4f}')

# ==================== 计算累计收益率 ====================
# np.cumprod(1 + portfolio_returns):计算累计乘积
# 1 + portfolio_returns:将收益率转换为增长因子(如收益率0.02转换为1.02)
# .cumprod():累计连乘,计算每天的累计收益
# 例如:[1.02, 0.98, 1.03] -> [1.02, 1.02×0.98, 1.02×0.98×1.03]
# - 1:减去1,转换为收益率形式
# 金融应用:跟踪投资组合的累计收益曲线,评估长期表现
cum_returns = np.cumprod(1 + portfolio_returns) - 1  # 计算累计收益率
print(f'\n年累计收益率: {cum_returns[-1]:.4f}')  # 打印最后一天的累计收益率

代码深度解析:

  1. @运算符:
    • 矩阵乘法运算符
    • 等价于 np.matmul()
    • 金融应用:权重×收益率
  2. np.random.seed():
    • 设置随机种子
    • 确保结果可复现
    • 调试时必不可少
  3. 广播机制:
    • 不同形状数组运算时的自动对齐
    • 例如: (100,) @ (100, 252) = (252,)
    • NumPy自动扩展维度

38.4 Pandas Series与NumPy数组

38.4.1 Series的创建与操作

理论背景:Pandas的设计理念

Pandas建立在NumPy之上,提供了两个核心数据结构: 1. Series: 带标签的一维数组 2. DataFrame: 带标签的二维表格

Series vs NumPy数组:

特性 NumPy数组 Pandas Series
索引 整数位置(0,1,2…) 自定义标签(日期、股票代码等)
缺失值 不支持(需用NaN手动表示) 原生支持(np.nan)
数据对齐 手动对齐 自动对齐(根据索引)
功能 纯数值计算 数据分析、统计、可视化
性能 极快 较慢(但有优化)
列表 38.5
# =============================================================================
# 题目:从Python列表创建Pandas Series并进行基本操作
# =============================================================================
# 本任务演示Series的创建、索引操作和统计函数
# 金融应用:Series常用于存储时间序列数据,如股票价格、收益率序列

# ==================== 导入必要的库 ====================
import pandas as pd  # Pandas数据分析库
import numpy as np  # NumPy数值计算库

# ==================== 方法1:从列表创建Series ====================
# 定义一个Python列表,包含6个整数
# 金融应用:可以是6天的股票成交量、6个月的销售额等
lst = [1, 3, 5, 6, 10, 23]  # 创建Python列表

# pd.Series(lst):从列表创建Series
# Series:带标签的一维数组,是Pandas的核心数据结构之一
# 返回值:Series对象,自动生成默认索引(0, 1, 2, ...)
# 金融应用:存储一维时间序列数据,如每日收盘价
s1 = pd.Series(lst)  # 从列表创建Series

# ==================== 显示Series及其属性 ====================
print('Series:')  # 打印标题
print(s1)  # 打印完整Series,左侧是索引,右侧是值

# s1.index:查看Series的索引对象
# 返回值:RangeIndex(start=0, stop=6, step=1),默认整数索引
print(f'\n索引: {s1.index}')  # 打印索引

# s1.values:查看Series的值(NumPy数组)
# 返回值:ndarray,包含所有数据值
# 金融应用:提取数值数组用于NumPy计算
print(f'值: {s1.values}')  # 打印值数组

# s1.dtype:查看Series的数据类型
# 返回值:dtype,如int64、float64、object等
print(f'类型: {s1.dtype}')  # 打印数据类型

# ==================== 方法2:创建带自定义索引的Series ====================
# pd.date_range():生成日期索引
# '2023-01-01':起始日期
# periods=6:生成6个日期
# freq='D':频率,D表示日(Daily)
# 返回值:DatetimeIndex对象
# 金融应用:生成交易日历、财报发布日期等
dates = pd.date_range('2023-01-01', periods=6, freq='D')  # 生成6个连续日期

# pd.Series(data, index=...):创建带自定义索引的Series
# 第一个参数:数据,6个浮点数(股票价格)
# index=dates:指定索引为日期
# 金融应用:存储股票的每日收盘价,索引为交易日期
prices = pd.Series([100.5, 102.3, 101.8, 103.2, 104.5, 103.9], index=dates)

print('\n带日期索引的Series:')  # 打印标题
print(prices)  # 打印Series,左侧显示日期索引

# ==================== 按日期切片 ====================
# prices['2023-01-02':'2023-01-04']:使用日期字符串切片
# '2023-01-02':起始日期(包含)
# '2023-01-04':结束日期(包含,Series切片与列表不同)
# 返回值:子Series,包含3个数据点
# 金融应用:提取特定时间段的股价数据,分析某一周的表现
print(f'\n按日期切片:')  # 打印标题
print(prices['2023-01-02':'2023-01-04'])  # 打印1月2日到1月4日的股价

# ==================== 统计函数 ====================
print(f'\n统计指标:')  # 打印标题

# prices.mean():计算均值(平均价格)
# 返回值:浮点数,所有值的算术平均
# 金融应用:计算股票的平均价格,评估价格水平
print(f'  均值: {prices.mean():.2f}')  # 打印均值,保留2位小数

# prices.std():计算标准差(价格波动率)
# 返回值:浮点数,标准差衡量价格离散程度
# 金融应用:衡量股价波动性,标准差越大波动越大
print(f'  标准差: {prices.std():.2f}')  # 打印标准差

# prices.max():计算最大值(最高价)
# 返回值:浮点数,所有值中的最大值
# 金融应用:找出期间内的最高股价
print(f'  最大值: {prices.max():.2f}')  # 打印最大值

# prices.min():计算最小值(最低价)
# 返回值:浮点数,所有值中的最小值
# 金融应用:找出期间内的最低股价
print(f'  最小值: {prices.min():.2f}')  # 打印最小值

38.4.2 Series与NumPy互操作

列表 38.6
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目二
import pandas as pd

#(1)通过列表创建[1,3,5,6,10,23]的Series
lst = [1,3,5,6,10,23]
s1 = pd.Series(lst)  # 创建Series序列s1
print(s1)  # 输出(1)通过列表创建[1,3,5,6,10,23]的Serie

代码深度解析:

  1. .values属性:
    • 返回Series的NumPy数组表示
    • 丢失索引信息
    • 用于性能关键的数值计算
  2. 自动对齐:
    • Pandas的强大功能
    • 根据索引自动匹配
    • 不匹配位置填充NaN
  3. 索引选择:
    • 位置索引: .iloc[]
    • 标签索引: .loc[]
    • 布尔索引: []内放布尔条件

38.5 数组的统计分析

38.5.1 描述性统计

金融理论:收益率的统计特征

现代投资组合理论假设资产收益率服从正态分布,但实证研究发现: 1. 尖峰厚尾: 极端收益概率高于正态分布 2. 偏度: 负偏(下跌更多)或正偏(上涨更多) 3. 波动聚集: 高波动期后跟随高波动期

列表 38.7
# =============================================================================
# 题目:NumPy数组的描述性统计和分布形状分析
# =============================================================================
# 本任务演示如何计算数组的统计指标,包括均值、标准差、分位数、偏度、峰度
# 金融应用:分析股票收益率的统计特征,评估投资风险和收益分布

# ==================== 导入必要的库 ====================
import numpy as np  # NumPy数值计算库
from scipy.stats import skew, kurtosis  # 从SciPy导入偏度和峰度函数

# ==================== 模拟股票收益率数据 ====================
# np.random.seed(42):设置随机种子,确保结果可复现
np.random.seed(42)  # 固定随机种子

# np.random.randn(252):生成252个标准正态分布随机数
# 252:A股每年的交易日数量
# * 0.02:乘以0.02(2%),将标准差调整为2%(典型的日收益率波动率)
# 返回值:一维数组,包含252个日收益率
# 金融应用:模拟某只股票一年的日收益率序列
returns = np.random.randn(252) * 0.02  # 年化日收益率

# ==================== 计算描述性统计指标 ====================
print('收益率描述统计:')  # 打印标题

# returns.mean():计算均值(平均收益率)
# 返回值:浮点数,所有收益率的算术平均
# 金融应用:计算股票的平均日收益率,评估收益水平
print(f'  均值: {returns.mean():.6f}')  # 打印均值,保留6位小数

# returns.std():计算标准差(收益率波动率)
# 返回值:浮点数,标准差衡量收益率离散程度
# 金融应用:衡量股价波动性,标准差越大风险越大
print(f'  标准差: {returns.std():.6f}')  # 打印标准差

# returns.min():计算最小值(最大单日亏损)
# 返回值:浮点数,所有收益率中的最小值
# 金融应用:找出期间内最大单日跌幅
print(f'  最小值: {returns.min():.6f}')  # 打印最小值

# returns.max():计算最大值(最大单日收益)
# 返回值:浮点数,所有收益率中的最大值
# 金融应用:找出期间内最大单日涨幅
print(f'  最大值: {returns.max():.6f}')  # 打印最大值

# np.median(returns):计算中位数(50%分位数)
# 返回值:浮点数,排序后位于中间的值
# 金融应用:中位数比均值更稳健,不受极端值影响
print(f'  中位数: {np.median(returns):.6f}')  # 打印中位数

# ==================== 计算分位数 ====================
print(f'\n分位数:')  # 打印标题

# np.percentile(returns, 25):计算25%分位数(第一四分位数Q1)
# 参数25:百分位数,25%的数据小于此值
# 返回值:浮点数,25%分位数
# 金融应用:了解收益率的下四分位数,评估较差表现
print(f'  25%分位数(Q1): {np.percentile(returns, 25):.6f}')  # 打印Q1

# np.percentile(returns, 50):计算50%分位数(中位数)
# 参数50:百分位数,50%的数据小于此值
# 返回值:浮点数,中位数(与np.median()相同)
print(f'  50%分位数(中位数): {np.percentile(returns, 50):.6f}')  # 打印中位数

# np.percentile(returns, 75):计算75%分位数(第三四分位数Q3)
# 参数75:百分位数,75%的数据小于此值
# 返回值:浮点数,75%分位数
# 金融应用:了解收益率的上四分位数,评估较好表现
print(f'  75%分位数(Q3): {np.percentile(returns, 75):.6f}')  # 打印Q3

# ==================== 计算分布形状指标 ====================
print(f'\n分布形状:')  # 打印标题

# skew(returns):计算偏度(Skewness)
# 偏度:衡量分布的不对称性
# skew < 0:左偏(长尾在左侧,负向极端值更多)
# skew > 0:右偏(长尾在右侧,正向极端值更多)
# skew = 0:对称(如正态分布)
# 金融应用:股票收益率通常呈现负偏(暴跌更多)
print(f'  偏度: {skew(returns):.4f}')  # 打印偏度

# kurtosis(returns):计算峰度(Kurtosis)
# 峰度:衡量分布的尖峰程度(超峰度,Excess Kurtosis)
# kurtosis < 0:平峰(比正态分布更平坦)
# kurtosis > 0:尖峰(比正态分布更尖,极端值更多)
# kurtosis = 0:与正态分布相同
# 金融应用:股票收益率通常呈现正峰度(极端收益比正态分布预测的更多)
print(f'  峰度: {kurtosis(returns):.4f}')  # 打印峰度

# ==================== 计算年化指标 ====================
# 年化收益率:日均值 × 252
annual_return = returns.mean() * 252  # 计算年化收益率

# 年化波动率:日标准差 × √252
# 公式:σ年化 = σ日 × √252(假设收益率不相关)
# np.sqrt(252):根号252
annual_vol = returns.std() * np.sqrt(252)  # 计算年化波动率

# 夏普比率(简化版):年化收益率 / 年化波动率
# 标准公式:SR = (Rp - Rf) / σp,这里假设无风险利率Rf=0
# 金融应用:衡量单位风险的超额收益,评估风险调整后收益
sharpe_ratio = annual_return / annual_vol  # 计算夏普比率

print(f'\n年化指标:')  # 打印标题
print(f'  年化收益率: {annual_return:.4f} ({annual_return:.2%})')  # 打印年化收益率(小数和百分数)
print(f'  年化波动率: {annual_vol:.4f} ({annual_vol:.2%})')  # 打印年化波动率(小数和百分数)
print(f'  夏普比率: {sharpe_ratio:.4f}')  # 打印夏普比率

代码深度解析:

  1. 偏度(Skewness):
    • 衡量分布的不对称性
    • skew < 0: 左偏(长尾在左侧)
    • skew > 0: 右偏(长尾在右侧)
  2. 峰度(Kurtosis):
    • 衡量分布的尖峰程度
    • 正态分布的峰度=0(超峰度)
    • kurtosis < 0: 平峰(比正态分布更平坦)
    • kurtosis > 0: 尖峰(比正态分布更尖)
  3. 夏普比率:
    • 衡量风险调整后收益
    • SR = (收益率 - 无风险利率) / 波动率
    • 这里简化为收益率/波动率

38.5.2 百分位数与分箱

金融应用:业绩排名与分档

列表 38.8
# =============================================================================
# 题目:使用百分位数和分箱分析基金经理业绩排名
# =============================================================================
# 本任务演示如何使用百分位数进行排名,以及分箱操作将数据分组
# 金融应用:基金业绩评估、股票分级、投资者分层分析

# ==================== 导入必要的库 ====================
import numpy as np  # NumPy数值计算库

# ==================== 模拟基金经理业绩数据 ====================
# np.random.seed(42):设置随机种子,确保结果可复现
np.random.seed(42)  # 固定随机种子

# np.random.randn(100):生成100个标准正态分布随机数
# * 0.15:乘以0.15(15%),标准差调整为15%(基金年化收益率的典型波动)
# + 0.08:加上0.08(8%),均值调整为8%(市场平均收益率)
# 返回值:一维数组,包含100只基金的年化收益率
# 金融应用:模拟100只公募基金的年化收益率
fund_returns = np.random.randn(100) * 0.15 + 0.08  # 100只基金的年化收益率

# ==================== 计算业绩分位数 ====================
print('基金业绩分位数:')  # 打印标题

# np.percentile(fund_returns, 50):计算50%分位数(中位数)
# 参数50:百分位数,50%的基金收益率低于此值
# 返回值:浮点数,中位数收益率
# 金融应用:了解基金行业中位数水平
print(f'  中位数(50%): {np.percentile(fund_returns, 50):.4f}')  # 打印中位数

# np.percentile(fund_returns, 75):计算75%分位数(前25%的门槛)
# 参数75:百分位数,75%的基金收益率低于此值(即前25%的最低门槛)
# 返回值:浮点数,75%分位数
# 金融应用:确定"优秀"基金的门槛(前25%)
print(f'  前25%(75%): {np.percentile(fund_returns, 75):.4f}')  # 打印75%分位数

# np.percentile(fund_returns, 90):计算90%分位数(前10%的门槛)
# 参数90:百分位数,90%的基金收益率低于此值(即前10%的最低门槛)
# 返回值:浮点数,90%分位数
# 金融应用:确定"顶尖"基金的门槛(前10%)
print(f'  前10%(90%): {np.percentile(fund_returns, 90):.4f}')  # 打印90%分位数

# np.percentile(fund_returns, 99):计算99%分位数(前1%的门槛)
# 参数99:百分位数,99%的基金收益率低于此值(即前1%的最低门槛)
# 返回值:浮点数,99%分位数
# 金融应用:确定"明星"基金的门槛(前1%)
print(f'  前1%(99%): {np.percentile(fund_returns, 99):.4f}')  # 打印99%分位数

# ==================== 分箱(Binning)操作 ====================
# 分箱:将连续数值分组到离散的箱子(bins)中
# 金融应用:将基金按业绩分为5档(1星到5星)

# 定义分位数阈值
# bins:要计算的分位数列表
# [0, 0.25, 0.5, 0.75, 1.0]:0%(最小值)、25%、50%、75%、100%(最大值)
bins = [0, 0.25, 0.5, 0.75, 1.0]  # 定义分位数列表

# np.quantile(fund_returns, bins):计算指定分位数的值
# 参数bins:分位数列表
# 返回值:一维数组,包含各分位数对应的收益率值
quantiles = np.quantile(fund_returns, bins)  # 计算分位数阈值

print(f'\n业绩分档阈值:')  # 打印标题
# enumerate(quantiles):遍历分位数数组,同时获取索引和值
# for i, q:i是索引(0-4),q是对应的分位数值
for i, q in enumerate(quantiles):  # 遍历分位数
    # i*25:将索引转换为百分比(0, 25, 50, 75, 100)
    print(f'  {i*25}%分位: {q:.4f}')  # 打印各分位数的阈值

# ==================== 使用digitize进行分箱 ====================
# np.digitize(fund_returns, quantiles):将每个数据点分配到对应的箱子
# fund_returns:要分箱的数据
# quantiles:箱子的边界值
# 返回值:整数数组,表示每个基金所在的箱子编号(0-4)
# 注意:箱子编号从1开始(大于第一个边界),所以需要减1
ranks = np.digitize(fund_returns, quantiles)  # 将基金分配到各档

print(f'\n基金数量分布:')  # 打印标题
# range(5):生成0-4的序列(5个箱子)
for i in range(5):  # 遍历5个箱子
    # ranks == i:布尔数组,True表示该基金在第i档
    # .sum():统计True的数量(即第i档的基金数量)
    count = (ranks == i).sum()  # 计算第i档的基金数量
    print(f'  第{i+1}档: {count}只基金')  # 打印各档的基金数量

38.6 数组的重塑与转置

38.6.1 形状操作

理论背景:数组重塑的内存视图

重塑(Reshaping)操作通常会返回视图(View)而非副本(Copy),这意味着修改重塑后的数组会影响原数组。

重要概念: - C顺序: 行优先(默认,C语言风格) - F顺序: 列优先(Fortran风格) - 内存连续: flags['C']flags['F']

列表 38.9
# =============================================================================
# 题目:NumPy数组的重塑、转置和展平操作
# =============================================================================
# 本任务演示如何改变数组的形状(维度),以及转置和展平操作
# 金融应用:数据预处理、矩阵运算、数据格式转换

# ==================== 导入必要的库 ====================
import numpy as np  # NumPy数值计算库

# ==================== 创建一维数组用于演示 ====================
# np.arange(1, 13):生成1到12的整数序列(不包含13)
# 返回值:一维数组,[1, 2, 3, ..., 12]
# 金融应用:创建12个月的数据序列
arr = np.arange(1, 13)  # 创建1-12的数组

print('原始数组(1维):')  # 打印标题
print(arr)  # 打印一维数组

# ==================== 重塑为3×4矩阵 ====================
# arr.reshape(3, 4):将一维数组重塑为3行4列的二维矩阵
# 参数3:行数
# 参数4:列数
# 注意:3×4=12,元素总数必须匹配(12个元素)
# 返回值:二维数组,形状为(3, 4)
# 金融应用:将月度数据重塑为季度×月度的矩阵
matrix_3x4 = arr.reshape(3, 4)  # 重塑为3×4矩阵

print('\n重塑为3×4矩阵:')  # 打印标题
print(matrix_3x4)  # 打印3×4矩阵

# ==================== 重塑为4×3矩阵 ====================
# arr.reshape(4, 3):将一维数组重塑为4行3列的二维矩阵
# 参数4:行数
# 参数3:列数
# 注意:4×3=12,元素总数必须匹配
# 返回值:二维数组,形状为(4, 3)
matrix_4x3 = arr.reshape(4, 3)  # 重塑为4×3矩阵

print('\n重塑为4×3矩阵:')  # 打印标题
print(matrix_4x3)  # 打印4×3矩阵

# ==================== 重塑为2×6矩阵 ====================
# arr.reshape(2, 6):将一维数组重塑为2行6列的二维矩阵
# 参数2:行数
# 参数6:列数
# 注意:2×6=12,元素总数必须匹配
# 返回值:二维数组,形状为(2, 6)
matrix_2x6 = arr.reshape(2, 6)  # 重塑为2×6矩阵

print('\n重塑为2×6矩阵:')  # 打印标题
print(matrix_2x6)  # 打印2×6矩阵

# ==================== 矩阵转置 ====================
# matrix_4x3.T:矩阵转置运算符,.T是转置属性
# 转置:行变列,列变行
# 原形状:(4, 3) -> 转置后形状:(3, 4)
# 返回值:转置后的矩阵
# 金融应用:收益率矩阵转置,从"日期×股票"转为"股票×日期"
print('\n4×3矩阵的转置:')  # 打印标题
print(matrix_4x3.T)  # 打印转置后的矩阵(3×4)

# ==================== 展平操作 ====================
print('\n展平为一维数组:')  # 打印标题

# matrix_3x4.ravel():将多维数组展平为一维数组
# 返回:视图(View),共享原数组的内存
# 修改返回值会影响原数组
# 性能优势:不复制数据,速度快,内存效率高
print(matrix_3x4.ravel())  # 展平并返回视图

# matrix_3x4.flatten():将多维数组展平为一维数组
# 返回:副本(Copy),独立的新数组
# 修改返回值不会影响原数组
# 性能考虑:需要复制数据,占用额外内存
print(matrix_3x4.flatten())  # 展平并返回副本

# ==================== 多维数组展平 ====================
# np.arange(24):生成0-23的整数序列(24个元素)
# .reshape(2, 3, 4):重塑为3维数组
# 2:第一个维度(如2个样本)
# 3:第二个维度(如3个时间点)
# 4:第三个维度(如4个特征)
# 返回值:3维数组,形状为(2, 3, 4)
# 金融应用:2只股票,3个时间点,4个财务指标的数据立方
arr_3d = np.arange(24).reshape(2, 3, 4)  # 创建3维数组

print(f'\n3维数组形状: {arr_3d.shape}')  # 打印3维数组的形状
print(f'展平后形状: {arr_3d.ravel().shape}')  # 打印展平后的形状(一维,24个元素)

金融应用: 收益率矩阵的宽转长

列表 38.10
# =============================================================================
# 题目:使用DataFrame转置实现宽格式与长格式的转换
# =============================================================================
# 本任务演示如何通过转置操作在宽格式和长格式之间转换数据
# 金融应用:数据可视化、统计分析、机器学习特征工程

# ==================== 导入必要的库 ====================
import pandas as pd  # Pandas数据分析库
import numpy as np  # NumPy数值计算库

# ==================== 创建宽格式数据 ====================
# 宽格式:行=日期,列=股票,每个单元格是收益率
# 金融应用:这是常见的数据存储格式,方便查看和比较

# np.random.randn(5, 3):生成5×3的标准正态分布随机矩阵
# 5:5个交易日
# 3:3只股票
# 返回值:二维数组,形状为(5, 3)
returns_wide = np.random.randn(5, 3)  # 生成5天3只股票的收益率矩阵

# pd.date_range('2023-01-01', periods=5):生成5个连续的交易日
# '2023-01-01':起始日期
# periods=5:生成5个日期
# 返回值:DatetimeIndex对象,包含5个日期
dates = pd.date_range('2023-01-01', periods=5)  # 生成日期索引

# 定义股票列表(列名)
# 金融应用:A股典型的权重股
stocks = ['贵州茅台', '五粮液', '招商银行']  # 3只股票的名称

# ==================== 创建宽格式DataFrame ====================
print('宽格式(日期×股票):')  # 打印标题

# pd.DataFrame(returns_wide, index=dates, columns=stocks):创建DataFrame
# returns_wide:数据,5×3的收益率矩阵
# index=dates:行索引,交易日期
# columns=stocks:列名,股票名称
# 返回值:DataFrame对象,宽格式
# 金融应用:宽格式便于查看各股票在各个日期的收益率
df_wide = pd.DataFrame(returns_wide, index=dates, columns=stocks)  # 创建宽格式DataFrame

print(df_wide)  # 打印宽格式DataFrame
# 输出格式:
#            贵州茅台    五粮液  招商银行
# 2023-01-01  0.123   -0.456   0.789
# 2023-01-02  1.234    0.567  -0.890
# ...

# ==================== 转置为长格式 ====================
# df_wide.T:DataFrame转置运算符,.T是转置属性
# 转置:行变列,列变行
# 原格式:行=日期(5行),列=股票(3列)
# 转置后:行=股票(3行),列=日期(5列)
# 返回值:转置后的DataFrame
# 金融应用:长格式便于分析单只股票的时间序列,或用于绘制K线图
print('\n长格式(股票×日期):')  # 打印标题
df_long = df_wide.T  # 转置DataFrame

print(df_long)  # 打印长格式DataFrame
# 输出格式:
#           2023-01-01  2023-01-02  2023-01-03  ...
# 贵州茅台      0.123      1.234      2.345  ...
# 五粮液      -0.456      0.567      0.678  ...
# 招商银行      0.789     -0.890     -0.901  ...