阅读提示

这篇讲解 Betalens 因子分组的核心 API——single_characteristicdouble_characteristicmulti_characteristic——以及它们的语义差异。学完之后,你能理解为什么叫”characteristic”而不是”factor”,以及什么时候用单因子、双因子、多因子分组。

导言

Betalens 的因子分组模块是整个框架的核心之一。从 quickstart 里的 single_characteristic(单因子分组),到 Barra 模型里常见的 multi_characteristic(多因子分组),再到学术研究里经典的 Fama-French 三因子、五因子模型用到的 double_characteristic(双因子独立分组),Betalens 都有对应的 API。

但理解这三种分组方式的关键,不是记住 API 怎么用,而是理解它们的语义区别——分组方式背后对应着不同的金融假设。

三个分组函数:一张对比表

函数 维度 适用场景 分组标签
single_characteristic 1 维(单因子) 单因子 IC 测试、最简多空策略 {metric}_label
double_characteristic 2 维(独立/依赖) 经典 FF 风格因子、Fama-MacBeth 截面回归 {metric1}_label × {metric2}_label
multi_characteristic N 维 Barra 多因子模型、复合因子研究 {name1}_label × {name2}_label × …

单因子分组:single_characteristic

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from betalens.factor.factor import (
get_absolute_trade_days,
get_tradable_pool,
pre_query_characteristic_data,
single_characteristic,
get_single_factor_weight,
)

days = get_absolute_trade_days("2020-04-30", "2024-04-30", "Y")
date_ranges, code_ranges = get_tradable_pool(days)

data = pre_query_characteristic_data(
days,
"股息率(报告期)",
table_name="fundamentals",
date_ranges=date_ranges,
code_ranges=code_ranges,
)

# 按股息率分成10组(q=10),低分位组(label=1)= 低股息率
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 10})
print(labeled.head())

返回的 DataFrame 结构:

1
2
3
4
5
                    股息率(报告期)  股息率(报告期)_label
input_ts code
2020-04-29 000001.SZ 3.24 10
2020-04-29 000002.SZ 2.81 9
2020-04-29 000004.SZ 0.00 1 ← 低股息率
  • 索引MultiIndex(input_ts, code)——输入时间戳 × 股票代码。
  • 标签列{metric}_label,取值 1~10(分成 10 组时)。
  • 标签语义:默认 ascending=True(升序),即 label=1 是因子值最低的 10%,label=10 是因子值最高的 10%。

分位数参数

1
2
3
4
5
6
7
8
# 分成5组
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 5})

# 也可以用分位点列表自定义边界
labeled = single_characteristic(
data, "股息率(报告期)",
{"股息率(报告期)": [0, 0.2, 0.4, 0.6, 0.8, 1.0]} # 自定义分位点
)

双因子分组:double_characteristic

双因子分组有两种模式:依赖分组dependent)和独立分组independent)。

依赖分组(最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
from betalens.factor.factor import double_characteristic

# FF 三因子中的 Size × BM 独立分组
# 先按市值分成5组,再在每组内按 BM 分成5组
double_labeled = double_characteristic(
size_data, # 市值因子数据(DataFrame, MultiIndex)
bm_data, # 账面市值比因子数据
metric1="市值",
metric2="账面市值比",
quantiles1={"市值": 5},
quantiles2={"账面市值比": 5},
sort_method="dependent", # 依赖分组:在市值组内再分 BM
)

依赖分组的含义:先按市值排序,在每个市值组内再按 BM 排序。最终得到 5×5=25 个组合,适合做 Fama-French 风格的因子检验。

独立分组

1
2
3
4
5
6
7
8
9
double_labeled_ind = double_characteristic(
size_data,
bm_data,
metric1="市值",
metric2="账面市值比",
quantiles1={"市值": 5},
quantiles2={"账面市值比": 5},
sort_method="independent", # 独立分组
)

独立分组:市值和 BM 各自分成 5 组,然后做一个交叉表,最终也是 25 个组合,但每组里的股票在两个维度上分布均匀。Barra 模型常用独立分组。

多因子分组:multi_characteristic

多因子分组是双因子分组的扩展,支持任意 N 个因子:

1
2
3
4
5
6
7
8
9
10
from betalens.factor.factor import multi_characteristic

multi_labeled = multi_characteristic(
[size_data, bm_data, momentum_data],
[
{"name": "市值", "quantiles": 5, "method": "dependent"},
{"name": "账面市值比", "quantiles": 5, "method": "dependent"},
{"name": "动量", "quantiles": 3, "method": "independent"},
],
)

生成 5×5×3=75 个组合。

生成多空权重

分组之后,下一步是用分组标签生成权重矩阵:

1
2
3
4
5
6
7
from betalens.factor.factor import get_single_factor_weight

weights = get_single_factor_weight(labeled, {
"factor_key": "股息率(报告期)",
"mode": "classic-long-short", # 多第10组,空第1组
})
weights["cash"] = 0 # 必须补 cash 列

get_single_factor_weightmode 参数:

mode 含义
classic-long-short 多最高因子组(label=10),空最低因子组(label=1),多头合计 1,空头合计 -1
top-only 只做多最高因子组(label=10),其余为现金
bottom-only 只做空最低因子组(label=1),其余为现金
freeplay 自定义分组权重(需配合 group_weights

开发者侧:分组标签的 MultiIndex 结构

从源码角度,single_characteristic 的核心逻辑是:

1
2
3
4
5
6
7
8
def single_characteristic(raw, metric, quantiles):
# raw: DataFrame, index = (input_ts, code), columns 包含 metric 列
def _label_group(series):
# pd.qcut: 按分位数分组
labels = pd.qcut(series, q=quantiles, labels=False, duplicates='drop')
return labels + 1 # label 从 1 开始
labeled = raw.groupby(level='input_ts')[metric].transform(_label_group)
return labeled.rename(f"{metric}_label")

理解这个结构有助于:

  1. Debug 分组异常:如果某个截面只有 8 只股票,分成 10 组会出问题(duplicates='drop' 会合并组)。
  2. 自定义分组逻辑:如果标准分组不满足需求,可以参考这个逻辑写自定义版本。
  3. 多因子相关性分析:知道索引是 (input_ts, code) 之后,可以用 pd.merge 合并多个因子数据再分组。

常见错误

1. single_characteristic 记成 single_factor

这是最常见的拼写错误。记住:Betalens 的分组函数名是 single_characteristic,不是 single_factor

1
2
3
4
5
# 错:AttributeError
labeled = single_factor(data, "股息率(报告期)", {"股息率(报告期)": 10})

# 对
labeled = single_characteristic(data, "股息率(报告期)", {"股息率(报告期)": 10})

2. 合并多因子时索引对不上

1
2
3
4
5
6
# 错:两个 DataFrame 的 MultiIndex 不一致(一个用 input_ts,一个用 datetime)
merged = pd.merge(data1, data2, on=["input_ts", "code"])

# 对:确保两边都用 input_ts
print(data1.index.names) # ['input_ts', 'code']
print(data2.index.names) # ['input_ts', 'code']

3. 分组后忘了两步——分组不等于权重

分组(single_characteristic)只是给股票打标签,生成权重矩阵(get_single_factor_weight)才是最终能输入回测的格式。

延伸阅读