4、NumPy与Pandas入门:从数组到数据框的量化基本功

说实话,很多刚入行的朋友问我:「量化分析到底难在哪?」我通常会反问一句:「你处理过一百万行行情数据吗?」如果没处理过,那第一步就是学会用 NumPy 和 Pandas。这两个库,说白了就是量化分析的左膀右臂。一个管数值计算,一个管表格操作。今天我们就从最基础的东西开始,一步步把它们变成你的肌肉记忆。

本章核心目标:掌握 NumPy 数组的向量化运算思维,熟练使用 Pandas 的 Series 和 DataFrame 进行数据清洗与时间序列处理。学完这一章,你就能独立处理大部分金融数据预处理工作。

4.1 NumPy 数组基础:告别 Python 原生循环

我记得刚做量化那会儿,用 Python 列表算股票收益率,一个循环跑下来,数据量稍微大点就卡得不行。后来才知道,NumPy 数组才是干这活的正经工具。

4.1.1 创建数组的几种方式

创建数组其实很简单,但不同场景有不同用法。我个人最常用的是这三种:

import numpy as np

# 从列表创建
arr1 = np.array([1, 2, 3, 4, 5])

# 创建全零数组,常用于初始化
arr2 = np.zeros((3, 4))

# 创建等差数组,做回测时间轴时很实用
arr3 = np.arange(0, 10, 2)  # [0, 2, 4, 6, 8]

小技巧:用 np.linspace(0, 1, 5) 可以生成等间隔的浮点数,做归一化处理时特别好用。

4.1.2 向量化运算:量化分析的灵魂

为什么说向量化运算重要?你想想看,如果你要对一万只股票的收益率做平方运算,用 Python 循环要写一万次,用 NumPy 只需要一次。这就是向量化的威力——它把循环放到了 C 语言层面执行,速度能快几十倍。

prices = np.array([100, 102, 101, 105, 108])
returns = np.diff(prices) / prices[:-1]  # 一行代码算收益率
squared_returns = returns ** 2           # 向量化平方运算

print(returns)
# [ 0.02  -0.0098  0.0396  0.0286]

我在项目中遇到过一个问题:用循环算 500 万行数据的移动平均,跑了快两分钟。改成 NumPy 的卷积操作后,三秒搞定。嗯,这就是向量化的魅力。

4.1.3 广播机制:不同形状数组也能算

广播机制刚开始可能有点绕,但用熟了会觉得特别自然。简单说,就是 NumPy 会自动把小数组「扩展」成大数组的形状再计算。

# 对每只股票减去当天市场均值
daily_returns = np.random.randn(5, 3)  # 5天,3只股票
market_mean = daily_returns.mean(axis=1, keepdims=True)  # 每行均值
excess_returns = daily_returns - market_mean  # 广播自动完成

曾经踩过的坑:广播虽然方便,但维度不对时会静默出错。我建议每次广播前都用 .shape 检查一下数组维度,尤其是 keepdims=True 这个参数,忘了加的话形状会从 (5,1) 变成 (5,),广播结果就全错了。

4.2 Pandas Series 与 DataFrame:表格数据的瑞士军刀

NumPy 管数值计算,Pandas 管表格操作。两者配合起来,基本能覆盖量化分析 90% 的数据处理需求。

4.2.1 Series:带标签的一维数组

Series 其实就是带索引的 NumPy 数组。为什么需要索引?因为金融数据天然带有时间标签,没有索引的数据就像没有日期的行情,毫无意义。

import pandas as pd

# 创建 Series,指定日期索引
dates = pd.date_range('2024-01-01', periods=5, freq='D')
close_prices = pd.Series([100, 102, 101, 105, 108], index=dates)

# 按标签取数据
print(close_prices['2024-01-03'])  # 101

# 布尔索引筛选
print(close_prices[close_prices > 102])

4.2.2 DataFrame:二维表格的核心

DataFrame 可以理解成多个 Series 拼在一起。每一列是一个 Series,每一行是一个样本。做量化时,通常行是时间,列是股票代码或因子名称。

# 创建 DataFrame
data = {
    'AAPL': [150, 152, 149, 155, 157],
    'GOOGL': [2800, 2810, 2790, 2830, 2850],
    'MSFT': [300, 305, 298, 310, 315]
}
df = pd.DataFrame(data, index=dates)

# 查看基本信息
print(df.head())      # 前5行
print(df.describe())  # 统计摘要
操作 代码示例 说明
选取列 df['AAPL'] 返回 Series
选取行 df.loc['2024-01-03'] 按标签索引
选取行列 df.iloc[0:3, 0:2] 按位置索引
条件筛选 df[df > 300] 返回布尔掩码

个人习惯:我一般用 .loc 做标签索引,用 .iloc 做位置索引。千万别混用,否则代码可读性会变得很差。

4.3 数据清洗与预处理:真实数据从来不会干干净净

你从数据商拿到的数据,几乎不可能直接用来建模。缺失值、异常值、重复数据……这些才是常态。我刚开始做量化时,花在数据清洗上的时间比建模还多。

4.3.1 处理缺失值

金融数据缺失的原因很多:停牌、数据源中断、节假日等等。处理方式取决于你的策略需求。

# 检查缺失值
print(df.isnull().sum())

# 方法一:删除缺失行(数据量大时用)
df_clean = df.dropna()

# 方法二:向前填充(时间序列常用)
df_filled = df.fillna(method='ffill')

# 方法三:插值填充(适合连续数据)
df_interpolated = df.interpolate()

曾经踩过的坑:有一次我用 dropna() 删除了所有缺失行,结果发现回测结果异常好。后来一查,原来缺失值都集中在市场大跌的日子,删除后相当于「幸存者偏差」。所以,处理缺失值前一定要先分析缺失模式。

4.3.2 检测与处理异常值

异常值可能是数据错误,也可能是真实的极端行情。我通常用两种方法判断:

  • 标准差法:超过均值 ± 3 倍标准差的值视为异常
  • 四分位距法:低于 Q1-1.5IQR 或高于 Q3+1.5IQR 的值视为异常
# 标准差法检测异常值
mean = df['AAPL'].mean()
std = df['AAPL'].std()
outliers = df[(df['AAPL'] > mean + 3*std) | (df['AAPL'] < mean - 3*std)]

# 处理方式:截断或替换
df['AAPL'] = df['AAPL'].clip(mean - 3*std, mean + 3*std)

4.3.3 重复数据处理

数据源偶尔会重复推送同一时刻的数据。重复数据会导致计算偏差,必须处理。

# 检查重复行
print(df.duplicated().sum())

# 删除重复行,保留第一个
df_unique = df.drop_duplicates(keep='first')

4.4 时间序列数据处理:量化分析的核心战场

量化分析本质上就是时间序列分析。Pandas 对时间序列的支持非常强大,用好这些功能能让你的代码简洁很多。

4.4.1 时间索引与重采样

不同策略需要不同频率的数据。日频策略用日线,高频策略用分钟线。重采样就是改变数据频率的过程。

# 创建分钟级数据
minute_data = pd.DataFrame({
    'price': np.random.randn(1000).cumsum() + 100
}, index=pd.date_range('2024-01-01', periods=1000, freq='T'))

# 重采样为小时线
hourly_data = minute_data.resample('H').agg({
    'price': ['first', 'max', 'min', 'last']
})
重采样规则 代码 适用场景
日线 .resample('D') 日频策略
周线 .resample('W') 周频调仓
月线 .resample('M') 月度因子分析
分钟线 .resample('5T') 高频交易

4.4.2 滚动窗口计算

移动平均、滚动标准差、滚动相关系数……这些是量化策略的基础构件。

# 20日移动平均
df['MA_20'] = df['AAPL'].rolling(window=20).mean()

# 滚动标准差(波动率)
df['volatility'] = df['AAPL'].rolling(window=20).std()

# 滚动相关系数
df['rolling_corr'] = df['AAPL'].rolling(window=20).corr(df['GOOGL'])

个人经验:滚动窗口的大小直接影响策略灵敏度。窗口越小,信号越灵敏但噪音也越大。我一般先用 20 日窗口做初步分析,再根据策略回测结果调整。

4.4.3 时间偏移与滞后变量

很多因子需要用到历史数据,比如「过去 5 天的收益率」。Pandas 的 shift() 函数就是干这个的。

# 创建滞后变量
df['return_today'] = df['AAPL'].pct_change()
df['return_yesterday'] = df['AAPL'].pct_change().shift(1)

# 计算未来收益率(用于预测)
df['future_return'] = df['AAPL'].pct_change().shift(-1)

曾经踩过的坑:用 shift(-1) 计算未来收益率时,最后一行会变成 NaN。如果不处理,回测时可能会用到未来数据。我建议每次 shift 后都检查一下首尾的 NaN 值。

NumPy 与 Pandas 知识体系 NumPy 数组 数组创建 向量化运算 广播机制 Pandas 数据结构 Series DataFrame 数据清洗 缺失值 异常值 重复数据 时间序列数据处理 重采样 滚动窗口 滞后变量 NumPy 负责高效数值计算,Pandas 负责结构化数据处理,两者结合覆盖量化分析 90% 的数据工作

好了,这一章的内容就到这里。NumPy 和 Pandas 是量化分析的地基,地基打不牢,后面盖什么楼都容易塌。我建议你打开 Jupyter Notebook,把上面的代码都敲一遍,尤其是数据清洗和时间序列那部分——这些才是实际工作中用得最多的。