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

本章学习目标

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

为什么需要NumPy数组?

Python原生列表的局限性

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

NumPy数组的设计哲学

  • 连续内存:元素在内存中连续存储
  • 同构数据:所有元素类型相同
  • 向量化操作:通过底层优化指令并行处理

NumPy性能优势:30倍提速

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

金融应用场景

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

数组创建的三种策略

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

常用生成函数:

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

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

Listing 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])
(2000, 500)
[[ 0.21286835  0.71661866 -0.32039462  0.08239331  0.37557726]
 [ 1.99367332 -0.79226854  0.81194438 -0.09726054 -0.68500609]
 [-1.36229143 -0.93272179 -0.16735951  0.90490057  0.71315935]
 [ 1.27674206  1.76844381  1.03884891 -0.70976226 -0.04694749]
 [ 0.99955956  0.12310658 -1.03571279  0.48472374 -1.97164068]]

数组的四大核心属性

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

一维数组的索引与切片

Listing 2
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 3
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 4
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 5
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]

关键区别:切片是视图(共享内存),.copy()是独立副本。

向量化运算:避免Python循环

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

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

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

性能对比实验

Listing 6
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.7238秒
向量化运算: 0.0077秒
性能提升: 223.7倍
结果一致: True

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

Listing 7
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 8
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.0041
  年化波动率: 0.0360
  夏普比率:   -0.1151
  年累计收益: -0.0048

@ 运算符与广播机制

@ 运算符:矩阵乘法

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

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

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

Pandas Series:带标签的一维数组

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

从列表创建Series

Listing 9
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 10
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 11
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

Listing 12
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目二
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
0     1
1     3
2     5
3     6
4    10
5    23
dtype: int64

收益率的统计特征

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

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

关键统计量:

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

描述性统计与分位数

Listing 13
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 14
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 15
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 16
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 17
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 18
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))

print(f'\n重塑为2×6矩阵:')
print(arr.reshape(2, 6))
原始数组(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]]

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

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

矩阵转置与展平

Listing 19
matrix = np.arange(1, 13).reshape(4, 3)
print('4×3矩阵:')
print(matrix)

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

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

转置(.T) → 3×4:
[[ 1  4  7 10]
 [ 2  5  8 11]
 [ 3  6  9 12]]

ravel()展平(视图):  [ 1  2  3  4  5  6  7  8  9 10 11 12]
flatten()展平(副本): [ 1  2  3  4  5  6  7  8  9 10 11 12]

ravel vs flatten

  • ravel():返回视图,修改会影响原数组
  • flatten():返回副本,修改不影响原数组

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

Listing 20
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()