共计 4629 个字符,预计需要花费 12 分钟才能阅读完成。
背景痛点
传统决策树算法(如 CART、C4.5)在处理分类变量时,通常使用信息增益或信息增益比作为分裂标准。这些方法在高基数分类变量(如邮政编码、用户 ID 等)上表现不佳,容易导致过拟合或计算效率低下。

CHAID(Chi-squared Automatic Interaction Detection)决策树采用卡方检验作为分裂标准,具有以下优势:
- 自动处理特征分箱,减少人工干预
- 更适合处理分类变量
- 通过统计显著性检验避免过拟合
技术对比
| 维度 | CHAID | CART/C4.5 |
|---|---|---|
| 分裂标准 | 卡方检验 | 信息增益 / 增益比 |
| 缺失值处理 | 作为独立类别 | 多数使用替代分裂 |
| 计算复杂度 | O(kn) | O(nlogn) |
| 变量类型 | 主要处理分类变量 | 可处理连续变量 |
| 剪枝方式 | 预剪枝(统计显著) | 后剪枝(代价复杂度) |
核心实现
1. 卡方检验实现
from scipy.stats import chi2_contingency
def chi_square_test(observed, correction=True):
"""
执行卡方检验,可选 Yates 连续性校正
Parameters
----------
observed : array-like
观察值的列联表
correction : bool
是否应用 Yates 校正(适用于 2x2 表)Returns
-------
chi2 : float
卡方统计量
p : float
p 值
dof : int
自由度
"""
chi2, p, dof, _ = chi2_contingency(observed, correction=correction)
return chi2, p, dof
2. 自动分箱逻辑
import pandas as pd
import numpy as np
class CHAIDBinner:
def __init__(self, max_depth=3, min_samples=50, alpha=0.05):
self.max_depth = max_depth
self.min_samples = min_samples
self.alpha = alpha
def fit(self, X, y):
"""自动分箱主逻辑"""
self.bins_ = {}
for col in X.columns:
self._fit_column(X[col], y, col)
return self
def _fit_column(self, x, y, col_name, depth=1):
"""递归分箱"""
if depth > self.max_depth or len(x) < self.min_samples:
self.bins_[col_name] = x.unique()
return
# 执行卡方检验
contingency = pd.crosstab(x, y)
chi2, p, _ = chi_square_test(contingency.values)
if p > self.alpha: # 不显著,停止分裂
self.bins_[col_name] = x.unique()
else:
# 递归处理子节点
groups = self._find_best_split(contingency)
for g in groups:
mask = x.isin(g)
self._fit_column(x[mask], y[mask], col_name, depth+1)
3. Bonferroni 校正实现
def bonferroni_correction(p_values, alpha=0.05):
"""
应用 Bonferroni 校正
Parameters
----------
p_values : array-like
原始 p 值数组
alpha : float
显著性水平
Returns
-------
rejected : array
是否拒绝原假设
"""
corrected_alpha = alpha / len(p_values)
return p_values < corrected_alpha
完整 CHAID 分类器实现
from typing import Dict, List, Union
import numpy as np
import pandas as pd
from scipy.stats import chi2_contingency
class CHAIDClassifier:
"""
CHAID 决策树分类器实现
Attributes
----------
max_depth : int
树的最大深度
min_samples : int
节点最小样本数
alpha : float
显著性水平
feature_importances_ : dict
特征重要性
"""
def __init__(self, max_depth=3, min_samples=50, alpha=0.05):
self.max_depth = max_depth
self.min_samples = min_samples
self.alpha = alpha
self.feature_importances_ = {}
def fit(self, X: pd.DataFrame, y: pd.Series):
"""训练 CHAID 模型"""
self.tree_ = self._build_tree(X, y)
self._calc_feature_importance(X, y)
return self
def _build_tree(self, X, y, depth=1):
"""递归构建决策树"""
if depth > self.max_depth or len(y) < self.min_samples:
return {'leaf': True, 'value': y.mode()[0]}
best_feature, best_groups = self._find_best_split(X, y)
if not best_feature:
return {'leaf': True, 'value': y.mode()[0]}
node = {
'feature': best_feature,
'groups': best_groups,
'depth': depth,
'children': []}
for group in best_groups:
mask = X[best_feature].isin(group)
child_node = self._build_tree(X[mask], y[mask], depth+1)
node['children'].append(child_node)
return node
def _find_best_split(self, X, y):
"""寻找最佳分裂特征"""
best_feature = None
best_groups = None
best_p = 1.0
p_values = []
features = []
for col in X.columns:
contingency = pd.crosstab(X[col], y)
if contingency.shape[0] == 1:
continue
_, p, _ = chi_square_test(contingency.values)
p_values.append(p)
features.append(col)
if not p_values:
return None, None
# Bonferroni 校正
significant = bonferroni_correction(p_values, self.alpha)
if not any(significant):
return None, None
# 选择最显著的特征
best_idx = np.argmin(p_values)
best_feature = features[best_idx]
# 根据卡方检验结果分组
contingency = pd.crosstab(X[best_feature], y)
best_groups = self._form_groups(contingency)
return best_feature, best_groups
def _form_groups(self, contingency):
"""根据卡方检验结果形成分组"""
# 简化的分组逻辑,实际应基于卡方检验结果
return [contingency.index.tolist()] # 这里需要更复杂的实现
def _calc_feature_importance(self, X, y):
"""计算特征重要性"""
for col in X.columns:
contingency = pd.crosstab(X[col], y)
if contingency.shape[0] > 1:
chi2, _, _ = chi_square_test(contingency.values)
self.feature_importances_[col] = chi2
else:
self.feature_importances_[col] = 0
生产建议
- 类别合并时的自由度陷阱
- 合并类别会减少自由度,可能影响卡方检验结果
-
建议保留原始类别或使用更智能的合并策略
-
大数据量下的分箱优化
- 使用近似卡方检验减少计算量
-
考虑使用分布式计算框架
-
与 GBDT 模型集成
- CHAID 可以作为特征工程工具
- 将决策路径作为新特征输入 GBDT
性能测试
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
import time
# 生成测试数据
X, y = make_classification(n_samples=10000, n_features=10,
n_informative=5, n_classes=2,
random_state=42)
X = pd.DataFrame(X, columns=[f'f{i}' for i in range(10)])
y = pd.Series(y)
# 转换为分类变量(模拟真实场景)for col in X.columns:
X[col] = pd.qcut(X[col], q=5, labels=False)
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 训练 CHAID 模型
start = time.time()
chaid = CHAIDClassifier(max_depth=4, min_samples=100)
chaid.fit(X_train, y_train)
chaid_time = time.time() - start
# 预测
y_pred = chaid.predict_proba(X_test)
chaid_auc = roc_auc_score(y_test, y_pred[:, 1])
print(f"CHAID 训练时间: {chaid_time:.2f}s, 测试 AUC: {chaid_auc:.4f}")
延伸思考
CHAID 特别适合营销响应模型的 Uplift Modeling 场景:
- 可以识别不同用户群体的响应差异
- 通过统计显著性检验确保发现的模式可靠
- 结果易于业务解释,便于制定营销策略
可以考虑将 CHAID 与以下技术结合:
- 倾向得分匹配(Propensity Score Matching)
- 元学习器(如 Two-Model Approach)
- 深度学习方法(如 CEVAE)
通过这种方式,可以构建更强大的 Uplift 模型,精准识别营销活动的敏感人群。
正文完
发表至: 未分类
近两天内
