38.数据特殊结构(数组)

本章概要

  • 学习材料:二维收益数组、权重向量与宽表收益数据。
  • 本章任务:运行 lst-portfolio-calclst-wide-long-convert,完成axis统计、逐行点积和宽长转换。
  • 完成后你将得到:列均值、逐行组合收益、shape与宽长表。
  • 自我检查:用显式循环复算每行点积并检查shape守恒;广播或轴错位时先检查原因。
  • 拓展练习:把数组方法应用到另一组三资产。

本章学习目标

  • 理解NumPy数组与Python列表的区别
  • 掌握数组的创建、索引与切片操作
  • 学会向量化运算及其性能优势
  • 掌握Pandas Series的创建与操作
  • 理解数组的统计分析方法
  • 掌握数组的重塑与转置操作

为什么需要NumPy数组?

Python原生列表的局限性

  • 性能问题:列表存储对象指针,访问需间接寻址
  • 内存效率低:每个元素都是完整Python对象
  • 不利于向量化计算:列表可混合类型

NumPy数组的设计哲学

  • 紧凑存储倾向:新建数组通常连续;切片、转置等视图可能非连续,应用 flags 实测
  • 同构数据:所有元素类型相同
  • 向量化操作:通过底层优化指令并行处理

NumPy性能优势:在特定基准中可能显著提速

# Python列表:1000万元素求和 ≈ 150ms
# NumPy数组:1000万元素求和 ≈ 5ms
# 性能倍数取决于硬件、版本、dtype、数组规模与具体运算,必须实测

金融应用场景

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

数组创建的三种策略

策略 方法 适用场景
从序列转换 np.array([1,2,3]) 小规模数据
生成函数 np.zeros(), np.ones() 初始化、模拟
从文件读取 np.loadtxt() 导入外部数据

常用生成函数:

  • np.arange(10):类似range()的数组版本
  • np.random.*:随机数生成系列函数

运行前预测|实操:创建股票收益率数组

  • 输入预测:运行前先写出 stocksdaysstock_day 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到stock_day 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“实操:创建股票收益率数组”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 实操:创建股票收益率数组

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目一
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])

任务复盘|实操:创建股票收益率数组

运行后核对:核对 stocksdaysstock_day 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

数组的四大核心属性

属性 说明 示例
shape 数组维度(行×列) (2000, 500)
ndim 维度数量 2
size 元素总数 1000000
dtype 数据类型 float64
  • shape返回元组,表示各维度大小
  • size等于shape各维度的乘积

一维数组的索引与切片

Listing 1: 一维数组索引与切片
import numpy as np

arr_1d = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])

print('基本索引:')
print(f'  arr_1d[0] = {arr_1d[0]}')     # 正索引:第一个元素
print(f'  arr_1d[-1] = {arr_1d[-1]}')   # 负索引:最后一个元素

print('\n切片操作:')
print(f'  arr_1d[2:7] = {arr_1d[2:7]}')   # 索引2到6
print(f'  arr_1d[::2] = {arr_1d[::2]}')   # 步长为2
基本索引:
  arr_1d[0] = 10
  arr_1d[-1] = 100

切片操作:
  arr_1d[2:7] = [30 40 50 60 70]
  arr_1d[::2] = [10 30 50 70 90]

切片语法:[start:stop:step]

  • start:起始索引(包含),省略则从头开始
  • stop:结束索引(不包含),省略则到末尾
  • step:步长,省略默认为1

金融应用

  • arr[2:7]:提取第3天到第7天的数据
  • arr[::2]:从日频数据中提取隔日数据

二维数组的索引

Listing 2: 二维数组索引操作
arr_2d = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
])

print('二维数组索引:')
print(f'  arr_2d[0, 0] = {arr_2d[0, 0]}')       # 第0行第0列
print(f'  arr_2d[1, :] = {arr_2d[1, :]}')        # 第1行所有列
print(f'  arr_2d[:, 2] = {arr_2d[:, 2]}')        # 所有行第2列
print(f'  arr_2d[0:2, 1:3] =\n{arr_2d[0:2, 1:3]}')  # 子矩阵
二维数组索引:
  arr_2d[0, 0] = 1
  arr_2d[1, :] = [5 6 7 8]
  arr_2d[:, 2] = [ 3  7 11]
  arr_2d[0:2, 1:3] =
[[2 3]
 [6 7]]

花式索引与布尔索引

Listing 3: 花式索引与布尔索引
arr_1d = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])

# 花式索引:使用整数数组,可不连续访问
indices = [0, 2, 4]
print(f'花式索引 arr_1d[{indices}]: {arr_1d[indices]}')

# 布尔索引:使用条件过滤
bool_idx = arr_1d > 50
print(f'布尔索引 arr_1d[>50]: {arr_1d[bool_idx]}')
花式索引 arr_1d[[0, 2, 4]]: [10 30 50]
布尔索引 arr_1d[>50]: [ 60  70  80  90 100]

金融应用

  • 花式索引:提取特定日期(季末、财报日)的数据
  • 布尔索引:筛选上涨股票、正收益交易日

视图(View) vs 副本(Copy)

Listing 4: 视图与副本的区别
arr = np.array([10, 20, 30, 40, 50])

slice_view = arr[1:4]        # 切片返回视图
slice_copy = arr[1:4].copy() # .copy()创建副本

slice_view[0] = 999          # 修改视图 → 影响原数组
slice_copy[0] = 888          # 修改副本 → 不影响原数组

print(f'原数组: {arr}')       # arr[1]变为999
print(f'副本:   {slice_copy}') # 独立,第0个是888
原数组: [ 10 999  30  40  50]
副本:   [888  30  40]

关键区别:基本切片通常返回共享内存的视图,高级索引通常返回副本;是否连续用 arr.flags 检查,.copy()显式创建独立副本。

向量化运算:避免Python循环

向量化(Vectorization):用数组表达式代替显式循环

  • 避免Python循环的解释器开销
  • 利用底层C/Fortran优化的SIMD指令
  • 充分利用CPU缓存和并行计算能力
# 循环方式(慢)
for i in range(n):
    result.append(arr1[i] + arr2[i])

# 向量化方式(快)
result = arr1 + arr2

性能对比实验

Listing 5: 向量化运算性能对比
展开实现代码
import numpy as np
import time

n = 10_000_000
arr1 = np.random.randn(n)
arr2 = np.random.randn(n)

# Python循环
start = time.time()
result_loop = [arr1[i] + arr2[i] for i in range(n)]
time_loop = time.time() - start

# NumPy向量化
start = time.time()
result_vec = arr1 + arr2
time_vec = time.time() - start

print(f'数组大小: {n:,} 元素')
print(f'Python循环: {time_loop:.4f}秒')
print(f'向量化运算: {time_vec:.4f}秒')
print(f'性能提升: {time_loop / time_vec:.1f}倍')
print(f'结果一致: {np.allclose(result_loop, result_vec)}')
数组大小: 10,000,000 元素
Python循环: 1.9893秒
向量化运算: 0.0110秒
性能提升: 180.2倍
结果一致: True

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

Listing 6: 向量化计算投资组合收益率
import numpy as np

n_stocks = 100   # 100只股票
n_days = 252     # 1年交易日

# 随机权重归一化
weights = np.random.random(n_stocks)
weights = weights / weights.sum()

# 模拟日收益率矩阵
np.random.seed(42)
returns = np.random.randn(n_stocks, n_days) * 0.02

# 矩阵乘法计算组合收益率
portfolio_returns = weights @ returns  # (100,) @ (100,252) = (252,)

投资组合统计指标

Listing 7: 投资组合年化统计指标
print('投资组合统计:')
print(f'  年化收益率: {portfolio_returns.mean() * 252:.4f}')
print(f'  年化波动率: {portfolio_returns.std() * np.sqrt(252):.4f}')

# 夏普比率 = 年化收益率 / 年化波动率(假设无风险利率为0)
sharpe = (portfolio_returns.mean() * 252) / (portfolio_returns.std() * np.sqrt(252))
print(f'  夏普比率:   {sharpe:.4f}')

# 累计收益率
cum_returns = np.cumprod(1 + portfolio_returns) - 1
print(f'  年累计收益: {cum_returns[-1]:.4f}')
投资组合统计:
  年化收益率: 0.0062
  年化波动率: 0.0359
  夏普比率:   0.1736
  年累计收益: 0.0056

@ 运算符与广播机制

@ 运算符:矩阵乘法

  • 等价于 np.matmul()
  • weights @ returns:权重向量 × 收益率矩阵

广播机制:不同形状数组的自动对齐

  • (100,) @ (100, 252)(252,)
  • NumPy自动扩展维度完成运算

Pandas Series:带标签的一维数组

特性 NumPy数组 Pandas Series
索引 整数位置 自定义标签
缺失值 需用NaN手动表示 原生支持
数据对齐 手动对齐 自动对齐
功能 纯数值计算 统计、可视化

从列表创建Series

Listing 8: 从列表创建Series
展开实现代码
import pandas as pd
import numpy as np

lst = [1, 3, 5, 6, 10, 23]
s1 = pd.Series(lst)

print('Series:')
print(s1)
print(f'\n索引: {s1.index}')
print(f'值:   {s1.values}')
print(f'类型: {s1.dtype}')
Series:
0     1
1     3
2     5
3     6
4    10
5    23
dtype: int64

索引: RangeIndex(start=0, stop=6, step=1)
值:   [ 1  3  5  6 10 23]
类型: int64

带日期索引的Series

Listing 9: 带日期索引的Series及切片
展开实现代码
import pandas as pd

dates = pd.date_range('2023-01-01', periods=6, freq='D')
prices = pd.Series(
    [100.5, 102.3, 101.8, 103.2, 104.5, 103.9],
    index=dates
)

print('带日期索引的Series:')
print(prices)

print('\n按日期切片(1月2日到4日):')
print(prices['2023-01-02':'2023-01-04'])
带日期索引的Series:
2023-01-01    100.5
2023-01-02    102.3
2023-01-03    101.8
2023-01-04    103.2
2023-01-05    104.5
2023-01-06    103.9
Freq: D, dtype: float64

按日期切片(1月2日到4日):
2023-01-02    102.3
2023-01-03    101.8
2023-01-04    103.2
Freq: D, dtype: float64

Series的统计函数

Listing 10: Series描述统计
展开实现代码
print('统计指标:')
print(f'  均值:   {prices.mean():.2f}')
print(f'  标准差: {prices.std():.2f}')
print(f'  最大值: {prices.max():.2f}')
print(f'  最小值: {prices.min():.2f}')
统计指标:
  均值:   102.70
  标准差: 1.47
  最大值: 104.50
  最小值: 100.50

索引选择方式

  • 位置索引:.iloc[]
  • 标签索引:.loc[]
  • 布尔索引:series[条件]

运行前预测|实操:通过列表创建Series

  • 输入预测:运行前先写出 lsts1 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到s1 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“实操:通过列表创建Series”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 实操:通过列表创建Series

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目二
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

任务复盘|实操:通过列表创建Series

运行后核对:核对 lsts1 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

收益率的统计特征

现代投资组合理论假设收益率服从正态分布,但实证发现:

  • 尖峰厚尾:极端收益概率高于正态分布
  • 偏度:负偏(暴跌更多)或正偏(上涨更多)
  • 波动聚集:高波动期后跟随高波动期

关键统计量:

  • 偏度(Skewness):分布不对称性
  • 峰度(Kurtosis):尖峰程度

描述性统计与分位数

Listing 11: 收益率描述性统计
import numpy as np
from scipy.stats import skew, kurtosis

np.random.seed(42)
returns = np.random.randn(252) * 0.02  # 模拟一年日收益率

print('收益率描述统计:')
print(f'  均值:   {returns.mean():.6f}')
print(f'  标准差: {returns.std():.6f}')
print(f'  最小值: {returns.min():.6f}')
print(f'  最大值: {returns.max():.6f}')
print(f'  中位数: {np.median(returns):.6f}')
收益率描述统计:
  均值:   -0.000075
  标准差: 0.019306
  最小值: -0.052395
  最大值: 0.077055
  中位数: 0.001184

分位数与分布形状

Listing 12: 分位数与分布形状指标
print('分位数:')
print(f'  Q1(25%): {np.percentile(returns, 25):.6f}')
print(f'  Q2(50%): {np.percentile(returns, 50):.6f}')
print(f'  Q3(75%): {np.percentile(returns, 75):.6f}')

print(f'\n分布形状:')
print(f'  偏度: {skew(returns):.4f}')     # <0左偏, >0右偏
print(f'  峰度: {kurtosis(returns):.4f}')  # >0尖峰, <0平峰
分位数:
  Q1(25%): -0.013711
  Q2(50%): 0.001184
  Q3(75%): 0.011861

分布形状:
  偏度: 0.3002
  峰度: 0.5796

年化指标计算

Listing 13: 年化收益率、波动率与夏普比率
展开实现代码
annual_return = returns.mean() * 252           # 年化收益率
annual_vol = returns.std() * np.sqrt(252)      # 年化波动率
sharpe_ratio = annual_return / annual_vol      # 夏普比率

print('年化指标:')
print(f'  年化收益率: {annual_return:.4f} ({annual_return:.2%})')
print(f'  年化波动率: {annual_vol:.4f} ({annual_vol:.2%})')
print(f'  夏普比率:   {sharpe_ratio:.4f}')
年化指标:
  年化收益率: -0.0190 (-1.90%)
  年化波动率: 0.3065 (30.65%)
  夏普比率:   -0.0619

年化公式

  • 年化收益率 = 日均收益率 × 252
  • 年化波动率 = 日标准差 × \(\sqrt{252}\)

百分位数与分箱:基金业绩排名

Listing 14: 基金业绩分位数排名
import numpy as np
np.random.seed(42)

# 模拟100只基金的年化收益率(均值8%,标准差15%)
fund_returns = np.random.randn(100) * 0.15 + 0.08

print('基金业绩分位数:')
print(f'  中位数(50%):  {np.percentile(fund_returns, 50):.4f}')
print(f'  前25%(75%):   {np.percentile(fund_returns, 75):.4f}')
print(f'  前10%(90%):   {np.percentile(fund_returns, 90):.4f}')
print(f'  前1%(99%):    {np.percentile(fund_returns, 99):.4f}')
基金业绩分位数:
  中位数(50%):  0.0610
  前25%(75%):   0.1409
  前10%(90%):   0.2309
  前1%(99%):    0.3173

分箱操作:基金分档

Listing 15: 使用digitize进行基金分档
展开实现代码
bins = [0, 0.25, 0.5, 0.75, 1.0]
quantiles = np.quantile(fund_returns, bins)

print('业绩分档阈值:')
for i, q in enumerate(quantiles):
    print(f'  {i*25}%分位: {q:.4f}')

ranks = np.digitize(fund_returns, quantiles)

print('\n各档基金数量:')
for i in range(5):
    count = (ranks == i).sum()
    print(f'  第{i+1}档: {count}只基金')
业绩分档阈值:
  0%分位: -0.3130
  25%分位: -0.0101
  50%分位: 0.0610
  75%分位: 0.1409
  100%分位: 0.3578

各档基金数量:
  第1档: 0只基金
  第2档: 25只基金
  第3档: 25只基金
  第4档: 25只基金
  第5档: 24只基金

数组重塑:reshape

Listing 16: 数组重塑操作
展开实现代码
import numpy as np

arr = np.arange(1, 13)
print(f'原始数组(1维): {arr}')

print(f'\n重塑为3×4矩阵:')
print(arr.reshape(3, 4))

print(f'\n重塑为4×3矩阵:')
print(arr.reshape(4, 3))
原始数组(1维): [ 1  2  3  4  5  6  7  8  9 10 11 12]

重塑为3×4矩阵:
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

重塑为4×3矩阵:
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]

注意:元素总数必须匹配(12 = 3×4 = 4×3 = 2×6)

矩阵转置与展平

Listing 17: 矩阵转置与展平
展开实现代码
matrix = np.arange(1, 7).reshape(3, 2)
print('3×2矩阵:')
print(matrix)

print(f'\n转置(.T) → 2×3:')
print(matrix.T)

print(f'\nravel()展平(视图):  {matrix.ravel()}')
print(f'flatten()展平(副本): {matrix.flatten()}')
3×2矩阵:
[[1 2]
 [3 4]
 [5 6]]

转置(.T) → 2×3:
[[1 3 5]
 [2 4 6]]

ravel()展平(视图):  [1 2 3 4 5 6]
flatten()展平(副本): [1 2 3 4 5 6]

展平选择:视图还是副本

ravel vs flatten

  • ravel():尽可能返回视图,但在非连续布局下也可能复制;用 np.shares_memory 核对
  • flatten():返回副本,修改不影响原数组

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

Listing 18: 收益率矩阵的宽长格式转换
展开实现代码
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=5)
stocks = ['贵州茅台', '五粮液', '招商银行']

df_wide = pd.DataFrame(
    np.random.randn(5, 3).round(4),
    index=dates, columns=stocks
)

print('宽格式(日期×股票):')
print(df_wide)

print('\n长格式(股票×日期) — 通过转置:')
print(df_wide.T)
宽格式(日期×股票):
              贵州茅台     五粮液    招商银行
2023-01-01  0.4967 -0.1383  0.6477
2023-01-02  1.5230 -0.2342 -0.2341
2023-01-03  1.5792  0.7674 -0.4695
2023-01-04  0.5426 -0.4634 -0.4657
2023-01-05  0.2420 -1.9133 -1.7249

长格式(股票×日期) — 通过转置:
      2023-01-01  2023-01-02  2023-01-03  2023-01-04  2023-01-05
贵州茅台      0.4967      1.5230      1.5792      0.5426      0.2420
五粮液      -0.1383     -0.2342      0.7674     -0.4634     -1.9133
招商银行      0.6477     -0.2341     -0.4695     -0.4657     -1.7249

本章总结

主题 核心要点
数组创建 np.array(), np.random, np.arange()
索引切片 [start:stop:step], 花式索引, 布尔索引
向量化运算 避免循环,性能提升数十倍
Series 带标签的一维数组,自动对齐
统计分析 均值、标准差、偏度、峰度
重塑转置 reshape(), .T, ravel()

随堂练习

  • 问题 1|需要准备哪些数据?:二维收益数组、权重向量与宽表收益数据。
  • 问题 2|需要完成哪些操作?:运行 lst-portfolio-calclst-wide-long-convert,完成axis统计、逐行点积和宽长转换。
  • 问题 3|应得到哪些结果?:列均值、逐行组合收益、shape与宽长表。
  • 问题 4|怎样确认结果可靠?:用显式循环复算每行点积并检查shape守恒;广播或轴错位时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把数组方法应用到另一组三资产。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:收益矩阵 returns 形状 (3日,2资产),权重 weights 形状 (2,);均为有限浮点数。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import numpy as np  # 导入数组工具以完成矩阵运算与数值检查
returns=np.array([[.01,.02],[-.02,.01],[.03,-.01]],dtype=float)  # 建立确定性收益输入
weights=np.array([.6,.4])  # 按资产列顺序固定组合权重
portfolio_returns=returns@weights  # 用矩阵乘法计算逐日组合收益
demeaned_returns=returns-returns.mean(axis=0)  # 按资产列去均值并满足条件合法广播
manual_column_means=np.array([returns[:,column_index].sum()/len(returns) for column_index in range(returns.shape[1])])  # 逐列手算两项资产均值
manual_portfolio_returns=np.array([sum(return_value*weight_value for return_value,weight_value in zip(return_row,weights)) for return_row in returns])  # 逐行点积核对组合收益
assert returns.shape==(3,2) and weights.shape==(2,)  # 核对日期×资产与权重维度约定
assert np.allclose(returns.mean(axis=0),manual_column_means)  # 用逐列手算核对轴向均值
assert np.allclose(portfolio_returns,manual_portfolio_returns)  # 用逐行点积核对矩阵乘法
print(portfolio_returns,demeaned_returns,returns.flags['C_CONTIGUOUS'])  # 输出组合收益、广播结果与连续性标志

教师参考解答|答案与说明 2

  • 解释答案:数组的 shape 决定轴的业务含义,广播只在维度兼容时成立;组合收益的矩阵乘法必须让资产列顺序与权重顺序完全一致。
  • 拓展应用答案:拓展应用到三资产周收益矩阵时,先声明周×资产字段,再替换权重并检查 shape、权重和为1及逐行手算结果。
  • 参考结果:3个组合收益、去均值矩阵与连续性标志。另行核对:用逐行点积核对 @,用 np.shares_memory 检查切片是视图还是副本。
  • 常见错误:把 * 当矩阵乘法;维度静默广播;把特定基准倍数或“所有视图连续”当事实。