Python计算上涨捕获率报KeyError: 'Date'问题排查
问题根因
全量拉取标普500成分股时抛出KeyError: 'Date'和核心计算逻辑无关,是接口请求、数据兼容性问题导致的,具体触发场景有三类:
pandas_datareader的Yahoo财经接口没有内置限流适配,无间隔批量请求时会触发Yahoo的反爬机制,返回空值或结构损坏的响应,返回的DataFrame不存在Date列,后续按列名读取时直接抛错。少量个股测试时请求密度低,不会触发限流,所以运行正常。- 维基百科抓取的原始成分股代码存在格式不兼容问题:比如部分带类别的代码用
.做分隔(如伯克希尔B股BRK.B),Yahoo财经要求这类代码用-做分隔(即BRK-B),格式不匹配时接口返回异常数据,缺失Date字段。此外成分股调整期列表里可能残留已退市、代码变更的标的,拉取这类标的也会返回异常。 - 不同版本
pandas_datareader对Yahoo行情的解析逻辑不一致:部分版本会默认把Date列设为DataFrame索引,不会作为显式列输出,代码如果直接按列名取Date就会触发KeyError。
可直接复用的修复方案
1. 封装带容错的行情拉取函数
加入重试、随机延时、数据校验逻辑,单只标的拉取失败不中断全量任务:
import time import random import pandas as pd import pandas_datareader.data as web def fetch_yahoo_quote(ticker: str, start_date: str, end_date: str, retry_times: int=3) -> pd.DataFrame | None: for attempt in range(retry_times): try: # 拉取行情后统一重置索引,兼容不同版本的Date字段解析逻辑 quote_df = web.DataReader(ticker, "yahoo", start=start_date, end=end_date).reset_index() # 强制校验必须字段存在 required_cols = {"Date", "Adj Close"} if not required_cols.issubset(quote_df.columns): raise ValueError("返回数据缺失必要字段") return quote_df except Exception as e: if attempt == retry_times - 1: print(f"[跳过] 标的{ticker}拉取失败: {str(e)}") return None # 指数退避+随机延时,降低触发限流概率 time.sleep(2**attempt + random.uniform(1, 3))
2. 预处理成分股代码列表
把维基百科抓取的原始代码转换成Yahoo兼容格式,过滤明显异常的代码:
# 假设raw_tickers是从维基百科抓到的原始代码列表 cleaned_tickers = [] for t in raw_tickers: if not isinstance(t, str) or len(t.strip()) == 0: continue # 替换代码中的点为横杠,适配Yahoo规则 t_clean = t.strip().replace(".", "-") cleaned_tickers.append(t_clean) # 去重 cleaned_tickers = list(set(cleaned_tickers))
3. 调整上涨捕获率计算逻辑
提前拉取基准SPY的行情做公共计算,避免重复拉取基准数据,同时用索引做日期对齐,不依赖硬编码的Date列位置:
def calc_upside_capture_ratio(start_date: str, end_date: str) -> pd.DataFrame: # 先拉取基准SPY数据,全流程只拉一次 spy_df = fetch_yahoo_quote("SPY", start_date, end_date) spy_df = spy_df.set_index("Date").sort_index() spy_df["ret"] = spy_df["Adj Close"].pct_change() # 筛选基准正收益交易日 spy_pos_days = spy_df[spy_df["ret"] > 0] # 计算基准正收益区间累计收益 spy_cum_ret = spy_pos_days["Adj Close"].iloc[-1] / spy_pos_days["Adj Close"].iloc[0] - 1 result = [] for idx, ticker in enumerate(cleaned_tickers): stock_df = fetch_yahoo_quote(ticker, start_date, end_date) if stock_df is None: result.append({"ticker": ticker, "upside_capture": None}) continue stock_df = stock_df.set_index("Date").sort_index() # 对齐基准正收益交易日 stock_pos_data = stock_df.reindex(spy_pos_days.index) if stock_pos_data["Adj Close"].isna().any(): # 存在缺失交易日的标的跳过 result.append({"ticker": ticker, "upside_capture": None}) continue # 计算个股正区间累计收益 stock_cum_ret = stock_pos_data["Adj Close"].iloc[-1] / stock_pos_data["Adj Close"].iloc[0] - 1 capture_ratio = stock_cum_ret / spy_cum_ret if spy_cum_ret != 0 else None result.append({"ticker": ticker, "upside_capture": capture_ratio}) # 每处理50只存一次中间结果,避免异常中断后从头重跑 if (idx + 1) % 50 == 0: pd.DataFrame(result).to_csv("upside_capture_temp.csv", index=False) time.sleep(random.uniform(2,4)) return pd.DataFrame(result)
注意事项
- 全量拉取时不要把延时设得太短,Yahoo财经对非授权API的请求阈值大概是每小时200次左右,随机延时设为1-4秒基本不会触发限流
- 如果后续还是频繁遇到拉取错误,可以把
pandas_datareader升级到最新稳定版,旧版本的Yahoo接口解析bug已经在新版本修复 - 不要用日收益率连乘的方式算累计收益,用区间首尾价格直接计算的精度更高,也能避免缺失值导致的计算错误
内容的提问来源于stack exchange,提问作者Justin gogo
相关产品推荐
相关产品推荐

