您的位置首页 >人工智能 >

熔岩热度做成横截面因子log1p 去极值、截面标准化与换手残差的 Python 全流程

我先联网确认 “熔岩热度” 这个说法在量化语境里指什么,并核对因子处理的行业口径。

原文确实存在,我读全文弄清 “熔岩热度” 是什么数据、它的处理链路怎么走。

已核实:原文里的 “熔岩” 是 Lava 消息面热度数据服务,字段和接口限制都已确认。以下是按指定格式重写的版本。

把熔岩(Lava)的日频个股热度面板做成可用的横截面因子,核心链路只有五步:按每批最多 20 个代码从 /panel/daily 拉自然日数据 → 折到交易日并剔除当天未完结的行 → 对计数型字段做 log1p 变换 → 在每个交易日的截面上用 MAD 或百分位截尾去极值 → 再做截面 z 标准化,最后用自有的个股换手率做一次对照回归,取残差。之所以必须这么走,是因为 mentions、agencies、weighted_heat 都是原始计数:截面上常见 “中位数个位数、头部上千” 的极端右偏,少数大票会把整个截面拉成长尾,而热度又天然和换手率高度相关 —— 不处理就直接丢进多因子框架,你拿到的其实是 “流动性 + 事件” 的混合物,而不是热度本身。另外三个口径要先钉死:weighted_heat 是当前机构分口径、不是 PIT;榜单里的 heat_z20 是相对自身近 20 日的时间序列 z 分,不能当截面 z 用;换手率、成交额和交易日历都不在熔岩里,得由你自己的行情库提供。

一、先看原料:哪些字段能用,哪些限制会咬人

数据板块接口 / MCP 工具在因子流程里的角色
个股热度(日频历史)/panel/daily · get_stock_heat_panel主原料:mentions、agencies、weighted_heat、is_first_mention_day
全市场热度/hot/stocks · get_market_hot_stocks抽查:盘中截面与日频方向是否一致
概念板块/concepts/by-stock · get_stock_concept_heat可选:打概念标签做组内标准化
机构 / 大 V(含 PIT)/agencies/pit · get_agency_pit口径提醒:面板的 weighted_heat 不是当日 PIT

几个硬限制,直接决定你的取数代码怎么写:

  • 单次最多 20 个代码,股票池大就必须分批;超出上限返回 400,不计费。
  • 区间最长 92 天,to 默认今天、默认回看 90 天。
  • 成功调用按次计费(每次 0.005 元,非 2xx 不计费),所以分批循环里别重复拉同一组,建议加本地缓存与请求指纹。
  • to 等于今天时,最后一行是未完结日,做日频因子必须剔除,否则等于提前用了当天还没走完的信息。

二、为什么原始热度不能直接进模型

三个老问题,每个都能把因子废掉:

问题表现后果
分布极度右偏中位数个位数、头部上千均值被少数股票绑架,z 分失真
长尾由少数大票主导权重集中在头部几只截面排序被 “谁今天最热” 单点决定
与换手天然相关热度高的票换手也高因子实际在赌流动性,不是在赌关注度

所以顺序很关键:先 log1p,再去极值,最后标准化。反过来做 —— 在原始尺度上按 1% / 99% 分位截尾 —— 会把大量中等热度的股票压成同一个值,信息直接损失掉,而且这种损失是不可逆的。

三、取数与自然日折交易日:两个容易错的细节

cell_1_fetch.py —— 地址与令牌均为占位符

import os, time, requests, pandas as pd

BASE = os.environ.get("LAVA_BASE", "/subscribe/v1")

TOKEN = os.environ.get("LAVA_API_KEY", "")

HEAD = {"Authorization": "Bearer " + TOKEN}

def get_panel(codes, start, end):

"""codes ≤ 20;start/end 为自然日;区间 ≤ 92 天"""

assert 1 <= len(codes) <= 20

r = requests.get(

BASE + "/panel/daily",

params={"type": "stock", "codes": ",".join(codes),

"from": start, "to": end},

headers=HEAD, timeout=60,

).json()

if not r.get("ok"):

raise RuntimeError(r.get("code"))

rows = r["data"] if isinstance(r["data"], list) else r["data"].get("list", [])

return pd.DataFrame(rows), r["meta"]["server_time"]

def pull_universe(codes, start, end, cache=None):

frames, cache = [], cache or {}

for i in range(0, len(codes), 20):

batch = tuple(codes[i:i + 20])

key = (batch, start, end)

if key in cache: 同一组别重复付费

frames.append(cache[key]); continue

df, ts = get_panel(list(batch), start, end)

df["fetched_at"] = ts

cache[key] = df

frames.append(df)

time.sleep(0.5) 礼貌间隔,示例值

return pd.concat(frames, ignore_index=True), cache

落库前先打印一行核对列名。不同接口版本对日期列和代码列的命名可能不一致,下文统一假设为 date 与 ts_code。

自然日折叠的规则,比代码本身更容易出事:

cell_2_calendar.py

def to_trade_day(panel, trade_days):

"""trade_days: 自有 A 股交易日列表(升序 datetime)"""

td = pd.Series(pd.to_datetime(trade_days)).sort_values().reset_index(drop=True)

panel = panel.copy()

panel["date"] = pd.to_datetime(panel["date"])

非交易日的内容顺延到下一个交易日(周末讨论不应丢掉)

idx = td.searchsorted(panel["date"], side="left")

keep = idx < len(td) 超出最后一个交易日就丢弃

panel = panel[keep]

panel["trade_day"] = td.values[idx[keep]]

agg = {"mentions": "sum",

"agencies": "max", 跨日求和会重复计同一主体

"weighted_heat": "sum"}

out = panel.groupby(["trade_day", "ts_code"], as_index=False).agg(agg)

return out

两个判断点:agencies 用 max 不用 sum,因为跨自然日重复提及的是同一批机构;如果你要的是精确的去重覆盖数,得改用 /coverage/stocks 的 7/30 日去重计数。另外,日频面板做不到以 15:00 为界的小时级切分,要严格的信息时点就得走 PIT 截面。

四、变换与去极值:MAD 与百分位怎么选

cell_3_winsor.py

import numpy as np

def mad_clip(s: pd.Series, k: float = 5.0) -> pd.Series:

med = s.median()

mad = (s - med).abs().median()

if mad == 0: 大量零值时 MAD 会塌成 0

return s.clip(s.quantile(0.01), s.quantile(0.99))

lo = med - k 1.4826 mad

hi = med + k 1.4826 mad

return s.clip(lo, hi)

def transform(day_df: pd.DataFrame, col: str) -> pd.Series:

x = np.log1p(day_df[col].astype(float)) 计数型变量先压偏

return mad_clip(x, k=5.0)

1.4826 这个系数不是装饰:MAD 乘以它之后,在正态分布下与标准差同尺度,这样 k 才有 “几倍标准差” 的直觉。k 常取 3—5,取 3 更狠、取 5 更保守。

方法规则(每交易日截面内)适用情形风险
百分位截尾低于 1% 分位记为该值,高于 99% 分位记为该值样本量大、尾部不离谱尾部极厚时仍留长尾
MAD 截尾med ± k × 1.4826 × MAD尾部很厚、零值多MAD=0 时会退化,必须有兜底

冷却期还有个隐形坑:冷门股集中的交易日,超过一半股票提及为 0,MAD 直接为 0,上下界塌成一个点,整列因子变成常数。上面的兜底换成百分位截尾,另一种做法是只在非零子样本上估 MAD—— 但要在元数据里记清你用了哪种。

五、截面标准化:z 分、排名分位、组内 z 分

cell_4_zscore.py

def xsection_z(df: pd.DataFrame, col: str, min_n: int = 30) -> pd.DataFrame:

out = []

for d, g in df.groupby("trade_day", sort=True):

x = transform(g, col)

if len(x.dropna()) < min_n: 小样本不估,宁缺勿滥

continue

sd = x.std(ddof=0) 截面用总体标准差

z = (x - x.mean()) / sd if sd > 0 else x np.nan

out.append(pd.DataFrame({

"trade_day": d, "ts_code": g["ts_code"].values,

f"{col}_z": z.values}))

return pd.concat(out, ignore_index=True) if out else pd.DataFrame()

注意这里和常见写法的一处差异:sd == 0 时输出 NaN,而不是 x 0.0。后者会把整列变成 0,因子直接失效还看不出来。

做法优点代价
截面 z 分保留距离信息,便于线性合成对剩余尾部仍敏感
截面排名分位对极值完全免疫丢幅度,并列多时分辨率低
组内 z 分(按概念 / 行业)消除板块整体冷热分组标签要本地维护,组小不稳

三个口径别混:heat_z20 回答 “这只票比它自己平时热多少”,截面 z 回答 “同一天里它比别人热多少”,组内 z 回答 “同板块里它比别人热多少”。混成一列,后面所有检验都失去意义。

六、换手对照:残差该怎么取,边界在哪

cell_5_resid.py —— turnover 来自自有行情库

def resid_vs_turnover(heat_z: pd.DataFrame, turn_z: pd.DataFrame,

min_n: int = 30) -> pd.DataFrame:

df = heat_z.merge(turn_z, on=["trade_day", "ts_code"], how="inner")

res = []

for d, g in df.groupby("trade_day", sort=True):

if len(g) < min_n:

continue

x = g["turnover_z"].astype(float).values

y = g["mentions_z"].astype(float).values

if not np.isfinite(x).all() or not np.isfinite(y).all():

continue

if x.std() == 0:

continue

X = np.column_stack([np.ones_like(x), x]) 必须有截距

beta, _ = np.linalg.lstsq(X, y, rcond=None)

resid = y - X @ beta

res.append(pd.DataFrame({

"trade_day": d, "ts_code": g["ts_code"].values,

"heat_resid": resid, "beta": beta[1], "n": len(g)}))

return pd.concat(res, ignore_index=True) if out else pd.DataFrame()

三件事必须在报告里写清楚,否则这个残差会被误读:

  1. 换手率本身也要走同一条清洗链(log → 去极值 → 行业 / 市值中性化 → z 分),否则你减掉的只是 “原始换手”,残差里仍留着流动性和市值的影子。
  2. 回归要带截距。不带截距的过原点回归,在热度均值和换手均值都不为零时,beta 会被系统性高估。
  3. 相关系数只是示例。某交易日 800 只股票、mentions_z 与 turnover_z 截面相关 0.45,这只说明两者有共同成分;回归后残差相关接近 0,也只说明线性关系被剥掉了,不代表残差有方向含义。

这些是统计分解,不是信号。残差大,只意味着 “在同等换手水平下,讨论得更多或更少”,至于该做多还是做空,必须由你自己的检验回答。

七、除了残差,还要看因子自己的换手率

“换手对照” 其实有两层含义,别只做一层:

对照对象算什么用来判断什么
个股换手率热度 z 对换手 z 的截面回归残差因子是否在赌流动性
因子换手率相邻两期多头组(或全样本排序)的成分变动比例因子稳定性、交易成本可承受度

因子换手率的算法很直接:取每期因子排名前 20% 的股票集合,与上一期比较,1 - |交集| / |并集| 或 变动只数 / 组合只数 都可以,但必须固定一种并前后一致。一个 RankIC 不错、但每期换掉 70% 持仓的因子,扣掉成本后大概率不赚钱 —— 这一步比多做一个变换重要得多。

八、落盘时把口径写成标签,别只存数字

元数据项要记录的内容
原料接口 /panel/daily、type=stock、from / to、meta.server_time
日期口径自然日顺延到下一交易日;未完结日已剔除;agencies 取 max
变换链log1p → MAD 截尾(k=5,含 MAD=0 兜底)→ 截面 z(ddof=0)
PIT 标注weighted_heat 为 “当前机构分口径,非 PIT”;严格回测改用 /hot/weighted 的 as_of_date 或本地重算
换手对照回归样本数下限、是否带截距、换手率清洗方式与行情来源
缺行处理只有 “代码在请求列表内且接口成功返回” 的缺失日才补 0,并单独记 “补零标记” 列

三个月后复查时,你唯一能依赖的就是这些标签。少了任何一项,某一天的因子值就说不清是怎么来的。

九、上线前的一份自检清单

检查项合格标准
覆盖率每个交易日有效样本占股票池比例稳定,无断崖
极值残留截尾后 1%/99% 分位之外仍有值的比例接近 0
分布形态每期截面偏度明显下降,均值接近 0、标准差接近 1
与市值 / 行业相关截面相关不高于你设定的阈值(建议先看绝对值)
因子换手率期均换手在可承受区间,且不随行情剧烈波动
缺失与补零补零比例可控,补零标记列存在
可复现性用同一份原始数据重跑,结果逐值一致

十、这套流程真正的价值

它给你的不是 “一个能赚钱的因子”,而是一张可复现、口径清楚、能被别人审阅的统计特征表。热度数据的价值不在原始计数有多热,而在你有没有把 “事件驱动的关注度” 和 “大票天然的高流动性” 分开 —— 分开之后,你才知道自己研究的到底是哪件事。

一句能落地的判断:先把 log1p、MAD 截尾、截面 z 这三步按固定参数固化成函数,再谈换手残差;顺序和兜底做对,后面的 IC、分层、换手率检验才有意义,否则你只是在给一堆长尾数字做排名。

【常见问题】

问题 1:熔岩热度为什么不能直接拿 mentions 做横截面因子?

回答 1:因为 mentions 是原始计数,截面分布极度右偏 —— 中位数常常只有个位数,头部却能到上千,均值会被少数大票绑架,z 分也随之失真;再加上熔岩热度天然和换手率高度相关,直接使用等于在赌流动性而不是关注度。标准做法是先 log1p 压偏,再在每日截面上做 MAD 或百分位去极值,最后做截面标准化,顺序不能反。

问题 2:横截面因子做去极值,MAD 法和百分位截尾该怎么选?

回答 2:看尾部厚度和零值比例。百分位截尾(1%/99%)适合样本量大、尾部不离谱的情形,实现简单但不能真正压住极厚尾部;MAD 法以中位数为中心、用 med ± k × 1.4826 × MAD 定上下界,k 常取 3—5,对非正态分布更稳健。熔岩热度这种零值很多的计数型数据更适合 MAD,但必须处理 MAD=0 的退化 —— 冷门股集中的交易日超过一半股票提及为 0 时,上下界会塌成一个点,整列因子变常数。

问题 3:熔岩热度因子做标准化,用 z 分还是排名分位?

回答 3:取决于你后面怎么用。截面 z 分保留距离信息,适合做线性加权合成,但对残留尾部仍敏感;截面排名分位对极值完全免疫,代价是丢掉幅度信息,并列值多时分辨率低。还有第三种:按概念或行业做组内标准化,能消除板块整体冷热,但需要本地维护分组标签,组太小会不稳。另外注意,熔岩榜单自带的 heat_z20 是相对自身近 20 日的时间序列 z 分,不能替代横截面标准化。

问题 4:用换手率做对照、取热度残差,这一步到底在做什么?

回答 4:它做的是一个统计分解:在每个截面上用换手 z 分对热度 z 分做带截距的一元回归,残差代表 “同等换手水平下,讨论得更多或更少” 的部分。它的作用是剥掉流动性共同成分,让熔岩热度因子更接近纯粹的事件关注度。但残差本身没有方向含义,残差大不等于该做多或做空;而且换手率自己也要走 log、去极值、行业与市值中性化,否则你减掉的只是原始换手,残差里仍留着市值影子。

问题 5:熔岩热度因子的换手率(因子自身换手)该怎么衡量?

回答 5:固定一种口径,取每期因子排名前 20% 的股票集合,与上一期比较,用 “变动只数 / 组合只数” 或 “1 - 交集 / 并集” 都可以,但前后必须一致。它回答的是交易成本可承受度:一个 RankIC 尚可、但每期换掉七成持仓的熔岩热度因子,扣费后大概率不赚钱。这一步和 “用个股换手率做残差对照” 是两件事,别混为一谈。

问题 6:用 Python 处理熔岩热度数据,最容易踩的坑有哪些?

回答 6:四个高频坑。第一,自然日直接 join 交易日历,周末讨论凭空丢失;正确做法是顺延到下一个交易日,并用 max 而不是 sum 聚合 agencies,避免重复计同一主体。第二,to 等于今天时最后一行是未完结日,不剔除就等于用了未来信息。第三,单次最多 20 个代码、区间最长 92 天,分批时不做缓存会重复付费,每次成功调用 0.005 元。第四,把 weighted_heat 当成 PIT 使用 —— 它是当前机构分口径,严格回测要改用 /hot/weighted 的 as_of_date 截面。

【参考资料】

[2026-11-13](把熔岩热度做成横截面因子:去极值、标准化与换手对照的 Python 走读)阿里云开发者社区

[2026-11-04](FPUS1 Factor Model Methodology:分位 winsorize 与截面标准化)FactorPulse

[2026-10-02](多因子策略回测总装车间:从数据清洗到 V1+Q1 质量校验的完整流程)CSDN

[2026-08-23](量化因子异常值处理:Python 去极值实战与中位数统计优势解析)51CTO 学堂

[2026-04-22](因子检验:极值处理与标准化预处理选项)米筐 Ricequant 文档

[2014-10-20](101 Formulaic Alphas:因子标准化处理)arXiv

免责声明:本文为数据处理与工程实现层面的技术说明,所有代码为示例,其中的地址、令牌、参数与数值均为占位或示例值,不对应任何真实标的或真实行情;文中提及的接口字段、单批代码上限、区间长度与计费规则以服务方 OpenAPI 最新文档为准,可能随时调整。文中任何关于因子有效性、方向或收益的表述均不构成投资建议,历史统计特征不代表未来表现,回测结果存在过拟合、数据窥探与生存者偏差等风险。涉及实盘使用,请自行完成独立检验、成本测算与合规审查,并遵守数据服务方的授权与使用条款。

今日大家都在搜的词: