第二章 数据获取与清洗:股票行情数据获取(日线、分钟线)、财务数据获取、数据清洗与预处理(去极值、填充缺失值)

做量化,数据就是你的弹药库。弹药不行,枪法再好也白搭。

这一章,咱们聊聊怎么搞到靠谱的数据,以及怎么把这些原始数据收拾得服服帖帖。我个人习惯把数据获取和清洗看作整个策略的生命线——源头脏了,后面所有模型都是垃圾进垃圾出。

2.1 股票行情数据获取(日线、分钟线)

行情数据,说白了就是股票每天或每分钟的价格、成交量这些。日线最常见,分钟线则用于高频或日内策略。

2.1.1 日线数据获取

日线数据,我一般从几个渠道拿:

  • 免费API:比如 Tushare、AKShare、Baostock。适合小规模研究。
  • 商业数据源:Wind、Choice、聚宽。数据质量高,但贵。
  • 自建数据库:用 MySQL 或 InfluxDB 存历史数据,每天增量更新。

举个例子,用 AKShare 拿日线,代码很简单:

import akshare as ak

# 获取平安银行日线数据
df = ak.stock_zh_a_hist(symbol="000001", period="daily", 
                         start_date="20230101", end_date="20231231")
print(df.head())

嗯,这里要注意:免费接口经常有频率限制。我遇到过一天拉5000只股票,结果被 ban 了IP。后来我加了随机延时,才稳定下来。

2.1.2 分钟线数据获取

分钟线数据量巨大。拿1分钟线来说,一只股票一天就有240条记录。全市场5000只股票,一天就是120万条。

我个人习惯用 多线程+分批下载。比如:

import akshare as ak
import time
from concurrent.futures import ThreadPoolExecutor

def fetch_minute(symbol):
    try:
        df = ak.stock_zh_a_hist_minute(symbol=symbol, period="1")
        time.sleep(0.5)  # 防止被封
        return df
    except:
        return None

# 用线程池并行拉取
symbols = ["000001", "000002", "000003"]
with ThreadPoolExecutor(max_workers=3) as executor:
    results = list(executor.map(fetch_minute, symbols))

你想想看,如果单线程一个一个拉,5000只股票得拉到猴年马月。多线程能快10倍以上。

避坑指南:我曾经一次性拉取全市场分钟线,结果内存爆了。建议分批次拉,每批500只,拉完存本地。

2.2 财务数据获取

财务数据是基本面因子的来源。比如市盈率、市净率、ROE这些。

财务数据获取比行情数据麻烦。因为财务数据有 报告期公告日 的区别。报告期是数据对应的会计期间,公告日是实际发布的日子。做因子时,必须用公告日的数据,否则就用了未来信息。

2.2.1 常用财务数据字段

字段 含义 频率
营业收入 公司主营业务收入 季度
净利润 扣非后净利润 季度
每股净资产 净资产/总股本 季度
ROE 净利润/净资产 季度

获取财务数据,我常用 Tushare 的 financial 接口:

import tushare as ts

pro = ts.pro_api('你的token')
df = pro.fina_indicator(ts_code='000001.SZ', 
                         start_date='20230101', 
                         end_date='20231231')
print(df[['end_date', 'roe', 'eps']])

这里有个坑:财务数据经常有 修正。比如公司发了一季报,后来又发更正公告。如果你用旧数据,因子值就是错的。我建议每次拉取时,都拉最新的版本。

2.3 数据清洗与预处理

数据拿到手,千万别直接用。原始数据里全是坑:缺失值、极端值、重复数据……

我总结了一套清洗流程,叫 「三去一补」:去重复、去极值、去异常、补缺失。

2.3.1 去极值(Winsorization)

极值就是那些离谱的数据。比如某股票一天涨了100倍,这明显是数据错误。或者某只小盘股突然放量100倍,也可能是异常。

我个人习惯用 MAD(中位数绝对偏差) 方法去极值。比3倍标准差更稳健:

import numpy as np

def winsorize_mad(series, n=5):
    """用MAD方法去极值"""
    median = series.median()
    mad = np.median(np.abs(series - median))
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

# 示例:对收益率去极值
returns = df['pct_chg']
df['pct_chg_clean'] = winsorize_mad(returns)

为什么用MAD?因为标准差对极值本身就很敏感。你想想看,如果数据里有一个100倍的标准差,那算出来的标准差本身就很大,再去极值就没效果了。MAD用中位数,不受极值影响。

2.3.2 填充缺失值

缺失值在金融数据里太常见了。停牌、新股上市、数据缺失……

填充方法,我按场景分:

  • 行情数据:用前向填充(ffill)。比如停牌期间,价格不变。
  • 财务数据:用最近一期数据填充。比如季报缺失,用上一季度的。
  • 因子数据:用截面中位数填充。避免引入未来信息。

代码示例:

# 前向填充行情数据
df['close'] = df['close'].ffill()

# 截面中位数填充因子
factor_df = factor_df.fillna(factor_df.median())
小技巧:我曾经用均值填充,结果因子效果变差了。后来发现,均值填充会压缩因子分布,导致区分度下降。中位数填充更稳健。

2.3.3 去重复与对齐

重复数据也是常见问题。比如同一个交易日,数据被拉了两遍。

我一般用 drop_duplicates 处理:

df = df.drop_duplicates(subset=['ts_code', 'trade_date'])

然后做 时间对齐。不同数据源的时间戳可能不一样。比如日线数据,有的用 '2023-01-01',有的用 '20230101'。统一转成 datetime 格式:

df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')

2.4 本章知识体系

下面这张图,是我自己总结的数据获取与清洗全流程。你可以把它贴在工位上:

数据获取与清洗全流程 数据获取 行情数据(日线/分钟线) 财务数据(基本面) 其他数据(因子/舆情) 数据清洗 去重复 去极值(MAD) 去异常 时间对齐 缺失值处理 行情:前向填充(ffill) 财务:最近一期填充 因子:截面中位数

核心要点:数据清洗不是一次性工作。每次新增数据,都要跑一遍清洗流程。我习惯把清洗写成函数,每天定时跑。

好了,这一章的内容就这些。数据获取和清洗,说白了就是「把脏活干漂亮」。你想想看,如果连数据都搞不定,后面那些花里胡哨的因子模型,全是空中楼阁。

我个人经验是:花80%的时间在数据上,20%的时间建模。数据干净了,模型自然就work了。

最后提醒一句:永远保留原始数据备份。我吃过一次亏,清洗时把原始数据覆盖了,结果想回溯都找不到。从那以后,我所有数据都保留 raw 和 clean 两个版本。

公众号:蓝海数据掘金营,微信deep3321