从原理到实践:使用Python实现高效CHAID决策树算法

1次阅读
没有评论

共计 4629 个字符,预计需要花费 12 分钟才能阅读完成。

image.webp

背景痛点

传统决策树算法(如 CART、C4.5)在处理分类变量时,通常使用信息增益或信息增益比作为分裂标准。这些方法在高基数分类变量(如邮政编码、用户 ID 等)上表现不佳,容易导致过拟合或计算效率低下。

从原理到实践:使用 Python 实现高效 CHAID 决策树算法

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

生产建议

  1. 类别合并时的自由度陷阱
  2. 合并类别会减少自由度,可能影响卡方检验结果
  3. 建议保留原始类别或使用更智能的合并策略

  4. 大数据量下的分箱优化

  5. 使用近似卡方检验减少计算量
  6. 考虑使用分布式计算框架

  7. 与 GBDT 模型集成

  8. CHAID 可以作为特征工程工具
  9. 将决策路径作为新特征输入 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 场景:

  1. 可以识别不同用户群体的响应差异
  2. 通过统计显著性检验确保发现的模式可靠
  3. 结果易于业务解释,便于制定营销策略

可以考虑将 CHAID 与以下技术结合:

  • 倾向得分匹配(Propensity Score Matching)
  • 元学习器(如 Two-Model Approach)
  • 深度学习方法(如 CEVAE)

通过这种方式,可以构建更强大的 Uplift 模型,精准识别营销活动的敏感人群。

正文完
 0
评论(没有评论)