第二章:Python数据处理基础:Pandas入门、NumPy数组操作、金融时间序列处理、数据清洗实战

说实话,金融数据处理这活儿,80%的时间都在跟数据较劲。不是格式不对,就是缺胳膊少腿。我刚开始做量化策略的时候,光是把交易所的Level-2行情数据洗干净,就熬了三个通宵。嗯,从那以后我就明白了——工具链再花哨,数据处理的基本功不过关,一切都是白搭。

这一章,咱们就聊聊Python里最核心的几个数据处理工具。Pandas、NumPy,还有金融时间序列那点事儿。我会把我在实战中踩过的坑、总结的经验,一股脑儿倒给你。

Python数据处理核心 NumPy 数组操作 创建数组 | 索引切片 广播机制 | 数学运算 Pandas 入门 DataFrame | Series 分组聚合 | 合并连接 金融时间序列 重采样 | 滑动窗口 时区处理 | 滞后算子 数据清洗实战 缺失值处理 异常值检测 重复数据去重 数据类型转换 标准化/归一化 金融数据对齐 从原始数据到可用特征

2.1 NumPy:金融计算的基石

NumPy这东西,说白了就是Python里的C语言。速度快,内存省。我见过不少新手用Python列表做金融计算,跑个回测要等半小时。换成NumPy数组,几秒钟搞定。为什么?因为底层是C写的,还用了SIMD指令集。

核心要点: NumPy数组(ndarray)是连续内存块,支持向量化操作。Python列表是对象指针数组,每个元素都是PyObject。性能差距就在这里。

创建数组的几种姿势

import numpy as np

# 从列表创建
prices = np.array([100.5, 101.2, 99.8, 102.1])

# 全零数组,常用于初始化
zeros = np.zeros((5, 3))  # 5行3列

# 等差数列,金融里算时间轴常用
time_axis = np.arange(0, 10, 0.5)  # 从0到10,步长0.5

# 随机数,蒙特卡洛模拟必备
np.random.seed(42)  # 固定种子,保证可复现
returns = np.random.randn(1000)  # 标准正态分布

我个人习惯,在写策略回测框架时,所有价格数据都用np.float64。精度够用,速度也快。别用float128,除非你算的是天文数字。

索引与切片:别小看这个

金融数据经常要取某一段。比如取最近20个交易日的收盘价。NumPy的切片效率极高,因为它不复制数据,只是创建一个视图。

data = np.array([100, 102, 101, 103, 105, 104, 106])

# 取最后3个
last_3 = data[-3:]  # [105, 104, 106]

# 布尔索引,筛选出大于102的值
mask = data > 102
filtered = data[mask]  # [103, 105, 104, 106]

# 花式索引,取第1、3、5个
indices = [0, 2, 4]
selected = data[indices]  # [100, 101, 105]
避坑指南: 我曾经在回测里用花式索引取数据,结果发现每次取的都是副本,修改原数组不会影响它。后来改用切片,性能提升了一倍。记住:切片返回视图,花式索引返回副本。

广播机制:让不同形状的数组做运算

这个机制太实用了。比如你有一个股票组合的权重向量,和一个收益率矩阵。想算组合收益率?直接乘就行。

weights = np.array([0.3, 0.4, 0.3])  # 3只股票的权重
returns = np.array([[0.01, 0.02, -0.01],  # 3个时间点,3只股票
                    [0.02, 0.01, 0.03],
                    [-0.01, 0.03, 0.02]])

# 广播:weights自动扩展成3x3
portfolio_returns = returns @ weights  # 矩阵乘法
# 结果:[0.008, 0.019, 0.014]

你想想看,如果没有广播,你得写个循环,一行一行算。又慢又丑。

2.2 Pandas:金融数据的瑞士军刀

Pandas就是为表格数据而生的。金融数据天然就是表格——时间戳、开盘价、收盘价、成交量...一行行排下来。用Pandas处理,顺手得不行。

DataFrame与Series

Series是一列数据,DataFrame是多列。我习惯把DataFrame想象成Excel表格,但比Excel灵活一万倍。

import pandas as pd

# 从字典创建DataFrame
data = {
    'date': ['2024-01-02', '2024-01-03', '2024-01-04'],
    'open': [100.5, 101.2, 99.8],
    'close': [101.2, 99.8, 102.1],
    'volume': [10000, 12000, 9500]
}
df = pd.DataFrame(data)

# 查看前几行
print(df.head())

# 基本统计
print(df.describe())
实战经验: 我建议你永远把时间列设为索引。这样后面做时间序列操作会方便很多。用 df.set_index('date', inplace=True) 一步搞定。

分组聚合:按股票、按日期汇总

金融数据经常要分组。比如按股票代码分组,算每只股票的日均收益率。Pandas的groupby就是干这个的。

# 假设有多只股票的数据
df = pd.DataFrame({
    'stock': ['AAPL', 'AAPL', 'GOOG', 'GOOG', 'MSFT', 'MSFT'],
    'date': ['2024-01-02', '2024-01-03', '2024-01-02', '2024-01-03', '2024-01-02', '2024-01-03'],
    'return': [0.01, -0.02, 0.03, 0.01, -0.01, 0.02]
})

# 按股票分组,算平均收益率
avg_return = df.groupby('stock')['return'].mean()
print(avg_return)
# stock
# AAPL   -0.005
# GOOG    0.020
# MSFT    0.005

嗯,这里要注意。groupby之后的操作,尽量用内置的聚合函数(mean, sum, std)。别自己写循环,慢得离谱。

合并与连接:把多张表拼起来

做金融分析,经常要把行情数据和财务数据合并。Pandas的merge就像SQL里的JOIN。

# 行情数据
prices = pd.DataFrame({
    'date': ['2024-01-02', '2024-01-03'],
    'close': [100, 102],
    'stock': 'AAPL'
})

# 财务数据
financials = pd.DataFrame({
    'date': ['2024-01-02', '2024-01-03'],
    'pe_ratio': [25.3, 24.8],
    'stock': 'AAPL'
})

# 内连接
merged = pd.merge(prices, financials, on=['date', 'stock'], how='inner')
print(merged)
注意: 合并时一定要确认连接键的唯一性。我曾经因为日期列里有重复值,合并后数据膨胀了三倍,查了半天才发现。用 df.duplicated() 先检查一下。

2.3 金融时间序列处理

金融数据最特殊的地方,就是时间。不是所有时间都有数据——周末、节假日都是空的。而且不同市场的时间还不一样。A股和美股的时间戳,差了12个小时。

重采样:把高频数据降频

我经常要把分钟级数据变成日线。Pandas的resample就是干这个的。

# 假设有分钟级数据
idx = pd.date_range('2024-01-02 09:30', '2024-01-02 16:00', freq='1min')
df = pd.DataFrame({'price': np.random.randn(len(idx)).cumsum() + 100}, index=idx)

# 重采样为日线,取收盘价
daily = df.resample('D').last()

# 或者取OHLC
ohlc = df.resample('D').ohlc()

我个人习惯,重采样时用 .last() 取收盘价,用 .first() 取开盘价。别用 .mean(),那算出来的是均价,不是金融里常用的OHLC。

滑动窗口:算移动平均线

技术分析里最常用的就是移动平均线。Pandas的rolling函数,一行代码搞定。

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

# 布林带:20日均线 ± 2倍标准差
df['std20'] = df['close'].rolling(window=20).std()
df['upper'] = df['ma20'] + 2 * df['std20']
df['lower'] = df['ma20'] - 2 * df['std20']
小技巧: 滑动窗口默认是用过去的数据。如果你想用未来的数据(比如中心移动平均),加个 center=True 参数。但注意,这会引入未来信息,回测时千万别用。

时区处理:跨市场数据对齐

做全球资产配置时,时区是个大麻烦。A股收盘是北京时间15:00,美股开盘是北京时间21:30。怎么对齐?

import pytz

# 设置时区
df_asia = df_asia.tz_localize('Asia/Shanghai')
df_us = df_us.tz_localize('US/Eastern')

# 统一转换到UTC
df_asia_utc = df_asia.tz_convert('UTC')
df_us_utc = df_us.tz_convert('UTC')

# 现在可以对齐了
aligned = pd.concat([df_asia_utc, df_us_utc], axis=1)

我曾经在做一个跨市场套利策略时,因为时区没处理好,信号早了8个小时触发。嗯,那次回测结果漂亮得不像真的——后来发现是数据对齐出了问题。

2.4 数据清洗实战

真实世界的金融数据,脏得超乎你想象。缺失值、异常值、重复数据、格式不一致...我敢说,任何一个金融数据集,至少10%的数据需要清洗。

缺失值处理

缺失值在金融数据里太常见了。停牌、节假日、数据源故障...处理方法就几种:删除、填充、插值。

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

# 删除有缺失的行
df_clean = df.dropna()

# 向前填充(用上一个有效值填充)
df_filled = df.fillna(method='ffill')

# 线性插值
df_interpolated = df.interpolate(method='linear')
我的建议: 金融时间序列,用向前填充(ffill)最合理。因为停牌期间,价格理论上应该沿用上一个交易日的收盘价。别用均值填充,那会引入未来信息。

异常值检测

异常值可能是数据错误,也可能是真实的极端行情(比如闪崩)。怎么区分?我一般用两种方法。

# 方法1:3σ原则
mean = df['return'].mean()
std = df['return'].std()
outliers = df[(df['return'] < mean - 3*std) | (df['return'] > mean + 3*std)]

# 方法2:IQR(四分位距)
Q1 = df['return'].quantile(0.25)
Q3 = df['return'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['return'] < Q1 - 1.5*IQR) | (df['return'] > Q3 + 1.5*IQR)]

我个人更倾向于IQR方法。因为金融收益率不是正态分布,3σ原则会漏掉很多尾部风险。

重复数据去重

数据源偶尔会重复推送。比如某一天的数据出现了两次。去重很简单,但要注意判断标准。

# 基于所有列去重
df_unique = df.drop_duplicates()

# 基于特定列去重(比如日期+股票代码)
df_unique = df.drop_duplicates(subset=['date', 'stock'])

# 保留最后一个重复值
df_unique = df.drop_duplicates(subset=['date', 'stock'], keep='last')
注意: 去重前一定要排序。否则你不知道保留的是哪个。我习惯先按时间排序,再用 keep='last' 保留最新的那条。

数据类型转换

从CSV读进来的数据,经常是字符串。比如价格列是'100.5'这种字符串。不转成float,没法做计算。

# 查看数据类型
print(df.dtypes)

# 转换类型
df['price'] = pd.to_numeric(df['price'], errors='coerce')
df['date'] = pd.to_datetime(df['date'])

# 处理千分位分隔符
df['volume'] = df['volume'].str.replace(',', '').astype(float)

嗯,这里要注意。errors='coerce' 会把无法转换的值变成NaN。这样你就能看到哪些数据有问题了。

金融数据对齐:不同频率的数据怎么合并

这是实战里最头疼的问题。你有日线数据,还有季度财报数据。怎么把两者对齐?

# 日线数据
daily = pd.DataFrame({'price': [100, 101, 102]}, 
                     index=pd.date_range('2024-01-02', periods=3))

# 季度数据
quarterly = pd.DataFrame({'eps': [1.2, 1.5]},
                         index=pd.to_datetime(['2024-03-31', '2024-06-30']))

# 向前填充:用最新的季度数据填充到每一天
daily['eps'] = quarterly['eps'].reindex(daily.index, method='ffill')

# 或者用merge_asof:按时间最近匹配
aligned = pd.merge_asof(daily, quarterly, left_index=True, right_index=True, direction='backward')

我强烈推荐 merge_asof。它比普通的merge更智能,会自动找到最近的时间点。做因子分析时,这个函数救了我无数次。

总结一下: 数据清洗没有银弹。每个数据集都有自己的脾气。我的经验是:先看数据,再动手。用 df.info()df.describe() 摸清底细,然后对症下药。别一上来就咔咔删数据,有时候那些「异常值」恰恰是最值钱的信息。

公众号:蓝海资料掘金营,微信deep3321