4. Pandas基础(下):数据清洗、合并与分组聚合
好,咱们接着聊Pandas。上一章我们把DataFrame和Series的基本操作过了一遍,这一章要啃的,是真正实战中绕不开的硬骨头——数据清洗、合并和分组聚合。
说实话,我做了这么多年量化,接触过的原始数据没有一份是干净的。要么缺字段,要么有重复,要么格式乱七八糟。你想想看,如果数据本身就有问题,后面再漂亮的策略也是白搭。所以这一章的内容,我个人觉得是整个Pandas里最实用的部分。
4.1 数据清洗:处理缺失值与重复值
数据清洗,说白了就是给数据“洗澡”。把脏的、乱的、重复的都处理掉。
4.1.1 缺失值处理
缺失值在量化数据里太常见了。比如某只股票停牌了,那天的收盘价就是NaN。又或者数据源本身就有问题,某几行数据没抓下来。
Pandas里判断缺失值,用isnull()和notnull()。我个人习惯先用info()看一眼整体情况。
import pandas as pd
import numpy as np
# 模拟一份有缺失的数据
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
'close': [100.0, np.nan, 102.0, np.nan],
'volume': [10000, 12000, np.nan, 15000]
})
print(df.info())
print(df.isnull().sum())
处理缺失值,主要有两种思路:
- 删除:用
dropna()。如果缺失的行不多,直接删掉最省事。 - 填充:用
fillna()。如果数据宝贵,就得想办法填上。
核心经验:量化回测中,我一般不会轻易删除数据。尤其是时间序列数据,删掉一行可能导致时间轴不连续,后面计算收益率时会出问题。
# 删除所有包含缺失值的行
df_clean = df.dropna()
# 用前一个有效值填充(向前填充)
df_filled = df.fillna(method='ffill')
# 用后一个有效值填充(向后填充)
df_filled_bf = df.fillna(method='bfill')
# 用固定值填充,比如用均值
df['close'] = df['close'].fillna(df['close'].mean())
嗯,这里要注意:用ffill填充价格数据时,如果股票连续停牌多天,你会看到一连串相同的价格。这在回测里会引入“未来信息”的错觉。我曾经踩过这个坑,回测收益高得离谱,实盘一跑就露馅了。
4.1.2 重复值处理
重复值在数据合并时特别容易出现。比如你从两个数据源分别拉了同一只股票的数据,合并后就有重复行。
判断重复用duplicated(),删除重复用drop_duplicates()。
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02'],
'close': [100.0, 100.0, 101.0, 101.0]
})
# 查看哪些行是重复的
print(df.duplicated())
# 删除重复行,默认保留第一个出现的
df_unique = df.drop_duplicates()
# 按指定列判断重复,比如只看date列
df_unique_date = df.drop_duplicates(subset=['date'])
小技巧:我习惯在每次合并数据后,立刻执行一次drop_duplicates()。这就像写完代码随手保存一样,是个好习惯。
4.2 数据合并:concat、merge、join
量化分析中,数据很少只来自一个地方。你要把行情数据、财务数据、因子数据拼在一起。Pandas提供了三种合并方式,各有各的用法。
4.2.1 concat:简单拼接
concat就像把几块积木直接拼在一起。可以按行拼,也可以按列拼。
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'B': [7, 8]})
# 按行拼接(增加行数)
df_row = pd.concat([df1, df2], axis=0)
# 按列拼接(增加列数)
df_col = pd.concat([df1, df2], axis=1)
按行拼接时,如果两个DataFrame的列名不完全一致,concat会用NaN填充缺失的列。我刚开始用的时候没注意这个,结果数据里莫名其妙多了一堆NaN,排查了半天。
4.2.2 merge:类似SQL的JOIN
merge是真正的“合并”,它根据某个共同的键(key)来匹配数据。这跟SQL里的JOIN操作一模一样。
# 行情数据
price_df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-02'],
'close': [100.0, 101.0]
})
# 财务数据
fund_df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-02'],
'pe': [15.0, 14.5]
})
# 内连接:只保留两边都有的日期
merged_inner = pd.merge(price_df, fund_df, on='date', how='inner')
# 左连接:保留左边所有日期,右边没有的填NaN
merged_left = pd.merge(price_df, fund_df, on='date', how='left')
| how参数 | 含义 | 类似SQL |
|---|---|---|
| inner | 只保留匹配上的行 | INNER JOIN |
| left | 保留左表所有行 | LEFT JOIN |
| right | 保留右表所有行 | RIGHT JOIN |
| outer | 保留两表所有行 | FULL OUTER JOIN |
避坑指南:我曾经在merge时忘了指定on参数,结果Pandas自动用所有同名列做匹配,导致数据量暴增,回测直接卡死。记住:merge时一定要明确指定连接键。
4.2.3 join:基于索引的合并
join其实是merge的一种特例,它默认用索引来合并。如果你把日期设成了索引,用join会非常方便。
price_df = price_df.set_index('date')
fund_df = fund_df.set_index('date')
# 基于索引合并
joined_df = price_df.join(fund_df, how='inner')
4.3 分组聚合:groupby
分组聚合是量化分析的核心操作。你想啊,我们经常要按股票代码分组,计算每只股票的均值、标准差、累计收益率。这就是groupby的拿手好戏。
它的逻辑很简单:拆分-应用-合并。先按某个字段把数据拆成若干组,然后对每组应用一个函数,最后把结果合并起来。
# 模拟多只股票的数据
df = pd.DataFrame({
'stock': ['A', 'A', 'B', 'B', 'C', 'C'],
'date': ['2024-01-01', '2024-01-02'] * 3,
'return': [0.01, 0.02, -0.01, 0.03, 0.005, 0.01]
})
# 按股票分组,计算每只股票的平均收益率
grouped = df.groupby('stock')['return'].mean()
print(grouped)
# 同时计算多个统计量
stats = df.groupby('stock')['return'].agg(['mean', 'std', 'min', 'max'])
print(stats)
你想想看,如果不用groupby,你要手动写循环去遍历每只股票,那代码得多啰嗦。Pandas的groupby一行就搞定了。
更高级的用法是transform。它跟agg的区别是:agg返回的是压缩后的结果(每组一行),而transform返回的是跟原DataFrame一样形状的结果。
# 计算每只股票收益率相对于自身均值的偏离
df['return_deviation'] = df.groupby('stock')['return'].transform(lambda x: x - x.mean())
print(df)
实战经验:我在做因子分析时,经常用groupby配合transform来做截面标准化。比如把每只股票的因子值减去当天所有股票的平均值,再除以标准差。这样就能消除市场整体的影响,只看个股的相对强弱。
知识体系总览
这一章的内容比较多,我画了一张图帮你理清思路:
这一章的内容就到这里。数据清洗、合并和分组聚合,是量化回测中每天都要用的基本功。你把这些练熟了,后面写策略框架就会顺手很多。
公众号:蓝海数据掘金营,微信deep3321