第三章 数据获取与清洗:数据源选择、标准化与异常处理

做量化策略,第一步就是搞数据。这一步要是没做好,后面所有模型都是空中楼阁。我个人习惯把数据获取和清洗看作整个策略开发的「地基工程」——地基歪了,楼盖得再漂亮也得塌。

今天咱们就聊聊数据这块。说白了,就是三件事:去哪拿数据、怎么让数据整齐、怎么处理脏数据。

3.1 数据源选择:Wind、Tushare、AKShare

国内做公募量化,数据源就那么几个。我这些年三个都用过,各有各的脾气。

数据源 特点 适用场景 成本
Wind 机构标配,数据全,接口稳定 公募、券商等专业机构 高(年费数万)
Tushare 社区活跃,数据覆盖广,有积分限制 个人研究、中小团队 低(部分免费)
AKShare 开源免费,更新快,接口丰富 快速原型、学习验证 免费

Wind 是行业标准。我在公募时,公司买的Wind终端,每天开盘前自动拉数据。它的好处是数据质量高,财务数据、估值数据都很全。但有个坑——Wind的Python接口(WindPy)偶尔会断连,尤其是长时间跑批量任务时。我曾经有个回测跑了一整夜,早上发现Wind连接断了,数据只拉到一半...嗯,从那以后我养成了「每拉1000条数据就检查一次连接」的习惯。

Tushare 是我个人比较喜欢的。它的数据结构设计得很规范,文档也清晰。不过要注意积分限制——高频数据、历史全量数据都需要消耗积分。我建议新手先用免费接口跑通流程,再考虑付费。

AKShare 是后起之秀。它最大的优势是「快」——接口更新速度极快,经常今天交易所出新规,明天AKShare就支持了。但缺点也很明显:接口稳定性不如前两者,偶尔会有字段名变动的情况。我一般用它做快速验证,正式策略还是用Wind或Tushare。

我的建议: 如果你在机构工作,老老实实用Wind。个人研究的话,Tushare + AKShare 组合拳最香——Tushare拿历史数据,AKShare拿实时数据。

3.2 数据格式标准化

数据拿到手,第一件事不是分析,而是「对齐」。不同数据源返回的格式千奇百怪——有的日期是字符串,有的是时间戳;有的股票代码带后缀,有的不带。

我一般会做三件事:

  1. 统一日期格式:全部转为 YYYY-MM-DD 格式
  2. 统一股票代码:去掉后缀,统一为6位数字
  3. 统一字段命名:比如「收盘价」统一叫 close,别一会儿 close_price 一会儿 closing_price

下面是我常用的标准化函数:

def standardize_data(df, source='wind'):
    """数据标准化处理"""
    # 统一日期
    if 'trade_date' in df.columns:
        df['date'] = pd.to_datetime(df['trade_date'])
    elif 'datetime' in df.columns:
        df['date'] = pd.to_datetime(df['datetime'])
    
    # 统一股票代码
    if 'code' in df.columns:
        df['stock_code'] = df['code'].str[:6]
    
    # 统一字段名
    rename_map = {
        'close': 'close',
        'close_price': 'close',
        'closing_price': 'close',
        'volume': 'volume',
        'vol': 'volume',
        'amount': 'amount',
        'amt': 'amount'
    }
    df.rename(columns=rename_map, inplace=True)
    
    return df

你想想看,如果每个数据源都用自己的命名方式,写策略时得写多少if-else?标准化这一步,省下来的时间够你喝好几杯咖啡了。

3.3 缺失值处理

缺失值,说白了就是「该有的数据没有」。原因很多:停牌、新股上市、数据源漏了...处理方式就三种:

  • 删除:缺失比例太高(比如超过50%),直接删掉
  • 填充:用前值、均值、中位数填充
  • 插值:用线性插值或时间序列方法补全

我个人习惯是:先看缺失比例,再决定怎么处理

经验法则:
  • 缺失 < 5%:用前值填充(forward fill)
  • 缺失 5%-20%:用线性插值
  • 缺失 > 20%:直接删除该特征或股票

我曾经遇到过一个情况:某只股票的市盈率数据,连续三个月都是缺失的。一开始我用了前值填充,结果回测表现特别好——后来发现是因为那只股票停牌了,前值填充相当于「用停牌前的数据预测停牌后的走势」,这明显是未来函数。嗯,从那以后我处理缺失值时,一定会先检查「为什么缺失」。

def handle_missing(df, method='ffill', threshold=0.2):
    """处理缺失值"""
    # 计算缺失比例
    missing_ratio = df.isnull().mean()
    
    # 删除缺失过多的列
    cols_to_drop = missing_ratio[missing_ratio > threshold].index
    df.drop(columns=cols_to_drop, inplace=True)
    
    # 处理剩余缺失
    if method == 'ffill':
        df.fillna(method='ffill', inplace=True)
        df.fillna(method='bfill', inplace=True)  # 开头缺失用后值
    elif method == 'interpolate':
        df.interpolate(method='linear', inplace=True)
    
    return df

3.4 异常值检测

异常值,就是那些「明显不对劲」的数据。比如某只股票一天涨了1000%,或者市盈率突然变成负数——这些大概率是数据错误。

检测方法主要有三种:

方法 原理 适用场景
3σ原则 超过均值±3倍标准差视为异常 正态分布的数据
箱线图法 超过Q1-1.5IQR或Q3+1.5IQR视为异常 非正态分布的数据
业务规则 根据常识设定上下限 所有场景(推荐)

我个人最推荐的是「业务规则法」。为什么?因为统计方法有时候会误判。举个例子:某只股票因为重大资产重组,一天涨了50%。按3σ原则,这可能是异常值;但从业务角度看,这是合理的。所以我的做法是:先用业务规则过滤明显错误,再用统计方法辅助判断

注意: 千万不要机械地删除所有异常值。有些「异常」其实是市场极端情况,比如2015年股灾、2020年疫情暴跌——这些恰恰是策略需要学习的场景。
def detect_outliers(df, col='pct_chg', method='business'):
    """异常值检测"""
    if method == 'business':
        # A股涨跌幅限制:±10%(ST股±5%)
        lower_bound = -0.105  # 留一点容错
        upper_bound = 0.105
        outliers = (df[col] < lower_bound) | (df[col] > upper_bound)
    elif method == 'iqr':
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        outliers = (df[col] < lower_bound) | (df[col] > upper_bound)
    
    return outliers

3.5 本章知识体系

下面这张图,把数据获取与清洗的整个流程串起来了。你可以把它当作操作手册——每次拿到新数据,按这个流程走一遍就行。

数据获取与清洗流程 数据源选择 Wind / Tushare / AKShare 数据获取 API调用 / 批量拉取 格式标准化 日期/代码/字段 缺失值处理 删除 / 填充 / 插值 异常值检测 3σ / 箱线图 / 业务规则 ✅ 干净数据 常见问题 • Wind连接断开 • Tushare积分不足 • AKShare字段变动 • 停牌导致缺失 • 涨跌停异常值 • 财务数据错位

数据清洗这件事,说白了就是「把原材料处理成能下锅的食材」。别嫌麻烦,这一步做扎实了,后面的策略开发会顺畅很多。我见过太多人急着跑模型,结果被脏数据坑得怀疑人生——嗯,我自己也踩过这个坑。

避坑指南: 每次清洗完数据,一定要做一次「完整性检查」——看看数据量对不对、日期是否连续、关键字段是否有空值。花5分钟检查,能省5小时debug。

蓝海数据掘金营,专注资料整理