4、因子构建基础:因子定义、因子计算逻辑、因子暴露度计算

好,咱们进入正题。因子构建这事儿,说白了就是给资产「拍X光片」——你得找到那些能解释收益差异的底层特征。我做了这么多年量化,见过太多人一上来就堆砌几百个因子,结果过拟合得一塌糊涂。今天咱们就聊聊,怎么把因子这件事做扎实。

4.1 因子定义:先想清楚你要抓什么

因子定义不是拍脑袋。我个人习惯,先问自己三个问题:

  • 这个因子背后有没有经济学逻辑?
  • 它能不能在样本外持续有效?
  • 它的计算是否可重复、可验证?

举个例子,碳金融里有个经典因子叫「碳强度因子」。你想想看,一家公司单位营收的碳排放量越高,未来面临的碳税风险就越大。这个逻辑很清晰——政策压力会传导到利润表。

因子定义三要素:

  • 名称:碳强度因子(Carbon Intensity Factor)
  • 含义:企业每单位营收产生的碳排放量
  • 方向:碳强度越高,预期收益越低(负向因子)

我在项目中遇到过一个问题:有人把「碳排放总量」直接当因子用。这其实有问题——大公司排放量天然高,但人家营收也高啊。你得做标准化处理,不然因子暴露度里全是规模效应。

4.2 因子计算逻辑:从原始数据到因子值

因子计算,说白了就是三步走:取数、加工、标准化。咱们拿碳强度因子来演示。

4.2.1 数据准备

你需要两类数据:

  • 碳排放数据(Scope 1 + Scope 2,单位:吨CO₂)
  • 营收数据(单位:万元)

嗯,这里要注意:Scope 3(供应链排放)数据质量参差不齐,我建议初期先别碰,容易引入噪声。

4.2.2 计算原始因子值

# Python示例:碳强度因子计算
import pandas as pd
import numpy as np

def calc_carbon_intensity(emission_df, revenue_df):
    """
    计算碳强度因子
    emission_df: 包含 ['ticker', 'year', 'scope1', 'scope2']
    revenue_df: 包含 ['ticker', 'year', 'revenue']
    """
    # 合并数据
    df = pd.merge(emission_df, revenue_df, on=['ticker', 'year'])
    
    # 计算总排放
    df['total_emission'] = df['scope1'] + df['scope2']
    
    # 碳强度 = 总排放 / 营收(单位:吨/万元)
    df['raw_factor'] = df['total_emission'] / df['revenue']
    
    return df[['ticker', 'year', 'raw_factor']]

这段代码看着简单,但坑不少。我曾经吃过一次亏——某家公司的营收数据是季度累计值,而碳排放是年度值,直接除出来个离谱的数字。所以数据对齐是第一步,千万别偷懒。

4.2.3 因子标准化

原始因子值没法直接用。不同行业的碳强度差异巨大——钢铁和互联网能比吗?所以要做行业中性化处理。

def standardize_factor(df, group_col='industry'):
    """
    行业中性化 + Z-score标准化
    """
    # 按行业分组,去均值
    df['demeaned'] = df.groupby(group_col)['raw_factor'].transform(
        lambda x: x - x.mean()
    )
    
    # 再除以标准差
    df['zscore_factor'] = df.groupby(group_col)['demeaned'].transform(
        lambda x: x / x.std()
    )
    
    # 缩尾处理(去掉极端值)
    df['zscore_factor'] = df['zscore_factor'].clip(-3, 3)
    
    return df

避坑指南:我曾经在缩尾处理时用了全局分位数,结果把钢铁行业的正常值给截掉了。后来改成按行业分位数处理,效果好了很多。记住:异常值要按行业语境来判断。

4.3 因子暴露度计算:把因子值映射到投资组合

因子暴露度,说白了就是「你的组合在多大程度上受这个因子影响」。计算方式有两种:

4.3.1 个股层面的暴露度

对于个股来说,因子暴露度就是标准化后的因子值本身。比如某公司碳强度Z-score是-1.5,那它的碳因子暴露度就是-1.5——意味着它比行业平均更「绿色」。

4.3.2 组合层面的暴露度

组合暴露度是持仓权重的加权平均:

def portfolio_exposure(holdings_df, factor_df):
    """
    计算组合的因子暴露度
    holdings_df: 包含 ['ticker', 'weight']
    factor_df: 包含 ['ticker', 'zscore_factor']
    """
    merged = pd.merge(holdings_df, factor_df, on='ticker')
    
    # 加权平均
    portfolio_exp = (merged['weight'] * merged['zscore_factor']).sum()
    
    return portfolio_exp

举个例子:假设组合里只有两只股票——A公司权重60%,碳暴露度0.8;B公司权重40%,碳暴露度-0.3。那组合暴露度就是 0.6×0.8 + 0.4×(-0.3) = 0.36。正数意味着组合整体偏「高碳」风格。

注意:因子暴露度会随时间变化。公司可能减排了,行业结构可能变了。我建议至少每季度重新计算一次暴露度。别指望一年前的数据还能指导现在的交易。

4.4 知识体系总览

下面这张图把因子构建的完整流程串起来了。你仔细看,每一步都有坑,每一步都需要判断。

因子构建核心流程 因子定义 经济学逻辑 可重复性验证 因子计算 数据清洗 → 原始值 行业中性化 → Z-score 暴露度计算 个股暴露度 组合加权暴露度 数据细节 • 碳排放:Scope1+Scope2 • 营收:年度/季度对齐 • 行业分类:GICS/Wind 标准化方法 • 行业均值去偏 • Z-score归一化 • 3σ缩尾处理 暴露度应用 • 多因子模型输入 • 风险归因分析 • 组合优化约束 核心原则 逻辑先行 → 数据验证 → 动态更新 避免过拟合,保持因子可解释性

4.5 几个实战经验

最后分享几个我踩过的坑,希望能帮你省点时间:

  1. 数据频率要匹配:碳排放数据通常是年度,营收数据可能是季度。别拿季度营收去匹配年度排放,会算出奇怪的季节性波动。
  2. 行业分类别用太细:我试过用申万三级行业做中性化,结果每个组里就两三家公司,标准化后全是极端值。建议用一级或二级行业。
  3. 因子暴露度不是越高越好:有人觉得暴露度绝对值越大,因子越有效。其实不然——暴露度只是度量,不是信号。关键看暴露度能否稳定预测收益。

一个小技巧:构建完因子后,先做一期「回测前体检」——看看因子值的截面分布是否合理,行业覆盖是否均匀,时间序列是否平稳。这些检查花不了10分钟,但能帮你筛掉80%的无效因子。

因子构建这件事,说白了就是「把经济学直觉变成可计算的数字」。别追求花哨的算法,先把基础打牢。数据干净、逻辑清晰、计算可复现——这三点做到了,你的因子就已经超过市场上大半的人了。

专注资料整理